Compare commits

...

63 Commits

Author SHA1 Message Date
Anthony Lapenna b72cce810e Merge branch 'release/1.11.4' 2017-03-03 12:48:12 +01:00
Anthony Lapenna ccaabf3b6b chore(version): bump version number 2017-03-03 12:36:24 +01:00
Anthony Lapenna 2232adbd8b merge branch 'feat484-external-endpoints' into release-1.11.4 2017-03-03 12:35:54 +01:00
Anthony Lapenna a78758123b style(cli): update error message 2017-02-07 16:27:40 +13:00
Anthony Lapenna f129bf3e97 refactor(api): refactor 2017-02-07 16:26:12 +13:00
Anthony Lapenna dc78ec5135 feat(endpoints): add the ability to define endpoints from an external source 2017-02-06 18:29:34 +13:00
Anthony Lapenna 10f7744a62 feat(authentication): add a --no-auth flag to disable authentication (#553) 2017-02-01 22:13:48 +13:00
Anthony Lapenna 0f81ad5654 feat(global): add a --no-auth flag to disable authentication 2017-02-01 22:10:07 +13:00
Anthony Lapenna 779fcf8e7f refactor(readme): remove useless version badge 2017-02-01 15:42:15 +13:00
Anthony Lapenna 7c2b186a61 refactor(assets): remove useless .jshintrc file 2017-02-01 15:40:49 +13:00
Anthony Lapenna fe0bf77bbb refactor(global): service separation #552 2017-02-01 12:26:29 +13:00
Anthony Lapenna 0abe8883d1 chore(dockerfiles): update data directory for windows Dockerfiles 2017-02-01 11:35:25 +13:00
Anthony Lapenna 84f2c2d735 Merge tag '1.11.3' into develop
Release 1.11.3
2017-02-01 11:02:15 +13:00
Anthony Lapenna 5d63c90203 Merge branch 'release/1.11.3' 2017-02-01 11:02:10 +13:00
Anthony Lapenna a97e7bbaae chore(version): bump version number 2017-02-01 11:02:05 +13:00
Anthony Lapenna f3cfb0a940 fix(cli): revert data/certs directories defaults to c:\data and c:\certs (#551) 2017-02-01 08:56:07 +13:00
Anthony Lapenna b1ca43934f Merge tag '1.11.2' into develop
Release 1.11.2
2017-01-26 17:44:00 +13:00
Anthony Lapenna 7afeb8a80d Merge branch 'release/1.11.2' 2017-01-26 17:43:53 +13:00
Anthony Lapenna f8ced03792 chore(version): bump version number 2017-01-26 17:43:47 +13:00
Jisu Park 1fdf56372b feat(containers): support container already pause message (#480) 2017-01-26 12:11:38 +13:00
Anthony Lapenna 835b273700 feat(api): force no-cache on HTML files 2017-01-26 11:45:03 +13:00
Anthony Lapenna fcc9203416 feat(node): add pagination to associated tasks 2017-01-26 10:35:05 +13:00
Anthony Lapenna e25c5a014c feat(swarm): set default sorting for Swarm nodes by role 2017-01-26 10:34:10 +13:00
Glowbal fa9ba303aa #414 feat(node-details): add ability to view and edit Swarm mode nodes (#417) 2017-01-26 10:12:04 +13:00
morph027 e6dee37af0 style(swarm): update node status filter for swarm mode nodes 2017-01-26 09:54:08 +13:00
Anthony Lapenna d03e992b4f feat(api): replace all calls to http.Error with custom Error writer 2017-01-24 16:35:48 +13:00
Anthony Lapenna 1a868be6ea fix(swarm): fix sorting issue with node table (#538) 2017-01-24 14:45:38 +13:00
Anthony Lapenna e2fc8af87a feat(ux): add the ability to change the number of paginated items on all entity tables (#537) 2017-01-24 14:28:40 +13:00
Anthony Lapenna 70933d1056 style(sidebar): add active class on Docker section (#534) 2017-01-24 09:39:13 +13:00
Anthony Lapenna 7e0b0a05de feat(authentication): clean the state and the browser local storage on logout 2017-01-23 17:04:34 +13:00
Anthony Lapenna 980f65a08a feat(api): initializes the endpoint with an empty slice instead of a pointer 2017-01-23 16:29:49 +13:00
Anthony Lapenna 8cf6d34362 style(container-creation): remove useless labels section (#532) 2017-01-23 16:10:12 +13:00
Anthony Lapenna 70f139514f fix(network-details): add a fallback for listing containers when APIV… (#531) 2017-01-23 16:06:51 +13:00
Anthony Lapenna fa4ec04c47 feat(state): introduce endpoint state (#529) 2017-01-23 12:14:34 +13:00
Anthony Lapenna 7ebe4af77d fix(images): fix an issue when deleting images with multiple tags (#526) 2017-01-22 14:42:12 +13:00
lpfeup 579241db92 #503 fix(container-stats): fix container stats timer not being properly canceled. (#504) 2017-01-21 18:04:28 +13:00
lpfeup 7d78871eee #446 fix(container-stats): fix issue in stats view with empty network data (#502) 2017-01-21 18:01:32 +13:00
Anthony Lapenna 3a6e9d2fbe fix(api): fix an issue introduced by the latest version of package gorilla/mux (#520) 2017-01-21 11:17:51 +13:00
Anthony Lapenna e4d98082dc fix(api): disable data directory creation (#495)
* fix(api): disable data directory creation

* feat(dockerhub): update volume instruction value for Windows Dockerfiles
2017-01-14 14:22:39 +13:00
Kilhog cd26051144 #476 fix(UX): Rename 'local' endpoint doesn't overwrite "unix://" (#477)
* #476 fix(UX): Rename 'local' endpoint doesn't overwrite "unix://"

* #477 fix(PR): Rename 'TYPE' in 'type'
2017-01-12 18:44:53 +13:00
Anthony Lapenna 27e584fc14 fix(api): check if admin user already exists when calling the /users/admin/init endpoint (#494) 2017-01-12 18:17:28 +13:00
Anthony Lapenna 2bdc9322de style(containers): update header text for published ports (#483) 2017-01-09 21:50:19 +13:00
Anthony Lapenna 35d5d75966 fix(api): update default value for data directory and TLS certs on Windows (#482) 2017-01-09 21:24:17 +13:00
Anthony Lapenna 2610e3d02a Merge tag '1.11.1' into develop
Release 1.11.1
2017-01-05 10:42:50 +13:00
Anthony Lapenna d579f62fa7 Merge branch 'release/1.11.1' 2017-01-05 10:42:46 +13:00
Anthony Lapenna d1b9820a29 chore(version): bump version number 2017-01-05 10:42:38 +13:00
Wouter Oet 13943c3d8b #372 feat(UX): Implement select all functionality (#437) 2017-01-05 09:15:41 +13:00
Anthony Lapenna d8b800ddbc feat(api): create platform dependant default values for CLI flags (#458) 2017-01-04 19:50:25 +13:00
Matthew Strickland 59f1a2f673 feat(templates): display container restart policy in container dashboard (#434) (#435) 2017-01-04 19:49:04 +13:00
Anthony Lapenna 9ee652c818 fix(api): creates the data directory if not exist (#452) 2017-01-03 08:32:53 +13:00
Anthony Lapenna 816c1ea448 chore(build-system): fix release tasks 2017-01-03 07:47:12 +13:00
Albert Domenech 0bacaef71a feat(images): initial aarch64/arm64 support (#447) 2017-01-03 07:42:21 +13:00
Anthony Lapenna 2ef821f118 style(service-details): update style for update failure action field (#443) 2016-12-31 13:32:20 +13:00
Anthony Lapenna 487cb4e755 Merge branch 'develop' of github.com:portainer/portainer into develop 2016-12-31 13:27:51 +13:00
Anthony Lapenna 06d3debf38 chore(build-system): fix grunt lint task 2016-12-31 13:27:35 +13:00
Anthony Lapenna 907f83aaff fix(global): remove automatic lowercase processing on image names (#442) 2016-12-31 13:25:42 +13:00
Gábor Kovács 4b747a78cd style(sidebar): Highlight active page in sidebar (#420)
* Issue #331

* New line
2016-12-31 13:12:51 +13:00
Anthony Lapenna d6f3dd8cda style(endpoint-initialization): update requirement message for local endpoint init (#424) 2016-12-31 13:00:30 +13:00
Anthony Lapenna 51632e367c fix(service-details): allow to specify the 0 value for replicas (#441) 2016-12-31 12:59:20 +13:00
Anthony Lapenna 6e98237419 feat(api): introduce cache busting mechanism (#439) 2016-12-31 12:20:38 +13:00
Anthony Lapenna ecc8857a32 fix(global): strip leading '/' in front of endpoints (#438) 2016-12-31 10:30:22 +13:00
Anthony Lapenna 7d05e81c37 chore(github): update ISSUE_TEMPLATE.md 2016-12-27 08:54:39 +13:00
Anthony Lapenna 6ce3fe7a9e Merge tag '1.11.0' into develop
Release 1.11.0
2016-12-26 13:30:20 +13:00
120 changed files with 3261 additions and 1729 deletions
+1
View File
@@ -37,6 +37,7 @@ Any other info e.g. Why do you consider this to be a bug? What did you expect to
**Technical details:**
* Portainer version:
* Portainer Docker image tag (latest/arm/windows...):
* Target Docker version (the host/cluster you manage):
* Target Swarm version (if applicable):
* Platform (windows/linux):
+1
View File
@@ -3,3 +3,4 @@ bower_components
dist
portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp
-1
View File
@@ -3,7 +3,6 @@
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
</p>
[![Microbadger version](https://images.microbadger.com/badges/version/portainer/portainer.svg)](https://microbadger.com/images/portainer/portainer "Latest version on Docker Hub")
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/latest/?badge=stable)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+64 -11
View File
@@ -44,7 +44,7 @@ func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.En
// Endpoints return an array containing all the endpoints.
func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
var endpoints []portainer.Endpoint
var endpoints = make([]portainer.Endpoint, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
@@ -67,20 +67,41 @@ func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
return endpoints, nil
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *EndpointService) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
for _, endpoint := range toCreate {
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
err := marshalAndStoreEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return nil
})
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
@@ -160,3 +181,35 @@ func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error {
return nil
})
}
// DeleteActive deletes the active endpoint.
func (service *EndpointService) DeleteActive() error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(activeEndpointBucketName))
err := bucket.Delete(internal.Itob(activeEndpointID))
if err != nil {
return err
}
return nil
})
}
func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
return nil
}
func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
return marshalAndStoreEndpoint(endpoint, bucket)
}
+71 -18
View File
@@ -1,6 +1,8 @@
package cli
import (
"time"
"github.com/portainer/portainer"
"os"
@@ -13,8 +15,11 @@ import (
type Service struct{}
const (
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
)
// ParseFlags parse the CLI flags and return a portainer.Flags struct
@@ -22,17 +27,20 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default("false").Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String(),
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
}
kingpin.Parse()
@@ -41,13 +49,37 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
if *flags.Endpoint != "" {
if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") {
if *flags.Endpoint != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateEndpoint(*flags.Endpoint)
if err != nil {
return err
}
err = validateExternalEndpoints(*flags.ExternalEndpoints)
if err != nil {
return err
}
err = validateSyncInterval(*flags.SyncInterval)
if err != nil {
return err
}
return nil
}
func validateEndpoint(endpoint string) error {
if endpoint != "" {
if !strings.HasPrefix(endpoint, "unix://") && !strings.HasPrefix(endpoint, "tcp://") {
return errInvalidEnpointProtocol
}
if strings.HasPrefix(*flags.Endpoint, "unix://") {
socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://")
if strings.HasPrefix(endpoint, "unix://") {
socketPath := strings.TrimPrefix(endpoint, "unix://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketNotFound
@@ -56,6 +88,27 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
}
}
}
return nil
}
func validateExternalEndpoints(externalEndpoints string) error {
if externalEndpoints != "" {
if _, err := os.Stat(externalEndpoints); err != nil {
if os.IsNotExist(err) {
return errEndpointsFileNotFound
}
return err
}
}
return nil
}
func validateSyncInterval(syncInterval string) error {
if syncInterval != defaultSyncInterval {
_, err := time.ParseDuration(syncInterval)
if err != nil {
return errInvalidSyncInterval
}
}
return nil
}
+16
View File
@@ -0,0 +1,16 @@
// +build !windows
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultSyncInterval = "60s"
)
+14
View File
@@ -0,0 +1,14 @@
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultSyncInterval = "60s"
)
+97 -33
View File
@@ -4,6 +4,7 @@ import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt"
"github.com/portainer/portainer/cli"
"github.com/portainer/portainer/cron"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/http"
@@ -12,7 +13,7 @@ import (
"log"
)
func main() {
func initCLI() *portainer.CLIFlags {
var cli portainer.CLIService = &cli.Service{}
flags, err := cli.ParseFlags(portainer.APIVersion)
if err != nil {
@@ -23,37 +24,76 @@ func main() {
if err != nil {
log.Fatal(err)
}
return flags
}
settings := &portainer.Settings{
HiddenLabels: *flags.Labels,
Logo: *flags.Logo,
}
var store = bolt.NewStore(*flags.Data)
err = store.Open()
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := file.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
}
defer store.Close()
return fileService
}
jwtService, err := jwt.NewService()
func initStore(dataStorePath string) *bolt.Store {
var store = bolt.NewStore(dataStorePath)
err := store.Open()
if err != nil {
log.Fatal(err)
}
return store
}
fileService, err := file.NewService(*flags.Data)
func initJWTService(authenticationEnabled bool) portainer.JWTService {
if authenticationEnabled {
jwtService, err := jwt.NewService()
if err != nil {
log.Fatal(err)
}
return jwtService
}
return nil
}
func initCryptoService() portainer.CryptoService {
return &crypto.Service{}
}
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
authorizeEndpointMgmt := true
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 initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Settings {
return &portainer.Settings{
HiddenLabels: *flags.Labels,
Logo: *flags.Logo,
Authentication: !*flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
}
}
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
endpoints, err := endpointService.Endpoints()
if err != nil {
log.Fatal(err)
}
return &endpoints[0]
}
var cryptoService portainer.CryptoService = &crypto.Service{}
// Initialize the active endpoint from the CLI only if there is no
// active endpoint defined yet.
var activeEndpoint *portainer.Endpoint
if *flags.Endpoint != "" {
activeEndpoint, err = store.EndpointService.GetActive()
if err == portainer.ErrEndpointNotFound {
func initActiveEndpoint(endpointService portainer.EndpointService, flags *portainer.CLIFlags) *portainer.Endpoint {
activeEndpoint, err := endpointService.GetActive()
if err == portainer.ErrEndpointNotFound {
if *flags.Endpoint != "" {
activeEndpoint = &portainer.Endpoint{
Name: "primary",
URL: *flags.Endpoint,
@@ -62,30 +102,54 @@ func main() {
TLSCertPath: *flags.TLSCert,
TLSKeyPath: *flags.TLSKey,
}
err = store.EndpointService.CreateEndpoint(activeEndpoint)
err = endpointService.CreateEndpoint(activeEndpoint)
if err != nil {
log.Fatal(err)
}
} else if err != nil {
log.Fatal(err)
} else if *flags.ExternalEndpoints != "" {
activeEndpoint = retrieveFirstEndpointFromDatabase(endpointService)
}
} else if err != nil {
log.Fatal(err)
}
return activeEndpoint
}
func main() {
flags := initCLI()
fileService := initFileService(*flags.Data)
store := initStore(*flags.Data)
defer store.Close()
jwtService := initJWTService(!*flags.NoAuth)
cryptoService := initCryptoService()
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
settings := initSettings(authorizeEndpointMgmt, flags)
activeEndpoint := initActiveEndpoint(store.EndpointService, flags)
var server portainer.Server = &http.Server{
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
Settings: settings,
TemplatesURL: *flags.Templates,
UserService: store.UserService,
EndpointService: store.EndpointService,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
ActiveEndpoint: activeEndpoint,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
Settings: settings,
TemplatesURL: *flags.Templates,
AuthDisabled: *flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
UserService: store.UserService,
EndpointService: store.EndpointService,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
ActiveEndpoint: activeEndpoint,
}
log.Printf("Starting Portainer on %s", *flags.Addr)
err = server.Start()
err := server.Start()
if err != nil {
log.Fatal(err)
}
+171
View File
@@ -0,0 +1,171 @@
package cron
import (
"encoding/json"
"io/ioutil"
"log"
"os"
"strings"
"github.com/portainer/portainer"
)
type (
endpointSyncJob struct {
logger *log.Logger
endpointService portainer.EndpointService
endpointFilePath string
}
synchronization struct {
endpointsToCreate []*portainer.Endpoint
endpointsToUpdate []*portainer.Endpoint
endpointsToDelete []*portainer.Endpoint
}
)
const (
// ErrEmptyEndpointArray is an error raised when the external endpoint source array is empty.
ErrEmptyEndpointArray = portainer.Error("External endpoint source is empty")
)
func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob {
return endpointSyncJob{
logger: log.New(os.Stderr, "", log.LstdFlags),
endpointService: endpointService,
endpointFilePath: endpointFilePath,
}
}
func endpointSyncError(err error, logger *log.Logger) bool {
if err != nil {
logger.Printf("Endpoint synchronization error: %s", err)
return true
}
return false
}
func isValidEndpoint(endpoint *portainer.Endpoint) bool {
if endpoint.Name != "" && endpoint.URL != "" {
if !strings.HasPrefix(endpoint.URL, "unix://") && !strings.HasPrefix(endpoint.URL, "tcp://") {
return false
}
return true
}
return false
}
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
for idx, v := range endpoints {
if endpoint.Name == v.Name && isValidEndpoint(&v) {
return idx
}
}
return -1
}
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var endpoint *portainer.Endpoint
if original.URL != updated.URL || original.TLS != updated.TLS {
endpoint = original
endpoint.URL = updated.URL
if updated.TLS {
endpoint.TLS = true
endpoint.TLSCACertPath = updated.TLSCACertPath
endpoint.TLSCertPath = updated.TLSCertPath
endpoint.TLSKeyPath = updated.TLSKeyPath
} else {
endpoint.TLS = false
endpoint.TLSCACertPath = ""
endpoint.TLSCertPath = ""
endpoint.TLSKeyPath = ""
}
}
return endpoint
}
func (sync synchronization) requireSync() bool {
if len(sync.endpointsToCreate) != 0 || len(sync.endpointsToUpdate) != 0 || len(sync.endpointsToDelete) != 0 {
return true
}
return false
}
// TMP: endpointSyncJob method to access logger, should be generic
func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
endpointsToCreate := make([]*portainer.Endpoint, 0)
endpointsToUpdate := make([]*portainer.Endpoint, 0)
endpointsToDelete := make([]*portainer.Endpoint, 0)
for idx := range storedEndpoints {
fidx := endpointExists(&storedEndpoints[idx], fileEndpoints)
if fidx != -1 {
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
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)
endpointsToUpdate = append(endpointsToUpdate, endpoint)
} else {
job.logger.Printf("No change detected for a stored endpoint. [name: %v] [url: %v]\n", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
}
} 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)
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
}
}
for idx, endpoint := range fileEndpoints {
if endpoint.Name == "" || endpoint.URL == "" {
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
continue
}
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
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)
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
}
}
return &synchronization{
endpointsToCreate: endpointsToCreate,
endpointsToUpdate: endpointsToUpdate,
endpointsToDelete: endpointsToDelete,
}
}
func (job endpointSyncJob) Sync() error {
data, err := ioutil.ReadFile(job.endpointFilePath)
if endpointSyncError(err, job.logger) {
return err
}
var fileEndpoints []portainer.Endpoint
err = json.Unmarshal(data, &fileEndpoints)
if endpointSyncError(err, job.logger) {
return err
}
if len(fileEndpoints) == 0 {
return ErrEmptyEndpointArray
}
storedEndpoints, err := job.endpointService.Endpoints()
if endpointSyncError(err, job.logger) {
return err
}
sync := job.prepareSyncData(storedEndpoints, fileEndpoints)
if sync.requireSync() {
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
if endpointSyncError(err, job.logger) {
return err
}
job.logger.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
}
return nil
}
func (job endpointSyncJob) Run() {
job.logger.Println("Endpoint synchronization job started.")
err := job.Sync()
endpointSyncError(err, job.logger)
}
+40
View File
@@ -0,0 +1,40 @@
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
}
+2 -1
View File
@@ -7,7 +7,8 @@ const (
// User errors.
const (
ErrUserNotFound = Error("User not found")
ErrUserNotFound = Error("User not found")
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
)
// Endpoint errors.
+28 -11
View File
@@ -1,13 +1,12 @@
package file
import (
"strconv"
"github.com/portainer/portainer"
"io"
"os"
"path"
"strconv"
)
const (
@@ -21,18 +20,28 @@ const (
TLSKeyFile = "key.pem"
)
// Service represents a service for managing files.
// Service represents a service for managing files and directories.
type Service struct {
dataStorePath string
fileStorePath string
}
// NewService initializes a new service.
func NewService(fileStorePath string) (*Service, error) {
// NewService initializes a new service. It creates a data directory and a directory to store files
// inside this directory if they don't exist.
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
service := &Service{
fileStorePath: fileStorePath,
dataStorePath: dataStorePath,
fileStorePath: path.Join(dataStorePath, fileStorePath),
}
err := service.createFolderInStoreIfNotExist(TLSStorePath)
// Checking if a mount directory exists is broken with Go on Windows.
// This will need to be reviewed after the issue has been fixed in Go.
// err := createDirectoryIfNotExist(dataStorePath, 0755)
// if err != nil {
// return nil, err
// }
err := service.createDirectoryInStoreIfNotExist(TLSStorePath)
if err != nil {
return nil, err
}
@@ -44,7 +53,7 @@ func NewService(fileStorePath string) (*Service, error) {
func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error {
ID := strconv.Itoa(int(endpointID))
endpointStorePath := path.Join(TLSStorePath, ID)
err := service.createFolderInStoreIfNotExist(endpointStorePath)
err := service.createDirectoryInStoreIfNotExist(endpointStorePath)
if err != nil {
return err
}
@@ -97,12 +106,20 @@ func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
return nil
}
// createFolderInStoreIfNotExist creates a new folder in the file store if it doesn't exists on the file system.
func (service *Service) createFolderInStoreIfNotExist(name string) error {
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
path := path.Join(service.fileStorePath, name)
return createDirectoryIfNotExist(path, 0700)
}
// createDirectoryIfNotExist creates a directory if it doesn't exists on the file system.
func createDirectoryIfNotExist(path string, mode uint32) error {
_, err := os.Stat(path)
if os.IsNotExist(err) {
os.Mkdir(path, 0600)
err = os.Mkdir(path, os.FileMode(mode))
if err != nil {
return err
}
} else if err != nil {
return err
}
+9
View File
@@ -16,6 +16,7 @@ import (
type AuthHandler struct {
*mux.Router
Logger *log.Logger
authDisabled bool
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
@@ -26,6 +27,9 @@ const (
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
// when the server has been started with the --no-auth flag
ErrAuthDisabled = portainer.Error("Authentication is disabled")
)
// NewAuthHandler returns a new instance of AuthHandler.
@@ -44,6 +48,11 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
return
}
if handler.authDisabled {
Error(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postAuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
+3 -3
View File
@@ -135,7 +135,7 @@ type unixSocketHandler struct {
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := net.Dial("unix", h.path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Error(w, err, http.StatusInternalServerError, nil)
return
}
c := httputil.NewClientConn(conn, nil)
@@ -143,7 +143,7 @@ func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
res, err := c.Do(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Error(w, err, http.StatusInternalServerError, nil)
return
}
defer res.Body.Close()
@@ -154,6 +154,6 @@ func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
if _, err := io.Copy(w, res.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Error(w, err, http.StatusInternalServerError, nil)
}
}
+43 -6
View File
@@ -16,13 +16,20 @@ import (
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
type EndpointHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
FileService portainer.FileService
server *Server
middleWareService *middleWareService
Logger *log.Logger
authorizeEndpointManagement bool
EndpointService portainer.EndpointService
FileService portainer.FileService
server *Server
middleWareService *middleWareService
}
const (
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
// when the server has been started with the --external-endpoints flag
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
)
// NewEndpointHandler returns a new instance of EndpointHandler.
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
h := &EndpointHandler{
@@ -65,6 +72,11 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
// if the active URL parameter is specified, will also define the new endpoint as the active endpoint.
// /endpoints(?active=true|false)
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postEndpointsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
@@ -203,6 +215,11 @@ func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *htt
// handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
vars := mux.Vars(r)
id := vars["id"]
@@ -260,7 +277,13 @@ type putEndpointsRequest struct {
}
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
// DELETE /endpoints/0 deletes the active endpoint
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
vars := mux.Vars(r)
id := vars["id"]
@@ -270,7 +293,14 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
var endpoint *portainer.Endpoint
if id == "0" {
endpoint, err = handler.EndpointService.GetActive()
endpointID = int(endpoint.ID)
} else {
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
}
if err == portainer.ErrEndpointNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
@@ -284,6 +314,13 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if id == "0" {
err = handler.EndpointService.DeleteActive()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
if endpoint.TLS {
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
+36
View File
@@ -0,0 +1,36 @@
package http
import (
"net/http"
"strings"
)
// FileHandler represents an HTTP API handler for managing static files.
type FileHandler struct {
http.Handler
}
func newFileHandler(assetPath string) *FileHandler {
h := &FileHandler{
Handler: http.FileServer(http.Dir(assetPath)),
}
return h
}
func isHTML(acceptContent []string) bool {
for _, accept := range acceptContent {
if strings.Contains(accept, "text/html") {
return true
}
}
return false
}
func (fileHandler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !isHTML(r.Header["Accept"]) {
w.Header().Set("Cache-Control", "max-age=31536000")
} else {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}
fileHandler.Handler.ServeHTTP(w, r)
}
+4 -2
View File
@@ -19,7 +19,7 @@ type Handler struct {
DockerHandler *DockerHandler
WebSocketHandler *WebSocketHandler
UploadHandler *UploadHandler
FileHandler http.Handler
FileHandler *FileHandler
}
const (
@@ -55,7 +55,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Error writes an API error message to the response and logger.
func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) {
// Log error.
logger.Printf("http error: %s (code=%d)", err, code)
if logger != nil {
logger.Printf("http error: %s (code=%d)", err, code)
}
// Write generic error response.
w.WriteHeader(code)
+19 -16
View File
@@ -9,7 +9,8 @@ import (
// Service represents a service to manage HTTP middlewares
type middleWareService struct {
jwtService portainer.JWTService
jwtService portainer.JWTService
authDisabled bool
}
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
@@ -37,24 +38,26 @@ func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handle
// middleWareAuthenticate provides Authentication middleware for handlers
func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var token string
if !service.authDisabled {
var token string
// Get token from the Authorization header
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
// Get token from the Authorization header
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
if token == "" {
Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
return
}
err := service.jwtService.VerifyToken(token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
err := service.jwtService.VerifyToken(token)
if err != nil {
Error(w, err, http.StatusUnauthorized, nil)
return
}
}
next.ServeHTTP(w, r)
+18 -13
View File
@@ -8,17 +8,19 @@ import (
// Server implements the portainer.Server interface
type Server struct {
BindAddress string
AssetsPath string
UserService portainer.UserService
EndpointService portainer.EndpointService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
FileService portainer.FileService
Settings *portainer.Settings
TemplatesURL string
ActiveEndpoint *portainer.Endpoint
Handler *Handler
BindAddress string
AssetsPath string
AuthDisabled bool
EndpointManagement bool
UserService portainer.UserService
EndpointService portainer.EndpointService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
FileService portainer.FileService
Settings *portainer.Settings
TemplatesURL string
ActiveEndpoint *portainer.Endpoint
Handler *Handler
}
func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
@@ -40,13 +42,15 @@ func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
// Start starts the HTTP server
func (server *Server) Start() error {
middleWareService := &middleWareService{
jwtService: server.JWTService,
jwtService: server.JWTService,
authDisabled: server.AuthDisabled,
}
var authHandler = NewAuthHandler()
authHandler.UserService = server.UserService
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
authHandler.authDisabled = server.AuthDisabled
var userHandler = NewUserHandler(middleWareService)
userHandler.UserService = server.UserService
userHandler.CryptoService = server.CryptoService
@@ -58,12 +62,13 @@ func (server *Server) Start() error {
var websocketHandler = NewWebSocketHandler()
// EndpointHandler requires a reference to the server to be able to update the active endpoint.
var endpointHandler = NewEndpointHandler(middleWareService)
endpointHandler.authorizeEndpointManagement = server.EndpointManagement
endpointHandler.EndpointService = server.EndpointService
endpointHandler.FileService = server.FileService
endpointHandler.server = server
var uploadHandler = NewUploadHandler(middleWareService)
uploadHandler.FileService = server.FileService
var fileHandler = http.FileServer(http.Dir(server.AssetsPath))
var fileHandler = newFileHandler(server.AssetsPath)
server.Handler = &Handler{
AuthHandler: authHandler,
+2 -5
View File
@@ -1,7 +1,6 @@
package http
import (
"fmt"
"io/ioutil"
"log"
"net/http"
@@ -40,15 +39,13 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht
resp, err := http.Get(handler.templatesURL)
if err != nil {
log.Print(err)
http.Error(w, fmt.Sprintf("Error making request to %s: %s", handler.templatesURL, err.Error()), http.StatusInternalServerError)
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Print(err)
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
w.Header().Set("Content-Type", "application/json")
+1 -1
View File
@@ -26,7 +26,7 @@ func NewUploadHandler(middleWareService *middleWareService) *UploadHandler {
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.Handle("/upload/tls/{endpointID}/{certificate:(ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostUploadTLS(w, r)
})))
return h
+20 -10
View File
@@ -227,18 +227,28 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
return
}
user := &portainer.User{
Username: "admin",
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
user, err := handler.UserService.User("admin")
if err == portainer.ErrUserNotFound {
user := &portainer.User{
Username: "admin",
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
if user != nil {
Error(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger)
return
}
}
+26 -14
View File
@@ -13,23 +13,28 @@ type (
// CLIFlags represents the available flags on the CLI.
CLIFlags struct {
Addr *string
Assets *string
Data *string
Endpoint *string
Labels *[]Pair
Logo *string
Templates *string
TLSVerify *bool
TLSCacert *string
TLSCert *string
TLSKey *string
Addr *string
Assets *string
Data *string
ExternalEndpoints *string
SyncInterval *string
Endpoint *string
Labels *[]Pair
Logo *string
Templates *string
NoAuth *bool
TLSVerify *bool
TLSCacert *string
TLSCert *string
TLSKey *string
}
// Settings represents Portainer settings.
Settings struct {
HiddenLabels []Pair `json:"hiddenLabels"`
Logo string `json:"logo"`
HiddenLabels []Pair `json:"hiddenLabels"`
Logo string `json:"logo"`
Authentication bool `json:"authentication"`
EndpointManagement bool `json:"endpointManagement"`
}
// User represent a user account.
@@ -94,6 +99,8 @@ type (
DeleteEndpoint(ID EndpointID) error
GetActive() (*Endpoint, error)
SetActive(endpoint *Endpoint) error
DeleteActive() error
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
}
// CryptoService represents a service for encrypting/hashing data.
@@ -114,11 +121,16 @@ type (
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
DeleteTLSFiles(endpointID EndpointID) error
}
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
EndpointWatcher interface {
WatchEndpointFile(endpointFilePath string) error
}
)
const (
// APIVersion is the version number of portainer API.
APIVersion = "1.11.0"
APIVersion = "1.11.4"
)
const (
+127 -171
View File
@@ -1,5 +1,8 @@
angular.module('portainer.filters', []);
angular.module('portainer.rest', ['ngResource']);
angular.module('portainer.services', []);
angular.module('portainer.helpers', []);
angular.module('portainer', [
'portainer.templates',
'ui.bootstrap',
'ui.router',
'ui.select',
@@ -9,9 +12,11 @@ angular.module('portainer', [
'angularUtils.directives.dirPagination',
'LocalStorageModule',
'angular-jwt',
'portainer.services',
'portainer.helpers',
'portainer.templates',
'portainer.filters',
'portainer.rest',
'portainer.helpers',
'portainer.services',
'auth',
'dashboard',
'container',
@@ -19,28 +24,29 @@ angular.module('portainer', [
'containerLogs',
'containers',
'createContainer',
'createNetwork',
'createService',
'createVolume',
'docker',
'endpoint',
'endpointInit',
'endpoints',
'events',
'images',
'image',
'images',
'main',
'network',
'networks',
'node',
'service',
'services',
'settings',
'sidebar',
'createService',
'stats',
'swarm',
'network',
'networks',
'createNetwork',
'task',
'templates',
'volumes',
'createVolume'])
'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider) {
'use strict';
@@ -49,8 +55,8 @@ angular.module('portainer', [
.setPrefix('portainer');
jwtOptionsProvider.config({
tokenGetter: ['localStorageService', function(localStorageService) {
return localStorageService.get('JWT');
tokenGetter: ['LocalStorage', function(LocalStorage) {
return LocalStorage.getJWT();
}],
unauthenticatedRedirector: ['$state', function($state) {
$state.go('auth', {error: 'Your session has expired'});
@@ -61,124 +67,121 @@ angular.module('portainer', [
$urlRouterProvider.otherwise('/auth');
$stateProvider
.state('root', {
abstract: true,
resolve: {
requiresLogin: ['StateManager', function (StateManager) {
var applicationState = StateManager.getState();
return applicationState.application.authentication;
}]
}
})
.state('auth', {
url: '/auth',
parent: 'root',
url: '/auth',
params: {
logout: false,
error: ''
},
views: {
"content": {
"content@": {
templateUrl: 'app/components/auth/auth.html',
controller: 'AuthenticationController'
}
},
data: {
requiresLogin: false
}
})
.state('containers', {
parent: 'root',
url: '/containers/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/containers/containers.html',
controller: 'ContainersController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('container', {
url: "^/containers/:id",
views: {
"content": {
"content@": {
templateUrl: 'app/components/container/container.html',
controller: 'ContainerController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('stats', {
url: "^/containers/:id/stats",
views: {
"content": {
"content@": {
templateUrl: 'app/components/stats/stats.html',
controller: 'StatsController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('logs', {
url: "^/containers/:id/logs",
views: {
"content": {
"content@": {
templateUrl: 'app/components/containerLogs/containerlogs.html',
controller: 'ContainerLogsController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('console', {
url: "^/containers/:id/console",
views: {
"content": {
"content@": {
templateUrl: 'app/components/containerConsole/containerConsole.html',
controller: 'ContainerConsoleController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('dashboard', {
parent: 'root',
url: '/dashboard',
views: {
"content": {
"content@": {
templateUrl: 'app/components/dashboard/dashboard.html',
controller: 'DashboardController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions', {
abstract: true,
url: "/actions",
views: {
"content": {
template: '<div ui-view="content"></div>'
"content@": {
template: '<div ui-view="content@"></div>'
},
"sidebar": {
template: '<div ui-view="sidebar"></div>'
"sidebar@": {
template: '<div ui-view="sidebar@"></div>'
}
}
})
@@ -186,328 +189,281 @@ angular.module('portainer', [
abstract: true,
url: "/create",
views: {
"content": {
template: '<div ui-view="content"></div>'
"content@": {
template: '<div ui-view="content@"></div>'
},
"sidebar": {
template: '<div ui-view="sidebar"></div>'
"sidebar@": {
template: '<div ui-view="sidebar@"></div>'
}
}
})
.state('actions.create.container', {
url: "/container",
views: {
"content": {
"content@": {
templateUrl: 'app/components/createContainer/createcontainer.html',
controller: 'CreateContainerController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.network', {
url: "/network",
views: {
"content": {
"content@": {
templateUrl: 'app/components/createNetwork/createnetwork.html',
controller: 'CreateNetworkController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.service', {
url: "/service",
views: {
"content": {
"content@": {
templateUrl: 'app/components/createService/createservice.html',
controller: 'CreateServiceController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('actions.create.volume', {
url: "/volume",
views: {
"content": {
"content@": {
templateUrl: 'app/components/createVolume/createvolume.html',
controller: 'CreateVolumeController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('docker', {
url: '/docker/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/docker/docker.html',
controller: 'DockerController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('endpoints', {
url: '/endpoints/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/endpoints/endpoints.html',
controller: 'EndpointsController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('endpoint', {
url: '^/endpoints/:id',
views: {
"content": {
"content@": {
templateUrl: 'app/components/endpoint/endpoint.html',
controller: 'EndpointController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('endpointInit', {
url: '/init/endpoint',
views: {
"content": {
"content@": {
templateUrl: 'app/components/endpointInit/endpointInit.html',
controller: 'EndpointInitController'
}
},
data: {
requiresLogin: true
}
})
.state('events', {
url: '/events/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/events/events.html',
controller: 'EventsController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('images', {
url: '/images/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/images/images.html',
controller: 'ImagesController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('image', {
url: '^/images/:id/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/image/image.html',
controller: 'ImageController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('networks', {
url: '/networks/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/networks/networks.html',
controller: 'NetworksController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('network', {
url: '^/networks/:id/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('node', {
url: '^/nodes/:id/',
views: {
"content@": {
templateUrl: 'app/components/node/node.html',
controller: 'NodeController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('services', {
url: '/services/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/services/services.html',
controller: 'ServicesController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('service', {
url: '^/service/:id/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/service/service.html',
controller: 'ServiceController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('settings', {
url: '/settings/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/settings/settings.html',
controller: 'SettingsController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('task', {
url: '^/task/:id',
views: {
"content": {
"content@": {
templateUrl: 'app/components/task/task.html',
controller: 'TaskController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('templates', {
url: '/templates/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/templates/templates.html',
controller: 'TemplatesController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('volumes', {
url: '/volumes/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/volumes/volumes.html',
controller: 'VolumesController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
})
.state('swarm', {
url: '/swarm/',
views: {
"content": {
"content@": {
templateUrl: 'app/components/swarm/swarm.html',
controller: 'SwarmController'
},
"sidebar": {
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
},
data: {
requiresLogin: true
}
});
@@ -528,30 +484,30 @@ angular.module('portainer', [
};
});
}])
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'EndpointMode', function ($rootScope, $state, Authentication, authManager, EndpointMode) {
authManager.checkAuthOnRefresh();
authManager.redirectWhenUnauthenticated();
Authentication.init();
$rootScope.$state = $state;
$rootScope.$on('tokenHasExpired', function($state) {
$state.go('auth', {error: 'Your session has expired'});
});
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
if (toState.name !== 'endpointInit' && (fromState.name === 'auth' || fromState.name === '' || fromState.name === 'endpointInit') && Authentication.isAuthenticated()) {
EndpointMode.determineEndpointMode();
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'Messages', function ($rootScope, $state, Authentication, authManager, StateManager, Messages) {
StateManager.initialize().then(function success(state) {
if (state.application.authentication) {
authManager.checkAuthOnRefresh();
authManager.redirectWhenUnauthenticated();
Authentication.init();
$rootScope.$on('tokenHasExpired', function($state) {
$state.go('auth', {error: 'Your session has expired'});
});
}
}, function error(err) {
Messages.error("Failure", err, 'Unable to retrieve application settings');
});
$rootScope.$state = $state;
}])
// This is your docker url that the api will use to make requests
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
.constant('DOCKER_ENDPOINT', '/api/docker')
.constant('CONFIG_ENDPOINT', '/api/settings')
.constant('AUTH_ENDPOINT', '/api/auth')
.constant('USERS_ENDPOINT', '/api/users')
.constant('ENDPOINTS_ENDPOINT', '/api/endpoints')
.constant('TEMPLATES_ENDPOINT', '/api/templates')
.constant('DOCKER_ENDPOINT', 'api/docker')
.constant('CONFIG_ENDPOINT', 'api/settings')
.constant('AUTH_ENDPOINT', 'api/auth')
.constant('USERS_ENDPOINT', 'api/users')
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
.constant('TEMPLATES_ENDPOINT', 'api/templates')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('UI_VERSION', 'v1.11.0');
.constant('UI_VERSION', 'v1.11.4');
+25 -3
View File
@@ -1,6 +1,6 @@
angular.module('auth', [])
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'Messages',
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, Messages) {
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'Messages',
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, Messages) {
$scope.authData = {
username: 'admin',
@@ -13,6 +13,23 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
error: false
};
if (!$scope.applicationState.application.authentication) {
EndpointService.getActive().then(function success(data) {
StateManager.updateEndpointState(true)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
});
}, function error(err) {
if (err.status === 404) {
$state.go('endpointInit');
} else {
Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
}
});
}
if ($stateParams.logout) {
Authentication.logout();
}
@@ -60,7 +77,12 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
var password = $sanitize($scope.authData.password);
Authentication.login(username, password).then(function success() {
EndpointService.getActive().then(function success(data) {
$state.go('dashboard');
StateManager.updateEndpointState(true)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
});
}, function error(err) {
if (err.status === 404) {
$state.go('endpointInit');
+30 -2
View File
@@ -174,6 +174,23 @@
</table>
</td>
</tr>
<tr ng-if="container.HostConfig.RestartPolicy.Name !== 'no'">
<td>Restart policies</td>
<td>
<table class="table table-bordered table-condensed">
<tr>
<td class="col-md-3">Name</td>
<td>{{ container.HostConfig.RestartPolicy.Name }}</td>
</tr>
<tr>
<td class="col-md-3">MaximumRetryCount</td>
<td>
{{ container.HostConfig.RestartPolicy.MaximumRetryCount }}
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
@@ -208,7 +225,18 @@
<div class="row" ng-if="!(container.NetworkSettings.Networks | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Connected networks"></rd-widget-header>
<rd-widget-header icon="fa-sitemap" title="Connected networks">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
@@ -219,7 +247,7 @@
<th>Actions</th>
</thead>
<tbody>
<tr dir-paginate="(key, value) in container.NetworkSettings.Networks | itemsPerPage: pagination_count">
<tr dir-paginate="(key, value) in container.NetworkSettings.Networks | itemsPerPage: state.pagination_count">
<td><a ui-sref="network({id: value.NetworkID})">{{ key }}</a></td>
<td>{{ value.IPAddress || '-' }}</td>
<td>{{ value.Gateway || '-' }}</td>
@@ -1,13 +1,18 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Settings',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Settings) {
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Pagination',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Pagination) {
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.config = {
Image: '',
Registry: ''
};
$scope.pagination_count = Settings.pagination_count;
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('container_networks');
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('container_networks', $scope.state.pagination_count);
};
var update = function () {
$('#loadingViewSpinner').show();
@@ -75,8 +80,8 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
$scope.commit = function () {
$('#createImageSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
$('#createImageSpinner').hide();
+18 -8
View File
@@ -3,6 +3,7 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="containers" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadContainersSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Containers</rd-header-content>
</rd-header>
@@ -11,7 +12,14 @@
<rd-widget>
<rd-widget-header icon="fa-server" title="Containers">
<div class="pull-right">
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
@@ -37,7 +45,9 @@
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="containers" ng-click="order('Status')">
State
@@ -66,7 +76,7 @@
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<th ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<a ui-sref="containers" ng-click="order('Host')">
Host IP
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
@@ -75,7 +85,7 @@
</th>
<th>
<a ui-sref="containers" ng-click="order('Ports')">
Exposed Ports
Published Ports
<span ng-show="sortType == 'Ports' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
@@ -83,14 +93,14 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span></td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="endpointMode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
<td>
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{p.public}}:{{ p.private }}
@@ -1,18 +1,22 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config',
function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config) {
.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination',
function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
$scope.state.displayAll = Settings.displayAll;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = false;
$scope.state.selectedItemCount = 0;
$scope.pagination_count = Settings.pagination_count;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('containers', $scope.state.pagination_count);
};
var update = function (data) {
$('#loadContainersSpinner').show();
$scope.state.selectedItemCount = 0;
@@ -28,7 +32,7 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
if (model.IP) {
$scope.state.displayIP = true;
}
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
}
return model;
@@ -77,6 +81,19 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
complete();
});
}
else if (action === Container.pause) {
action({id: c.Id}, function (d) {
if (d.message) {
Messages.send("Container is already paused", c.Id);
} else {
Messages.send("Container " + msg, c.Id);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to pause container');
complete();
});
}
else {
action({id: c.Id}, function (d) {
Messages.send("Container " + msg, c.Id);
@@ -94,6 +111,15 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
}
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredContainers, function (container) {
if (container.Checked !== allSelected) {
container.Checked = allSelected;
$scope.selectItem(container);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
@@ -152,7 +178,7 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
Config.$promise.then(function (c) {
$scope.containersToHideLabels = c.hiddenLabels;
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
Info.get({}, function (d) {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: Settings.displayAll ? 1 : 0});
@@ -72,7 +72,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
Network.query({}, function (d) {
var networks = d;
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
networks = d.filter(function (network) {
if (network.Scope === 'global') {
return network;
@@ -141,7 +141,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
}
function prepareImageConfig(config) {
var image = _.toLower(config.Image);
var image = config.Image;
var registry = $scope.formValues.Registry;
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
@@ -220,7 +220,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
var containerName = container;
if (container && typeof container === 'object') {
containerName = $filter('trimcontainername')(container.Names[0]);
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
containerName = $filter('swarmcontainername')(container);
}
}
@@ -95,16 +95,6 @@
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<!-- labels -->
<div class="form-group">
<label for="container_labels" class="col-sm-1 control-label text-left">Labels</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
</div>
<!-- !labels-->
</form>
</rd-widget-body>
</rd-widget>
@@ -269,7 +259,7 @@
<!-- tab-network -->
<div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group" ng-if="globalNetworkCount === 0 && endpointMode.provider !== 'DOCKER_SWARM_MODE'">
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
@@ -289,10 +279,10 @@
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
<div class="col-sm-9">
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="endpointMode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
<select ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option>
</select>
</div>
+3 -3
View File
@@ -6,7 +6,7 @@
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider !== 'DOCKER_SWARM'">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
<rd-widget-body classes="no-padding">
@@ -33,7 +33,7 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
<rd-widget-body classes="no-padding">
@@ -60,7 +60,7 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
<rd-widget-body classes="no-padding">
@@ -1,6 +1,11 @@
angular.module('endpoint', [])
.controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Messages',
function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
if (!$scope.applicationState.application.endpointManagement) {
$state.go('endpoints');
}
$scope.state = {
error: '',
uploadInProgress: false
@@ -19,7 +24,8 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
var TLSCACert = $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null;
var TLSCert = $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null;
var TLSKey = $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null;
EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey).then(function success(data) {
var type = $scope.endpointType;
EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey, type).then(function success(data) {
Messages.send("Endpoint updated", $scope.endpoint.Name);
$state.go('endpoints');
}, function error(err) {
+13 -7
View File
@@ -21,19 +21,19 @@
<!-- endpoin-type radio -->
<div class="form-group">
<div class="radio">
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType">Manage the Docker instance where Portainer is running</label>
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType" ng-click="cleanError()">Manage the Docker instance where Portainer is running</label>
</div>
<div class="radio">
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType">Manage a remote Docker instance</label>
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType" ng-click="cleanError()">Manage a remote Docker instance</label>
</div>
</div>
<!-- endpoint-type radio -->
<!-- local-endpoint -->
<div ng-if="formValues.endpointType === 'local'" style="margin-top: 25px;">
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: ensure that the Docker socket is bind mounted in the Portainer container at <code>/var/run/docker.sock</code></span>
</div>
<i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 5px;"></i>
<span class="small text-primary">This feature is not available with Docker <b>on</b> Windows yet.</span>
<div class="small text-primary">On Linux / Mac, ensure that you have started Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code></div>
</div>
<!-- connect button -->
<div class="form-group" style="margin-top: 10px;">
@@ -41,7 +41,10 @@
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</p>
<button type="submit" class="btn btn-primary pull-right" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
<span class="pull-right">
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
<button type="submit" class="btn btn-primary" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
</span>
</div>
</div>
<!-- !connect button -->
@@ -122,7 +125,10 @@
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</p>
<button type="submit" class="btn btn-primary pull-right" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
<span class="pull-right">
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
<button type="submit" class="btn btn-primary" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
</span>
</div>
</div>
<!-- !connect button -->
@@ -1,6 +1,6 @@
angular.module('endpointInit', [])
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'Messages',
function ($scope, $state, EndpointService, Messages) {
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Messages',
function ($scope, $state, EndpointService, StateManager, Messages) {
$scope.state = {
error: '',
uploadInProgress: false
@@ -15,27 +15,39 @@ function ($scope, $state, EndpointService, Messages) {
TLSKey: null
};
EndpointService.getActive().then(function success(data) {
if (!_.isEmpty($scope.applicationState.endpoint)) {
$state.go('dashboard');
}, function error(err) {
if (err.status !== 404) {
Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
}
});
}
$scope.cleanError = function() {
$scope.state.error = '';
};
$scope.createLocalEndpoint = function() {
$('#initEndpointSpinner').show();
$scope.state.error = '';
var name = "local";
var URL = "unix:///var/run/docker.sock";
var TLS = false;
EndpointService.createLocalEndpoint(name, URL, TLS, true).then(function success(data) {
$state.go('dashboard');
StateManager.updateEndpointState(false)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
EndpointService.deleteEndpoint(0)
.then(function success() {
$('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to connect to the Docker endpoint';
});
});
}, function error(err) {
$('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to create endpoint';
});
};
$scope.createRemoteEndpoint = function() {
$('#initEndpointSpinner').show();
$scope.state.error = '';
var name = $scope.formValues.Name;
var URL = $scope.formValues.URL;
@@ -43,9 +55,20 @@ function ($scope, $state, EndpointService, Messages) {
var TLSCAFile = $scope.formValues.TLSCACert;
var TLSCertFile = $scope.formValues.TLSCert;
var TLSKeyFile = $scope.formValues.TLSKey;
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true).then(function success(data) {
$state.go('dashboard');
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true)
.then(function success(data) {
StateManager.updateEndpointState(false)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
EndpointService.deleteEndpoint(0)
.then(function success() {
$('#initEndpointSpinner').hide();
$scope.state.error = 'Unable to connect to the Docker endpoint';
});
});
}, function error(err) {
$('#initEndpointSpinner').hide();
$scope.state.uploadInProgress = false;
$scope.state.error = err.msg;
}, function update(evt) {
+28 -8
View File
@@ -3,11 +3,24 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="endpoints" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadEndpointsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Endpoint management</rd-header-content>
</rd-header>
<div class="row">
<div class="row" ng-if="!applicationState.application.endpointManagement">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title="Endpoint management is not available">
</rd-widget-header>
<rd-widget-body>
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag. Endpoint management via the UI is disabled.</span>
</rd-wigdet-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="applicationState.application.endpointManagement">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plus" title="Add a new endpoint">
@@ -101,11 +114,18 @@
<rd-widget>
<rd-widget-header icon="fa-plug" title="Endpoints">
<div class="pull-right">
<i id="loadEndpointsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<div class="pull-left" ng-if="applicationState.application.endpointManagement">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<div class="pull-right">
@@ -117,7 +137,7 @@
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th ng-if="applicationState.application.endpointManagement"></th>
<th>
<a ui-sref="endpoints" ng-click="order('Name')">
Name
@@ -139,16 +159,16 @@
<span ng-show="sortType == 'TLS' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th></th>
<th ng-if="applicationState.application.endpointManagement"></th>
</tr>
</thead>
<tbody>
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<td><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td ng-if="applicationState.application.endpointManagement"><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
<td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td>
<td>{{ endpoint.URL | stripprotocol }}</td>
<td><i class="fa fa-shield" aria-hidden="true" ng-if="endpoint.TLS"></i></td>
<td>
<td ng-if="applicationState.application.endpointManagement">
<span ng-if="endpoint.Id !== activeEndpoint.Id">
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
</span>
@@ -1,14 +1,14 @@
angular.module('endpoints', [])
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Settings', 'Messages',
function ($scope, $state, EndpointService, Settings, Messages) {
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Messages', 'Pagination',
function ($scope, $state, EndpointService, Messages, Pagination) {
$scope.state = {
error: '',
uploadInProgress: false,
selectedItemCount: 0
selectedItemCount: 0,
pagination_count: Pagination.getPaginationCount('endpoints')
};
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.pagination_count = Settings.pagination_count;
$scope.formValues = {
Name: '',
@@ -24,6 +24,10 @@ function ($scope, $state, EndpointService, Settings, Messages) {
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('endpoints', $scope.state.pagination_count);
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
+10 -2
View File
@@ -3,6 +3,7 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="events" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadEventsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Events</rd-header-content>
</rd-header>
@@ -12,7 +13,14 @@
<rd-widget>
<rd-widget-header icon="fa-history" title="Events">
<div class="pull-right">
<i id="loadEventsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
@@ -49,7 +57,7 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count)">
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td>{{ event.Time|getisodatefromtimestamp }}</td>
<td>{{ event.Type }}</td>
<td>{{ event.Details }}</td>
+7 -3
View File
@@ -1,16 +1,20 @@
angular.module('events', [])
.controller('EventsController', ['$scope', 'Settings', 'Messages', 'Events',
function ($scope, Settings, Messages, Events) {
.controller('EventsController', ['$scope', 'Messages', 'Events', 'Pagination',
function ($scope, Messages, Events, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('events');
$scope.sortType = 'Time';
$scope.sortReverse = true;
$scope.pagination_count = Settings.pagination_count;
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('events', $scope.state.pagination_count);
};
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
+2 -2
View File
@@ -21,8 +21,8 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
Messages.send('Image successfully tagged');
+13 -3
View File
@@ -3,6 +3,7 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="images" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadImagesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Images</rd-header-content>
</rd-header>
@@ -50,7 +51,14 @@
<rd-widget>
<rd-widget-header icon="fa-clone" title="Images">
<div class="pull-right">
<i id="loadImagesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
@@ -66,7 +74,9 @@
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="images" ng-click="order('Id')">
Id
@@ -98,7 +108,7 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
<td>
+18 -5
View File
@@ -1,11 +1,11 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Settings',
function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) {
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination',
function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('images');
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
$scope.pagination_count = Settings.pagination_count;
$scope.config = {
Image: '',
@@ -17,6 +17,19 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) {
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('images', $scope.state.pagination_count);
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredImages, function (image) {
if (image.Checked !== allSelected) {
image.Checked = allSelected;
$scope.selectItem(image);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
@@ -27,8 +40,8 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) {
$scope.pullImage = function() {
$('#pullImageSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
+4 -2
View File
@@ -1,6 +1,6 @@
angular.module('main', [])
.controller('MainController', ['$scope', '$cookieStore',
function ($scope, $cookieStore) {
.controller('MainController', ['$scope', '$cookieStore', 'StateManager',
function ($scope, $cookieStore, StateManager) {
/**
* Sidebar Toggle & Cookie Control
@@ -10,6 +10,8 @@ function ($scope, $cookieStore) {
return window.innerWidth;
};
$scope.applicationState = StateManager.getState();
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
if (newValue >= mobileView) {
if (angular.isDefined($cookieStore.get('toggle'))) {
+51 -22
View File
@@ -1,6 +1,6 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', '$state', '$stateParams', 'Network', 'Container', 'ContainerHelper', 'Messages',
function ($scope, $state, $stateParams, Network, Container, ContainerHelper, Messages) {
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Messages',
function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Messages) {
$scope.removeNetwork = function removeNetwork(networkId) {
$('#loadingViewSpinner').show();
@@ -38,34 +38,63 @@ function ($scope, $state, $stateParams, Network, Container, ContainerHelper, Mes
function getNetwork() {
$('#loadingViewSpinner').show();
Network.get({id: $stateParams.id}, function (d) {
$scope.network = d;
getContainersInNetwork(d);
Network.get({id: $stateParams.id}, function success(data) {
$scope.network = data;
getContainersInNetwork(data);
}, function error(err) {
$('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve network info");
Messages.error("Failure", err, "Unable to retrieve network info");
});
}
function filterContainersInNetwork(network, containers) {
if ($scope.containersToHideLabels) {
containers = ContainerHelper.hideContainers(containers, $scope.containersToHideLabels);
}
var containersInNetwork = [];
containers.forEach(function(container) {
var containerInNetwork = network.Containers[container.Id];
containerInNetwork.Id = container.Id;
// Name is not available in Docker 1.9
if (!containerInNetwork.Name) {
containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]);
}
containersInNetwork.push(containerInNetwork);
});
$scope.containersInNetwork = containersInNetwork;
}
function getContainersInNetwork(network) {
if (network.Containers) {
Container.query({
filters: {network: [$stateParams.id]}
}, function (containersInNetworkResult) {
if ($scope.containersToHideLabels) {
containersInNetworkResult = ContainerHelper.hideContainers(containersInNetworkResult, $scope.containersToHideLabels);
}
var containersInNetwork = [];
containersInNetworkResult.forEach(function(container) {
var containerInNetwork = network.Containers[container.Id];
containerInNetwork.Id = container.Id;
containersInNetwork.push(containerInNetwork);
if ($scope.applicationState.endpoint.apiVersion < 1.24) {
Container.query({}, function success(data) {
var containersInNetwork = data.filter(function filter(container) {
if (container.HostConfig.NetworkMode === network.Name) {
return container;
}
});
filterContainersInNetwork(network, containersInNetwork);
$('#loadingViewSpinner').hide();
}, function error(err) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve containers in network");
});
$scope.containersInNetwork = containersInNetwork;
});
} else {
Container.query({
filters: {network: [$stateParams.id]}
}, function success(data) {
filterContainersInNetwork(network, data);
$('#loadingViewSpinner').hide();
}, function error(err) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", err, "Unable to retrieve containers in network");
});
}
}
}
getNetwork();
Config.$promise.then(function (c) {
$scope.containersToHideLabels = c.hiddenLabels;
getNetwork();
});
}]);
+15 -5
View File
@@ -3,6 +3,7 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadNetworksSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Networks</rd-header-content>
</rd-header>
@@ -23,12 +24,12 @@
</div>
<!-- !name-input -->
<!-- tag-note -->
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_SWARM' || endpointMode.provider === 'DOCKER_SWARM_MODE'">
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
</div>
</div>
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
</div>
@@ -52,7 +53,14 @@
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Networks">
<div class="pull-right">
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
@@ -68,7 +76,9 @@
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="networks" ng-click="order('Name')">
Name
@@ -121,7 +131,7 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
<td>{{ network.Id }}</td>
+17 -4
View File
@@ -1,19 +1,23 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Settings',
function ($scope, $state, Network, Config, Messages, Settings) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Pagination',
function ($scope, $state, Network, Config, Messages, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.pagination_count = Settings.pagination_count;
$scope.config = {
Name: ''
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('networks', $scope.state.pagination_count);
};
function prepareNetworkConfiguration() {
var config = angular.copy($scope.config);
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
config.Driver = 'overlay';
// Force IPAM Driver to 'default', should not be required.
// See: https://github.com/docker/docker/issues/25735
@@ -47,6 +51,15 @@ function ($scope, $state, Network, Config, Messages, Settings) {
$scope.sortType = sortType;
};
$scope.selectItems = function(allSelected) {
angular.forEach($scope.state.filteredNetworks, function (network) {
if (network.Checked !== allSelected) {
network.Checked = allSelected;
$scope.selectItem(network);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
+273
View File
@@ -0,0 +1,273 @@
<rd-header>
<rd-header-title title="Node details">
<a data-toggle="tooltip" title="Refresh" ui-sref="node({id: node.Id})" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="swarm">Swarm nodes</a> > <a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a>
</rd-header-content>
</rd-header>
<div class="row" ng-if="!node">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div ng-if="loading">
<i class="fa fa-cog fa-spin"></i> Loading...
</div>
<rd-widget ng-if="!loading">
<rd-widget-header icon="fa-object-group" title="Node does not exist"></rd-widget-header>
<rd-widget-body>
<p>It looks like the node you wish to inspect does not exist.</p>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Node specification"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
<input type="text" class="input-sm" ng-model="node.Name" placeholder="e.g. my-manager" ng-change="updateNodeAttribute(node, 'Name')">
</td>
</tr>
<tr>
<td>Host name</td>
<td>{{ node.Hostname }}</td>
</tr>
<tr>
<td>Role</td>
<td>{{ node.Role }}</td>
</tr>
<tr>
<td>Availability</td>
<td>
<div class="input-group input-group-sm">
<select name="nodeAvailability" class="selectpicker form-control" ng-model="node.Availability" ng-change="updateNodeAttribute(node, 'Availability')">
<option value="active">Active</option>
<option value="pause">Pause</option>
<option value="drain">Drain</option>
</select>
</div>
</td>
</tr>
<tr>
<td>Status</td>
<td><span class="label label-{{ node.Status|nodestatusbadge }}">{{ node.Status }}</span></td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<p class="small text-muted">
View the Docker Swarm mode Node documentation <a href="https://docs.docker.com/engine/swarm/manage-nodes/" target="self">here</a>.
</p>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(node, ['Name', 'Availability'])" ng-click="updateNode(node)">Apply changes</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node && node.Role === 'manager'">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Manager status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Leader</td>
<td>
<span ng-if="node.Leader"><i class="fa fa-check green-icon" aria-hidden="true"></i> Yes</span>
<span ng-if="!node.Leader"><i class="fa fa-times red-icon" aria-hidden="true"></i> No</span>
</td>
</tr>
<tr>
<td>Reachability</td>
<td>{{ node.Reachability }}</td>
</tr>
<tr>
<td>Manager address</td>
<td>{{ node.ManagerAddr }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Node description"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>CPU</td>
<td>{{ node.CPUs / 1000000000 }}</td>
</tr>
<tr>
<td>Memory</td>
<td>{{ node.Memory|humansize: 2 }}</td>
</tr>
<tr>
<td>Platform</td>
<td>{{ node.PlatformOS }} {{ node.PlatformArchitecture }} </td>
</tr>
<tr>
<td>Docker Engine version</td>
<td>{{ node.EngineVersion }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Node labels">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="addLabel(node)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="!node.Labels || node.Labels.length === 0">
<p>There are no labels for this node.</p>
</rd-widget-body>
<rd-widget-body classes="no-padding" ng-if="node.Labels && node.Labels.length > 0">
<table class="table">
<thead>
<tr>
<th>Label</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="label in node.Labels">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(node, label)">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(node, label)">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel(node, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(node, ['Labels'])" ng-click="updateNode(node)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>
</div>
<div class="row" ng-if="node && tasks.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>
<a ui-sref="node" ng-click="order('Status')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="node" ng-click="order('Slot')">
Slot
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="node" ng-click="order('Image')">
Image
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="node" ng-click="order('Updated')">
Last update
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
<td>{{ task.Slot }}</td>
<td>{{ task.Image }}</td>
<td>{{ task.Updated|getisodate }}</td>
</tr>
</tbody>
</table>
<div ng-if="tasks" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
+112
View File
@@ -0,0 +1,112 @@
angular.module('node', [])
.controller('NodeController', ['$scope', '$state', '$stateParams', 'LabelHelper', 'Node', 'NodeHelper', 'Task', 'Pagination', 'Messages',
function ($scope, $state, $stateParams, LabelHelper, Node, NodeHelper, Task, Pagination, Messages) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('node_tasks');
$scope.loading = true;
$scope.tasks = [];
$scope.displayNode = false;
$scope.sortType = 'Status';
$scope.sortReverse = false;
var originalNode = {};
var editedKeys = [];
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('node_tasks', $scope.state.pagination_count);
};
$scope.updateNodeAttribute = function updateNodeAttribute(node, key) {
editedKeys.push(key);
};
$scope.addLabel = function addLabel(node) {
node.Labels.push({ key: '', value: '', originalValue: '', originalKey: '' });
$scope.updateNodeAttribute(node, 'Labels');
};
$scope.removeLabel = function removeLabel(node, index) {
var removedElement = node.Labels.splice(index, 1);
if (removedElement !== null) {
$scope.updateNodeAttribute(node, 'Labels');
}
};
$scope.updateLabel = function updateLabel(node, label) {
if (label.value !== label.originalValue || label.key !== label.originalKey) {
$scope.updateNodeAttribute(node, 'Labels');
}
};
$scope.hasChanges = function(node, elements) {
if (!elements) {
elements = Object.keys(originalNode);
}
var hasChanges = false;
elements.forEach(function(key) {
hasChanges = hasChanges || ((editedKeys.indexOf(key) >= 0) && node[key] !== originalNode[key]);
});
return hasChanges;
};
$scope.cancelChanges = function(node) {
editedKeys.forEach(function(key) {
node[key] = originalNode[key];
});
editedKeys = [];
};
$scope.updateNode = function updateNode(node) {
var config = NodeHelper.nodeToConfig(node.Model);
config.Name = node.Name;
config.Availability = node.Availability;
config.Role = node.Role;
config.Labels = LabelHelper.fromKeyValueToLabelHash(node.Labels);
Node.update({ id: node.Id, version: node.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
Messages.send("Node successfully updated", "Node updated");
$state.go('node', {id: node.Id}, {reload: true});
}, function (e) {
$('#loadServicesSpinner').hide();
Messages.error("Failure", e, "Failed to update node");
});
};
function loadNodeAndTasks() {
$scope.loading = true;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
Node.get({ id: $stateParams.id}, function(d) {
if (d.message) {
Messages.error("Failure", e, "Unable to inspect the node");
} else {
var node = new NodeViewModel(d);
originalNode = angular.copy(node);
$scope.node = node;
getTasks(d);
}
$scope.loading = false;
});
} else {
$scope.loading = false;
}
}
function getTasks(node) {
if (node) {
Task.query({filters: {node: [node.ID]}}, function (tasks) {
$scope.tasks = tasks.map(function (task) {
return new TaskViewModel(task, [node]);
});
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve tasks associated to the node");
});
}
}
loadNodeAndTasks();
}]);
+21 -13
View File
@@ -200,17 +200,14 @@
<td>Update Failure Action</td>
<td>
<div class="form-group">
<div class="col-sm-3">
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="continue" ng-change="changeUpdateFailureAction(service)">
Continue
</label>
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="pause" ng-change="changeUpdateFailureAction(service)">
Pause
</label>
</div>
<div class="col-sm-8"></div>
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="continue" ng-change="changeUpdateFailureAction(service)">
Continue
</label>
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="pause" ng-change="changeUpdateFailureAction(service)">
Pause
</label>
</div>
</td>
</tr>
@@ -230,7 +227,18 @@
<div class="row" ng-if="tasks.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks"></rd-widget-header>
<rd-widget-header icon="fa-tasks" title="Associated tasks">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
@@ -267,7 +275,7 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
<td>{{ task.Slot }}</td>
+10 -4
View File
@@ -1,13 +1,14 @@
angular.module('service', [])
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Settings',
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Settings) {
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination',
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
$scope.service = {};
$scope.tasks = [];
$scope.displayNode = false;
$scope.sortType = 'Status';
$scope.sortReverse = false;
$scope.pagination_count = Settings.pagination_count;
var previousServiceValues = {};
@@ -16,6 +17,10 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('service_tasks', $scope.state.pagination_count);
};
$scope.renameService = function renameService(service) {
updateServiceAttribute(service, 'Name', service.newServiceName || service.name);
service.EditName = false;
@@ -25,7 +30,8 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
service.EditImage = false;
};
$scope.scaleService = function scaleService(service) {
updateServiceAttribute(service, 'Replicas', service.newServiceReplicas || service.Replicas);
var replicas = service.newServiceReplicas === null || isNaN(service.newServiceReplicas) ? service.Replicas : service.newServiceReplicas;
updateServiceAttribute(service, 'Replicas', replicas);
service.EditReplicas = false;
};
+10 -2
View File
@@ -3,6 +3,7 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="services" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadServicesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Services</rd-header-content>
</rd-header>
@@ -12,7 +13,14 @@
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Services">
<div class="pull-right">
<i id="loadServicesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12">
@@ -52,7 +60,7 @@
</th>
</thead>
<tbody>
<tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="service.Checked" ng-change="selectItem(service)"/></td>
<td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td>
<td>{{ service.Image }}</td>
+20 -16
View File
@@ -1,11 +1,28 @@
angular.module('services', [])
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Settings',
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settings) {
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination',
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('services');
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.pagination_count = Settings.pagination_count;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('services', $scope.state.pagination_count);
};
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.scaleService = function scaleService(service) {
$('#loadServicesSpinner').show();
@@ -23,19 +40,6 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settin
});
};
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function () {
$('#loadServicesSpinner').show();
var counter = 0;
+17 -17
View File
@@ -17,41 +17,41 @@
</li>
<li class="sidebar-title"><span>Endpoint actions</span></li>
<li class="sidebar-list">
<a ui-sref="dashboard">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
<a ui-sref="dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
</li>
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
<a ui-sref="services">Services <span class="menu-icon fa fa-list-alt"></span></a>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
<a ui-sref="containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="images">Images <span class="menu-icon fa fa-clone"></span></a>
<a ui-sref="images" ui-sref-active="active">Images <span class="menu-icon fa fa-clone"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="networks">Networks <span class="menu-icon fa fa-sitemap"></span></a>
<a ui-sref="networks" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a>
<a ui-sref="volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes"></span></a>
</li>
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a>
</li>
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_SWARM' || (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER')">
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')">
<a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a>
</li>
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="docker" ui-sref-active="active">Docker <span class="menu-icon fa fa-th"></span></a>
</li>
<li class="sidebar-title"><span>Portainer settings</span></li>
<li class="sidebar-list">
<a ui-sref="settings">Password <span class="menu-icon fa fa-lock"></span></a>
<li class="sidebar-list" ng-if="applicationState.application.authentication">
<a ui-sref="settings" ui-sref-active="active">Password <span class="menu-icon fa fa-lock"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="endpoints">Endpoints <span class="menu-icon fa fa-plug"></span></a>
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a>
</li>
</ul>
<div class="sidebar-footer">
+8 -4
View File
@@ -1,6 +1,6 @@
angular.module('sidebar', [])
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'EndpointMode', 'Messages',
function ($scope, $state, Settings, Config, EndpointService, EndpointMode, Messages) {
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'Messages',
function ($scope, $state, Settings, Config, EndpointService, StateManager, Messages) {
Config.$promise.then(function (c) {
$scope.logo = c.logo;
@@ -10,8 +10,12 @@ function ($scope, $state, Settings, Config, EndpointService, EndpointMode, Messa
$scope.switchEndpoint = function(endpoint) {
EndpointService.setActive(endpoint.Id).then(function success(data) {
EndpointMode.determineEndpointMode();
$state.reload();
StateManager.updateEndpointState(true)
.then(function success() {
$state.reload();
}, function error(err) {
Messages.error("Failure", err, "Unable to connect to the Docker endpoint");
});
}, function error(err) {
Messages.error("Failure", err, "Unable to switch to new endpoint");
});
+13 -2
View File
@@ -54,7 +54,18 @@
</div>
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Processes"></rd-widget-header>
<rd-widget-header icon="fa-tasks" title="Processes">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
@@ -69,7 +80,7 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: pagination_count)">
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
</tr>
</tbody>
+27 -14
View File
@@ -1,17 +1,20 @@
angular.module('stats', [])
.controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
.controller('StatsController', ['Pagination', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
// TODO: Force memory scale to 0 - max memory
$scope.ps_args = '';
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
$scope.sortType = 'CMD';
$scope.sortReverse = false;
$scope.pagination_count = Settings.pagination_count;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
};
$scope.getTop = function () {
ContainerTop.get($stateParams.id, {
ps_args: $scope.ps_args
@@ -114,6 +117,12 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
});
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
function setUpdateStatsTimeout() {
if(!destroyed) {
timeout = $timeout(updateStats, 5000);
}
}
function updateStats() {
Container.stats({id: $stateParams.id}, function (d) {
var arr = Object.keys(d).map(function (key) {
@@ -129,15 +138,17 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
updateCpuChart(d);
updateMemoryChart(d);
updateNetworkChart(d);
timeout = $timeout(updateStats, 5000);
setUpdateStatsTimeout();
}, function () {
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
timeout = $timeout(updateStats, 5000);
setUpdateStatsTimeout();
});
}
var destroyed = false;
var timeout;
$scope.$on('$destroy', function () {
destroyed = true;
$timeout.cancel(timeout);
});
@@ -162,16 +173,18 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
$scope.networkName = Object.keys(data.networks)[0];
data.network = data.networks[$scope.networkName];
}
var rxBytes = 0, txBytes = 0;
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
// These will be zero on first call, ignore to prevent large graph spike
rxBytes = data.network.rx_bytes - lastRxBytes;
txBytes = data.network.tx_bytes - lastTxBytes;
if(data.network) {
var rxBytes = 0, txBytes = 0;
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
// These will be zero on first call, ignore to prevent large graph spike
rxBytes = data.network.rx_bytes - lastRxBytes;
txBytes = data.network.tx_bytes - lastTxBytes;
}
lastRxBytes = data.network.rx_bytes;
lastTxBytes = data.network.tx_bytes;
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
networkChart.removeData();
}
lastRxBytes = data.network.rx_bytes;
lastTxBytes = data.network.tx_bytes;
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
networkChart.removeData();
}
function calculateCPUPercent(stats) {
+71 -49
View File
@@ -16,14 +16,14 @@
<tbody>
<tr>
<td>Nodes</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
</tr>
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Images</td>
<td>{{ info.Images }}</td>
</tr>
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Swarm version</td>
<td>{{ docker.Version|swarmversion }}</td>
</tr>
@@ -31,29 +31,29 @@
<td>Docker API version</td>
<td>{{ docker.ApiVersion }}</td>
</tr>
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Strategy</td>
<td>{{ swarm.Strategy }}</td>
</tr>
<tr>
<td>Total CPU</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
<td ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</td>
</tr>
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Operating system</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Kernel version</td>
<td>{{ info.KernelVersion }}</td>
</tr>
<tr ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<td>Go version</td>
<td>{{ docker.GoVersion }}</td>
</tr>
@@ -65,18 +65,29 @@
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM'">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-header icon="fa-hdd-o" title="Node status">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th>
<a ui-sref="swarm" ng-click="order('Name')">
<a ui-sref="swarm" ng-click="order('name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
@@ -94,30 +105,30 @@
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('IP')">
<a ui-sref="swarm" ng-click="order('ip')">
IP
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'ip' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ip' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Engine')">
<a ui-sref="swarm" ng-click="order('version')">
Engine
<span ng-show="sortType == 'Engine' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Engine' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'version' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'version' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Status')">
<a ui-sref="swarm" ng-click="order('status')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr dir-paginate="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td>{{ node.name }}</td>
<td>{{ node.cpu }}</td>
<td>{{ node.memory }}</td>
@@ -133,60 +144,71 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-header icon="fa-hdd-o" title="Node status">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th>
<a ui-sref="swarm" ng-click="order('Name')">
<a ui-sref="swarm" ng-click="order('Description.Hostname')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'Description.Hostname' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Description.Hostname' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('type')">
<a ui-sref="swarm" ng-click="order('Spec.Role')">
Role
<span ng-show="sortType == 'type' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'type' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'Spec.Role' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Spec.Role' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('cpu')">
<a ui-sref="swarm" ng-click="order('Description.Resources.NanoCPUs')">
CPU
<span ng-show="sortType == 'cpu' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'cpu' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'Description.Resources.NanoCPUs' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Description.Resources.NanoCPUs' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('memory')">
<a ui-sref="swarm" ng-click="order('Description.Resources.MemoryBytes')">
Memory
<span ng-show="sortType == 'memory' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'memory' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'Description.Resources.MemoryBytes' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Description.Resources.MemoryBytes' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Engine')">
<a ui-sref="swarm" ng-click="order('Description.Engine.EngineVersion')">
Engine
<span ng-show="sortType == 'Engine' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Engine' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'Description.Engine.EngineVersion' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Description.Engine.EngineVersion' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Status')">
<a ui-sref="swarm" ng-click="order('node.Status.State')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'node.Status.State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'node.Status.State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<td>{{ node.Description.Hostname }}</td>
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="node({id: node.ID})">{{ node.Description.Hostname }}</a></td>
<td>{{ node.Spec.Role }}</td>
<td>{{ node.Description.Resources.NanoCPUs / 1000000000 }}</td>
<td>{{ node.Description.Resources.MemoryBytes|humansize }}</td>
+11 -7
View File
@@ -1,28 +1,32 @@
angular.module('swarm', [])
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Settings',
function ($scope, Info, Version, Node, Settings) {
$scope.sortType = 'Name';
$scope.sortReverse = true;
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Pagination',
function ($scope, Info, Version, Node, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
$scope.sortType = 'Spec.Role';
$scope.sortReverse = false;
$scope.info = {};
$scope.docker = {};
$scope.swarm = {};
$scope.totalCPU = 0;
$scope.totalMemory = 0;
$scope.pagination_count = Settings.pagination_count;
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('swarm_nodes', $scope.state.pagination_count);
};
Version.get({}, function (d) {
$scope.docker = d;
});
Info.get({}, function (d) {
$scope.info = d;
if ($scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
Node.query({}, function(d) {
$scope.nodes = d;
var CPU = 0, memory = 0;
+14 -10
View File
@@ -3,6 +3,7 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadTemplatesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Templates</rd-header-content>
</rd-header>
@@ -13,12 +14,12 @@
</rd-widget-custom-header>
<rd-widget-body classes="padding">
<form class="form-horizontal">
<div class="form-group" ng-if="globalNetworkCount === 0 && endpointMode.provider === 'DOCKER_SWARM'">
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<div class="col-sm-12">
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
</div>
<div class="form-group" ng-if="endpointMode.provider === 'DOCKER_SWARM_MODE'">
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
@@ -41,10 +42,10 @@
<div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
<div class="col-sm-10">
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="var.value">
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="endpointMode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="var.value">
<select ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
@@ -106,21 +107,24 @@
</div>
</div>
<div class="row" ng-if="state.selectedTemplate">
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-rocket" title="Available templates">
<div class="pull-right">
<i id="loadTemplatesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-body classes="padding">
<div class="template-list">
<div dir-paginate="tpl in templates | itemsPerPage: pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
<img class="logo" ng-src="{{ tpl.logo }}" />
<div class="title">{{ tpl.title }}</div>
<div class="description">{{ tpl.description }}</div>
@@ -1,19 +1,23 @@
angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Settings',
function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Settings) {
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Pagination',
function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Pagination) {
$scope.state = {
selectedTemplate: null,
showAdvancedOptions: false
showAdvancedOptions: false,
pagination_count: Pagination.getPaginationCount('templates')
};
$scope.formValues = {
network: "",
name: "",
ports: []
};
$scope.pagination_count = Settings.pagination_count;
var selectedItem = -1;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('templates', $scope.state.pagination_count);
};
$scope.addPortBinding = function() {
$scope.formValues.ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
};
@@ -115,7 +119,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
if (v.value || v.set) {
var val;
if (v.type && v.type === 'container') {
if ($scope.endpointMode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') {
val = $filter('swarmcontainername')(v.value);
} else {
var container = v.value;
@@ -134,7 +138,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
}
function prepareImageConfig(config, template) {
var image = _.toLower(template.image);
var image = template.image;
var registry = template.registry || '';
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
@@ -206,7 +210,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
var containersToHideLabels = c.hiddenLabels;
Network.query({}, function (d) {
var networks = d;
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
networks = d.filter(function (network) {
if (network.Scope === 'global') {
return network;
+13 -3
View File
@@ -3,6 +3,7 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="volumes" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadVolumesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Volumes</rd-header-content>
</rd-header>
@@ -11,7 +12,14 @@
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes">
<div class="pull-right">
<i id="loadVolumesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<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>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
@@ -28,7 +36,9 @@
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="volumes" ng-click="order('Name')">
Name
@@ -53,7 +63,7 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td>{{ volume.Name|truncate:50 }}</td>
<td>{{ volume.Driver }}</td>
+16 -3
View File
@@ -1,20 +1,33 @@
angular.module('volumes', [])
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Settings',
function ($scope, $state, Volume, Messages, Settings) {
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Pagination',
function ($scope, $state, Volume, Messages, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('volumes');
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.config = {
Name: ''
};
$scope.pagination_count = Settings.pagination_count;
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('volumes', $scope.state.pagination_count);
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredVolumes, function (volume) {
if (volume.Checked !== allSelected) {
volume.Checked = allSelected;
$scope.selectItem(volume);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
+6 -3
View File
@@ -1,11 +1,14 @@
angular
.module('portainer')
.directive('rdHeaderContent', function rdHeaderContent() {
.directive('rdHeaderContent', ['Authentication', function rdHeaderContent(Authentication) {
var directive = {
requires: '^rdHeader',
transclude: true,
template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right"><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u>log out <i class="fa fa-sign-out" aria-hidden="true"></i></u></a></div></div>',
link: function (scope, iElement, iAttrs) {
scope.username = Authentication.getCredentials().username;
},
template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u>log out <i class="fa fa-sign-out" aria-hidden="true"></i></u></a></div></div>',
restrict: 'E'
};
return directive;
});
}]);
+3 -3
View File
@@ -1,16 +1,16 @@
angular
.module('portainer')
.directive('rdHeaderTitle', ['$rootScope', function rdHeaderTitle($rootScope) {
.directive('rdHeaderTitle', ['Authentication', function rdHeaderTitle(Authentication) {
var directive = {
requires: '^rdHeader',
scope: {
title: '@'
},
link: function (scope, iElement, iAttrs) {
scope.username = $rootScope.username;
scope.username = Authentication.getCredentials().username;
},
transclude: true,
template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span></div>',
template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box" ng-if="username"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span></div>',
restrict: 'E'
};
return directive;
@@ -67,7 +67,7 @@ angular.module('portainer.filters', [])
.filter('nodestatusbadge', function () {
'use strict';
return function (text) {
if (text === 'Unhealthy') {
if (text === 'down' || text === 'Unhealthy') {
return 'danger';
}
return 'success';
+20
View File
@@ -0,0 +1,20 @@
angular.module('portainer.helpers')
.factory('ContainerHelper', [function ContainerHelperFactory() {
'use strict';
return {
hideContainers: function(containers, containersToHideLabels) {
return containers.filter(function (container) {
var filterContainer = false;
containersToHideLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
}
};
}]);
+30
View File
@@ -0,0 +1,30 @@
angular.module('portainer.helpers')
.factory('ImageHelper', [function ImageHelperFactory() {
'use strict';
return {
createImageConfigForCommit: function(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
repo: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
},
createImageConfigForContainer: function (imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
fromImage: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
};
}]);
+32
View File
@@ -0,0 +1,32 @@
angular.module('portainer.helpers')
.factory('InfoHelper', [function InfoHelperFactory() {
'use strict';
return {
determineEndpointMode: function(info) {
var mode = {
provider: '',
role: ''
};
if (_.startsWith(info.ServerVersion, 'swarm')) {
mode.provider = "DOCKER_SWARM";
if (info.SystemStatus[0][1] === 'primary') {
mode.role = "PRIMARY";
} else {
mode.role = "REPLICA";
}
} else {
if (!info.Swarm || _.isEmpty(info.Swarm.NodeID)) {
mode.provider = "DOCKER_STANDALONE";
} else {
mode.provider = "DOCKER_SWARM_MODE";
if (info.Swarm.ControlAvailable) {
mode.role = "MANAGER";
} else {
mode.role = "WORKER";
}
}
}
return mode;
}
};
}]);
+23
View File
@@ -0,0 +1,23 @@
angular.module('portainer.helpers')
.factory('LabelHelper', [function LabelHelperFactory() {
'use strict';
return {
fromLabelHashToKeyValue: function(labels) {
if (labels) {
return Object.keys(labels).map(function(key) {
return {key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true};
});
}
return [];
},
fromKeyValueToLabelHash: function(labelKV) {
var labels = {};
if (labelKV) {
labelKV.forEach(function(label) {
labels[label.key] = label.value;
});
}
return labels;
}
};
}]);
+14
View File
@@ -0,0 +1,14 @@
angular.module('portainer.helpers')
.factory('NodeHelper', [function NodeHelperFactory() {
'use strict';
return {
nodeToConfig: function(node) {
return {
Name: node.Spec.Name,
Role: node.Spec.Role,
Labels: node.Spec.Labels,
Availability: node.Spec.Availability
};
}
};
}]);
+17
View File
@@ -0,0 +1,17 @@
angular.module('portainer.helpers')
.factory('ServiceHelper', [function ServiceHelperFactory() {
'use strict';
return {
serviceToConfig: function(service) {
return {
Name: service.Spec.Name,
Labels: service.Spec.Labels,
TaskTemplate: service.Spec.TaskTemplate,
Mode: service.Spec.Mode,
UpdateConfig: service.Spec.UpdateConfig,
Networks: service.Spec.Networks,
EndpointSpec: service.Spec.EndpointSpec
};
}
};
}]);
+40
View File
@@ -0,0 +1,40 @@
angular.module('portainer.helpers')
.factory('TemplateHelper', [function TemplateHelperFactory() {
'use strict';
return {
getPortBindings: function(ports) {
var bindings = [];
ports.forEach(function (port) {
var portAndProtocol = _.split(port, '/');
var binding = {
containerPort: portAndProtocol[0],
protocol: portAndProtocol[1]
};
bindings.push(binding);
});
return bindings;
},
//Not used atm, may prove useful later
getVolumeBindings: function(volumes) {
var bindings = [];
volumes.forEach(function (volume) {
bindings.push({ containerPath: volume });
});
return bindings;
},
//Not used atm, may prove useful later
getEnvBindings: function(env) {
var bindings = [];
env.forEach(function (envvar) {
var binding = {
name: envvar.name
};
if (envvar.set) {
binding.value = envvar.set;
}
bindings.push(binding);
});
return bindings;
}
};
}]);
+20
View File
@@ -0,0 +1,20 @@
function ContainerViewModel(data) {
this.Id = data.Id;
this.Status = data.Status;
this.State = data.State;
this.Names = data.Names;
// Unavailable in Docker < 1.10
if (data.NetworkSettings && !_.isEmpty(data.NetworkSettings.Networks)) {
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
}
this.Image = data.Image;
this.Command = data.Command;
this.Checked = false;
this.Ports = [];
for (var i = 0; i < data.Ports.length; ++i) {
var p = data.Ports[i];
if (p.PublicPort) {
this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort });
}
}
}
@@ -1,86 +1,3 @@
function ImageViewModel(data) {
this.Id = data.Id;
this.Tag = data.Tag;
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
this.VirtualSize = data.VirtualSize;
}
function TaskViewModel(data, node_data) {
this.Id = data.ID;
this.Created = data.CreatedAt;
this.Updated = data.UpdatedAt;
this.Slot = data.Slot;
this.Status = data.Status.State;
if (node_data) {
for (var i = 0; i < node_data.length; ++i) {
if (data.NodeID === node_data[i].ID) {
this.Node = node_data[i].Description.Hostname;
}
}
}
}
function ServiceViewModel(data) {
this.Model = data;
this.Id = data.ID;
this.Name = data.Spec.Name;
this.Image = data.Spec.TaskTemplate.ContainerSpec.Image;
this.Version = data.Version.Index;
if (data.Spec.Mode.Replicated) {
this.Mode = 'replicated' ;
this.Replicas = data.Spec.Mode.Replicated.Replicas;
} else {
this.Mode = 'global';
}
this.Labels = data.Spec.Labels;
if (data.Spec.TaskTemplate.ContainerSpec) {
this.ContainerLabels = data.Spec.TaskTemplate.ContainerSpec.Labels;
}
if (data.Spec.TaskTemplate.ContainerSpec.Env) {
this.Env = data.Spec.TaskTemplate.ContainerSpec.Env;
}
if (data.Endpoint.Ports) {
this.Ports = data.Endpoint.Ports;
}
if (data.Spec.UpdateConfig) {
this.UpdateParallelism = (typeof data.Spec.UpdateConfig.Parallelism !== undefined) ? data.Spec.UpdateConfig.Parallelism || 0 : 1;
this.UpdateDelay = data.Spec.UpdateConfig.Delay || 0;
this.UpdateFailureAction = data.Spec.UpdateConfig.FailureAction || 'pause';
} else {
this.UpdateParallelism = 1;
this.UpdateDelay = 0;
this.UpdateFailureAction = 'pause';
}
this.Checked = false;
this.Scale = false;
this.EditName = false;
}
function ContainerViewModel(data) {
this.Id = data.Id;
this.Status = data.Status;
this.State = data.State;
this.Names = data.Names;
// Unavailable in Docker < 1.10
if (data.NetworkSettings && !_.isEmpty(data.NetworkSettings.Networks)) {
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
}
this.Image = data.Image;
this.Command = data.Command;
this.Checked = false;
this.Ports = [];
for (var i = 0; i < data.Ports.length; ++i) {
var p = data.Ports[i];
if (p.PublicPort) {
this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort });
}
}
}
function createEventDetails(event) {
var eventAttr = event.Actor.Attributes;
var details = '';
+9
View File
@@ -0,0 +1,9 @@
function ImageViewModel(data) {
this.Id = data.Id;
this.Tag = data.Tag;
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
this.VirtualSize = data.VirtualSize;
}
+35
View File
@@ -0,0 +1,35 @@
function NodeViewModel(data) {
this.Model = data;
this.Id = data.ID;
this.Version = data.Version.Index;
this.Name = data.Spec.Name;
this.Role = data.Spec.Role;
this.CreatedAt = data.CreatedAt;
this.UpdatedAt = data.UpdatedAt;
this.Availability = data.Spec.Availability;
var labels = data.Spec.Labels;
if (labels) {
this.Labels = Object.keys(labels).map(function(key) {
return { key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true };
});
} else {
this.Labels = [];
}
this.Hostname = data.Description.Hostname;
this.PlatformArchitecture = data.Description.Platform.Architecture;
this.PlatformOS = data.Description.Platform.OS;
this.CPUs = data.Description.Resources.NanoCPUs;
this.Memory = data.Description.Resources.MemoryBytes;
this.EngineVersion = data.Description.Engine.EngineVersion;
this.EngineLabels = data.Description.Engine.Labels;
this.Plugins = data.Description.Engine.Plugins;
this.Status = data.Status.State;
if (data.ManagerStatus) {
this.Leader = data.ManagerStatus.Leader;
this.Reachability = data.ManagerStatus.Reachability;
this.ManagerAddr = data.ManagerStatus.Addr;
}
}
+36
View File
@@ -0,0 +1,36 @@
function ServiceViewModel(data) {
this.Model = data;
this.Id = data.ID;
this.Name = data.Spec.Name;
this.Image = data.Spec.TaskTemplate.ContainerSpec.Image;
this.Version = data.Version.Index;
if (data.Spec.Mode.Replicated) {
this.Mode = 'replicated' ;
this.Replicas = data.Spec.Mode.Replicated.Replicas;
} else {
this.Mode = 'global';
}
this.Labels = data.Spec.Labels;
if (data.Spec.TaskTemplate.ContainerSpec) {
this.ContainerLabels = data.Spec.TaskTemplate.ContainerSpec.Labels;
}
if (data.Spec.TaskTemplate.ContainerSpec.Env) {
this.Env = data.Spec.TaskTemplate.ContainerSpec.Env;
}
if (data.Endpoint.Ports) {
this.Ports = data.Endpoint.Ports;
}
if (data.Spec.UpdateConfig) {
this.UpdateParallelism = (typeof data.Spec.UpdateConfig.Parallelism !== undefined) ? data.Spec.UpdateConfig.Parallelism || 0 : 1;
this.UpdateDelay = data.Spec.UpdateConfig.Delay || 0;
this.UpdateFailureAction = data.Spec.UpdateConfig.FailureAction || 'pause';
} else {
this.UpdateParallelism = 1;
this.UpdateDelay = 0;
this.UpdateFailureAction = 'pause';
}
this.Checked = false;
this.Scale = false;
this.EditName = false;
}
+15
View File
@@ -0,0 +1,15 @@
function TaskViewModel(data, node_data) {
this.Id = data.ID;
this.Created = data.CreatedAt;
this.Updated = data.UpdatedAt;
this.Slot = data.Slot;
this.Status = data.Status.State;
this.Image = data.Spec.ContainerSpec ? data.Spec.ContainerSpec.Image : '';
if (node_data) {
for (var i = 0; i < node_data.length; ++i) {
if (data.NodeID === node_data[i].ID) {
this.Node = node_data[i].Description.Hostname;
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
angular.module('portainer.rest')
.factory('Auth', ['$resource', 'AUTH_ENDPOINT', function AuthFactory($resource, AUTH_ENDPOINT) {
'use strict';
return $resource(AUTH_ENDPOINT, {}, {
login: {
method: 'POST'
}
});
}]);
+4
View File
@@ -0,0 +1,4 @@
angular.module('portainer.rest')
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
return $resource(CONFIG_ENDPOINT).get();
}]);
+37
View File
@@ -0,0 +1,37 @@
angular.module('portainer.rest')
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/containers/:id/:action', {
name: '@name'
}, {
query: {method: 'GET', params: {all: 0, action: 'json', filters: '@filters' }, isArray: true},
get: {method: 'GET', params: {action: 'json'}},
stop: {method: 'POST', params: {id: '@id', t: 5, action: 'stop'}},
restart: {method: 'POST', params: {id: '@id', t: 5, action: 'restart'}},
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
changes: {method: 'GET', params: {action: 'changes'}, isArray: true},
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
start: {
method: 'POST', params: {id: '@id', action: 'start'},
transformResponse: genericHandler
},
create: {
method: 'POST', params: {action: 'create'},
transformResponse: genericHandler
},
remove: {
method: 'DELETE', params: {id: '@id', v: 0},
transformResponse: genericHandler
},
rename: {
method: 'POST', params: {id: '@id', action: 'rename', name: '@name'},
transformResponse: genericHandler
},
exec: {
method: 'POST', params: {id: '@id', action: 'exec'},
transformResponse: genericHandler
}
});
}]);
+7
View File
@@ -0,0 +1,7 @@
angular.module('portainer.rest')
.factory('ContainerCommit', ['$resource', 'Settings', function ContainerCommitFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/commit', {}, {
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}}
});
}]);
+20
View File
@@ -0,0 +1,20 @@
angular.module('portainer.rest')
.factory('ContainerLogs', ['$http', 'Settings', function ContainerLogsFactory($http, Settings) {
'use strict';
return {
get: function (id, params, callback) {
$http({
method: 'GET',
url: Settings.url + '/containers/' + id + '/logs',
params: {
'stdout': params.stdout || 0,
'stderr': params.stderr || 0,
'timestamps': params.timestamps || 0,
'tail': params.tail || 'all'
}
}).success(callback).error(function (data, status, headers, config) {
console.log(error, data);
});
}
};
}]);
+15
View File
@@ -0,0 +1,15 @@
angular.module('portainer.rest')
.factory('ContainerTop', ['$http', 'Settings', function ($http, Settings) {
'use strict';
return {
get: function (id, params, callback, errorCallback) {
$http({
method: 'GET',
url: Settings.url + '/containers/' + id + '/top',
params: {
ps_args: params.ps_args
}
}).success(callback);
}
};
}]);
+13
View File
@@ -0,0 +1,13 @@
angular.module('portainer.rest')
.factory('Endpoints', ['$resource', 'ENDPOINTS_ENDPOINT', function EndpointsFactory($resource, ENDPOINTS_ENDPOINT) {
'use strict';
return $resource(ENDPOINTS_ENDPOINT + '/:id/:action', {}, {
create: { method: 'POST' },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
remove: { method: 'DELETE', params: { id: '@id'} },
getActiveEndpoint: { method: 'GET', params: { id: '0' } },
setActiveEndpoint: { method: 'POST', params: { id: '@id', action: 'active' } }
});
}]);
+10
View File
@@ -0,0 +1,10 @@
angular.module('portainer.rest')
.factory('Events', ['$resource', 'Settings', function EventFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/events', {}, {
query: {
method: 'GET', params: {since: '@since', until: '@until'},
isArray: true, transformResponse: jsonObjectsToArrayHandler
}
});
}]);
+10
View File
@@ -0,0 +1,10 @@
angular.module('portainer.rest')
.factory('Exec', ['$resource', 'Settings', function ExecFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/exec/:id/:action', {}, {
resize: {
method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'},
transformResponse: genericHandler
}
});
}]);
+25
View File
@@ -0,0 +1,25 @@
angular.module('portainer.rest')
.factory('Image', ['$resource', 'Settings', function ImageFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/images/:id/:action', {}, {
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true},
get: {method: 'GET', params: {action: 'json'}},
search: {method: 'GET', params: {action: 'search'}},
history: {method: 'GET', params: {action: 'history'}, isArray: true},
insert: {method: 'POST', params: {id: '@id', action: 'insert'}},
tag: {method: 'POST', params: {id: '@id', action: 'tag', force: 0, repo: '@repo', tag: '@tag'}},
inspect: {method: 'GET', params: {id: '@id', action: 'json'}},
push: {
method: 'POST', params: {action: 'push', id: '@tag'},
isArray: true, transformResponse: jsonObjectsToArrayHandler
},
create: {
method: 'POST', params: {action: 'create', fromImage: '@fromImage', tag: '@tag'},
isArray: true, transformResponse: jsonObjectsToArrayHandler
},
remove: {
method: 'DELETE', params: {id: '@id'},
isArray: true, transformResponse: deleteImageHandler
}
});
}]);
+5
View File
@@ -0,0 +1,5 @@
angular.module('portainer.rest')
.factory('Info', ['$resource', 'Settings', function InfoFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/info', {});
}]);
+12
View File
@@ -0,0 +1,12 @@
angular.module('portainer.rest')
.factory('Network', ['$resource', 'Settings', function NetworkFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/networks/:id/:action', {id: '@id'}, {
query: {method: 'GET', isArray: true},
get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
remove: { method: 'DELETE', transformResponse: genericHandler },
connect: {method: 'POST', params: {action: 'connect'}},
disconnect: {method: 'POST', params: {action: 'disconnect'}}
});
}]);
+10
View File
@@ -0,0 +1,10 @@
angular.module('portainer.rest')
.factory('Node', ['$resource', 'Settings', function NodeFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/nodes/:id/:action', {}, {
query: {method: 'GET', isArray: true},
get: {method: 'GET', params: {id: '@id'}},
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
remove: { method: 'DELETE', params: {id: '@id'} }
});
}]);
@@ -56,7 +56,7 @@ function deleteImageHandler(data) {
response.push({message: data});
}
// A JSON object is returned on failure (Docker = 1.12)
else if (!isJSONArray) {
else if (!isJSONArray(data)) {
var json = angular.fromJson(data);
response.push(json);
}
+11
View File
@@ -0,0 +1,11 @@
angular.module('portainer.rest')
.factory('Service', ['$resource', 'Settings', function ServiceFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/services/:id/:action', {}, {
get: { method: 'GET', params: {id: '@id'} },
query: { method: 'GET', isArray: true },
create: { method: 'POST', params: {action: 'create'} },
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
remove: { method: 'DELETE', params: {id: '@id'} }
});
}]);
+7
View File
@@ -0,0 +1,7 @@
angular.module('portainer.rest')
.factory('Swarm', ['$resource', 'Settings', function SwarmFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/swarm', {}, {
get: {method: 'GET'}
});
}]);
+8
View File
@@ -0,0 +1,8 @@
angular.module('portainer.rest')
.factory('Task', ['$resource', 'Settings', function TaskFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/tasks/:id', {}, {
get: { method: 'GET', params: {id: '@id'} },
query: { method: 'GET', isArray: true, params: {filters: '@filters'} }
});
}]);
+6
View File
@@ -0,0 +1,6 @@
angular.module('portainer.rest')
.factory('Templates', ['$resource', 'TEMPLATES_ENDPOINT', function TemplatesFactory($resource, TEMPLATES_ENDPOINT) {
return $resource(TEMPLATES_ENDPOINT, {}, {
get: {method: 'GET', isArray: true}
});
}]);
+12
View File
@@ -0,0 +1,12 @@
angular.module('portainer.rest')
.factory('Users', ['$resource', 'USERS_ENDPOINT', function UsersFactory($resource, USERS_ENDPOINT) {
'use strict';
return $resource(USERS_ENDPOINT + '/:username/:action', {}, {
create: { method: 'POST' },
get: { method: 'GET', params: { username: '@username' } },
update: { method: 'PUT', params: { username: '@username' } },
checkPassword: { method: 'POST', params: { username: '@username', action: 'passwd' } },
checkAdminUser: { method: 'GET', params: { username: 'admin', action: 'check' } },
initAdminUser: { method: 'POST', params: { username: 'admin', action: 'init' } }
});
}]);
+5
View File
@@ -0,0 +1,5 @@
angular.module('portainer.rest')
.factory('Version', ['$resource', 'Settings', function VersionFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/version', {});
}]);
+12
View File
@@ -0,0 +1,12 @@
angular.module('portainer.rest')
.factory('Volume', ['$resource', 'Settings', function VolumeFactory($resource, Settings) {
'use strict';
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, {
query: {method: 'GET'},
get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
remove: {
method: 'DELETE', transformResponse: genericHandler
}
});
}]);
+38
View File
@@ -0,0 +1,38 @@
angular.module('portainer.services')
.factory('Authentication', ['$q', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', function AuthenticationFactory($q, Auth, jwtHelper, LocalStorage, StateManager) {
'use strict';
var credentials = {};
return {
init: function() {
var jwt = LocalStorage.getJWT();
if (jwt) {
var tokenPayload = jwtHelper.decodeToken(jwt);
credentials.username = tokenPayload.username;
}
},
login: function(username, password) {
return $q(function (resolve, reject) {
Auth.login({username: username, password: password}).$promise
.then(function(data) {
LocalStorage.storeJWT(data.jwt);
credentials.username = username;
resolve();
}, function() {
reject();
});
});
},
logout: function() {
StateManager.clean();
LocalStorage.clean();
},
isAuthenticated: function() {
var jwt = LocalStorage.getJWT();
return jwt && !jwtHelper.isTokenExpired(jwt);
},
getCredentials: function() {
return credentials;
}
};
}]);

Some files were not shown because too many files have changed in this diff Show More