Compare commits

...

88 Commits

Author SHA1 Message Date
Anthony Lapenna 9c277733d5 Merge branch 'release/1.16.4' 2018-03-11 20:30:12 +10:00
Anthony Lapenna ec2a9e149b chore(version): bump version number 2018-03-11 20:30:07 +10:00
Anthony Lapenna aa41fd02ef feat(log-viewer): use only one switch to manage collection/autoscroll (#1713)
* feat(log-viewer): use only one switch to manage collection/autoscroll

* feat(log-viewer): add the ability to clear selection

* style(log-viewer): update unselect button design
2018-03-11 20:29:13 +10:00
Anthony Lapenna 28c73323bf refactor(extensions): review bouncer settings for extensions endpoint (#1711) 2018-03-10 08:18:59 +10:00
Herwono W. Wijaya b389e3c65a fix(service-logs): fix services log view breadcrumb link (#1709) 2018-03-10 08:09:03 +10:00
Anthony Lapenna 02b3d54a75 fix(extensions): fix invalid storidge API URL (#1707) 2018-03-09 19:50:48 +10:00
Anthony Lapenna f1a21c07bd feat(storidge): add extension check on endpoint switch (#1693)
* feat(storidge): add extension check on endpoint switch

* feat(storidge): add extension check post login
2018-03-09 08:49:43 +10:00
Anthony Lapenna 403de0d319 chore(momentjs): upgrade momentjs version (#1701) 2018-03-08 11:42:50 +10:00
Anthony Lapenna a76ccff7c9 refactor(xterm): update xtermjs to latest version (#1692) 2018-03-06 17:40:02 +10:00
Anthony Lapenna 1ae9832980 Merge tag '1.16.3' into develop
Release 1.16.3
2018-03-03 09:20:05 +10:00
Anthony Lapenna 8a9619c7e8 Merge branch 'release/1.16.3' 2018-03-03 09:19:59 +10:00
Anthony Lapenna 9634cf1563 chore(version): bump version number 2018-03-03 09:19:54 +10:00
Mauro Cortellazzi 716cd033b2 feat(events): add missing events support (#1682) 2018-03-02 18:21:26 +10:00
Anthony Lapenna 28bca85e01 feat(registries): remove actual password from registry password input (#1687) 2018-03-02 18:16:33 +10:00
Anthony Lapenna 73e6498d2f refactor(swarm-visualizer): move task border logic to a filter (#1686) 2018-03-02 09:00:34 +10:00
Mauro Cortellazzi 1b8d5e89d1 feat(swarm-visualizer): swarm visualizer color by service (#1683) 2018-03-02 08:10:14 +10:00
Anthony Lapenna 76aeee7237 feat(templates): add support for the name property (#1680) 2018-02-28 08:59:31 +01:00
Anthony Lapenna b9a1c68ea0 feat(security): check user existence for each protected requests (#1679) 2018-02-28 08:09:51 +01:00
Anthony Lapenna b8f8df5f48 fix(endpoints-creation): remove endpoint if an error is raised during creation (#1678) 2018-02-28 07:52:40 +01:00
Anthony Lapenna 0c5152fb5f feat(log-viewer): introduce the log viewer component (#1666) 2018-02-28 07:19:28 +01:00
Anthony Lapenna 81de2a5afb feat(image-build): add the ability to build images (#1672) 2018-02-28 07:19:06 +01:00
Anthony Lapenna e065bd4a47 style(containers): update label color for unhealthy containers (#1677) 2018-02-28 05:54:13 +01:00
Anthony Lapenna 9b80b6adb2 refactor(code-editor): introduce code-editor component (#1674)
* refactor(code-editor): introduce code-editor component

* refactor(code-editor): add some extra validation
2018-02-27 08:19:21 +01:00
Anthony Lapenna eb43579378 feat(storidge): introduce endpoint extensions and proxy Storidge API (#1661) 2018-02-23 03:10:26 +01:00
Anthony Lapenna b5e256c967 fix(services): use the Public URL instead of a manager IP (#1665) 2018-02-21 10:55:51 +01:00
Boissier Florian ae5416583e style(containers): update quick actions tooltips messages (#1659) 2018-02-17 09:44:29 +01:00
Anthony Lapenna 5b9cb1a883 feat(api): use the stack ProjectPath as the working directory during deployment (#1648) 2018-02-09 10:55:51 +01:00
Anthony Lapenna b040b3ff8c Merge tag '1.16.2' into develop
Release 1.16.2
2018-02-08 09:27:27 +01:00
Anthony Lapenna 3ff49542f3 Merge branch 'release/1.16.2' 2018-02-08 09:27:20 +01:00
Anthony Lapenna 27dcfd043b chore(version): bump version number 2018-02-08 09:27:13 +01:00
Anthony Lapenna 1de0619fd5 fix(api): ignore Docker login errors during stack deployment (#1635) 2018-02-07 08:37:01 +01:00
Anthony Lapenna 1c67db0c70 feat(ux): enable auto-focus on search field (#1636) 2018-02-06 16:58:05 +01:00
Anthony Lapenna 7365e69c59 fix(config-creation): fix an issue setting config editor as read-only (#1634) 2018-02-06 14:23:08 +01:00
Anthony Lapenna 23a565243a Merge branch 'develop' of github.com:portainer/portainer into develop 2018-02-01 13:29:43 +01:00
Anthony Lapenna 27dceadba1 refactor(app): introduce new project structure for the frontend (#1623) 2018-02-01 13:27:52 +01:00
Anthony Lapenna 6f471cef34 Merge branch 'master' into develop 2018-01-31 21:35:20 +01:00
Ben Yanke e6422a6d75 style(container-details): fix a typo in container status 2018-01-31 20:28:36 +01:00
Anthony Lapenna 56cab429de Revert "feat(container-details): fix typo in container status" (#1619)
This reverts commit 5f742c2163.
2018-01-31 19:11:20 +01:00
Ben Yanke 5f742c2163 feat(container-details): fix typo in container status 2018-01-31 19:09:10 +01:00
Anthony Lapenna f31f29fa2f feat(volumes): check if volumes are used in service definitions (#1601) 2018-01-25 08:13:56 +01:00
Anthony Lapenna 672819f3af refactor(api): remove CLI deprecation related code (#1602) 2018-01-24 21:58:58 +01:00
Anthony Lapenna 0ff0c3ed0d Merge tag '1.16.1' into develop
Release 1.16.1
2018-01-23 16:53:03 +01:00
Anthony Lapenna 54750f002a Merge branch 'release/1.16.1' 2018-01-23 16:52:59 +01:00
Anthony Lapenna 4c2dfb3346 chore(version): bump version number 2018-01-23 16:52:54 +01:00
Miguel A. C 8ae3abf29e fix(service-details): avoid sending unmodified restart policy settings when updating a service (#1576) 2018-01-23 10:06:58 +01:00
Anthony Lapenna 362f036a68 fix(state): ensure API version >= 1.25 before extension check (#1594)
* fix(state): ensure API version >= 1.25 before extension check
2018-01-23 09:50:14 +01:00
Anthony Lapenna 0d0072a50e extension(storidge): support cluster shutdown (#1589) 2018-01-23 09:49:29 +01:00
Anthony Lapenna 173ea372c2 fix(extension): bypass the error returned by plugin service during ex… (#1586)
* fix(extension): bypass the error returned by plugin service during extension check

* feat(plugins): bypass the error returned by plugin service during plugin retrieval
2018-01-23 09:47:36 +01:00
Anthony Lapenna 8c75f705e2 chore(dependency): upgrade jquery version to latest (#1592) 2018-01-22 17:44:49 +01:00
Anthony Lapenna b1863430df revert: revert PR 1366 (#1588) 2018-01-22 10:06:47 +01:00
Anthony Lapenna c51db23c32 Merge tag '1.16.0' into develop
Release 1.16.0
2018-01-21 17:30:18 +01:00
Anthony Lapenna c40f120da2 Merge branch 'release/1.16.0' 2018-01-21 17:30:13 +01:00
Anthony Lapenna a7cb0ca823 chore(version): bump version number 2018-01-21 17:30:06 +01:00
Anthony Lapenna 7817d4bd0b extension(storidge): add Storidge extension (#1581) 2018-01-21 17:26:24 +01:00
Miguel A. C edadce359c feat(stack-details): add stack deploy prune option (#1567)
* feat(stack-details): add stack deploy prune option

* fix go fmt issues

* add changes proposed by reviewer

* refactor deployStack as suggested by codeclimate
2018-01-20 18:05:01 +01:00
Anthony Lapenna e1bf9599ef fix(stack-details): fix broken link for services published ports (#1578) 2018-01-20 11:31:26 +01:00
RobbyVoid c3ba9e6a53 feat(networks): Show untruncated network name as link title (#1574)
If the network name was truncated (40 characters) it should be visible as a mouse over title
2018-01-19 12:41:18 +01:00
Vincent Besançon 10174b98b9 refactor(api): Fixed typo in check health cli flag (#1570) 2018-01-17 16:34:15 +01:00
1138-4EB 6acfb580dc feat(cli): Add CLI flag for health-check (#1366) 2018-01-15 19:34:07 +01:00
Miguel A. C 340ec841fe feat(swarm-visualizer): add auto-refresh to the cluster visualizer (#1561) 2018-01-12 16:10:02 +01:00
Anthony Lapenna a515b96a46 fix(app): fix a Javascript error related to missing $state parameter (#1562) 2018-01-09 20:06:19 +01:00
Anthony Lapenna 46da85c8cf feat(services): bind enter key when scaling a service (#1560) 2018-01-09 10:59:33 +01:00
Anthony Lapenna f52ac8fb12 feat(UX): improve UX for service update (#1558) 2018-01-09 10:40:30 +01:00
Miguel A. C 0e28aebd65 feat(service): add force update in service list/detail (#1536) 2018-01-08 22:06:56 +01:00
Anthony Lapenna 35892525ff docs(api): document the stack management endpoint (#1557) 2018-01-08 18:27:45 +01:00
Anthony Lapenna d2f3309842 refactor(api): rename file package to filesystem (#1555) 2018-01-06 18:53:12 +01:00
Rahul Ruikar 03f6cc0acf feat(templates): add labels to container template (#1538) 2018-01-06 18:24:51 +01:00
cbrherms f8c7ee7ae6 feat(container-creation): add support for mac assignments (#1546)
* feat(container-creation): add support for mac assignments (#1524)

* refactor(container-creation): code relocation to relevant function

* style(container-creation): fix typo in environment variables function
2018-01-06 11:53:03 +01:00
Thomas Krzero 00daedca30 fix(service): check endpoint spec existence before update 2018-01-05 14:49:41 +01:00
Anthony Lapenna e2b8633aac fix(stack-details): fix an issue related to env vars (#1512) 2018-01-05 14:32:23 +01:00
Anthony Lapenna 50dbb572b1 fix(containers): update the persisted filters after refresh (#1553) 2018-01-05 14:31:20 +01:00
Anthony Lapenna 95b595d2a9 fix(UAC): fix an issue with network/volume ownership update (#1552) 2018-01-05 13:43:25 +01:00
Anthony Lapenna f57ce8b327 feat(containers): trim the @sha256 suffix in the image name (#1551) 2018-01-05 12:32:53 +01:00
Anthony Lapenna 5787df5599 refactor(stack): replace $stateParams usage with $transition$.params() 2018-01-04 21:53:10 +01:00
Anthony Lapenna 52ac9504c1 chore(codefresh): fix the build_frontend step (#1547) 2018-01-02 13:14:26 +01:00
Yassir Hannoun 1da64f2e75 * fix(containers): display a subset of the sha images name in the containers datatable
* Removed unnecessary filter

* refactor(common): improve trimshasum  filter

* refactor(common): improve trimshasum filter
2017-12-22 19:39:06 +01:00
Miguel A. C 8bf3f669d0 feat(service): add logging driver config in service create/update (#1516) 2017-12-22 10:05:31 +01:00
Anthony Lapenna eec10541b3 fix(users): fix invalid Authentication value (#1528) 2017-12-21 19:56:54 +01:00
Anthony Lapenna e0b09f20b0 fix(cache): add a cache validity mechanism (#1527) 2017-12-21 19:49:39 +01:00
Miguel A. C 8e40eb1844 feat(service): add hosts file entries in service create/update (#1511) 2017-12-21 09:53:34 +01:00
Anthony Lapenna c9e060d574 fix(container-logs): add missing dependency to Notifications (#1514) 2017-12-18 21:24:51 +01:00
Anthony Lapenna 9c9e16b2b2 fix(containers): fix the ability to stop/pause a healthy container (#1507) 2017-12-14 10:31:16 +01:00
Anthony Lapenna 35f7ce5f3d Merge tag '1.15.5' into develop
Release 1.15.5
2017-12-11 16:04:03 +01:00
Anthony Lapenna 45e7938c5c Merge branch 'release/1.15.5' 2017-12-11 16:03:58 +01:00
Anthony Lapenna fbd9139928 chore(version): bump version number 2017-12-11 16:03:53 +01:00
Anthony Lapenna d0da9860af style(datatables): use normal font weight for table headers (#1496) 2017-12-11 16:03:00 +01:00
Duvel 46d8dba137 style(networks): change the label of the add button (#1495) 2017-12-11 15:50:59 +01:00
Anthony Lapenna 3660f6eeb5 Merge tag '1.15.4' into develop
Release 1.15.4
2017-12-10 10:10:01 +01:00
430 changed files with 7079 additions and 2630 deletions
+36
View File
@@ -0,0 +1,36 @@
package archive
import (
"archive/tar"
"bytes"
)
// TarFileInBuffer will create a tar archive containing a single file named via fileName and using the content
// specified in fileContent. Returns the archive as a byte array.
func TarFileInBuffer(fileContent []byte, fileName string) ([]byte, error) {
var buffer bytes.Buffer
tarWriter := tar.NewWriter(&buffer)
header := &tar.Header{
Name: fileName,
Mode: 0600,
Size: int64(len(fileContent)),
}
err := tarWriter.WriteHeader(header)
if err != nil {
return nil, err
}
_, err = tarWriter.Write(fileContent)
if err != nil {
return nil, err
}
err = tarWriter.Close()
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
+20
View File
@@ -0,0 +1,20 @@
package bolt
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion8() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Extensions = []portainer.EndpointExtension{}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
+7
View File
@@ -89,6 +89,13 @@ func (m *Migrator) Migrate() error {
}
}
if m.CurrentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
+6 -22
View File
@@ -1,7 +1,6 @@
package cli
import (
"log"
"time"
"github.com/portainer/portainer"
@@ -31,12 +30,11 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').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(),
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(),
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
@@ -46,12 +44,12 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
// Deprecated flags
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
}
kingpin.Parse()
@@ -97,8 +95,6 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return errAdminPassExcludeAdminPassFile
}
displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels)
return nil
}
@@ -142,15 +138,3 @@ func validateSyncInterval(syncInterval string) error {
}
return nil
}
func displayDeprecationWarnings(templates, logo string, labels []portainer.Pair) {
if templates != "" {
log.Println("Warning: the --templates / -t flag is deprecated and will be removed in future versions.")
}
if logo != "" {
log.Println("Warning: the --logo flag is deprecated and will be removed in future versions.")
}
if labels != nil {
log.Println("Warning: the --hide-label / -l flag is deprecated and will be removed in future versions.")
}
}
+3 -2
View File
@@ -7,7 +7,7 @@ import (
"github.com/portainer/portainer/cron"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/exec"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/filesystem"
"github.com/portainer/portainer/git"
"github.com/portainer/portainer/http"
"github.com/portainer/portainer/jwt"
@@ -31,7 +31,7 @@ func initCLI() *portainer.CLIFlags {
}
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := file.NewService(dataStorePath, "")
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
}
@@ -218,6 +218,7 @@ func main() {
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
err = store.EndpointService.CreateEndpoint(endpoint)
if err != nil {
+6
View File
@@ -57,6 +57,12 @@ const (
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
)
// Endpoint extensions error
const (
ErrEndpointExtensionNotSupported = Error("This extension is not supported")
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
)
// Version errors.
const (
ErrDBVersionNotFound = Error("DB version not found")
+15 -17
View File
@@ -23,61 +23,59 @@ func NewStackManager(binaryPath string) *StackManager {
}
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) error {
func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
err := runCommandAndCaptureStdErr(command, registryArgs, nil)
if err != nil {
return err
}
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
}
}
if dockerhub.Authentication {
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
err := runCommandAndCaptureStdErr(command, dockerhubArgs, nil)
if err != nil {
return err
}
runCommandAndCaptureStdErr(command, dockerhubArgs, nil, "")
}
return nil
}
// Logout executes the docker logout command.
func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
// Deploy executes the docker stack deploy command.
func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
if prune {
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
} else {
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
}
env := make([]string, 0)
for _, envvar := range stack.Env {
env = append(env, envvar.Name+"="+envvar.Value)
}
return runCommandAndCaptureStdErr(command, args, env)
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
}
// Remove executes the docker stack rm command.
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
func runCommandAndCaptureStdErr(command string, args []string, env []string) error {
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Dir = workingDir
if env != nil {
cmd.Env = os.Environ()
@@ -1,4 +1,4 @@
package file
package filesystem
import (
"bytes"
+9 -20
View File
@@ -35,24 +35,6 @@ func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
return h
}
func (handler *DockerHandler) checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
for _, authorizedUserID := range endpoint.AuthorizedUsers {
if authorizedUserID == userID {
return true
}
}
memberships, _ := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
for _, membership := range memberships {
if membership.TeamID == authorizedTeamID {
return true
}
}
}
return false
}
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
@@ -75,7 +57,14 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && !handler.checkEndpointAccessControl(endpoint, tokenData.ID) {
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
@@ -85,7 +74,7 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
+2
View File
@@ -136,6 +136,7 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
err = handler.EndpointService.CreateEndpoint(endpoint)
@@ -372,6 +373,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
}
handler.ProxyManager.DeleteProxy(string(endpointID))
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
+143
View File
@@ -0,0 +1,143 @@
package handler
import (
"encoding/json"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// ExtensionHandler represents an HTTP API handler for managing Settings.
type ExtensionHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
ProxyManager *proxy.Manager
}
// NewExtensionHandler returns a new instance of ExtensionHandler.
func NewExtensionHandler(bouncer *security.RequestBouncer) *ExtensionHandler {
h := &ExtensionHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/{endpointId}/extensions",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostExtensions))).Methods(http.MethodPost)
h.Handle("/{endpointId}/extensions/{extensionType}",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleDeleteExtensions))).Methods(http.MethodDelete)
return h
}
type (
postExtensionRequest struct {
Type int `valid:"required"`
URL string `valid:"required"`
}
)
func (handler *ExtensionHandler) handlePostExtensions(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["endpointId"])
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(id)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
var req postExtensionRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
extensionType := portainer.EndpointExtensionType(req.Type)
var extension *portainer.EndpointExtension
for _, ext := range endpoint.Extensions {
if ext.Type == extensionType {
extension = &ext
}
}
if extension != nil {
extension.URL = req.URL
} else {
extension = &portainer.EndpointExtension{
Type: extensionType,
URL: req.URL,
}
endpoint.Extensions = append(endpoint.Extensions, *extension)
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, extension, handler.Logger)
}
func (handler *ExtensionHandler) handleDeleteExtensions(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["endpointId"])
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(id)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
extType, err := strconv.Atoi(vars["extensionType"])
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
extensionType := portainer.EndpointExtensionType(extType)
for idx, ext := range endpoint.Extensions {
if ext.Type == extensionType {
endpoint.Extensions = append(endpoint.Extensions[:idx], endpoint.Extensions[idx+1:]...)
}
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
+97
View File
@@ -0,0 +1,97 @@
package extensions
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// StoridgeHandler represents an HTTP API handler for proxying requests to the Docker API.
type StoridgeHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
TeamMembershipService portainer.TeamMembershipService
ProxyManager *proxy.Manager
}
// NewStoridgeHandler returns a new instance of StoridgeHandler.
func NewStoridgeHandler(bouncer *security.RequestBouncer) *StoridgeHandler {
h := &StoridgeHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.PathPrefix("/{id}/extensions/storidge").Handler(
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToStoridgeAPI)))
return h
}
func (handler *StoridgeHandler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
var storidgeExtension *portainer.EndpointExtension
for _, extension := range endpoint.Extensions {
if extension.Type == portainer.StoridgeEndpointExtension {
storidgeExtension = &extension
}
}
if storidgeExtension == nil {
httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionNotSupported, http.StatusInternalServerError, handler.Logger)
return
}
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
var proxy http.Handler
proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r)
}
+11 -3
View File
@@ -8,6 +8,7 @@ import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/handler/extensions"
)
// Handler is a collection of all the service handlers.
@@ -19,6 +20,8 @@ type Handler struct {
EndpointHandler *EndpointHandler
RegistryHandler *RegistryHandler
DockerHubHandler *DockerHubHandler
ExtensionHandler *ExtensionHandler
StoridgeHandler *extensions.StoridgeHandler
ResourceHandler *ResourceHandler
StackHandler *StackHandler
StatusHandler *StatusHandler
@@ -48,11 +51,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
if strings.Contains(r.URL.Path, "/docker/") {
switch {
case strings.Contains(r.URL.Path, "/docker"):
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
} else if strings.Contains(r.URL.Path, "/stacks") {
case strings.Contains(r.URL.Path, "/stacks"):
http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r)
} else {
case strings.Contains(r.URL.Path, "/extensions/storidge"):
http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/extensions"):
http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r)
default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
}
case strings.HasPrefix(r.URL.Path, "/api/registries"):
+4 -4
View File
@@ -5,7 +5,7 @@ import (
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/filesystem"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
@@ -138,11 +138,11 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http
}
if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA)
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
} else {
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
err := handler.FileService.DeleteTLSFiles(file.LDAPStorePath)
err := handler.FileService.DeleteTLSFiles(filesystem.LDAPStorePath)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
}
@@ -169,7 +169,7 @@ func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter
}
if (req.LDAPSettings.TLSConfig.TLS || req.LDAPSettings.StartTLS) && !req.LDAPSettings.TLSConfig.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA)
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
req.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
}
+50 -17
View File
@@ -9,7 +9,7 @@ import (
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/filesystem"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
@@ -37,6 +37,14 @@ type StackHandler struct {
StackManager portainer.StackManager
}
type stackDeploymentConfig struct {
endpoint *portainer.Endpoint
stack *portainer.Stack
prune bool
dockerhub *portainer.DockerHub
registries []portainer.Registry
}
// NewStackHandler returns a new instance of StackHandler.
func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler {
h := &StackHandler{
@@ -78,6 +86,7 @@ type (
putStackRequest struct {
StackFileContent string `valid:"required"`
Env []portainer.Pair `valid:""`
Prune bool `valid:"-"`
}
)
@@ -166,7 +175,7 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter,
ID: portainer.StackID(stackName + "_" + swarmID),
Name: stackName,
SwarmID: swarmID,
EntryPoint: file.ComposeFileDefaultName,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: req.Env,
}
@@ -207,7 +216,14 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter,
return
}
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
config := stackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: false,
}
err = handler.deployStack(&config)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -264,7 +280,7 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
}
if req.PathInRepository == "" {
req.PathInRepository = file.ComposeFileDefaultName
req.PathInRepository = filesystem.ComposeFileDefaultName
}
stacks, err := handler.StackService.Stacks()
@@ -334,7 +350,14 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
return
}
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
config := stackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: false,
}
err = handler.deployStack(&config)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -404,7 +427,7 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r
ID: portainer.StackID(stackName + "_" + swarmID),
Name: stackName,
SwarmID: swarmID,
EntryPoint: file.ComposeFileDefaultName,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: env,
}
@@ -445,7 +468,14 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r
return
}
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
config := stackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: false,
}
err = handler.deployStack(&config)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -637,7 +667,14 @@ func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Reque
return
}
err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries)
config := stackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: req.Prune,
}
err = handler.deployStack(&config)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
@@ -732,22 +769,18 @@ func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Re
}
}
func (handler *StackHandler) deployStack(endpoint *portainer.Endpoint, stack *portainer.Stack, dockerhub *portainer.DockerHub, registries []portainer.Registry) error {
func (handler *StackHandler) deployStack(config *stackDeploymentConfig) error {
handler.stackCreationMutex.Lock()
err := handler.StackManager.Login(dockerhub, registries, endpoint)
handler.StackManager.Login(config.dockerhub, config.registries, config.endpoint)
err := handler.StackManager.Deploy(config.stack, config.prune, config.endpoint)
if err != nil {
handler.stackCreationMutex.Unlock()
return err
}
err = handler.StackManager.Deploy(stack, endpoint)
if err != nil {
handler.stackCreationMutex.Unlock()
return err
}
err = handler.StackManager.Logout(endpoint)
err = handler.StackManager.Logout(config.endpoint)
if err != nil {
handler.stackCreationMutex.Unlock()
return err
+56
View File
@@ -0,0 +1,56 @@
package proxy
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"github.com/portainer/portainer/archive"
)
type postDockerfileRequest struct {
Content string
}
// buildOperation inspects the "Content-Type" header to determine if it needs to alter the request.
// If the value of the header is empty, it means that a Dockerfile is posted via upload, the function
// will extract the file content from the request body, tar it, and rewrite the body.
// If the value of the header contains "application/json", it means that the content of a Dockerfile is posted
// in the request payload as JSON, the function will create a new file called Dockerfile inside a tar archive and
// rewrite the body of the request.
// In any other case, it will leave the request unaltered.
func buildOperation(request *http.Request) error {
contentTypeHeader := request.Header.Get("Content-Type")
if contentTypeHeader != "" && !strings.Contains(contentTypeHeader, "application/json") {
return nil
}
var dockerfileContent []byte
if contentTypeHeader == "" {
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return err
}
dockerfileContent = body
} else {
var req postDockerfileRequest
if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
return err
}
dockerfileContent = []byte(req.Content)
}
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile")
if err != nil {
return err
}
request.Body = ioutil.NopCloser(bytes.NewReader(buffer))
request.ContentLength = int64(len(buffer))
request.Header.Set("Content-Type", "application/x-tar")
return nil
}
+12 -11
View File
@@ -17,14 +17,14 @@ type proxyFactory struct {
SettingsService portainer.SettingsService
}
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler {
u.Scheme = "http"
return factory.createReverseProxy(u)
return newSingleHostReverseProxyWithHostHeader(u)
}
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
u.Scheme = "https"
proxy := factory.createReverseProxy(u)
proxy := factory.createDockerReverseProxy(u)
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
if err != nil {
return nil, err
@@ -34,7 +34,12 @@ func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpo
return proxy, nil
}
func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL) http.Handler {
u.Scheme = "http"
return factory.createDockerReverseProxy(u)
}
func (factory *proxyFactory) newDockerSocketProxy(path string) http.Handler {
proxy := &socketProxy{}
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
@@ -46,13 +51,13 @@ func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
return proxy
}
func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReverseProxy {
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL) *httputil.ReverseProxy {
proxy := newSingleHostReverseProxyWithHostHeader(u)
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
TeamMembershipService: factory.TeamMembershipService,
SettingsService: factory.SettingsService,
dockerTransport: newHTTPTransport(),
dockerTransport: &http.Transport{},
}
proxy.Transport = transport
return proxy
@@ -65,7 +70,3 @@ func newSocketTransport(socketPath string) *http.Transport {
},
}
}
func newHTTPTransport() *http.Transport {
return &http.Transport{}
}
+40 -6
View File
@@ -3,6 +3,7 @@ package proxy
import (
"net/http"
"net/url"
"strings"
"github.com/orcaman/concurrent-map"
"github.com/portainer/portainer"
@@ -10,14 +11,16 @@ import (
// Manager represents a service used to manage Docker proxies.
type Manager struct {
proxyFactory *proxyFactory
proxies cmap.ConcurrentMap
proxyFactory *proxyFactory
proxies cmap.ConcurrentMap
extensionProxies cmap.ConcurrentMap
}
// NewManager initializes a new proxy Service
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager {
return &Manager{
proxies: cmap.New(),
proxies: cmap.New(),
extensionProxies: cmap.New(),
proxyFactory: &proxyFactory{
ResourceControlService: resourceControlService,
TeamMembershipService: teamMembershipService,
@@ -38,16 +41,16 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
if endpointURL.Scheme == "tcp" {
if endpoint.TLSConfig.TLS {
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, endpoint)
if err != nil {
return nil, err
}
} else {
proxy = manager.proxyFactory.newHTTPProxy(endpointURL)
proxy = manager.proxyFactory.newDockerHTTPProxy(endpointURL)
}
} else {
// Assume unix:// scheme
proxy = manager.proxyFactory.newSocketProxy(endpointURL.Path)
proxy = manager.proxyFactory.newDockerSocketProxy(endpointURL.Path)
}
manager.proxies.Set(string(endpoint.ID), proxy)
@@ -67,3 +70,34 @@ func (manager *Manager) GetProxy(key string) http.Handler {
func (manager *Manager) DeleteProxy(key string) {
manager.proxies.Remove(key)
}
// CreateAndRegisterExtensionProxy creates a new HTTP reverse proxy for an extension and adds it to the registered proxies.
func (manager *Manager) CreateAndRegisterExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
extensionURL, err := url.Parse(extensionAPIURL)
if err != nil {
return nil, err
}
proxy := manager.proxyFactory.newExtensionHTTPPRoxy(extensionURL)
manager.extensionProxies.Set(key, proxy)
return proxy, nil
}
// GetExtensionProxy returns the extension proxy associated to a key
func (manager *Manager) GetExtensionProxy(key string) http.Handler {
proxy, ok := manager.extensionProxies.Get(key)
if !ok {
return nil
}
return proxy.(http.Handler)
}
// DeleteExtensionProxies deletes all the extension proxies associated to a key
func (manager *Manager) DeleteExtensionProxies(key string) {
for _, k := range manager.extensionProxies.Keys() {
if strings.Contains(k, key+"_") {
manager.extensionProxies.Remove(k)
}
}
}
+16
View File
@@ -27,6 +27,7 @@ type (
labelBlackList []portainer.Pair
}
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
operationRequest func(*http.Request) error
)
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
@@ -59,6 +60,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
return p.proxyNodeRequest(request)
case strings.HasPrefix(path, "/tasks"):
return p.proxyTaskRequest(request)
case strings.HasPrefix(path, "/build"):
return p.proxyBuildRequest(request)
default:
return p.executeDockerRequest(request)
}
@@ -228,6 +231,10 @@ func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response
}
}
func (p *proxyTransport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
return p.interceptAndRewriteRequest(request, buildOperation)
}
// restrictedOperation ensures that the current user has the required authorizations
// before executing the original request.
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
@@ -300,6 +307,15 @@ func (p *proxyTransport) rewriteOperation(request *http.Request, operation restr
return p.executeRequestAndRewriteResponse(request, operation, executor)
}
func (p *proxyTransport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) {
err := operation(request)
if err != nil {
return nil, err
}
return p.executeDockerRequest(request)
}
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
response, err := p.executeDockerRequest(request)
if err != nil {
+19
View File
@@ -121,3 +121,22 @@ func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedReques
}
return false
}
// AuthorizedEndpointAccess ensure that the user can access the specified endpoint.
// It will check if the user is part of the authorized users or part of a team that is
// listed in the authorized teams.
func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
for _, authorizedUserID := range endpoint.AuthorizedUsers {
if authorizedUserID == userID {
return true
}
}
for _, membership := range memberships {
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
if membership.TeamID == authorizedTeamID {
return true
}
}
}
return false
}
+12 -1
View File
@@ -12,6 +12,7 @@ type (
// RequestBouncer represents an entity that manages API request accesses
RequestBouncer struct {
jwtService portainer.JWTService
userService portainer.UserService
teamMembershipService portainer.TeamMembershipService
authDisabled bool
}
@@ -27,9 +28,10 @@ type (
)
// NewRequestBouncer initializes a new RequestBouncer
func NewRequestBouncer(jwtService portainer.JWTService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
func NewRequestBouncer(jwtService portainer.JWTService, userService portainer.UserService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
return &RequestBouncer{
jwtService: jwtService,
userService: userService,
teamMembershipService: teamMembershipService,
authDisabled: authDisabled,
}
@@ -136,6 +138,15 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
httperror.WriteErrorResponse(w, err, http.StatusUnauthorized, nil)
return
}
_, err = bouncer.userService.User(tokenData.ID)
if err != nil && err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
return
}
} else {
tokenData = &portainer.TokenData{
Role: portainer.AdministratorRole,
+11 -1
View File
@@ -3,6 +3,7 @@ package http
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/handler"
"github.com/portainer/portainer/http/handler/extensions"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
@@ -40,7 +41,7 @@ type Server struct {
// Start starts the HTTP server
func (server *Server) Start() error {
requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled)
requestBouncer := security.NewRequestBouncer(server.JWTService, server.UserService, server.TeamMembershipService, server.AuthDisabled)
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService)
var fileHandler = handler.NewFileHandler(filepath.Join(server.AssetsPath, "public"))
@@ -96,6 +97,13 @@ func (server *Server) Start() error {
stackHandler.GitService = server.GitService
stackHandler.RegistryService = server.RegistryService
stackHandler.DockerHubService = server.DockerHubService
var extensionHandler = handler.NewExtensionHandler(requestBouncer)
extensionHandler.EndpointService = server.EndpointService
extensionHandler.ProxyManager = proxyManager
var storidgeHandler = extensions.NewStoridgeHandler(requestBouncer)
storidgeHandler.EndpointService = server.EndpointService
storidgeHandler.TeamMembershipService = server.TeamMembershipService
storidgeHandler.ProxyManager = proxyManager
server.Handler = &handler.Handler{
AuthHandler: authHandler,
@@ -114,6 +122,8 @@ func (server *Server) Start() error {
WebSocketHandler: websocketHandler,
FileHandler: fileHandler,
UploadHandler: uploadHandler,
ExtensionHandler: extensionHandler,
StoridgeHandler: storidgeHandler,
}
if server.SSL {
+35 -19
View File
@@ -12,13 +12,17 @@ type (
// CLIFlags represents the available flags on the CLI.
CLIFlags struct {
Addr *string
AdminPassword *string
AdminPasswordFile *string
Assets *string
Data *string
ExternalEndpoints *string
SyncInterval *string
Endpoint *string
ExternalEndpoints *string
Labels *[]Pair
Logo *string
NoAuth *bool
NoAnalytics *bool
Templates *string
TLSVerify *bool
TLSCacert *string
TLSCert *string
@@ -26,12 +30,7 @@ type (
SSL *bool
SSLCert *string
SSLKey *string
AdminPassword *string
AdminPasswordFile *string
// Deprecated fields
Logo *string
Templates *string
Labels *[]Pair
SyncInterval *string
}
// Status represents the application status.
@@ -172,13 +171,14 @@ type (
// Endpoint represents a Docker endpoint with all the info required
// to connect to it.
Endpoint struct {
ID EndpointID `json:"Id"`
Name string `json:"Name"`
URL string `json:"URL"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
ID EndpointID `json:"Id"`
Name string `json:"Name"`
URL string `json:"URL"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
Extensions []EndpointExtension `json:"Extensions"`
// Deprecated fields
// Deprecated in DBVersion == 4
@@ -188,6 +188,16 @@ type (
TLSKeyPath string `json:"TLSKey,omitempty"`
}
// EndpointExtension represents a extension associated to an endpoint.
EndpointExtension struct {
Type EndpointExtensionType `json:"Type"`
URL string `json:"URL"`
}
// EndpointExtensionType represents the type of an endpoint extension. Only
// one extension of each type can be associated to an endpoint.
EndpointExtensionType int
// ResourceControlID represents a resource control identifier.
ResourceControlID int
@@ -381,18 +391,18 @@ type (
// StackManager represents a service to manage stacks.
StackManager interface {
Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) error
Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint)
Logout(endpoint *Endpoint) error
Deploy(stack *Stack, endpoint *Endpoint) error
Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
Remove(stack *Stack, endpoint *Endpoint) error
}
)
const (
// APIVersion is the version number of the Portainer API.
APIVersion = "1.15.4"
APIVersion = "1.16.4"
// DBVersion is the version number of the Portainer database.
DBVersion = 7
DBVersion = 8
// DefaultTemplatesURL represents the default URL for the templates definitions.
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
)
@@ -453,3 +463,9 @@ const (
// ConfigResourceControl represents a resource control associated to a Docker config
ConfigResourceControl
)
const (
_ EndpointExtensionType = iota
// StoridgeEndpointExtension represents the Storidge extension
StoridgeEndpointExtension
)
+386 -11
View File
@@ -56,7 +56,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.15.4"
version: "1.16.4"
title: "Portainer API"
contact:
email: "info@portainer.io"
@@ -77,6 +77,8 @@ tags:
description: "Manage Portainer settings"
- name: "status"
description: "Information about the Portainer instance"
- name: "stacks"
description: "Manage Docker stacks"
- name: "users"
description: "Manage users"
- name: "teams"
@@ -436,6 +438,285 @@ paths:
schema:
$ref: "#/definitions/GenericError"
/endpoints/{endpointId}/stacks:
get:
tags:
- "stacks"
summary: "List stacks"
description: |
List all stacks based on the current user authorizations.
Will return all stacks if using an administrator account otherwise it
will only return the list of stacks the user have access to.
**Access policy**: restricted
operationId: "StackList"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
responses:
200:
description: "Success"
schema:
$ref: "#/definitions/StackListResponse"
403:
description: "Unauthorized"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Access denied to resource"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
post:
tags:
- "stacks"
summary: "Deploy a new stack"
description: |
Deploy a new stack into a Docker environment specified via the endpoint identifier.
**Access policy**: restricted
operationId: "StackCreate"
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "method"
in: "query"
description: "Stack deployment method. Possible values: string or repository."
required: true
type: "string"
- in: "body"
name: "body"
description: "Stack details. Used when"
required: true
schema:
$ref: "#/definitions/StackCreateRequest"
responses:
200:
description: "Success"
schema:
$ref: "#/definitions/StackCreateResponse"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request data format"
404:
description: "Endpoint not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Endpoint not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
/endpoints/{endpointId}/stacks/{id}:
get:
tags:
- "stacks"
summary: "Inspect a stack"
description: |
Retrieve details about a stack.
**Access policy**: restricted
operationId: "StackInspect"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "id"
in: "path"
description: "Stack identifier"
required: true
type: "string"
responses:
200:
description: "Success"
schema:
$ref: "#/definitions/Stack"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request"
403:
description: "Unauthorized"
schema:
$ref: "#/definitions/GenericError"
404:
description: "Stack not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Stack not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
put:
tags:
- "stacks"
summary: "Update a stack"
description: |
Update a stack.
**Access policy**: restricted
operationId: "StackUpdate"
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "id"
in: "path"
description: "Stack identifier"
required: true
type: "string"
- in: "body"
name: "body"
description: "Stack details"
required: true
schema:
$ref: "#/definitions/StackUpdateRequest"
responses:
200:
description: "Success"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request data format"
404:
description: "Stack not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Stack not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
delete:
tags:
- "stacks"
summary: "Remove a stack"
description: |
Remove a stack.
**Access policy**: restricted
operationId: "StackDelete"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "id"
in: "path"
description: "Stack identifier"
required: true
type: "string"
responses:
200:
description: "Success"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request"
403:
description: "Unauthorized"
schema:
$ref: "#/definitions/GenericError"
404:
description: "Stack not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Stack not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
/endpoints/{endpointId}/stacks/{id}/stackfile:
get:
tags:
- "stacks"
summary: "Retrieve the content of the Stack file for the specified stack"
description: |
Get Stack file content.
**Access policy**: restricted
operationId: "StackFileInspect"
produces:
- "application/json"
parameters:
- name: "endpointId"
in: "path"
description: "Endpoint identifier"
required: true
type: "integer"
- name: "id"
in: "path"
description: "Stack identifier"
required: true
type: "string"
responses:
200:
description: "Success"
schema:
$ref: "#/definitions/StackFileInspectResponse"
400:
description: "Invalid request"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Invalid request"
403:
description: "Unauthorized"
schema:
$ref: "#/definitions/GenericError"
404:
description: "Stack not found"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Stack not found"
500:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
/registries:
get:
tags:
@@ -536,7 +817,7 @@ paths:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Endpoint not found"
err: "Registry not found"
500:
description: "Server error"
schema:
@@ -593,13 +874,6 @@ paths:
description: "Server error"
schema:
$ref: "#/definitions/GenericError"
503:
description: "Endpoint management disabled"
schema:
$ref: "#/definitions/GenericError"
examples:
application/json:
err: "Endpoint management is disabled"
delete:
tags:
- "registries"
@@ -1869,7 +2143,7 @@ definitions:
description: "Is analytics enabled"
Version:
type: "string"
example: "1.15.4"
example: "1.16.4"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"
@@ -1883,7 +2157,7 @@ definitions:
DisplayDonationHeader:
type: "boolean"
example: true
description: "Whether to display or not the donation message in the header."\
description: "Whether to display or not the donation message in the header."
DisplayExternalContributors:
type: "boolean"
example: false
@@ -2612,3 +2886,104 @@ definitions:
type: "string"
example: "nginx:latest"
description: "The Docker image associated to the template"
StackCreateRequest:
type: "object"
required:
- "Name"
- "SwarmID"
properties:
Name:
type: "string"
example: "myStack"
description: "Name of the stack"
SwarmID:
type: "string"
example: "jpofkc0i9uo9wtx1zesuk649w"
description: "Cluster identifier of the Swarm cluster"
StackFileContent:
type: "string"
example: "version: 3\n services:\n web:\n image:nginx"
description: "Content of the Stack file. Required when using the 'string' deployment method."
GitRepository:
type: "string"
example: "https://github.com/openfaas/faas"
description: "URL of a public Git repository hosting the Stack file. Required when using the 'repository' deployment method."
PathInRepository:
type: "string"
example: "docker-compose.yml"
description: "Path to the Stack file inside the Git repository. Required when using the 'repository' deployment method."
Env:
type: "array"
description: "A list of environment variables used during stack deployment"
items:
$ref: "#/definitions/Stack_Env"
Stack_Env:
properties:
name:
type: "string"
example: "MYSQL_ROOT_PASSWORD"
value:
type: "string"
example: "password"
StackCreateResponse:
type: "object"
properties:
Id:
type: "string"
example: "myStack_jpofkc0i9uo9wtx1zesuk649w"
description: "Id of the stack"
StackListResponse:
type: "array"
items:
$ref: "#/definitions/Stack"
Stack:
type: "object"
properties:
Id:
type: "string"
example: "myStack_jpofkc0i9uo9wtx1zesuk649w"
description: "Stack identifier"
Name:
type: "string"
example: "myStack"
description: "Stack name"
EntryPoint:
type: "string"
example: "docker-compose.yml"
description: "Path to the Stack file"
SwarmID:
type: "string"
example: "jpofkc0i9uo9wtx1zesuk649w"
description: "Cluster identifier of the Swarm cluster where the stack is deployed"
ProjectPath:
type: "string"
example: "/data/compose/myStack_jpofkc0i9uo9wtx1zesuk649w"
description: "Path on disk to the repository hosting the Stack file"
Env:
type: "array"
description: "A list of environment variables used during stack deployment"
items:
$ref: "#/definitions/Stack_Env"
StackUpdateRequest:
type: "object"
properties:
StackFileContent:
type: "string"
example: "version: 3\n services:\n web:\n image:nginx"
description: "New content of the Stack file."
Env:
type: "array"
description: "A list of environment variables used during stack deployment"
items:
$ref: "#/definitions/Stack_Env"
Prune:
type: "boolean"
example: false
description: "Prune services that are no longer referenced"
StackFileInspectResponse:
type: "object"
properties:
StackFileContent:
type: "string"
example: "version: 3\n services:\n web:\n image:nginx"
description: "Content of the Stack file."
+8 -60
View File
@@ -5,70 +5,18 @@ angular.module('portainer', [
'ngCookies',
'ngSanitize',
'ngFileUpload',
'ngMessages',
'ngResource',
'angularUtils.directives.dirPagination',
'LocalStorageModule',
'angular-jwt',
'angular-google-analytics',
'ui',
'angular-json-tree',
'angular-loading-bar',
'angular-clipboard',
'luegg.directives',
'portainer.templates',
'portainer.filters',
'portainer.rest',
'portainer.helpers',
'portainer.services',
'auth',
'dashboard',
'config',
'configs',
'container',
'containerConsole',
'containerLogs',
'containerStats',
'containerInspect',
'serviceLogs',
'containers',
'createConfig',
'createContainer',
'createNetwork',
'createRegistry',
'createSecret',
'createService',
'createVolume',
'createStack',
'engine',
'endpoint',
'endpointAccess',
'endpoints',
'events',
'image',
'images',
'initAdmin',
'initEndpoint',
'main',
'network',
'networks',
'node',
'registries',
'registry',
'registryAccess',
'secrets',
'secret',
'service',
'services',
'settings',
'settingsAuthentication',
'sidebar',
'stack',
'stacks',
'swarm',
'swarmVisualizer',
'task',
'team',
'teams',
'templates',
'user',
'users',
'userSettings',
'volume',
'volumes',
'portainer.app',
'portainer.docker',
'extension.storidge',
'rzModule']);
+3 -3
View File
@@ -7,7 +7,7 @@ angular.module('portainer')
StateManager.initialize()
.then(function success(state) {
if (state.application.authentication) {
initAuthentication(authManager, Authentication, $rootScope);
initAuthentication(authManager, Authentication, $rootScope, $state);
}
if (state.application.analytics) {
initAnalytics(Analytics, $rootScope);
@@ -30,12 +30,12 @@ angular.module('portainer')
}]);
function initAuthentication(authManager, Authentication, $rootScope) {
function initAuthentication(authManager, Authentication, $rootScope, $state) {
authManager.checkAuthOnRefresh();
authManager.redirectWhenUnauthenticated();
Authentication.init();
$rootScope.$on('tokenHasExpired', function() {
$state.go('auth', {error: 'Your session has expired'});
$state.go('portainer.auth', {error: 'Your session has expired'});
});
}
@@ -1,70 +0,0 @@
angular.module('containerLogs', [])
.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container',
function ($scope, $transition$, $anchorScroll, ContainerLogs, Container) {
$scope.state = {};
$scope.state.displayTimestampsOut = false;
$scope.state.displayTimestampsErr = false;
$scope.stdout = '';
$scope.stderr = '';
$scope.tailLines = 2000;
Container.get({id: $transition$.params().id}, function (d) {
$scope.container = d;
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve container info');
});
function getLogs() {
getLogsStdout();
getLogsStderr();
}
function getLogsStderr() {
ContainerLogs.get($transition$.params().id, {
stdout: 0,
stderr: 1,
timestamps: $scope.state.displayTimestampsErr,
tail: $scope.tailLines
}, function (data, status, headers, config) {
// Replace carriage returns with newlines to clean up output
data = data.replace(/[\r]/g, '\n');
// Strip 8 byte header from each line of output
data = data.substring(8);
data = data.replace(/\n(.{8})/g, '\n');
$scope.stderr = data;
});
}
function getLogsStdout() {
ContainerLogs.get($transition$.params().id, {
stdout: 1,
stderr: 0,
timestamps: $scope.state.displayTimestampsOut,
tail: $scope.tailLines
}, function (data, status, headers, config) {
// Replace carriage returns with newlines to clean up output
data = data.replace(/[\r]/g, '\n');
// Strip 8 byte header from each line of output
data = data.substring(8);
data = data.replace(/\n(.{8})/g, '\n');
$scope.stdout = data;
});
}
// initial call
getLogs();
var logIntervalId = window.setInterval(getLogs, 5000);
$scope.$on('$destroy', function () {
// clearing interval when view changes
clearInterval(logIntervalId);
});
$scope.toggleTimestampsOut = function () {
getLogsStdout();
};
$scope.toggleTimestampsErr = function () {
getLogsStderr();
};
}]);
@@ -1,54 +0,0 @@
<rd-header>
<rd-header-title title="Container logs"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Logs
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-server"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">Name</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-info-circle" title="Stdout logs"></rd-widget-header>
<rd-widget-taskbar>
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
<label for="displayAllTsOut">Display timestamps</label>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="panel-body">
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title="Stderr logs"></rd-widget-header>
<rd-widget-taskbar>
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
<label for="displayAllTsErr">Display timestamps</label>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="panel-body">
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
-144
View File
@@ -1,144 +0,0 @@
<rd-header>
<rd-header-title title="Network list">
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Networks</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<networks-datatable
title="Networks" title-icon="fa-sitemap"
dataset="networks" table-key="networks"
order-by="Name" show-text-filter="true"
remove-action="removeAction"
show-ownership-column="applicationState.application.authentication"
></networks-datatable>
</div>
</div>
<!-- <div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="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-taskbar classes="col-lg-12">
<div class="pull-left">
<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>
<a class="btn btn-primary" type="button" ui-sref="actions.create.network"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add network</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a 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>
</a>
</th>
<th>
<a ng-click="order('StackName')">
Stack
<span ng-show="sortType == 'StackName' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'StackName' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('Scope')">
Scope
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('Driver')">
Driver
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('IPAM.Driver')">
IPAM Driver
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('IPAM.Config[0].Subnet')">
IPAM Subnet
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('IPAM.Config[0].Gateway')">
IPAM Gateway
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: network.Checked}">
<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.StackName ? network.StackName : '-' }}</td>
<td>{{ network.Scope }}</td>
<td>{{ network.Driver }}</td>
<td>{{ network.IPAM.Driver }}</td>
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="network.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ network.ResourceControl.Ownership ? network.ResourceControl.Ownership : network.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!networks">
<td colspan="9" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="networks.length == 0">
<td colspan="9" class="text-center text-muted">No networks available.</td>
</tr>
</tbody>
</table>
<div ng-if="networks" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div> -->
@@ -1,78 +0,0 @@
angular.module('serviceLogs', [])
.controller('ServiceLogsController', ['$scope', '$transition$', '$anchorScroll', 'ServiceLogs', 'Service',
function ($scope, $transition$, $anchorScroll, ServiceLogs, Service) {
$scope.state = {};
$scope.state.displayTimestampsOut = false;
$scope.state.displayTimestampsErr = false;
$scope.stdout = '';
$scope.stderr = '';
$scope.tailLines = 2000;
function getLogs() {
getLogsStdout();
getLogsStderr();
}
function getLogsStderr() {
ServiceLogs.get($transition$.params().id, {
stdout: 0,
stderr: 1,
timestamps: $scope.state.displayTimestampsErr,
tail: $scope.tailLines
}, function (data, status, headers, config) {
// Replace carriage returns with newlines to clean up output
data = data.replace(/[\r]/g, '\n');
// Strip 8 byte header from each line of output
data = data.substring(8);
data = data.replace(/\n(.{8})/g, '\n');
$scope.stderr = data;
});
}
function getLogsStdout() {
ServiceLogs.get($transition$.params().id, {
stdout: 1,
stderr: 0,
timestamps: $scope.state.displayTimestampsOut,
tail: $scope.tailLines
}, function (data, status, headers, config) {
// Replace carriage returns with newlines to clean up output
data = data.replace(/[\r]/g, '\n');
// Strip 8 byte header from each line of output
data = data.substring(8);
data = data.replace(/\n(.{8})/g, '\n');
$scope.stdout = data;
});
}
function getService() {
Service.get({id: $transition$.params().id}, function (d) {
$scope.service = d;
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve service info');
});
}
function initView() {
getService();
getLogs();
var logIntervalId = window.setInterval(getLogs, 5000);
$scope.$on('$destroy', function () {
// clearing interval when view changes
clearInterval(logIntervalId);
});
$scope.toggleTimestampsOut = function () {
getLogsStdout();
};
$scope.toggleTimestampsErr = function () {
getLogsStderr();
};
}
initView();
}]);
@@ -1,54 +0,0 @@
<rd-header>
<rd-header-title title="Service logs"></rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > <a ui-sref="service({id: service.ID})">{{ service.Spec.Name }}</a> > Logs
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-list-alt"></i>
</div>
<div class="title">{{ service.Spec.Name }}</div>
<div class="comment">Name</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-info-circle" title="Stdout logs"></rd-widget-header>
<rd-widget-taskbar>
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
<label for="displayAllTsOut">Display timestamps</label>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="panel-body">
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title="Stderr logs"></rd-widget-header>
<rd-widget-taskbar>
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
<label for="displayAllTsErr">Display timestamps</label>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="panel-body">
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
-90
View File
@@ -1,90 +0,0 @@
<!-- Sidebar -->
<div id="sidebar-wrapper">
<div class="sidebar-header">
<a ng-click="toggleSidebar()" class="interactive">
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
<span class="menu-icon glyphicon glyphicon-transfer"></span>
</a>
</div>
<div class="sidebar-content">
<ul class="sidebar">
<li class="sidebar-title"><span>Active endpoint</span></li>
<li class="sidebar-title">
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)">
</select>
</li>
<li class="sidebar-title"><span>Endpoint actions</span></li>
<li class="sidebar-list">
<a ui-sref="dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="toggle && displayExternalContributors && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')">
<a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a>
</div>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="images" ui-sref-active="active">Images <span class="menu-icon fa fa-clone fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="networks" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.30 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="configs" ui-sref-active="active">Configs <span class="menu-icon fa fa-file-code-o fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="(applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC') && (!applicationState.application.authentication || isAdmin)">
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
</li>
<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 fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
<a ui-sref="engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th fa-fw"></span></a>
</li>
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
<span>Portainer settings</span>
</li>
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
<a ui-sref="users" ui-sref-active="active">User management <span class="menu-icon fa fa-users fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'users' || $state.current.name === 'user' || $state.current.name === 'teams' || $state.current.name === 'team')">
<a ui-sref="teams" ui-sref-active="active">Teams</a>
</div>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="settings" ui-sref-active="active">Settings <span class="menu-icon fa fa-cogs fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'settings' || $state.current.name === 'settings_authentication' || $state.current.name === 'settings_about') && applicationState.application.authentication && isAdmin">
<a ui-sref="settings_authentication" ui-sref-active="active">Authentication</a></div>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'settings' || $state.current.name === 'settings_authentication' || $state.current.name === 'settings_about')">
<a ui-sref="settings_about" ui-sref-active="active">About</a>
</div>
</li>
</ul>
<div class="sidebar-footer-content">
<img src="images/logo_small.png" class="img-responsive logo" alt="Portainer">
<span class="version">{{ uiVersion }}</span>
</div>
</div>
</div>
<!-- End Sidebar -->
@@ -1,70 +0,0 @@
angular.module('swarmVisualizer', [])
.controller('SwarmVisualizerController', ['$q', '$scope', '$document', 'NodeService', 'ServiceService', 'TaskService', 'Notifications',
function ($q, $scope, $document, NodeService, ServiceService, TaskService, Notifications) {
$scope.state = {
ShowInformationPanel: true,
DisplayOnlyRunningTasks: false
};
function assignServiceName(services, tasks) {
for (var i = 0; i < services.length; i++) {
var service = services[i];
for (var j = 0; j < tasks.length; j++) {
var task = tasks[j];
if (task.ServiceId === service.Id) {
task.ServiceName = service.Name;
}
}
}
}
function assignTasksToNode(nodes, tasks) {
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
node.Tasks = [];
for (var j = 0; j < tasks.length; j++) {
var task = tasks[j];
if (task.NodeId === node.Id) {
node.Tasks.push(task);
}
}
}
}
function prepareVisualizerData(nodes, services, tasks) {
var visualizerData = {};
assignServiceName(services, tasks);
assignTasksToNode(nodes, tasks);
visualizerData.nodes = nodes;
$scope.visualizerData = visualizerData;
}
function initView() {
$q.all({
nodes: NodeService.nodes(),
services: ServiceService.services(),
tasks: TaskService.tasks()
})
.then(function success(data) {
var nodes = data.nodes;
$scope.nodes = nodes;
var services = data.services;
$scope.services = services;
var tasks = data.tasks;
$scope.tasks = tasks;
prepareVisualizerData(nodes, services, tasks);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to initialize cluster visualizer');
});
}
initView();
}]);
+5 -4
View File
@@ -1,6 +1,6 @@
angular.module('portainer')
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', 'cfpLoadingBarProvider',
function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) {
.config(['$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', 'cfpLoadingBarProvider',
function ($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) {
'use strict';
var environment = '@@ENVIRONMENT';
@@ -16,7 +16,7 @@ angular.module('portainer')
return LocalStorage.getJWT();
}],
unauthenticatedRedirector: ['$state', function($state) {
$state.go('auth', {error: 'Your session has expired'});
$state.go('portainer.auth', {error: 'Your session has expired'});
}]
});
$httpProvider.interceptors.push('jwtInterceptor');
@@ -26,6 +26,8 @@ angular.module('portainer')
toastr.options.timeOut = 3000;
Terminal.applyAddon(fit);
$uibTooltipProvider.setTriggers({
'mouseenter': 'mouseleave',
'click': 'click',
@@ -37,5 +39,4 @@ angular.module('portainer')
cfpLoadingBarProvider.parentSelector = '#loadingbar-placeholder';
$urlRouterProvider.otherwise('/auth');
configureRoutes($stateProvider);
}]);
+2 -1
View File
@@ -11,4 +11,5 @@ angular.module('portainer')
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
.constant('PAGINATION_MAX_ITEMS', 10);
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('APPLICATION_CACHE_VALIDITY', 3600);
@@ -1,8 +0,0 @@
angular.module('portainer').component('porAccessManagement', {
templateUrl: 'app/directives/accessManagement/porAccessManagement.html',
controller: 'porAccessManagementController',
bindings: {
accessControlledEntity: '<',
updateAccess: '&'
}
});
@@ -1,9 +0,0 @@
angular.module('portainer').component('porImageRegistry', {
templateUrl: 'app/directives/imageRegistry/porImageRegistry.html',
controller: 'porImageRegistryController',
bindings: {
'image': '=',
'registry': '=',
'autoComplete': '<'
}
});
-1
View File
@@ -1 +0,0 @@
angular.module('ui', []);
+508
View File
@@ -0,0 +1,508 @@
angular.module('portainer.docker', ['portainer.app'])
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
'use strict';
var docker = {
name: 'docker',
parent: 'root',
abstract: true
};
var configs = {
name: 'docker.configs',
url: '/configs',
views: {
'content@': {
templateUrl: 'app/docker/views/configs/configs.html',
controller: 'ConfigsController'
}
}
};
var config = {
name: 'docker.configs.config',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/configs/edit/config.html',
controller: 'ConfigController'
}
}
};
var configCreation = {
name: 'docker.configs.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/configs/create/createconfig.html',
controller: 'CreateConfigController'
}
}
};
var containers = {
name: 'docker.containers',
url: '/containers',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/containers.html',
controller: 'ContainersController'
}
},
params: {
selectedContainers: []
}
};
var container = {
name: 'docker.containers.container',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/edit/container.html',
controller: 'ContainerController'
}
}
};
var containerConsole = {
name: 'docker.containers.container.console',
url: '/console',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/console/containerconsole.html',
controller: 'ContainerConsoleController'
}
}
};
var containerCreation = {
name: 'docker.containers.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/create/createcontainer.html',
controller: 'CreateContainerController'
}
},
params: {
from: ''
}
};
var containerInspect = {
name: 'docker.containers.container.inspect',
url: '/inspect',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/inspect/containerinspect.html',
controller: 'ContainerInspectController'
}
}
};
var containerLogs = {
name: 'docker.containers.container.logs',
url: '/logs',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/logs/containerlogs.html',
controller: 'ContainerLogsController'
}
}
};
var containerStats = {
name: 'docker.containers.container.stats',
url: '/stats',
views: {
'content@': {
templateUrl: 'app/docker/views/containers/stats/containerstats.html',
controller: 'ContainerStatsController'
}
}
};
var dashboard = {
name: 'docker.dashboard',
url: '/dashboard',
views: {
'content@': {
templateUrl: 'app/docker/views/dashboard/dashboard.html',
controller: 'DashboardController'
}
}
};
var engine = {
name: 'docker.engine',
url: '/engine',
views: {
'content@': {
templateUrl: 'app/docker/views/engine/engine.html',
controller: 'EngineController'
}
}
};
var events = {
name: 'docker.events',
url: '/events',
views: {
'content@': {
templateUrl: 'app/docker/views/events/events.html',
controller: 'EventsController'
}
}
};
var images = {
name: 'docker.images',
url: '/images',
views: {
'content@': {
templateUrl: 'app/docker/views/images/images.html',
controller: 'ImagesController'
}
}
};
var image = {
name: 'docker.images.image',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/images/edit/image.html',
controller: 'ImageController'
}
}
};
var imageBuild = {
name: 'docker.images.build',
url: '/build',
views: {
'content@': {
templateUrl: 'app/docker/views/images/build/buildimage.html',
controller: 'BuildImageController'
}
}
};
var networks = {
name: 'docker.networks',
url: '/networks',
views: {
'content@': {
templateUrl: 'app/docker/views/networks/networks.html',
controller: 'NetworksController'
}
}
};
var network = {
name: 'docker.networks.network',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/networks/edit/network.html',
controller: 'NetworkController'
}
}
};
var networkCreation = {
name: 'docker.networks.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/networks/create/createnetwork.html',
controller: 'CreateNetworkController'
}
}
};
var nodes = {
name: 'docker.nodes',
url: '/nodes',
abstract: true
};
var node = {
name: 'docker.nodes.node',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/nodes/edit/node.html',
controller: 'NodeController'
}
}
};
var secrets = {
name: 'docker.secrets',
url: '/secrets',
views: {
'content@': {
templateUrl: 'app/docker/views/secrets/secrets.html',
controller: 'SecretsController'
}
}
};
var secret = {
name: 'docker.secrets.secret',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/secrets/edit/secret.html',
controller: 'SecretController'
}
}
};
var secretCreation = {
name: 'docker.secrets.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/secrets/create/createsecret.html',
controller: 'CreateSecretController'
}
}
};
var services = {
name: 'docker.services',
url: '/services',
views: {
'content@': {
templateUrl: 'app/docker/views/services/services.html',
controller: 'ServicesController'
}
}
};
var service = {
name: 'docker.services.service',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/services/edit/service.html',
controller: 'ServiceController'
}
}
};
var serviceCreation = {
name: 'docker.services.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/services/create/createservice.html',
controller: 'CreateServiceController'
}
}
};
var serviceLogs = {
name: 'docker.services.service.logs',
url: '/logs',
views: {
'content@': {
templateUrl: 'app/docker/views/services/logs/servicelogs.html',
controller: 'ServiceLogsController'
}
}
};
var stacks = {
name: 'docker.stacks',
url: '/stacks',
views: {
'content@': {
templateUrl: 'app/docker/views/stacks/stacks.html',
controller: 'StacksController'
}
}
};
var stack = {
name: 'docker.stacks.stack',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/stacks/edit/stack.html',
controller: 'StackController'
}
}
};
var stackCreation = {
name: 'docker.stacks.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/stacks/create/createstack.html',
controller: 'CreateStackController'
}
}
};
var swarm = {
name: 'docker.swarm',
url: '/swarm',
views: {
'content@': {
templateUrl: 'app/docker/views/swarm/swarm.html',
controller: 'SwarmController'
}
}
};
var swarmVisualizer = {
name: 'docker.swarm.visualizer',
url: '/visualizer',
views: {
'content@': {
templateUrl: 'app/docker/views/swarm/visualizer/swarmvisualizer.html',
controller: 'SwarmVisualizerController'
}
}
};
var tasks = {
name: 'docker.tasks',
url: '/tasks',
abstract: true
};
var task = {
name: 'docker.tasks.task',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/tasks/edit/task.html',
controller: 'TaskController'
}
}
};
var taskLogs = {
name: 'docker.tasks.task.logs',
url: '/logs',
views: {
'content@': {
templateUrl: 'app/docker/views/tasks/logs/tasklogs.html',
controller: 'TaskLogsController'
}
}
};
var templates = {
name: 'docker.templates',
url: '/templates',
views: {
'content@': {
templateUrl: 'app/docker/views/templates/templates.html',
controller: 'TemplatesController'
}
},
params: {
key: 'containers',
hide_descriptions: false
}
};
var templatesLinuxServer = {
name: 'docker.templates.linuxserver',
url: '/linuxserver',
views: {
'content@': {
templateUrl: 'app/docker/views/templates/templates.html',
controller: 'TemplatesController'
}
},
params: {
key: 'linuxserver.io',
hide_descriptions: true
}
};
var volumes = {
name: 'docker.volumes',
url: '/volumes',
views: {
'content@': {
templateUrl: 'app/docker/views/volumes/volumes.html',
controller: 'VolumesController'
}
}
};
var volume = {
name: 'docker.volumes.volume',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/docker/views/volumes/edit/volume.html',
controller: 'VolumeController'
}
}
};
var volumeCreation = {
name: 'docker.volumes.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/docker/views/volumes/create/createvolume.html',
controller: 'CreateVolumeController'
}
}
};
$stateRegistryProvider.register(configs);
$stateRegistryProvider.register(config);
$stateRegistryProvider.register(configCreation);
$stateRegistryProvider.register(containers);
$stateRegistryProvider.register(container);
$stateRegistryProvider.register(containerConsole);
$stateRegistryProvider.register(containerCreation);
$stateRegistryProvider.register(containerInspect);
$stateRegistryProvider.register(containerLogs);
$stateRegistryProvider.register(containerStats);
$stateRegistryProvider.register(docker);
$stateRegistryProvider.register(dashboard);
$stateRegistryProvider.register(engine);
$stateRegistryProvider.register(events);
$stateRegistryProvider.register(images);
$stateRegistryProvider.register(image);
$stateRegistryProvider.register(imageBuild);
$stateRegistryProvider.register(networks);
$stateRegistryProvider.register(network);
$stateRegistryProvider.register(networkCreation);
$stateRegistryProvider.register(nodes);
$stateRegistryProvider.register(node);
$stateRegistryProvider.register(secrets);
$stateRegistryProvider.register(secret);
$stateRegistryProvider.register(secretCreation);
$stateRegistryProvider.register(services);
$stateRegistryProvider.register(service);
$stateRegistryProvider.register(serviceCreation);
$stateRegistryProvider.register(serviceLogs);
$stateRegistryProvider.register(stacks);
$stateRegistryProvider.register(stack);
$stateRegistryProvider.register(stackCreation);
$stateRegistryProvider.register(swarm);
$stateRegistryProvider.register(swarmVisualizer);
$stateRegistryProvider.register(tasks);
$stateRegistryProvider.register(task);
$stateRegistryProvider.register(taskLogs);
$stateRegistryProvider.register(templates);
$stateRegistryProvider.register(templatesLinuxServer);
$stateRegistryProvider.register(volumes);
$stateRegistryProvider.register(volume);
$stateRegistryProvider.register(volumeCreation);
}]);
@@ -16,13 +16,13 @@
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="actions.create.config">
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.configs.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add config
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -62,7 +62,7 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="config({id: item.Id})">{{ item.Name }}</a>
<a ui-sref="docker.configs.config({id: item.Id})">{{ item.Name }}</a>
</td>
<td>{{ item.CreatedAt | getisodate }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
@@ -1,5 +1,5 @@
angular.module('ui').component('configsDatatable', {
templateUrl: 'app/directives/ui/datatables/configs-datatable/configsDatatable.html',
angular.module('portainer.docker').component('configsDatatable', {
templateUrl: 'app/docker/components/datatables/configs-datatable/configsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -38,7 +38,7 @@
</thead>
<tbody>
<tr dir-paginate="(key, value) in $ctrl.dataset | itemsPerPage: $ctrl.state.paginatedItemLimit" ng-class="{active: item.Checked}">
<td><a ui-sref="network({id: value.NetworkID})">{{ key }}</a></td>
<td><a ui-sref="docker.networks.network({id: value.NetworkID})">{{ key }}</a></td>
<td>{{ value.IPAddress || '-' }}</td>
<td>{{ value.Gateway || '-' }}</td>
<td>{{ value.MacAddress || '-' }}</td>
@@ -1,5 +1,5 @@
angular.module('ui').component('containerNetworksDatatable', {
templateUrl: 'app/directives/ui/datatables/container-networks-datatable/containerNetworksDatatable.html',
angular.module('portainer.docker').component('containerNetworksDatatable', {
templateUrl: 'app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -13,7 +13,7 @@
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -1,5 +1,5 @@
angular.module('ui').component('containerProcessesDatatable', {
templateUrl: 'app/directives/ui/datatables/container-processes-datatable/containerProcessesDatatable.html',
angular.module('portainer.docker').component('containerProcessesDatatable', {
templateUrl: 'app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -82,13 +82,13 @@
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
</div>
<button type="button" class="btn btn-sm btn-primary" ui-sref="actions.create.container">
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.containers.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add container
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters">
@@ -186,8 +186,8 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="container({ id: item.Id })" ng-if="!$ctrl.swarmContainers">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
<a ui-sref="container({ id: item.Id })" ng-if="$ctrl.swarmContainers">{{ item | swarmcontainername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
<a ui-sref="docker.containers.container({ id: item.Id })" ng-if="!$ctrl.swarmContainers">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
<a ui-sref="docker.containers.container({ id: item.Id })" ng-if="$ctrl.swarmContainers">{{ item | swarmcontainername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
</td>
<td>
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ item.Status }}</span>
@@ -195,14 +195,14 @@
</td>
<td ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect">
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="stats({id: item.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="containerlogs({id: item.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="console({id: item.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="inspect({id: item.Id})"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id})" title="Stats"><i class="fa fa-area-chart space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id})" title="Logs"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({id: item.Id})" title="Console"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id})" title="Inspect"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
</div>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td><a ui-sref="image({ id: item.Image })">{{ item.Image | hideshasum }}</a></td>
<td><a ui-sref="docker.images.image({ id: item.Image })">{{ item.Image | trimshasum }}</a></td>
<td>{{ item.IP ? item.IP : '-' }}</td>
<td ng-if="$ctrl.swarmContainers">{{ item.hostIP }}</td>
<td>
@@ -1,5 +1,5 @@
angular.module('ui').component('containersDatatable', {
templateUrl: 'app/directives/ui/datatables/containers-datatable/containersDatatable.html',
angular.module('portainer.docker').component('containersDatatable', {
templateUrl: 'app/docker/components/datatables/containers-datatable/containersDatatable.html',
controller: 'ContainersDatatableController',
bindings: {
title: '@',
@@ -1,4 +1,4 @@
angular.module('ui')
angular.module('portainer.docker')
.controller('ContainersDatatableController', ['PaginationService', 'DatatableService',
function (PaginationService, DatatableService) {
@@ -70,16 +70,22 @@ function (PaginationService, DatatableService) {
for (var i = 0; i < this.dataset.length; i++) {
var item = this.dataset[i];
if (item.Checked && item.Status === 'paused') {
this.state.noPausedItemsSelected = false;
} else if (item.Checked && (item.Status === 'stopped' || item.Status === 'created')) {
this.state.noStoppedItemsSelected = false;
} else if (item.Checked && item.Status === 'running') {
this.state.noRunningItemsSelected = false;
if (item.Checked) {
this.updateSelectionStateBasedOnItemStatus(item);
}
}
};
this.updateSelectionStateBasedOnItemStatus = function(item) {
if (item.Status === 'paused') {
this.state.noPausedItemsSelected = false;
} else if (['stopped', 'created'].indexOf(item.Status) !== -1) {
this.state.noStoppedItemsSelected = false;
} else if (['running', 'healthy', 'unhealthy', 'starting'].indexOf(item.Status) !== -1) {
this.state.noRunningItemsSelected = false;
}
};
this.changePaginationLimit = function() {
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
};
@@ -138,7 +144,20 @@ function (PaginationService, DatatableService) {
}
availableStateFilters.push({ label: item.Status, display: true });
}
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
};
this.updateStoredFilters = function(storedFilters) {
var datasetFilters = this.filters.state.values;
for (var i = 0; i < datasetFilters.length; i++) {
var filter = datasetFilters[i];
existingFilter = _.find(storedFilters, ['label', filter.label]);
if (existingFilter && !existingFilter.display) {
filter.display = existingFilter.display;
this.filters.state.enabled = true;
}
}
};
this.$onInit = function() {
@@ -153,7 +172,7 @@ function (PaginationService, DatatableService) {
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
this.updateStoredFilters(storedFilters.state.values);
}
this.filters.state.open = false;
@@ -13,7 +13,7 @@
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -1,5 +1,5 @@
angular.module('ui').component('eventsDatatable', {
templateUrl: 'app/directives/ui/datatables/events-datatable/eventsDatatable.html',
angular.module('portainer.docker').component('eventsDatatable', {
templateUrl: 'app/docker/components/datatables/events-datatable/eventsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -25,10 +25,13 @@
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
</ul>
</div>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.images.build">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Build a new image
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters">
@@ -99,7 +102,7 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="image({id: item.Id})" class="monospaced">{{ item.Id | truncate:20 }}</a>
<a ui-sref="docker.images.image({id: item.Id})" class="monospaced">{{ item.Id | truncate:20 }}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="::item.ContainerCount === 0">Unused</span>
</td>
<td>
@@ -1,5 +1,5 @@
angular.module('ui').component('imagesDatatable', {
templateUrl: 'app/directives/ui/datatables/images-datatable/imagesDatatable.html',
angular.module('portainer.docker').component('imagesDatatable', {
templateUrl: 'app/docker/components/datatables/images-datatable/imagesDatatable.html',
controller: 'ImagesDatatableController',
bindings: {
title: '@',
@@ -1,4 +1,4 @@
angular.module('ui')
angular.module('portainer.docker')
.controller('ImagesDatatableController', ['PaginationService', 'DatatableService',
function (PaginationService, DatatableService) {
@@ -16,13 +16,13 @@
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="actions.create.network">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add volume
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.networks.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add network
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -97,7 +97,7 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="network({id: item.Id})">{{ item.Name | truncate:40 }}</a>
<a ui-sref="docker.networks.network({id: item.Id})" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td>{{ item.Scope }}</td>
@@ -1,5 +1,5 @@
angular.module('ui').component('networksDatatable', {
templateUrl: 'app/directives/ui/datatables/networks-datatable/networksDatatable.html',
angular.module('portainer.docker').component('networksDatatable', {
templateUrl: 'app/docker/components/datatables/networks-datatable/networksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -13,7 +13,7 @@
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -58,7 +58,7 @@
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td><a ui-sref="task({id: item.Id})" class="monospaced">{{ item.Id }}</a></td>
<td><a ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a></td>
<td><span class="label label-{{ item.Status.State | taskstatusbadge }}">{{ item.Status.State }}</span></td>
<td>{{ item.Slot ? item.Slot : '-' }}</td>
<td>{{ item.Spec.ContainerSpec.Image | hideshasum }}</td>
@@ -1,5 +1,5 @@
angular.module('ui').component('nodeTasksDatatable', {
templateUrl: 'app/directives/ui/datatables/node-tasks-datatable/nodeTasksDatatable.html',
angular.module('portainer.docker').component('nodeTasksDatatable', {
templateUrl: 'app/docker/components/datatables/node-tasks-datatable/nodeTasksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -13,7 +13,7 @@
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -73,7 +73,7 @@
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<a ui-sref="node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Hostname }}</a>
<a ui-sref="docker.nodes.node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Hostname }}</a>
<span ng-if="!$ctrl.accessToNodeDetails">{{ item.Hostname }}</span>
</td>
<td>{{ item.Role }}</td>
@@ -1,5 +1,5 @@
angular.module('ui').component('nodesDatatable', {
templateUrl: 'app/directives/ui/datatables/nodes-datatable/nodesDatatable.html',
angular.module('portainer.docker').component('nodesDatatable', {
templateUrl: 'app/docker/components/datatables/nodes-datatable/nodesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -13,7 +13,7 @@
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -1,5 +1,5 @@
angular.module('ui').component('nodesSsDatatable', {
templateUrl: 'app/directives/ui/datatables/nodes-ss-datatable/nodesSSDatatable.html',
angular.module('portainer.docker').component('nodesSsDatatable', {
templateUrl: 'app/docker/components/datatables/nodes-ss-datatable/nodesSSDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -16,13 +16,13 @@
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="actions.create.secret">
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.secrets.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add secret
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -62,7 +62,7 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="secret({id: item.Id})">{{ item.Name }}</a>
<a ui-sref="docker.secrets.secret({id: item.Id})">{{ item.Name }}</a>
</td>
<td>{{ item.CreatedAt | getisodate }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
@@ -1,5 +1,5 @@
angular.module('ui').component('secretsDatatable', {
templateUrl: 'app/directives/ui/datatables/secrets-datatable/secretsDatatable.html',
angular.module('portainer.docker').component('secretsDatatable', {
templateUrl: 'app/docker/components/datatables/secrets-datatable/secretsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -12,17 +12,23 @@
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="actions.create.service">
<div class="btn-group" role="group" aria-label="...">
<button ng-if="$ctrl.showForceUpdateButton" type="button" class="btn btn-sm btn-primary"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.forceUpdateAction($ctrl.state.selectedItems)">
<i class="fa fa-refresh space-right" aria-hidden="true"></i>Update
</button>
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
</div>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.services.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add service
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -90,7 +96,7 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="service({id: item.Id})">{{ item.Name }}</a>
<a ui-sref="docker.services.service({id: item.Id})">{{ item.Name }}</a>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td>{{ item.Image | hideshasum }}</td>
@@ -102,13 +108,13 @@
</a>
</span>
<span ng-if="item.Mode === 'replicated' && item.Scale">
<input class="input-sm" type="number" ng-model="item.Replicas" />
<input class="input-sm" type="number" ng-model="item.Replicas" on-enter-key="$ctrl.scaleAction(item)" />
<a class="interactive" ng-click="item.Scale = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="$ctrl.scaleAction(item)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
<td>
<a ng-if="item.Ports && item.Ports.length > 0 && $ctrl.swarmManagerIp && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.swarmManagerIp }}:{{ p.PublishedPort }}" target="_blank">
<a ng-if="item.Ports && item.Ports.length > 0 && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.publicUrl }}:{{ p.PublishedPort }}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
</a>
<span ng-if="!item.Ports || item.Ports.length === 0 || !$ctrl.swarmManagerIp" >-</span>
@@ -1,5 +1,5 @@
angular.module('ui').component('servicesDatatable', {
templateUrl: 'app/directives/ui/datatables/services-datatable/servicesDatatable.html',
angular.module('portainer.docker').component('servicesDatatable', {
templateUrl: 'app/docker/components/datatables/services-datatable/servicesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -12,6 +12,8 @@ angular.module('ui').component('servicesDatatable', {
showOwnershipColumn: '<',
removeAction: '<',
scaleAction: '<',
swarmManagerIp: '<'
publicUrl: '<',
forceUpdateAction: '<',
showForceUpdateButton: '<'
}
});
@@ -13,7 +13,7 @@
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover">
@@ -54,15 +54,21 @@
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showLogsButton">Actions</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td><a ui-sref="task({id: item.Id})" class="monospaced">{{ item.Id }}</a></td>
<td><a ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a></td>
<td><span class="label label-{{ item.Status.State | taskstatusbadge }}">{{ item.Status.State }}</span></td>
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
<td>{{ item.Updated | getisodate }}</td>
<td ng-if="$ctrl.showLogsButton">
<a ui-sref="docker.tasks.task.logs({id: item.Id})">
<i class="fa fa-file-text-o" aria-hidden="true"></i> View logs
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
@@ -1,5 +1,5 @@
angular.module('ui').component('tasksDatatable', {
templateUrl: 'app/directives/ui/datatables/tasks-datatable/tasksDatatable.html',
angular.module('portainer.docker').component('tasksDatatable', {
templateUrl: 'app/docker/components/datatables/tasks-datatable/tasksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
@@ -10,6 +10,7 @@ angular.module('ui').component('tasksDatatable', {
reverseOrder: '<',
nodes: '<',
showTextFilter: '<',
showSlotColumn: '<'
showSlotColumn: '<',
showLogsButton: '<'
}
});
@@ -16,13 +16,13 @@
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="actions.create.volume">
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.volumes.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add volume
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters">
@@ -100,7 +100,7 @@
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="volume({id: item.Id})" class="monospaced">{{ item.Id | truncate:25 }}</a>
<a ui-sref="docker.volumes.volume({id: item.Id})" class="monospaced">{{ item.Id | truncate:25 }}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="item.dangling">Unused</span>
</td>
<td>{{ item.StackName ? item.StackName : '-' }}</td>
@@ -1,5 +1,5 @@
angular.module('ui').component('volumesDatatable', {
templateUrl: 'app/directives/ui/datatables/volumes-datatable/volumesDatatable.html',
angular.module('portainer.docker').component('volumesDatatable', {
templateUrl: 'app/docker/components/datatables/volumes-datatable/volumesDatatable.html',
controller: 'VolumesDatatableController',
bindings: {
title: '@',
@@ -1,4 +1,4 @@
angular.module('ui')
angular.module('portainer.docker')
.controller('VolumesDatatableController', ['PaginationService', 'DatatableService',
function (PaginationService, DatatableService) {
@@ -0,0 +1,12 @@
angular.module('portainer.docker').component('dockerSidebarContent', {
templateUrl: 'app/docker/components/dockerSidebarContent/dockerSidebarContent.html',
bindings: {
'endpointApiVersion': '<',
'swarmManagement': '<',
'standaloneManagement': '<',
'adminAccess': '<',
'externalContributions': '<',
'sidebarToggledOn': '<',
'currentState': '<'
}
});
@@ -0,0 +1,42 @@
<li class="sidebar-list">
<a ui-sref="docker.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.sidebarToggledOn && $ctrl.externalContributions && ($ctrl.currentState === 'docker.templates' || $ctrl.currentState === 'docker.templates.linuxserver')">
<a ui-sref="docker.templates.linuxserver" ui-sref-active="active">LinuxServer.io</a>
</div>
</li>
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.25 && $ctrl.swarmManagement">
<a ui-sref="docker.stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
<a ui-sref="docker.services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.images" ui-sref-active="active">Images <span class="menu-icon fa fa-clone fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.networks" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="docker.volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.30 && $ctrl.swarmManagement">
<a ui-sref="docker.configs" ui-sref-active="active">Configs <span class="menu-icon fa fa-file-code-o fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.25 && $ctrl.swarmManagement">
<a ui-sref="docker.secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess">
<a ui-sref="docker.events" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
<a ui-sref="docker.swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
<a ui-sref="docker.engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th fa-fw"></span></a>
</li>
@@ -0,0 +1,9 @@
angular.module('portainer.docker').component('porImageRegistry', {
templateUrl: 'app/docker/components/imageRegistry/porImageRegistry.html',
controller: 'porImageRegistryController',
bindings: {
'image': '=',
'registry': '=',
'autoComplete': '<'
}
});
@@ -1,4 +1,4 @@
angular.module('portainer')
angular.module('portainer.docker')
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'ImageService', 'Notifications',
function ($q, RegistryService, DockerHubService, ImageService, Notifications) {
var ctrl = this;
@@ -0,0 +1,8 @@
angular.module('portainer.docker').component('logViewer', {
templateUrl: 'app/docker/components/log-viewer/logViewer.html',
controller: 'LogViewerController',
bindings: {
data: '=',
logCollectionChange: '<'
}
});
@@ -0,0 +1,53 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-file-text-o" title="Log viewer settings"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
Auto-refresh logs
<portainer-tooltip position="bottom" message="Disabling this option allows you to pause the log collection process and the auto-scrolling."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="$ctrl.state.logCollection" ng-change="$ctrl.state.autoScroll = $ctrl.state.logCollection; $ctrl.logCollectionChange($ctrl.state.logCollection)"><i></i>
</label>
</div>
</div>
<div class="form-group">
<label for="logs_search" class="col-sm-1 control-label text-left">
Search
</label>
<div class="col-sm-11">
<input class="form-control" type="text" name="logs_search" ng-model="$ctrl.state.search" ng-change="$ctrl.state.selectedLines.length = 0;" placeholder="Filter...">
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.copySupported">
<label class="col-sm-1 control-label text-left">
Actions
</label>
<div class="col-sm-11">
<button class="btn btn-primary btn-sm" ng-click="$ctrl.copy()" ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</button>
<button class="btn btn-primary btn-sm" ng-click="$ctrl.copySelection()" ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length || !$ctrl.state.selectedLines.length"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy selected lines</button>
<button class="btn btn-primary btn-sm" ng-click="$ctrl.clearSelection()" ng-disabled="$ctrl.state.selectedLines.length === 0"><i class="fa fa-times space-right" aria-hidden="true"></i>Unselect</button>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
</span>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" style="height:54%;">
<div class="col-sm-12" style="height:100%;">
<pre class="log_viewer" scroll-glue="$ctrl.state.autoScroll" force-glue>
<div ng-repeat="line in $ctrl.state.filteredLogs = ($ctrl.data | filter:$ctrl.state.search) track by $index" class="line" ng-if="line"><p class="inner_line" ng-click="$ctrl.selectLine(line)" ng-class="{ 'line_selected': $ctrl.state.selectedLines.indexOf(line) > -1 }">{{ line }}</p></div>
<div ng-if="!$ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
<div ng-if="$ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]" class="line"><p class="inner_line">No logs available</p></div>
</pre>
</div>
</div>
@@ -0,0 +1,39 @@
angular.module('portainer.docker')
.controller('LogViewerController', ['clipboard',
function (clipboard) {
var ctrl = this;
this.state = {
copySupported: clipboard.supported,
logCollection: true,
autoScroll: true,
search: '',
filteredLogs: [],
selectedLines: []
};
this.copy = function() {
clipboard.copyText(this.state.filteredLogs);
$('#refreshRateChange').show();
$('#refreshRateChange').fadeOut(2000);
};
this.copySelection = function() {
clipboard.copyText(this.state.selectedLines);
$('#refreshRateChange').show();
$('#refreshRateChange').fadeOut(2000);
};
this.clearSelection = function() {
this.state.selectedLines = [];
};
this.selectLine = function(line) {
var idx = this.state.selectedLines.indexOf(line);
if (idx === -1) {
this.state.selectedLines.push(line);
} else {
this.state.selectedLines.splice(idx, 1);
}
};
}]);
@@ -4,39 +4,23 @@ function includeString(text, values) {
});
}
angular.module('portainer.filters')
.filter('truncate', function () {
'use strict';
return function (text, length, end) {
if (isNaN(length)) {
length = 10;
}
function strToHash(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
return hash;
}
if (end === undefined) {
end = '...';
}
function hashToHexColor(hash) {
var color = '#';
for (var i = 0; i < 3;) {
color += ('00' + ((hash >> i++ * 8) & 0xFF).toString(16)).slice(-2);
}
return color;
}
if (text.length <= length || text.length - end.length <= length) {
return text;
} else {
return String(text).substring(0, length - end.length) + end;
}
};
})
.filter('truncatelr', function () {
'use strict';
return function (text, max, left, right) {
max = isNaN(max) ? 50 : max;
left = isNaN(left) ? 25 : left;
right = isNaN(right) ? 25 : right;
if (text.length <= max) {
return text;
} else {
return text.substring(0, left) + '[...]' + text.substring(text.length - right, text.length);
}
};
})
angular.module('portainer.docker')
.filter('visualizerTask', function () {
'use strict';
return function (text) {
@@ -51,6 +35,14 @@ angular.module('portainer.filters')
return 'running';
};
})
.filter('visualizerTaskBorderColor', function () {
'use strict';
return function (str) {
var hash = strToHash(str);
var color = hashToHexColor(hash);
return color;
};
})
.filter('taskstatusbadge', function () {
'use strict';
return function (text) {
@@ -66,7 +58,7 @@ angular.module('portainer.filters')
labelStyle = 'primary';
} else if (includeString(status, ['running'])) {
labelStyle = 'success';
}
}
return labelStyle;
};
})
@@ -74,11 +66,11 @@ angular.module('portainer.filters')
'use strict';
return function (text) {
var status = _.toLower(text);
if (includeString(status, ['paused', 'starting'])) {
if (includeString(status, ['paused', 'starting', 'unhealthy'])) {
return 'warning';
} else if (includeString(status, ['created'])) {
return 'info';
} else if (includeString(status, ['stopped', 'unhealthy', 'dead', 'exited'])) {
} else if (includeString(status, ['stopped', 'dead', 'exited'])) {
return 'danger';
}
return 'success';
@@ -124,12 +116,6 @@ angular.module('portainer.filters')
return '';
};
})
.filter('capitalize', function () {
'use strict';
return function (text) {
return _.capitalize(text);
};
})
.filter('getstatetext', function () {
'use strict';
return function (state) {
@@ -154,12 +140,6 @@ angular.module('portainer.filters')
return 'Stopped';
};
})
.filter('stripprotocol', function() {
'use strict';
return function (url) {
return url.replace(/.*?:\/\//g, '');
};
})
.filter('getstatelabel', function () {
'use strict';
return function (state) {
@@ -175,20 +155,6 @@ angular.module('portainer.filters')
return 'label-default';
};
})
.filter('humansize', function () {
'use strict';
return function (bytes, round, base) {
if (!round) {
round = 1;
}
if (!base) {
base = 10;
}
if (bytes || bytes === 0) {
return filesize(bytes, {base: base, round: round});
}
};
})
.filter('containername', function () {
'use strict';
return function (container) {
@@ -227,18 +193,6 @@ angular.module('portainer.filters')
return [];
};
})
.filter('getisodatefromtimestamp', function () {
'use strict';
return function (timestamp) {
return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss');
};
})
.filter('getisodate', function () {
'use strict';
return function (date) {
return moment(date).format('YYYY-MM-DD HH:mm:ss');
};
})
.filter('command', function () {
'use strict';
return function (command) {
@@ -247,39 +201,6 @@ angular.module('portainer.filters')
}
};
})
.filter('key', function () {
'use strict';
return function (pair, separator) {
return pair.slice(0, pair.indexOf(separator));
};
})
.filter('value', function () {
'use strict';
return function (pair, separator) {
return pair.slice(pair.indexOf(separator) + 1);
};
})
.filter('emptyobject', function () {
'use strict';
return function (obj) {
return _.isEmpty(obj);
};
})
.filter('ipaddress', function () {
'use strict';
return function (ip) {
return ip.slice(0, ip.indexOf('/'));
};
})
.filter('arraytostr', function () {
'use strict';
return function (arr, separator) {
if (arr) {
return _.join(arr, separator);
}
return '';
};
})
.filter('hideshasum', function () {
'use strict';
return function (imageName) {
@@ -289,21 +210,6 @@ angular.module('portainer.filters')
return '';
};
})
.filter('ownershipicon', function () {
'use strict';
return function (ownership) {
switch (ownership) {
case 'private':
return 'fa fa-eye-slash';
case 'administrators':
return 'fa fa-eye-slash';
case 'restricted':
return 'fa fa-users';
default:
return 'fa fa-eye';
}
};
})
.filter('availablenodecount', function () {
'use strict';
return function (nodes) {
@@ -345,4 +251,13 @@ angular.module('portainer.filters')
return function (createdBy) {
return createdBy.replace('/bin/sh -c #(nop) ', '').replace('/bin/sh -c ', 'RUN ');
};
})
.filter('trimshasum', function () {
'use strict';
return function (imageName) {
if (imageName.indexOf('sha256:') === 0) {
return imageName.substring(7, 19);
}
return _.split(imageName, '@sha256')[0];
};
});
@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('ConfigHelper', [function ConfigHelperFactory() {
'use strict';
return {
@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('ContainerHelper', [function ContainerHelperFactory() {
'use strict';
var helper = {};
@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('ImageHelper', [function ImageHelperFactory() {
'use strict';
@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('InfoHelper', [function InfoHelperFactory() {
'use strict';
return {
@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('LabelHelper', [function LabelHelperFactory() {
'use strict';
return {
+14
View File
@@ -0,0 +1,14 @@
angular.module('portainer.docker')
.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
};
}
};
}]);
@@ -1,4 +1,4 @@
angular.module('portainer.helpers')
angular.module('portainer.docker')
.factory('SecretHelper', [function SecretHelperFactory() {
'use strict';
return {
@@ -1,4 +1,5 @@
angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHelperFactory() {
angular.module('portainer.docker')
.factory('ServiceHelper', [function ServiceHelperFactory() {
'use strict';
var helper = {};
@@ -141,17 +142,17 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
}
};
helper.translateHumanDurationToNanos = function(humanDuration) {
helper.translateHumanDurationToNanos = function(humanDuration) {
var nanos;
var regex = /^([0-9]+)(h|m|s|ms|us|ns)$/i;
var matches = humanDuration.match(regex);
if (matches !== null && matches.length === 3) {
var duration = parseInt(matches[1], 10);
var unit = matches[2];
var unit = matches[2];
// Moment.js cannot use micro or nanoseconds
switch (unit) {
case 'ns':
case 'ns':
nanos = duration;
break;
case 'us':
@@ -159,7 +160,7 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
break;
default:
nanos = moment.duration(duration, unit).asMilliseconds() * 1000000;
}
}
}
return nanos;
};
@@ -171,9 +172,8 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
// e.g 3540000000000 nanoseconds = 59m
// e.g 3600000000000 nanoseconds = 1h
helper.translateNanosToHumanDuration = function(nanos) {
helper.translateNanosToHumanDuration = function(nanos) {
var humanDuration = '0s';
var conversionFromNano = {};
conversionFromNano['ns'] = 1;
conversionFromNano['us'] = conversionFromNano['ns'] * 1000;
@@ -181,15 +181,67 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
conversionFromNano['s'] = conversionFromNano['ms'] * 1000;
conversionFromNano['m'] = conversionFromNano['s'] * 60;
conversionFromNano['h'] = conversionFromNano['m'] * 60;
Object.keys(conversionFromNano).forEach(function(unit) {
Object.keys(conversionFromNano).forEach(function(unit) {
if ( nanos % conversionFromNano[unit] === 0 && (nanos / conversionFromNano[unit]) > 0) {
humanDuration = (nanos / conversionFromNano[unit]) + unit;
}
});
return humanDuration;
};
helper.translateLogDriverOptsToKeyValue = function(logOptions) {
var options = [];
if (logOptions) {
Object.keys(logOptions).forEach(function(key) {
options.push({
key: key,
value: logOptions[key],
originalKey: key,
originalValue: logOptions[key],
added: true
});
});
}
return options;
};
helper.translateKeyValueToLogDriverOpts = function(keyValueLogDriverOpts) {
var options = {};
if (keyValueLogDriverOpts) {
keyValueLogDriverOpts.forEach(function(option) {
if (option.key && option.key !== '' && option.value && option.value !== '') {
options[option.key] = option.value;
}
});
}
return options;
};
helper.translateHostsEntriesToHostnameIP = function(entries) {
var ipHostEntries = [];
if (entries) {
entries.forEach(function(entry) {
if (entry.indexOf(' ') && entry.split(' ').length === 2) {
var keyValue = entry.split(' ');
ipHostEntries.push({ hostname: keyValue[1], ip: keyValue[0]});
}
});
}
return ipHostEntries;
};
helper.translateHostnameIPToHostsEntries = function(entries) {
var ipHostEntries = [];
if (entries) {
entries.forEach(function(entry) {
if (entry.ip && entry.hostname) {
ipHostEntries.push(entry.ip + ' ' + entry.hostname);
}
});
}
return ipHostEntries;
};
return helper;
}]);
+30
View File
@@ -0,0 +1,30 @@
angular.module('portainer.docker')
.factory('VolumeHelper', [function VolumeHelperFactory() {
'use strict';
var helper = {};
helper.createDriverOptions = function(optionArray) {
var options = {};
optionArray.forEach(function (option) {
options[option.name] = option.value;
});
return options;
};
helper.isVolumeUsedByAService = function(volume, services) {
for (var i = 0; i < services.length; i++) {
var service = services[i];
var mounts = service.Mounts;
for (var j = 0; j < mounts.length; j++) {
var mount = mounts[j];
if (mount.Source === volume.Id) {
return true;
}
}
}
return false;
};
return helper;
}]);
@@ -34,7 +34,37 @@ function ContainerViewModel(data) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
} else {
this.ResourceControl = { Ownership: 'public' };
}
}
function ContainerStatsViewModel(data) {
this.Date = data.read;
this.MemoryUsage = data.memory_stats.usage;
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
if (data.cpu_stats.cpu_usage.percpu_usage) {
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
}
this.Networks = _.values(data.networks);
}
function ContainerDetailsViewModel(data) {
this.Model = data;
this.Id = data.Id;
this.State = data.State;
this.Created = data.Created;
this.Name = data.Name;
this.NetworkSettings = data.NetworkSettings;
this.Args = data.Args;
this.Image = data.Image;
this.Config = data.Config;
this.HostConfig = data.HostConfig;
this.Mounts = data.Mounts;
if (data.Portainer) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
}
}
@@ -37,6 +37,33 @@ function createEventDetails(event) {
case 'attach':
details = 'Container ' + eventAttr.name + ' attached';
break;
case 'detach':
details = 'Container ' + eventAttr.name + ' detached';
break;
case 'copy':
details = 'Container ' + eventAttr.name + ' copied';
break;
case 'export':
details = 'Container ' + eventAttr.name + ' exported';
break;
case 'health_status':
details = 'Container ' + eventAttr.name + ' executed health status';
break;
case 'oom':
details = 'Container ' + eventAttr.name + ' goes in out of memory';
break;
case 'rename':
details = 'Container ' + eventAttr.name + ' renamed';
break;
case 'resize':
details = 'Container ' + eventAttr.name + ' resized';
break;
case 'top':
details = 'Showed running processes for container ' + eventAttr.name;
break;
case 'update':
details = 'Container ' + eventAttr.name + ' updated';
break;
default:
if (event.Action.indexOf('exec_create') === 0) {
details = 'Exec instance created';
@@ -52,15 +79,27 @@ function createEventDetails(event) {
case 'delete':
details = 'Image deleted';
break;
case 'import':
details = 'Image ' + event.Actor.ID + ' imported';
break;
case 'load':
details = 'Image ' + event.Actor.ID + ' loaded';
break;
case 'tag':
details = 'New tag created for ' + eventAttr.name;
break;
case 'untag':
details = 'Image untagged';
break;
case 'save':
details = 'Image ' + event.Actor.ID + ' saved';
break;
case 'pull':
details = 'Image ' + event.Actor.ID + ' pulled';
break;
case 'push':
details = 'Image ' + event.Actor.ID + ' pushed';
break;
default:
details = 'Unsupported event';
}
@@ -73,6 +112,9 @@ function createEventDetails(event) {
case 'destroy':
details = 'Network ' + eventAttr.name + ' deleted';
break;
case 'remove':
details = 'Network ' + eventAttr.name + ' removed';
break;
case 'connect':
details = 'Container connected to ' + eventAttr.name + ' network';
break;
+31
View File
@@ -0,0 +1,31 @@
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;
this.ContainerCount = data.ContainerCount;
}
function ImageBuildModel(data) {
this.hasError = false;
var buildLogs = [];
for (var i = 0; i < data.length; i++) {
var line = data[i];
if (line.stream) {
line = line.stream.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
buildLogs.push(line);
}
if (line.errorDetail) {
buildLogs.push(line.errorDetail.message);
this.hasError = true;
}
}
this.buildLogs = buildLogs;
}
@@ -19,7 +19,5 @@ function NetworkViewModel(data) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
} else {
this.ResourceControl = { Ownership: 'public' };
}
}
@@ -41,6 +41,15 @@ function ServiceViewModel(data, runningTasks, allTasks, nodes) {
this.RestartMaxAttempts = 0;
this.RestartWindow = 0;
}
if (data.Spec.TaskTemplate.LogDriver) {
this.LogDriverName = data.Spec.TaskTemplate.LogDriver.Name || '';
this.LogDriverOpts = data.Spec.TaskTemplate.LogDriver.Options || [];
} else {
this.LogDriverName = '';
this.LogDriverOpts = [];
}
this.Constraints = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Constraints || [] : [];
this.Preferences = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Preferences || [] : [];
this.Platforms = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Platforms || [] : [];
@@ -2,7 +2,7 @@ function StackViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Checked = false;
this.Env = data.Env;
this.Env = data.Env ? data.Env : [];
if (data.ResourceControl && data.ResourceControl.Id !== 0) {
this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
}

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