Compare commits

...

23 Commits

Author SHA1 Message Date
Anthony Lapenna
034e29cd74 Merge branch 'release/1.13.4' 2017-06-29 16:37:28 +02:00
Anthony Lapenna
0e0764eff8 chore(version): bump version number 2017-06-29 16:37:22 +02:00
Anthony Lapenna
e47db0b8c9 feat(volumes): display mount point for each volume (#967) 2017-06-29 16:14:17 +02:00
Anthony Lapenna
6d401dcd59 fix(templates): fix the ability to pull an image within an offline environment (#961) 2017-06-29 16:05:39 +02:00
Anthony Lapenna
6609c2e928 style(container-details): review responsiveness for the join network section 2017-06-29 16:04:49 +02:00
Adam Snodgrass
a161d25d48 feat(container-details): add section to join networks (#927) 2017-06-29 15:49:35 +02:00
Anthony Lapenna
4adedf9436 fix(service-details): fix an issue where secret target would be overwritten (#964) 2017-06-29 08:37:05 +02:00
Anthony Lapenna
1168e94534 fix(service-creation): fix an issue when selecting a volume from available volumes (#963) 2017-06-29 07:41:37 +02:00
Anthony Lapenna
b57bfe3eee Create CODE_OF_CONDUCT.md (#946) 2017-06-22 05:11:40 +02:00
Anthony Lapenna
3592e88e4f Merge tag '1.13.3' into develop
Release 1.13.3
2017-06-20 13:21:16 +02:00
Anthony Lapenna
219cde4733 Merge branch 'release/1.13.3' 2017-06-20 13:21:12 +02:00
Anthony Lapenna
c82cd50d87 chore(version): bump version number 2017-06-20 13:21:06 +02:00
Anthony Lapenna
dae4893fe1 feat(endpoint): remove the active endpoint edition restriction (#941) 2017-06-20 13:18:08 +02:00
Anthony Lapenna
1e686f0428 feat(state): persist application state in localstorage instead of ses… (#940) 2017-06-20 13:07:24 +02:00
Anthony Lapenna
08c5a5a4f6 feat(registries): add registry management (#930) 2017-06-20 13:00:32 +02:00
eliat123
9360f24d89 feat(service-details): add quick navigation menu anchors (#875) 2017-06-20 12:54:27 +02:00
Anthony Lapenna
d0477b216f Merge branch 'develop' of github.com:portainer/portainer into develop 2017-06-17 17:05:52 +02:00
Anthony Lapenna
a812f4729c docs(README): update links to portainer.io 2017-06-17 17:05:34 +02:00
Anthony Lapenna
db324998e3 fix(templates): display templates without platform (#937) 2017-06-17 16:50:35 +02:00
Gabriel Lewertowski
4ec65a80df fix(user-creation): sanitize username and password (#934) 2017-06-17 15:25:23 +02:00
Anthony Lapenna
f2b9700345 chore(codeclimate): update mass_threshold for the duplication engine 2017-06-17 15:20:19 +02:00
Anthony Lapenna
d8f8ab785c fix(service-details): fix the ability to sort tasks (#931) 2017-06-15 22:52:49 +02:00
Anthony Lapenna
b316efe80b Merge tag '1.13.2' into develop
Release 1.13.2
2017-06-05 08:42:20 +02:00
102 changed files with 2507 additions and 680 deletions

View File

@@ -12,7 +12,8 @@ engines:
enabled: true
config:
languages:
- javascript
javascript:
mass_threshold: 80
eslint:
enabled: true
config:

46
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at anthony.lapenna@portainer.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -1,13 +1,13 @@
<p align="center">
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
<img title="portainer" src='https://portainer.io/images/logo_alt.png' />
</p>
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable)
[![Codefresh build status]( https://g.codefresh.io/api/badges/build?repoOwner=portainer&repoName=portainer&branch=develop&pipelineName=portainer-ci&accountName=deviantony&type=cf-1)]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci)
[![Slack](http://portainer.io/slack/badge.svg)](http://portainer.io/slack/)
[![Slack](https://portainer.io/slack/badge.svg)](https://portainer.io/slack/)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
@@ -19,7 +19,7 @@
## Demo
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
<img src="https://portainer.io/images/screenshots/portainer.gif" width="77%"/>
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
@@ -35,7 +35,7 @@ Please note that the public demo cluster is **reset every 15min**.
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Gitter (chat): https://gitter.im/portainer/Lobby
* Slack: http://portainer.io/slack/
* Slack: https://portainer.io/slack/
## Reporting bugs and contributing

View File

@@ -23,6 +23,8 @@ type Store struct {
ResourceControlService *ResourceControlService
VersionService *VersionService
SettingsService *SettingsService
RegistryService *RegistryService
DockerHubService *DockerHubService
db *bolt.DB
checkForDataMigration bool
@@ -37,6 +39,8 @@ const (
endpointBucketName = "endpoints"
resourceControlBucketName = "resource_control"
settingsBucketName = "settings"
registryBucketName = "registries"
dockerhubBucketName = "dockerhub"
)
// NewStore initializes a new Store and the associated services
@@ -50,6 +54,8 @@ func NewStore(storePath string) (*Store, error) {
ResourceControlService: &ResourceControlService{},
VersionService: &VersionService{},
SettingsService: &SettingsService{},
RegistryService: &RegistryService{},
DockerHubService: &DockerHubService{},
}
store.UserService.store = store
store.TeamService.store = store
@@ -58,6 +64,8 @@ func NewStore(storePath string) (*Store, error) {
store.ResourceControlService.store = store
store.VersionService.store = store
store.SettingsService.store = store
store.RegistryService.store = store
store.DockerHubService.store = store
_, err := os.Stat(storePath + "/" + databaseFileName)
if err != nil && os.IsNotExist(err) {
@@ -74,40 +82,26 @@ func NewStore(storePath string) (*Store, error) {
// Open opens and initializes the BoltDB database.
func (store *Store) Open() error {
path := store.Path + "/" + databaseFileName
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.db = db
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
registryBucketName, dockerhubBucketName}
return db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(versionBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(userBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(teamBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(resourceControlBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(teamMembershipBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(settingsBucketName))
if err != nil {
return err
for _, bucket := range bucketsToCreate {
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
if err != nil {
return err
}
}
return nil
})
}

View File

@@ -0,0 +1,61 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// DockerHubService represents a service for managing registries.
type DockerHubService struct {
store *Store
}
const (
dbDockerHubKey = "DOCKERHUB"
)
// DockerHub returns the DockerHub object.
func (service *DockerHubService) DockerHub() (*portainer.DockerHub, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(dockerhubBucketName))
value := bucket.Get([]byte(dbDockerHubKey))
if value == nil {
return portainer.ErrDockerHubNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var dockerhub portainer.DockerHub
err = internal.UnmarshalDockerHub(data, &dockerhub)
if err != nil {
return nil, err
}
return &dockerhub, nil
}
// StoreDockerHub persists a DockerHub object.
func (service *DockerHubService) StoreDockerHub(dockerhub *portainer.DockerHub) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(dockerhubBucketName))
data, err := internal.MarshalDockerHub(dockerhub)
if err != nil {
return err
}
err = bucket.Put([]byte(dbDockerHubKey), data)
if err != nil {
return err
}
return nil
})
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/boltdb/bolt"
)
// EndpointService represents a service for managing users.
// EndpointService represents a service for managing endpoints.
type EndpointService struct {
store *Store
}

View File

@@ -47,6 +47,16 @@ func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
return json.Unmarshal(data, endpoint)
}
// MarshalRegistry encodes a registry to binary format.
func MarshalRegistry(registry *portainer.Registry) ([]byte, error) {
return json.Marshal(registry)
}
// UnmarshalRegistry decodes a registry from a binary data.
func UnmarshalRegistry(data []byte, registry *portainer.Registry) error {
return json.Unmarshal(data, registry)
}
// MarshalResourceControl encodes a resource control object to binary format.
func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) {
return json.Marshal(rc)
@@ -67,6 +77,16 @@ func UnmarshalSettings(data []byte, settings *portainer.Settings) error {
return json.Unmarshal(data, settings)
}
// MarshalDockerHub encodes a Dockerhub object to binary format.
func MarshalDockerHub(settings *portainer.DockerHub) ([]byte, error) {
return json.Marshal(settings)
}
// UnmarshalDockerHub decodes a Dockerhub object from a binary data.
func UnmarshalDockerHub(data []byte, settings *portainer.DockerHub) error {
return json.Unmarshal(data, settings)
}
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.

View File

@@ -0,0 +1,114 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// RegistryService represents a service for managing registries.
type RegistryService struct {
store *Store
}
// Registry returns an registry by ID.
func (service *RegistryService) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrRegistryNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var registry portainer.Registry
err = internal.UnmarshalRegistry(data, &registry)
if err != nil {
return nil, err
}
return &registry, nil
}
// Registries returns an array containing all the registries.
func (service *RegistryService) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var registry portainer.Registry
err := internal.UnmarshalRegistry(v, &registry)
if err != nil {
return err
}
registries = append(registries, registry)
}
return nil
})
if err != nil {
return nil, err
}
return registries, nil
}
// CreateRegistry creates a new registry.
func (service *RegistryService) CreateRegistry(registry *portainer.Registry) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
id, _ := bucket.NextSequence()
registry.ID = portainer.RegistryID(id)
data, err := internal.MarshalRegistry(registry)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(registry.ID)), data)
if err != nil {
return err
}
return nil
})
}
// UpdateRegistry updates an registry.
func (service *RegistryService) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
data, err := internal.MarshalRegistry(registry)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteRegistry deletes an registry.
func (service *RegistryService) DeleteRegistry(ID portainer.RegistryID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(registryBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}

View File

@@ -91,6 +91,22 @@ func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portaine
}
}
func initDockerHub(dockerHubService portainer.DockerHubService) error {
_, err := dockerHubService.DockerHub()
if err == portainer.ErrDockerHubNotFound {
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
return dockerHubService.StoreDockerHub(dockerhub)
} else if err != nil {
return err
}
return nil
}
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
_, err := settingsService.Settings()
if err == portainer.ErrSettingsNotFound {
@@ -146,6 +162,11 @@ func main() {
log.Fatal(err)
}
err = initDockerHub(store.DockerHubService)
if err != nil {
log.Fatal(err)
}
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
if *flags.Endpoint != "" {
@@ -199,6 +220,8 @@ func main() {
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
RegistryService: store.RegistryService,
DockerHubService: store.DockerHubService,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,

View File

@@ -42,6 +42,12 @@ const (
ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Registry errors.
const (
ErrRegistryNotFound = Error("Registry not found")
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
)
// Version errors.
const (
ErrDBVersionNotFound = Error("DB version not found")
@@ -52,6 +58,11 @@ const (
ErrSettingsNotFound = Error("Settings not found")
)
// DockerHub errors.
const (
ErrDockerHubNotFound = Error("Dockerhub not found")
)
// Crypto errors.
const (
ErrCryptoHashFailure = Error("Unable to hash data")

View File

@@ -0,0 +1,87 @@
package handler
import (
"encoding/json"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// DockerHubHandler represents an HTTP API handler for managing DockerHub.
type DockerHubHandler struct {
*mux.Router
Logger *log.Logger
DockerHubService portainer.DockerHubService
}
// NewDockerHubHandler returns a new instance of OldDockerHubHandler.
func NewDockerHubHandler(bouncer *security.RequestBouncer) *DockerHubHandler {
h := &DockerHubHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/dockerhub",
bouncer.PublicAccess(http.HandlerFunc(h.handleGetDockerHub))).Methods(http.MethodGet)
h.Handle("/dockerhub",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutDockerHub))).Methods(http.MethodPut)
return h
}
// handleGetDockerHub handles GET requests on /dockerhub
func (handler *DockerHubHandler) handleGetDockerHub(w http.ResponseWriter, r *http.Request) {
dockerhub, err := handler.DockerHubService.DockerHub()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, dockerhub, handler.Logger)
return
}
// handlePutDockerHub handles PUT requests on /dockerhub
func (handler *DockerHubHandler) handlePutDockerHub(w http.ResponseWriter, r *http.Request) {
var req putDockerHubRequest
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
}
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
if req.Authentication {
dockerhub.Authentication = true
dockerhub.Username = req.Username
dockerhub.Password = req.Password
}
err = handler.DockerHubService.StoreDockerHub(dockerhub)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
}
}
type putDockerHubRequest struct {
Authentication bool `valid:""`
Username string `valid:""`
Password string `valid:""`
}

View File

@@ -17,6 +17,8 @@ type Handler struct {
TeamHandler *TeamHandler
TeamMembershipHandler *TeamMembershipHandler
EndpointHandler *EndpointHandler
RegistryHandler *RegistryHandler
DockerHubHandler *DockerHubHandler
ResourceHandler *ResourceHandler
StatusHandler *StatusHandler
SettingsHandler *SettingsHandler
@@ -50,6 +52,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/endpoints") {
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/registries") {
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/dockerhub") {
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/resource_controls") {
http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {

View File

@@ -0,0 +1,312 @@
package handler
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// RegistryHandler represents an HTTP API handler for managing Docker registries.
type RegistryHandler struct {
*mux.Router
Logger *log.Logger
RegistryService portainer.RegistryService
}
// NewRegistryHandler returns a new instance of RegistryHandler.
func NewRegistryHandler(bouncer *security.RequestBouncer) *RegistryHandler {
h := &RegistryHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/registries",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostRegistries))).Methods(http.MethodPost)
h.Handle("/registries",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetRegistries))).Methods(http.MethodGet)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetRegistry))).Methods(http.MethodGet)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutRegistry))).Methods(http.MethodPut)
h.Handle("/registries/{id}/access",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutRegistryAccess))).Methods(http.MethodPut)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteRegistry))).Methods(http.MethodDelete)
return h
}
// handleGetRegistries handles GET requests on /registries
func (handler *RegistryHandler) handleGetRegistries(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
registries, err := handler.RegistryService.Registries()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
filteredRegistries, err := security.FilterRegistries(registries, securityContext)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, filteredRegistries, handler.Logger)
}
// handlePostRegistries handles POST requests on /registries
func (handler *RegistryHandler) handlePostRegistries(w http.ResponseWriter, r *http.Request) {
var req postRegistriesRequest
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
}
registries, err := handler.RegistryService.Registries()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
for _, r := range registries {
if r.URL == req.URL {
httperror.WriteErrorResponse(w, portainer.ErrRegistryAlreadyExists, http.StatusConflict, handler.Logger)
return
}
}
registry := &portainer.Registry{
Name: req.Name,
URL: req.URL,
Authentication: req.Authentication,
Username: req.Username,
Password: req.Password,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
}
err = handler.RegistryService.CreateRegistry(registry)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postRegistriesResponse{ID: int(registry.ID)}, handler.Logger)
}
type postRegistriesRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
Authentication bool `valid:""`
Username string `valid:""`
Password string `valid:""`
}
type postRegistriesResponse struct {
ID int `json:"Id"`
}
// handleGetRegistry handles GET requests on /registries/:id
func (handler *RegistryHandler) handleGetRegistry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
registryID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.ErrRegistryNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, registry, handler.Logger)
}
// handlePutRegistryAccess handles PUT requests on /registries/:id/access
func (handler *RegistryHandler) handlePutRegistryAccess(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
registryID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putRegistryAccessRequest
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
}
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.ErrRegistryNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range req.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
registry.AuthorizedUsers = authorizedUserIDs
}
if req.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range req.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
registry.AuthorizedTeams = authorizedTeamIDs
}
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putRegistryAccessRequest struct {
AuthorizedUsers []int `valid:"-"`
AuthorizedTeams []int `valid:"-"`
}
// handlePutRegistry handles PUT requests on /registries/:id
func (handler *RegistryHandler) handlePutRegistry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
registryID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putRegistriesRequest
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
}
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.ErrRegistryNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
registries, err := handler.RegistryService.Registries()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
for _, r := range registries {
if r.URL == req.URL && r.ID != registry.ID {
httperror.WriteErrorResponse(w, portainer.ErrRegistryAlreadyExists, http.StatusConflict, handler.Logger)
return
}
}
if req.Name != "" {
registry.Name = req.Name
}
if req.URL != "" {
registry.URL = req.URL
}
if req.Authentication {
registry.Authentication = true
registry.Username = req.Username
registry.Password = req.Password
} else {
registry.Authentication = false
registry.Username = ""
registry.Password = ""
}
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putRegistriesRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
Authentication bool `valid:""`
Username string `valid:""`
Password string `valid:""`
}
// handleDeleteRegistry handles DELETE requests on /registries/:id
func (handler *RegistryHandler) handleDeleteRegistry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
registryID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
_, err = handler.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.ErrRegistryNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.RegistryService.DeleteRegistry(portainer.RegistryID(registryID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}

View File

@@ -60,6 +60,24 @@ func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []po
return filteredUsers
}
// FilterRegistries filters registries based on user role and team memberships.
// Non administrator users only have access to authorized endpoints.
func FilterRegistries(registries []portainer.Registry, context *RestrictedRequestContext) ([]portainer.Registry, error) {
filteredRegistries := registries
if !context.IsAdmin {
filteredRegistries = make([]portainer.Registry, 0)
for _, registry := range registries {
if isRegistryAccessAuthorized(&registry, context.UserID, context.UserMemberships) {
filteredRegistries = append(filteredRegistries, registry)
}
}
}
return filteredRegistries, nil
}
// FilterEndpoints filters endpoints based on user role and team memberships.
// Non administrator users only have access to authorized endpoints.
func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestContext) ([]portainer.Endpoint, error) {
@@ -78,6 +96,22 @@ func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestC
return filteredEndpoints, nil
}
func isRegistryAccessAuthorized(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
for _, authorizedUserID := range registry.AuthorizedUsers {
if authorizedUserID == userID {
return true
}
}
for _, membership := range memberships {
for _, authorizedTeamID := range registry.AuthorizedTeams {
if membership.TeamID == authorizedTeamID {
return true
}
}
}
return false
}
func isEndpointAccessAuthorized(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
for _, authorizedUserID := range endpoint.AuthorizedUsers {
if authorizedUserID == userID {

View File

@@ -25,6 +25,8 @@ type Server struct {
CryptoService portainer.CryptoService
JWTService portainer.JWTService
FileService portainer.FileService
RegistryService portainer.RegistryService
DockerHubService portainer.DockerHubService
Handler *handler.Handler
SSL bool
SSLCert string
@@ -66,6 +68,10 @@ func (server *Server) Start() error {
endpointHandler.EndpointService = server.EndpointService
endpointHandler.FileService = server.FileService
endpointHandler.ProxyManager = proxyManager
var registryHandler = handler.NewRegistryHandler(requestBouncer)
registryHandler.RegistryService = server.RegistryService
var dockerHubHandler = handler.NewDockerHubHandler(requestBouncer)
dockerHubHandler.DockerHubService = server.DockerHubService
var resourceHandler = handler.NewResourceHandler(requestBouncer)
resourceHandler.ResourceControlService = server.ResourceControlService
var uploadHandler = handler.NewUploadHandler(requestBouncer)
@@ -78,6 +84,8 @@ func (server *Server) Start() error {
TeamHandler: teamHandler,
TeamMembershipHandler: teamMembershipHandler,
EndpointHandler: endpointHandler,
RegistryHandler: registryHandler,
DockerHubHandler: dockerHubHandler,
ResourceHandler: resourceHandler,
SettingsHandler: settingsHandler,
StatusHandler: statusHandler,

View File

@@ -94,6 +94,30 @@ type (
Role UserRole
}
// RegistryID represents a registry identifier.
RegistryID int
// Registry represents a Docker registry with all the info required
// to connect to it.
Registry struct {
ID RegistryID `json:"Id"`
Name string `json:"Name"`
URL string `json:"URL"`
Authentication bool `json:"Authentication"`
Username string `json:"Username"`
Password string `json:"Password"`
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
}
// DockerHub represents all the required information to connect and use the
// Docker Hub.
DockerHub struct {
Authentication bool `json:"Authentication"`
Username string `json:"Username"`
Password string `json:"Password"`
}
// EndpointID represents an endpoint identifier.
EndpointID int
@@ -217,6 +241,21 @@ type (
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
}
// RegistryService represents a service for managing registry data.
RegistryService interface {
Registry(ID RegistryID) (*Registry, error)
Registries() ([]Registry, error)
CreateRegistry(registry *Registry) error
UpdateRegistry(ID RegistryID, registry *Registry) error
DeleteRegistry(ID RegistryID) error
}
// DockerHubService represents a service for managing the DockerHub object.
DockerHubService interface {
DockerHub() (*DockerHub, error)
StoreDockerHub(registry *DockerHub) error
}
// SettingsService represents a service for managing application settings.
SettingsService interface {
Settings() (*Settings, error)
@@ -266,7 +305,7 @@ type (
const (
// APIVersion is the version number of the Portainer API.
APIVersion = "1.13.2"
APIVersion = "1.13.4"
// DBVersion is the version number of the Portainer database.
DBVersion = 2
// DefaultTemplatesURL represents the default URL for the templates definitions.

View File

@@ -28,6 +28,7 @@ angular.module('portainer', [
'containers',
'createContainer',
'createNetwork',
'createRegistry',
'createSecret',
'createService',
'createVolume',
@@ -43,6 +44,9 @@ angular.module('portainer', [
'network',
'networks',
'node',
'registries',
'registry',
'registryAccess',
'secrets',
'secret',
'service',
@@ -69,7 +73,6 @@ angular.module('portainer', [
}
localStorageServiceProvider
.setStorageType('sessionStorage')
.setPrefix('portainer');
jwtOptionsProvider.config({
@@ -253,6 +256,19 @@ angular.module('portainer', [
}
}
})
.state('actions.create.registry', {
url: '/registry',
views: {
'content@': {
templateUrl: 'app/components/createRegistry/createregistry.html',
controller: 'CreateRegistryController'
},
'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('actions.create.secret', {
url: '/secret',
views: {
@@ -431,6 +447,45 @@ angular.module('portainer', [
}
}
})
.state('registries', {
url: '/registries/',
views: {
'content@': {
templateUrl: 'app/components/registries/registries.html',
controller: 'RegistriesController'
},
'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('registry', {
url: '^/registries/:id',
views: {
'content@': {
templateUrl: 'app/components/registry/registry.html',
controller: 'RegistryController'
},
'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('registry.access', {
url: '^/registries/:id/access',
views: {
'content@': {
templateUrl: 'app/components/registryAccess/registryAccess.html',
controller: 'RegistryAccessController'
},
'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('secrets', {
url: '^/secrets/',
views: {
@@ -687,7 +742,8 @@ angular.module('portainer', [
.constant('TEAM_MEMBERSHIPS_ENDPOINT', 'api/team_memberships')
.constant('RESOURCE_CONTROL_ENDPOINT', 'api/resource_controls')
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
.constant('DOCKERHUB_ENDPOINT', 'api/dockerhub')
.constant('REGISTRIES_ENDPOINT', 'api/registries')
.constant('TEMPLATES_ENDPOINT', 'api/templates')
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
.constant('PAGINATION_MAX_ITEMS', 10);
// .constant('UI_VERSION', 'v1.13.2');

View File

@@ -17,11 +17,11 @@
<!-- !access-control-switch -->
<!-- restricted-access -->
<div class="form-group" ng-if="formValues.enableAccessControl" style="margin-bottom: 0">
<div class="ownership_wrapper">
<div class="boxselector_wrapper">
<div ng-if="isAdmin">
<input type="radio" id="access_administrators" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="administrators">
<label for="access_administrators">
<div class="ownership_header">
<div class="boxselector_header">
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Administrators
</div>
@@ -31,7 +31,7 @@
<div ng-if="isAdmin">
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="restricted">
<label for="access_restricted">
<div class="ownership_header">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>
@@ -43,7 +43,7 @@
<div ng-if="!isAdmin">
<input type="radio" id="access_private" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="private">
<label for="access_private">
<div class="ownership_header">
<div class="boxselector_header">
<i ng-class="'private' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Private
</div>
@@ -55,7 +55,7 @@
<div ng-if="!isAdmin && availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="restricted">
<label for="access_restricted">
<div class="ownership_header">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>

View File

@@ -63,11 +63,11 @@
<!-- edit-ownership-choices -->
<tr ng-if="state.editOwnership">
<td colspan="2">
<div class="ownership_wrapper">
<div class="boxselector_wrapper">
<div ng-if="isAdmin">
<input type="radio" id="access_administrators" ng-model="formValues.Ownership" value="administrators">
<label for="access_administrators">
<div class="ownership_header">
<div class="boxselector_header">
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Administrators
</div>
@@ -77,7 +77,7 @@
<div ng-if="isAdmin">
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" value="restricted">
<label for="access_restricted">
<div class="ownership_header">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>
@@ -89,7 +89,7 @@
<div ng-if="!isAdmin && state.canChangeOwnershipToTeam && availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" value="restricted">
<label for="access_restricted">
<div class="ownership_header">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>
@@ -104,7 +104,7 @@
<div>
<input type="radio" id="access_public" ng-model="formValues.Ownership" value="public">
<label for="access_public">
<div class="ownership_header">
<div class="boxselector_header">
<i ng-class="'public' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Public
</div>

View File

@@ -134,21 +134,11 @@
</div>
</div>
<!-- !tag-description -->
<!-- name-and-registry-inputs -->
<!-- image-and-registry -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional">
</div>
<por-image-registry image="config.Image" registry="config.Registry"></por-image-registry>
</div>
<!-- !name-and-registry-inputs -->
<!-- !image-and-registry -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
@@ -300,6 +290,22 @@
<div class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
<hr />
<form class="form-horizontal">
<!-- network-input -->
<div class="row">
<label for="container_network" class="col-sm-3 col-lg-2 control-label text-left">Join a Network</label>
<div class="col-sm-5 col-lg-4">
<select class="form-control" ng-model="selectedNetwork" id="container_network">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Id">{{ net.Name }}</option>
</select>
</div>
<div class="col-sm-1">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!selectedNetwork" ng-click="containerJoinNetwork(container, selectedNetwork)">Join Network</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -197,5 +197,42 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con
});
};
$scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) {
$('#joinNetworkSpinner').show();
Network.connect({id: networkId}, { Container: $stateParams.id }, function (d) {
if (container.message) {
$('#joinNetworkSpinner').hide();
Notifications.error('Error', d, 'Unable to connect container to network');
} else {
$('#joinNetworkSpinner').hide();
Notifications.success('Container joined network', $stateParams.id);
$state.go('container', {id: $stateParams.id}, {reload: true});
}
}, function (e) {
$('#joinNetworkSpinner').hide();
Notifications.error('Failure', e, 'Unable to connect container to network');
});
};
Network.query({}, function (d) {
var networks = d;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
networks = d.filter(function (network) {
if (network.Scope === 'global') {
return network;
}
});
networks.push({Name: 'bridge'});
networks.push({Name: 'host'});
networks.push({Name: 'none'});
}
$scope.availableNetworks = networks;
if (!_.find(networks, {'Name': 'bridge'})) {
networks.push({Name: 'nat'});
}
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve networks');
});
update();
}]);

View File

@@ -1,6 +1,6 @@
angular.module('containers', [])
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'Info', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider',
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, Info, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'SystemService', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider',
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, SystemService, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
$scope.state.displayAll = true;
@@ -202,15 +202,18 @@ angular.module('containers', [])
return swarm_hosts;
}
function initView(){
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
Info.get({}, function (d) {
function initView() {
var provider = $scope.applicationState.endpoint.mode.provider;
$q.when(provider !== 'DOCKER_SWARM' || SystemService.info())
.then(function success(data) {
if (provider === 'DOCKER_SWARM') {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: $scope.state.displayAll ? 1 : 0});
});
} else {
}
update({all: $scope.state.displayAll ? 1 : 0});
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve cluster information');
});
}
initView();

View File

@@ -1,8 +1,8 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference.
angular.module('createContainer', [])
.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator',
function ($q, $scope, $state, $stateParams, $filter, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) {
.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator',
function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) {
$scope.formValues = {
alwaysPull: true,
@@ -94,7 +94,7 @@ function ($q, $scope, $state, $stateParams, $filter, Info, Container, ContainerH
function prepareImageConfig(config) {
var image = config.Image;
var registry = $scope.formValues.Registry;
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry.URL);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
$scope.imageConfig = imageConfig;
}
@@ -299,7 +299,7 @@ function ($q, $scope, $state, $stateParams, $filter, Info, Container, ContainerH
};
function createContainer(config, accessControlData) {
$q.when(!$scope.formValues.alwaysPull || ImageService.pullImage($scope.config.Image, $scope.formValues.Registry))
$q.when(!$scope.formValues.alwaysPull || ImageService.pullImage($scope.config.Image, $scope.formValues.Registry, true))
.finally(function final() {
ContainerService.createAndStartContainer(config)
.then(function success(data) {

View File

@@ -21,21 +21,11 @@
<div class="col-sm-12 form-section-title">
Image configuration
</div>
<!-- image-and-registry-inputs -->
<!-- image-and-registry -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div>
<por-image-registry image="config.Image" registry="formValues.Registry"></por-image-registry>
</div>
<!-- !image-and-registry-inputs -->
<!-- !image-and-registry -->
<!-- always-pull -->
<div class="form-group">
<div class="col-sm-12">

View File

@@ -0,0 +1,49 @@
angular.module('createRegistry', [])
.controller('CreateRegistryController', ['$scope', '$state', 'RegistryService', 'Notifications',
function ($scope, $state, RegistryService, Notifications) {
$scope.state = {
RegistryType: 'quay'
};
$scope.formValues = {
Name: 'Quay',
URL: 'quay.io',
Authentication: true,
Username: '',
Password: ''
};
$scope.selectQuayRegistry = function() {
$scope.formValues.Name = 'Quay';
$scope.formValues.URL = 'quay.io';
$scope.formValues.Authentication = true;
};
$scope.selectCustomRegistry = function() {
$scope.formValues.Name = '';
$scope.formValues.URL = '';
$scope.formValues.Authentication = false;
};
$scope.addRegistry = function() {
$('#createRegistrySpinner').show();
var registryName = $scope.formValues.Name;
var registryURL = $scope.formValues.URL.replace(/^https?\:\/\//i, '');
var authentication = $scope.formValues.Authentication;
var username = $scope.formValues.Username;
var password = $scope.formValues.Password;
RegistryService.createRegistry(registryName, registryURL, authentication, username, password)
.then(function success(data) {
Notifications.success('Registry successfully created');
$state.go('registries');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create registry');
})
.finally(function final() {
$('#createRegistrySpinner').hide();
});
};
}]);

View File

@@ -0,0 +1,117 @@
<rd-header>
<rd-header-title title="Create registry">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="display:none"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="registries">Registries</a> > Add registry
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
Registry provider
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-click="selectQuayRegistry()">
<input type="radio" id="registry_quay" ng-model="state.RegistryType" value="quay">
<label for="registry_quay">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
Quay.io
</div>
<p>Quay container registry</p>
</label>
</div>
<div ng-click="selectCustomRegistry()">
<input type="radio" id="registry_custom" ng-model="state.RegistryType" value="custom">
<label for="registry_custom">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
Custom registry
</div>
<p>Define your own registry</p>
</label>
</div>
</div>
</div>
<div class="col-sm-12 form-section-title" ng-if="state.RegistryType === 'custom'">
Important notice
</div>
<div class="form-group" ng-if="state.RegistryType === 'custom'">
<span class="col-sm-12 text-muted small">
Docker requires you to connect to a <a href="https://docs.docker.com/registry/deploying/#running-a-domain-registry" target="_blank">secure registry</a>.
You can find more information about how to connect to an insecure registry <a href="https://docs.docker.com/registry/insecure/" target="_blank">in the Docker documentation</a>.
</span>
</div>
<div class="col-sm-12 form-section-title">
Registry details
</div>
<!-- name-input -->
<div class="form-group" ng-if="state.RegistryType === 'custom'">
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_name" ng-model="formValues.Name" placeholder="e.g. my-registry">
</div>
</div>
<!-- !name-input -->
<!-- registry-url-input -->
<div class="form-group" ng-if="state.RegistryType === 'custom'">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry. Any protocol will be stripped."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:5000 or myregistry.domain.tld">
</div>
</div>
<!-- !registry-url-input -->
<!-- authentication-checkbox -->
<div class="form-group" ng-if="state.RegistryType === 'custom'">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.Authentication"><i></i>
</label>
</div>
</div>
<!-- !authentication-checkbox -->
<!-- authentication-credentials -->
<div ng-if="formValues.Authentication || state.RegistryType === 'quay'">
<!-- credentials-user -->
<div class="form-group">
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="credentials_username" ng-model="formValues.Username">
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="credentials_password" ng-model="formValues.Password">
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- !authentication-credentials -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.Authentication && (!formValues.Username || !formValues.Password))" ng-click="addRegistry()">Add registry</button>
<i id="createRegistrySpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -1,13 +1,13 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference.
angular.module('createService', [])
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator) {
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator', 'RegistryService', 'HttpRequestHelper',
function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator, RegistryService, HttpRequestHelper) {
$scope.formValues = {
Name: '',
Image: '',
Registry: '',
Registry: {},
Mode: 'replicated',
Replicas: 1,
Command: '',
@@ -105,7 +105,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
};
function prepareImageConfig(config, input) {
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry);
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry.URL);
config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + imageConfig.tag;
}
@@ -222,7 +222,9 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
var secrets = [];
angular.forEach(input.Secrets, function(secret) {
if (secret.model) {
secrets.push(SecretHelper.secretConfig(secret.model));
var s = SecretHelper.secretConfig(secret.model);
s.File.Name = s.SecretName;
secrets.push(s);
}
});
config.TaskTemplate.ContainerSpec.Secrets = secrets;
@@ -257,6 +259,10 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
}
function createNewService(config, accessControlData) {
var registry = $scope.formValues.Registry;
var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
Service.create(config).$promise
.then(function success(data) {
var serviceIdentifier = data.ID;

View File

@@ -23,21 +23,11 @@
<div class="col-sm-12 form-section-title">
Image configuration
</div>
<!-- image-and-registry-inputs -->
<!-- image-and-registry -->
<div class="form-group">
<label for="service_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="formValues.Image" id="service_image" placeholder="e.g. nginx:latest">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div>
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry>
</div>
<!-- !image-and-registry-inputs -->
<!-- !image-and-registry -->
<div class="col-sm-12 form-section-title">
Scheduling
</div>
@@ -252,7 +242,7 @@
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.Source">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Id">{{ vol.Id|truncate:30 }}</option>
</select>
</div>
<!-- !volume -->

View File

@@ -1,6 +1,6 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'InfoService', 'ResourceControlService', 'Authentication', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
function ($scope, $state, VolumeService, InfoService, ResourceControlService, Authentication, Notifications, ControllerDataPipeline, FormValidator) {
.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'SystemService', 'ResourceControlService', 'Authentication', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
function ($scope, $state, VolumeService, SystemService, ResourceControlService, Authentication, Notifications, ControllerDataPipeline, FormValidator) {
$scope.formValues = {
Driver: 'local',
@@ -69,7 +69,7 @@ function ($scope, $state, VolumeService, InfoService, ResourceControlService, Au
function initView() {
$('#loadingViewSpinner').show();
InfoService.getVolumePlugins()
SystemService.getVolumePlugins()
.then(function success(data) {
$scope.availableVolumeDrivers = data;
})

View File

@@ -1,6 +1,6 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications',
function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) {
.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'SystemService', 'Notifications',
function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, SystemService, Notifications) {
$scope.containerData = {
total: 0
@@ -68,7 +68,7 @@ function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, Info,
Image.query({}).$promise,
Volume.query({}).$promise,
Network.query({}).$promise,
Info.get({}).$promise
SystemService.info()
]).then(function (d) {
prepareContainerData(d[0]);
prepareImageData(d[1]);

View File

@@ -8,7 +8,7 @@
<rd-header-content>Docker</rd-header-content>
</rd-header>
<div class="row" ng-if="state.loaded">
<div class="row" ng-if="info && version">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-code" title="Engine version"></rd-widget-header>
@@ -50,7 +50,7 @@
</div>
</div>
<div class="row" ng-if="state.loaded">
<div class="row" ng-if="info && version">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-th" title="Engine status"></rd-widget-header>
@@ -92,7 +92,7 @@
</div>
</div>
<div class="row" ng-if="state.loaded && info.Plugins">
<div class="row" ng-if="info && info.Plugins">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title="Engine plugins"></rd-widget-header>

View File

@@ -1,24 +1,26 @@
angular.module('docker', [])
.controller('DockerController', ['$scope', 'Info', 'Version', 'Notifications',
function ($scope, Info, Version, Notifications) {
$scope.state = {
loaded: false
};
.controller('DockerController', ['$q', '$scope', 'SystemService', 'Notifications',
function ($q, $scope, SystemService, Notifications) {
$scope.info = {};
$scope.version = {};
Info.get({}, function (infoData) {
$scope.info = infoData;
Version.get({}, function (versionData) {
$scope.version = versionData;
$scope.state.loaded = true;
$('#loadingViewSpinner').hide();
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve engine details');
function initView() {
$('#loadingViewSpinner').show();
$q.all({
version: SystemService.version(),
info: SystemService.info()
})
.then(function success(data) {
$scope.version = data.version;
$scope.info = data.info;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve engine details');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve engine information');
$('#loadingViewSpinner').hide();
});
}
initView();
}]);

View File

@@ -38,7 +38,7 @@
<portainer-tooltip position="bottom" message="URL or IP address where exposed containers will be reachable. This field is optional and will default to the endpoint URL."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input ng-disabled="endpointType === 'local'" type="text" class="form-control" id="endpoint_public_url" ng-model="endpoint.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
<input type="text" class="form-control" id="endpoint_public_url" ng-model="endpoint.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
</div>
</div>
<!-- !endpoint-public-url-input -->

View File

@@ -41,137 +41,5 @@
</div>
</div>
<div class="row" ng-if="endpoint">
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users and groups">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="state.pagination_count_accesses" ng-change="changePaginationCountAccesses()">
<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-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="authorizeAllAccesses()" ng-disabled="accesses.length === 0 || filteredUsers.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="state.filterUsers" 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>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAccesses('Name')">
Name
<span ng-show="sortTypeAccesses == 'Name' && !sortReverseAccesses" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeAccesses == 'Name' && sortReverseAccesses" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAccesses('Type')">
Type
<span ng-show="sortTypeAccesses == 'Type' && !sortReverseAccesses" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeAccesses == 'Type' && sortReverseAccesses" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="authorizeAccess(user)" class="interactive" dir-paginate="user in accesses | filter:state.filterUsers | orderBy:sortTypeAccesses:sortReverseAccesses | itemsPerPage: state.pagination_count_accesses">
<td>{{ user.Name }}</td>
<td>
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
{{ user.Type }}
</td>
</tr>
<tr ng-if="!accesses">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="accesses.length === 0 || (accesses | filter:state.filterUsers | orderBy:sortTypeAccesses:sortReverseAccesses | itemsPerPage: state.pagination_count_accesses).length === 0">
<td colspan="2" class="text-center text-muted">No user or team available.</td>
</tr>
</tbody>
</table>
<div ng-if="accesses" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users and groups">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="state.pagination_count_authorizedAccesses" ng-change="changePaginationCountAuthorizedAccesses()">
<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-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="unauthorizeAllAccesses()" ng-disabled="authorizedAccesses.length === 0 || filteredAuthorizedUsers.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="state.filterAuthorizedUsers" 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>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAuthorizedAccesses('Name')">
Name
<span ng-show="sortTypeAuthorizedAccesses == 'Name' && !sortReverseAuthorizedAccesses" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeAuthorizedAccesses == 'Name' && sortReverseAuthorizedAccesses" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAuthorizedAccesses('Type')">
Type
<span ng-show="sortTypeAuthorizedAccesses == 'Type' && !sortReverseAuthorizedAccesses" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeAuthorizedAccesses == 'Type' && sortReverseAuthorizedAccesses" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="unauthorizeAccess(user)" class="interactive" pagination-id="table_authaccess" dir-paginate="user in authorizedAccesses | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedAccesses:sortReverseAuthorizedAccesses | itemsPerPage: state.pagination_count_authorizedAccesses">
<td>{{ user.Name }}</td>
<td>
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
{{ user.Type }}
</td>
</tr>
<tr ng-if="!authorizedAccesses">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="authorizedAccesses.length === 0 || (authorizedAccesses | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedAccesses:sortReverseAuthorizedAccesses | itemsPerPage: state.pagination_count_authorizedAccesses).length === 0">
<td colspan="2" class="text-center text-muted">No authorized user or team.</td>
</tr>
</tbody>
</table>
<div ng-if="authorizedAccesses" class="pull-left pagination-controls">
<dir-pagination-controls pagination-id="table_authaccess"></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<por-access-management ng-if="endpoint" access-controlled-entity="endpoint" update-access="updateAccess(userAccesses, teamAccesses)">
</por-access-management>

View File

@@ -1,177 +1,18 @@
angular.module('endpointAccess', [])
.controller('EndpointAccessController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'UserService', 'TeamService', 'Pagination', 'Notifications',
function ($q, $scope, $state, $stateParams, $filter, EndpointService, UserService, TeamService, Pagination, Notifications) {
.controller('EndpointAccessController', ['$scope', '$stateParams', 'EndpointService', 'Notifications',
function ($scope, $stateParams, EndpointService, Notifications) {
$scope.state = {
pagination_count_accesses: Pagination.getPaginationCount('endpoint_access_accesses'),
pagination_count_authorizedAccesses: Pagination.getPaginationCount('endpoint_access_authorizedAccesses')
};
$scope.sortTypeAccesses = 'Type';
$scope.sortReverseAccesses = false;
$scope.orderAccesses = function(sortType) {
$scope.sortReverseAccesses = ($scope.sortTypeAccesses === sortType) ? !$scope.sortReverseAccesses : false;
$scope.sortTypeAccesses = sortType;
};
$scope.changePaginationCountAccesses = function() {
Pagination.setPaginationCount('endpoint_access_accesses', $scope.state.pagination_count_accesses);
};
$scope.sortTypeAuthorizedAccesses = 'Type';
$scope.sortReverseAuthorizedAccesses = false;
$scope.orderAuthorizedAccesses = function(sortType) {
$scope.sortReverseAuthorizedAccesses = ($scope.sortTypeAuthorizedAccesses === sortType) ? !$scope.sortReverseAuthorizedAccesses : false;
$scope.sortTypeAuthorizedAccesses = sortType;
};
$scope.changePaginationCountAuthorizedAccesses = function() {
Pagination.setPaginationCount('endpoint_access_authorizedAccesses', $scope.state.pagination_count_authorizedAccesses);
};
$scope.authorizeAllAccesses = function() {
var authorizedUsers = [];
var authorizedTeams = [];
angular.forEach($scope.authorizedAccesses, function (a) {
if (a.Type === 'user') {
authorizedUsers.push(a.Id);
} else if (a.Type === 'team') {
authorizedTeams.push(a.Id);
}
});
angular.forEach($scope.accesses, function (a) {
if (a.Type === 'user') {
authorizedUsers.push(a.Id);
} else if (a.Type === 'team') {
authorizedTeams.push(a.Id);
}
});
EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams)
.then(function success(data) {
$scope.authorizedAccesses = $scope.authorizedAccesses.concat($scope.accesses);
$scope.accesses = [];
Notifications.success('Endpoint accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update endpoint accesses');
});
};
$scope.unauthorizeAllAccesses = function() {
EndpointService.updateAccess($stateParams.id, [], [])
.then(function success(data) {
$scope.accesses = $scope.accesses.concat($scope.authorizedAccesses);
$scope.authorizedAccesses = [];
Notifications.success('Endpoint accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update endpoint accesses');
});
};
$scope.authorizeAccess = function(access) {
var authorizedUsers = [];
var authorizedTeams = [];
angular.forEach($scope.authorizedAccesses, function (a) {
if (a.Type === 'user') {
authorizedUsers.push(a.Id);
} else if (a.Type === 'team') {
authorizedTeams.push(a.Id);
}
});
if (access.Type === 'user') {
authorizedUsers.push(access.Id);
} else if (access.Type === 'team') {
authorizedTeams.push(access.Id);
}
EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams)
.then(function success(data) {
removeAccessFromArray(access, $scope.accesses);
$scope.authorizedAccesses.push(access);
Notifications.success('Endpoint accesses successfully updated', access.Name);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update endpoint accesses');
});
};
$scope.unauthorizeAccess = function(access) {
var authorizedUsers = [];
var authorizedTeams = [];
angular.forEach($scope.authorizedAccesses, function (a) {
if (a.Type === 'user') {
authorizedUsers.push(a.Id);
} else if (a.Type === 'team') {
authorizedTeams.push(a.Id);
}
});
if (access.Type === 'user') {
_.remove(authorizedUsers, function(n) {
return n === access.Id;
});
} else if (access.Type === 'team') {
_.remove(authorizedTeams, function(n) {
return n === access.Id;
});
}
EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams)
.then(function success(data) {
removeAccessFromArray(access, $scope.authorizedAccesses);
$scope.accesses.push(access);
Notifications.success('Endpoint accesses successfully updated', access.Name);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update endpoint accesses');
});
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
return EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams);
};
function initView() {
$('#loadingViewSpinner').show();
$q.all({
endpoint: EndpointService.endpoint($stateParams.id),
users: UserService.users(false),
teams: TeamService.teams()
})
EndpointService.endpoint($stateParams.id)
.then(function success(data) {
$scope.endpoint = data.endpoint;
$scope.accesses = [];
var users = data.users.map(function (user) {
return new EndpointAccessUserViewModel(user);
});
var teams = data.teams.map(function (team) {
return new EndpointAccessTeamViewModel(team);
});
$scope.accesses = $scope.accesses.concat(users, teams);
$scope.authorizedAccesses = [];
angular.forEach($scope.endpoint.AuthorizedUsers, function(userID) {
for (var i = 0, l = $scope.accesses.length; i < l; i++) {
if ($scope.accesses[i].Type === 'user' && $scope.accesses[i].Id === userID) {
$scope.authorizedAccesses.push($scope.accesses[i]);
$scope.accesses.splice(i, 1);
return;
}
}
});
angular.forEach($scope.endpoint.AuthorizedTeams, function(teamID) {
for (var i = 0, l = $scope.accesses.length; i < l; i++) {
if ($scope.accesses[i].Type === 'team' && $scope.accesses[i].Id === teamID) {
$scope.authorizedAccesses.push($scope.accesses[i]);
$scope.accesses.splice(i, 1);
return;
}
}
});
$scope.endpoint = data;
})
.catch(function error(err) {
$scope.accesses = [];
$scope.authorizedAccesses = [];
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
})
.finally(function final(){
@@ -179,14 +20,5 @@ function ($q, $scope, $state, $stateParams, $filter, EndpointService, UserServic
});
}
function removeAccessFromArray(access, accesses) {
for (var i = 0, l = accesses.length; i < l; i++) {
if (access.Type === accesses[i].Type && access.Id === accesses[i].Id) {
accesses.splice(i, 1);
return;
}
}
}
initView();
}]);

View File

@@ -183,12 +183,7 @@
<td>{{ endpoint.URL | stripprotocol }}</td>
<td>
<span ng-if="applicationState.application.endpointManagement">
<span ng-if="endpoint.Id !== activeEndpointID">
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
</span>
<span class="small text-muted" ng-if="endpoint.Id === activeEndpointID">
<i class="fa fa-lock" aria-hidden="true"></i> You cannot edit the active endpoint
</span>
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
</span>
<span ng-if="applicationState.application.authentication">
<a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>

View File

@@ -101,7 +101,6 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
EndpointService.endpoints()
.then(function success(data) {
$scope.endpoints = data;
$scope.activeEndpointID = EndpointProvider.endpointID();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve endpoints');

View File

@@ -1,6 +1,6 @@
angular.module('events', [])
.controller('EventsController', ['$scope', 'Notifications', 'Events', 'Pagination',
function ($scope, Notifications, Events, Pagination) {
.controller('EventsController', ['$scope', 'Notifications', 'SystemService', 'Pagination',
function ($scope, Notifications, SystemService, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('events');
$scope.sortType = 'Time';
@@ -15,18 +15,22 @@ function ($scope, Notifications, Events, Pagination) {
Pagination.setPaginationCount('events', $scope.state.pagination_count);
};
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
function initView() {
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
Events.query({since: from, until: to},
function(d) {
$scope.events = d.map(function (item) {
return new EventViewModel(item);
$('#loadEventsSpinner').show();
SystemService.events(from, to)
.then(function success(data) {
$scope.events = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load events');
})
.finally(function final() {
$('#loadEventsSpinner').hide();
});
$('#loadEventsSpinner').hide();
},
function (e) {
$('#loadEventsSpinner').hide();
Notifications.error('Failure', e, 'Unable to load events');
});
}
initView();
}]);

View File

@@ -54,21 +54,11 @@
<rd-widget-header icon="fa-tag" title="Tag the image"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-and-registry-inputs -->
<!-- image-and-registry -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div>
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry>
</div>
<!-- !name-and-registry-inputs -->
<!-- !image-and-registry -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
@@ -78,7 +68,7 @@
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="tagImage()">Tag</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Image" ng-click="tagImage()">Tag</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>

View File

@@ -1,17 +1,17 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$stateParams', '$state', 'ImageService', 'Notifications',
function ($scope, $stateParams, $state, ImageService, Notifications) {
$scope.config = {
.controller('ImageController', ['$scope', '$stateParams', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications',
function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService, Notifications) {
$scope.formValues = {
Image: '',
Registry: ''
};
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var image = $scope.formValues.Image;
var registry = $scope.formValues.Registry;
ImageService.tagImage($stateParams.id, image, registry)
ImageService.tagImage($stateParams.id, image, registry.URL)
.then(function success(data) {
Notifications.success('Image successfully tagged');
$state.go('image', {id: $stateParams.id}, {reload: true});
@@ -24,28 +24,35 @@ function ($scope, $stateParams, $state, ImageService, Notifications) {
});
};
$scope.pushTag = function(tag) {
$scope.pushTag = function(repository) {
$('#loadingViewSpinner').show();
ImageService.pushImage(tag)
.then(function success() {
Notifications.success('Image successfully pushed');
RegistryService.retrieveRegistryFromRepository(repository)
.then(function success(data) {
var registry = data;
return ImageService.pushImage(repository, registry);
})
.then(function success(data) {
Notifications.success('Image successfully pushed', repository);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to push image tag');
Notifications.error('Failure', err, 'Unable to push image to repository');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
$scope.pullTag = function(tag) {
$scope.pullTag = function(repository) {
$('#loadingViewSpinner').show();
ImageService.pullTag(tag)
RegistryService.retrieveRegistryFromRepository(repository)
.then(function success(data) {
Notifications.success('Image successfully pulled', tag);
var registry = data;
return ImageService.pullImage(repository, registry, false);
})
.catch(function error(err){
.then(function success(data) {
Notifications.success('Image successfully pulled', repository);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to pull image');
})
.finally(function final() {
@@ -53,15 +60,15 @@ function ($scope, $stateParams, $state, ImageService, Notifications) {
});
};
$scope.removeTag = function(id) {
$scope.removeTag = function(repository) {
$('#loadingViewSpinner').show();
ImageService.deleteImage(id, false)
ImageService.deleteImage(repository, false)
.then(function success() {
if ($scope.image.RepoTags.length === 1) {
Notifications.success('Image successfully deleted', id);
Notifications.success('Image successfully deleted', repository);
$state.go('images', {}, {reload: true});
} else {
Notifications.success('Tag successfully deleted', id);
Notifications.success('Tag successfully deleted', repository);
$state.go('image', {id: $stateParams.id}, {reload: true});
}
})

View File

@@ -15,21 +15,11 @@
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-and-registry-inputs -->
<!-- image-and-registry -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div>
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry>
</div>
<!-- !name-and-registry-inputs -->
<!-- !image-and-registry -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
@@ -39,7 +29,7 @@
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="pullImage()">Pull</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Image" ng-click="pullImage()">Pull</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>

View File

@@ -7,7 +7,7 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService)
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
$scope.config = {
$scope.formValues = {
Image: '',
Registry: ''
};
@@ -40,10 +40,11 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService)
$scope.pullImage = function() {
$('#pullImageSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
ImageService.pullImage(image, registry)
var image = $scope.formValues.Image;
var registry = $scope.formValues.Registry;
ImageService.pullImage(image, registry, false)
.then(function success(data) {
Notifications.success('Image successfully pulled', image);
$state.reload();
})
.catch(function error(err) {

View File

@@ -0,0 +1,150 @@
<rd-header>
<rd-header-title title="Registries">
<a data-toggle="tooltip" title="Refresh" ui-sref="registries" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Registry management</rd-header-content>
</rd-header>
<div class="row" ng-if="dockerhub">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-database" title="DockerHub">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- note -->
<div class="form-group">
<span class="col-sm-12 text-muted small">
The DockerHub registry can be used by any user. You can specify the credentials that will be used to push &amp; pull images here.
</span>
</div>
<!-- !note -->
<!-- authentication-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to push/pull private images."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="dockerhub.Authentication"><i></i>
</label>
</div>
</div>
<!-- !authentication-checkbox -->
<!-- authentication-credentials -->
<div ng-if="dockerhub.Authentication">
<!-- credentials-user -->
<div class="form-group">
<label for="hub_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="hub_username" ng-model="dockerhub.Username">
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="hub_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="hub_password" ng-model="dockerhub.Password">
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- !authentication-credentials -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="dockerhub.Authentication && (!dockerhub.Username || !dockerhub.Password)" ng-click="updateDockerHub()">Update</button>
<i id="updateDockerhubSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-database" title="Available registries">
<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.registry"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add registry</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 ui-sref="registries" 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 ui-sref="registries" ng-click="order('URL')">
URL
<span ng-show="sortType == 'URL' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'URL' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr dir-paginate="registry in (state.filteredRegistries = (registries | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="registry.Checked" ng-change="selectItem(registry)" /></td>
<td>
<a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a>
<span ng-if="registry.Authentication" style="margin-left: 5px;">
<i class="fa fa-shield" aria-hidden="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip" uib-tooltip="Authentication is enabled for this registry."></i>
</span>
</td>
<td>{{ registry.URL }}</td>
<td>
<span ng-if="applicationState.application.authentication">
<a ui-sref="registry.access({id: registry.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
</span>
</td>
</tr>
<tr ng-if="!registries">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="registries.length == 0">
<td colspan="3" class="text-center text-muted">No registries available.</td>
</tr>
</tbody>
</table>
<div ng-if="registries" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>

View File

@@ -0,0 +1,113 @@
angular.module('registries', [])
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'Pagination',
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, Pagination) {
$scope.state = {
selectedItemCount: 0,
pagination_count: Pagination.getPaginationCount('registries')
};
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.updateDockerHub = function() {
$('#updateDockerhubSpinner').show();
var dockerhub = $scope.dockerhub;
DockerHubService.update(dockerhub)
.then(function success(data) {
Notifications.success('DockerHub registry updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update DockerHub details');
})
.finally(function final() {
$('#updateDockerhubSpinner').hide();
});
};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('endpoints', $scope.state.pagination_count);
};
$scope.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredRegistries, function (registry) {
if (registry.Checked !== allSelected) {
registry.Checked = allSelected;
$scope.selectItem(registry);
}
});
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function() {
ModalService.confirmDeletion(
'Do you want to remove the selected registries?',
function onConfirm(confirmed) {
if(!confirmed) { return; }
removeRegistries();
}
);
};
function removeRegistries() {
$('#loadingViewSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadingViewSpinner').hide();
}
};
var registries = $scope.registries;
angular.forEach(registries, function (registry) {
if (registry.Checked) {
counter = counter + 1;
RegistryService.deleteRegistry(registry.Id)
.then(function success(data) {
var index = registries.indexOf(registry);
registries.splice(index, 1);
Notifications.success('Registry deleted', registry.Name);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove registry');
})
.finally(function final() {
complete();
});
}
});
}
function initView() {
$('#loadingViewSpinner').show();
$q.all({
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub()
})
.then(function success(data) {
$scope.registries = data.registries;
$scope.dockerhub = data.dockerhub;
})
.catch(function error(err) {
$scope.registries = [];
Notifications.error('Failure', err, 'Unable to retrieve registries');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initView();
}]);

View File

@@ -0,0 +1,78 @@
<rd-header>
<rd-header-title title="Registry details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="registries">Registries</a> > <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_name" ng-model="registry.Name" placeholder="e.g. my-registry">
</div>
</div>
<!-- !name-input -->
<!-- registry-url-input -->
<div class="form-group">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" ng-model="registry.URL" placeholder="e.g. 10.0.0.10:5000 or myregistry.domain.tld">
</div>
</div>
<!-- !registry-url-input -->
<!-- authentication-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="registry.Authentication"><i></i>
</label>
</div>
</div>
<!-- !authentication-checkbox -->
<!-- authentication-credentials -->
<div ng-if="registry.Authentication">
<!-- credentials-user -->
<div class="form-group">
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="credentials_username" ng-model="registry.Username">
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="credentials_password" ng-model="registry.Password">
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- !authentication-credentials -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!registry.Name || !registry.URL || (registry.Authentication && (!registry.Username || !registry.Password))" ng-click="updateRegistry()">Update registry</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="registries">Cancel</a>
<i id="updateRegistrySpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -0,0 +1,37 @@
angular.module('registry', [])
.controller('RegistryController', ['$scope', '$state', '$stateParams', '$filter', 'RegistryService', 'Notifications',
function ($scope, $state, $stateParams, $filter, RegistryService, Notifications) {
$scope.updateRegistry = function() {
$('#updateRegistrySpinner').show();
var registry = $scope.registry;
RegistryService.updateRegistry(registry)
.then(function success(data) {
Notifications.success('Registry successfully updated');
$state.go('registries');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update registry');
})
.finally(function final() {
$('#updateRegistrySpinner').hide();
});
};
function initView() {
$('#loadingViewSpinner').show();
var registryID = $stateParams.id;
RegistryService.registry(registryID)
.then(function success(data) {
$scope.registry = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initView();
}]);

View File

@@ -0,0 +1,45 @@
<rd-header>
<rd-header-title title="Registry access">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="registries">Registries</a> > <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a> > Access management
</rd-header-content>
</rd-header>
<div class="row" ng-if="registry">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title="Registry"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
{{ registry.Name }}
</td>
</tr>
<tr>
<td>URL</td>
<td>
{{ registry.URL }}
</td>
</tr>
<tr>
<td colspan="2">
<span class="small text-muted">
You can select which user or team can access this registry by moving them to the authorized accesses table. Simply click
on a user or team entry to move it from one table to the other.
</span>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<por-access-management ng-if="registry" access-controlled-entity="registry" update-access="updateAccess(userAccesses, teamAccesses)">
</por-access-management>

View File

@@ -0,0 +1,24 @@
angular.module('registryAccess', [])
.controller('RegistryAccessController', ['$scope', '$stateParams', 'RegistryService', 'Notifications',
function ($scope, $stateParams, RegistryService, Notifications) {
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
return RegistryService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams);
};
function initView() {
$('#loadingViewSpinner').show();
RegistryService.registry($stateParams.id)
.then(function success(data) {
$scope.registry = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
})
.finally(function final(){
$('#loadingViewSpinner').hide();
});
}
initView();
}]);

View File

@@ -1,4 +1,4 @@
<div ng-if="service.ServiceConstraints">
<div ng-if="service.ServiceConstraints" id="service-placement-constraints">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Placement constraints">
<div class="nopadding">

View File

@@ -1,4 +1,4 @@
<div>
<div id="service-container-labels">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container labels">
<div class="nopadding">

View File

@@ -1,4 +1,4 @@
<div ng-if="service.EnvironmentVariables">
<div ng-if="service.EnvironmentVariables" id="service-env-variables">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Environment variables">
<div class="nopadding">

View File

@@ -1,4 +1,4 @@
<div ng-if="service.ServiceMounts">
<div ng-if="service.ServiceMounts" id="service-mounts">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Mounts">
<div class="nopadding">

View File

@@ -1,4 +1,4 @@
<div>
<div id="service-network-specs">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Networks"></rd-widget-header>
<rd-widget-body ng-if="!service.VirtualIPs || service.VirtualIPs.length === 0">

View File

@@ -1,4 +1,4 @@
<div>
<div id="service-resources">
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Resource limits and reservations">
</rd-widget-header>

View File

@@ -1,4 +1,4 @@
<div>
<div id="service-restart-policy">
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Restart policy">
</rd-widget-header>

View File

@@ -1,4 +1,4 @@
<div ng-if="applicationState.endpoint.apiVersion >= 1.25">
<div ng-if="applicationState.endpoint.apiVersion >= 1.25" id="service-secrets">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Secrets">
</rd-widget-header>

View File

@@ -1,4 +1,4 @@
<div>
<div id="service-labels">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Service labels">
<div class="nopadding">

View File

@@ -1,4 +1,4 @@
<div ng-if="tasks.length > 0 && nodes">
<div ng-if="tasks.length > 0 && nodes" id="service-tasks">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks">
<div class="pull-right">
@@ -18,28 +18,28 @@
<tr>
<th>Id</th>
<th>
<a ui-sref="service" ng-click="order('Status')">
<a ng-click="order('Status.State')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'Status.State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status.State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="service.Mode !== 'global'">
<a ui-sref="service" ng-click="order('Slot')">
<a ng-click="order('Slot')">
Slot
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('Node')">
<a ng-click="order('NodeId')">
Node
<span ng-show="sortType == 'Node' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Node' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'NodeId' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'NodeId' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('Updated')">
<a ng-click="order('Updated')">
Last update
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
@@ -57,9 +57,10 @@
</tr>
</tbody>
</table>
<div ng-if="tasks" class="pagination-controls">
<div ng-if="tasks" class="pagination-controls" >
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -1,4 +1,4 @@
<div>
<div id="service-update-config">
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Update configuration">
</rd-widget-header>

View File

@@ -111,7 +111,7 @@
<li><a href ng-click="goToItem('service-labels')">Service labels</a></li>
<li><a href ng-click="goToItem('service-secrets')">Secrets</a></li>
<li><a href ng-click="goToItem('service-tasks')">Tasks</a></li>
<ul>
</ul>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -1,12 +1,12 @@
angular.module('service', [])
.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
$scope.tasks = [];
$scope.sortType = 'Status';
$scope.sortReverse = false;
$scope.sortType = 'Updated';
$scope.sortReverse = true;
$scope.lastVersion = 0;
@@ -37,7 +37,11 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
};
$scope.goToItem = function(hash) {
$anchorScroll(hash);
if ($location.hash() !== hash) {
$location.hash(hash);
} else {
$anchorScroll();
}
};
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
@@ -291,15 +295,22 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
.then(function success(data) {
$scope.tasks = data.tasks;
$scope.nodes = data.nodes;
$scope.secrets = data.secrets.map(function (secret) {
return new SecretViewModel(secret);
});
$timeout(function() {
$anchorScroll();
});
})
.catch(function error(err) {
$scope.secrets = [];
Notifications.error('Failure', err, 'Unable to retrieve service details');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}

View File

@@ -64,6 +64,9 @@
<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"></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"></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"></span></a>
</li>

View File

@@ -3,6 +3,7 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="swarm" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Swarm</rd-header-content>
</rd-header>

View File

@@ -1,6 +1,6 @@
angular.module('swarm', [])
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Pagination',
function ($scope, Info, Version, Node, Pagination) {
.controller('SwarmController', ['$q', '$scope', 'SystemService', 'NodeService', 'Pagination', 'Notifications',
function ($q, $scope, SystemService, NodeService, Pagination, Notifications) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
$scope.sortType = 'Spec.Role';
@@ -20,30 +20,6 @@ function ($scope, Info, Version, Node, Pagination) {
Pagination.setPaginationCount('swarm_nodes', $scope.state.pagination_count);
};
Version.get({}, function (d) {
$scope.docker = d;
});
Info.get({}, function (d) {
$scope.info = d;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
Node.query({}, function(d) {
$scope.nodes = d.map(function (node) {
return new NodeViewModel(node);
});
var CPU = 0, memory = 0;
angular.forEach(d, function(node) {
CPU += node.Description.Resources.NanoCPUs;
memory += node.Description.Resources.MemoryBytes;
});
$scope.totalCPU = CPU / 1000000000;
$scope.totalMemory = memory;
});
} else {
extractSwarmInfo(d);
}
});
function extractSwarmInfo(info) {
// Swarm info is available in SystemStatus object
var systemStatus = info.SystemStatus;
@@ -84,4 +60,43 @@ function ($scope, Info, Version, Node, Pagination) {
node.version = info[offset + 8][1];
$scope.swarm.Status.push(node);
}
function processTotalCPUAndMemory(nodes) {
var CPU = 0, memory = 0;
angular.forEach(nodes, function(node) {
CPU += node.CPUs;
memory += node.Memory;
});
$scope.totalCPU = CPU / 1000000000;
$scope.totalMemory = memory;
}
function initView() {
$('#loadingViewSpinner').show();
var provider = $scope.applicationState.endpoint.mode.provider;
$q.all({
version: SystemService.version(),
info: SystemService.info(),
nodes: provider !== 'DOCKER_SWARM_MODE' || NodeService.nodes()
})
.then(function success(data) {
$scope.docker = data.version;
$scope.info = data.info;
if (provider === 'DOCKER_SWARM_MODE') {
var nodes = data.nodes;
processTotalCPUAndMemory(nodes);
$scope.nodes = nodes;
} else {
extractSwarmInfo(data.info);
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve cluster details');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initView();
}]);

View File

@@ -69,7 +69,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer
generatedVolumeIds.push(volumeId);
});
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration, template, data);
return ImageService.pullImage(template.Image, template.Registry);
return ImageService.pullImage(template.Image, { URL: template.Registry }, true);
})
.then(function success(data) {
return ContainerService.createAndStartContainer(templateConfiguration);

View File

@@ -1,6 +1,6 @@
angular.module('users', [])
.controller('UsersController', ['$q', '$scope', '$state', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication',
function ($q, $scope, $state, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication) {
.controller('UsersController', ['$q', '$scope', '$state', '$sanitize', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication',
function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication) {
$scope.state = {
userCreationError: '',
selectedItemCount: 0,
@@ -59,8 +59,8 @@ function ($q, $scope, $state, UserService, TeamService, TeamMembershipService, M
$scope.addUser = function() {
$('#createUserSpinner').show();
$scope.state.userCreationError = '';
var username = $scope.formValues.Username;
var password = $scope.formValues.Password;
var username = $sanitize($scope.formValues.Username);
var password = $sanitize($scope.formValues.Password);
var role = $scope.formValues.Administrator ? 1 : 2;
var teamIds = [];
angular.forEach($scope.formValues.Teams, function(team) {

View File

@@ -53,6 +53,13 @@
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="volumes" ng-click="order('Mountpoint')">
Mount point
<span ng-show="sortType == 'Mountpoint' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Mountpoint' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.authentication">
<a ui-sref="volumes" ng-click="order('ResourceControl.Ownership')">
Ownership
@@ -65,8 +72,9 @@
<tbody>
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td><a ui-sref="volume({id: volume.Id})">{{ volume.Id|truncate:50 }}</a></td>
<td><a ui-sref="volume({id: volume.Id})">{{ volume.Id|truncate:25 }}</a></td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint | truncate:52 }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="volume.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
@@ -75,10 +83,10 @@
</td>
</tr>
<tr ng-if="!volumes">
<td colspan="6" class="text-center text-muted">Loading...</td>
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="volumes.length == 0">
<td colspan="6" class="text-center text-muted">No volumes available.</td>
<td colspan="5" class="text-center text-muted">No volumes available.</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,8 @@
angular.module('portainer').component('porAccessManagement', {
templateUrl: 'app/directives/accessManagement/porAccessManagement.html',
controller: 'porAccessManagementController',
bindings: {
accessControlledEntity: '<',
updateAccess: '&'
}
});

View File

@@ -0,0 +1,134 @@
<div class="row">
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users and teams">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="$ctrl.state.pagination_count_accesses" ng-change="$ctrl.changePaginationCountAccesses()">
<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-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="$ctrl.authorizeAllAccesses()" ng-disabled="$ctrl.accesses.length === 0 || $ctrl.filteredUsers.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="$ctrl.state.filterUsers" 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>
<a ng-click="$ctrl.orderAccesses('Name')">
Name
<span ng-show="$ctrl.state.sortAccessesBy == 'Name' && !$ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.state.sortAccessesBy == 'Name' && $ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.orderAccesses('Type')">
Type
<span ng-show="$ctrl.state.sortAccessesBy == 'Type' && !$ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.state.sortAccessesBy == 'Type' && $ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="$ctrl.authorizeAccess(user)" class="interactive" dir-paginate="user in $ctrl.accesses | filter:$ctrl.state.filterUsers | orderBy:$ctrl.state.sortAccessesBy:$ctrl.state.sortAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_accesses">
<td>{{ user.Name }}</td>
<td>
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
{{ user.Type }}
</td>
</tr>
<tr ng-if="!$ctrl.accesses">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.accesses.length === 0 || ($ctrl.accesses | filter:$ctrl.state.filterUsers | orderBy:$ctrl.state.sortAccessesBy:$ctrl.state.sortAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_accesses).length === 0">
<td colspan="2" class="text-center text-muted">No user or team available.</td>
</tr>
</tbody>
</table>
<div ng-if="$ctrl.accesses" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users and teams">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="$ctrl.state.pagination_count_authorizedAccesses" ng-change="$ctrl.changePaginationCountAuthorizedAccesses()">
<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-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="$ctrl.unauthorizeAllAccesses()" ng-disabled="$ctrl.authorizedAccesses.length === 0 || $ctrl.filteredAuthorizedUsers.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="$ctrl.state.filterAuthorizedUsers" 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>
<a ng-click="$ctrl.orderAuthorizedAccesses('Name')">
Name
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Name' && !$ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Name' && $ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.orderAuthorizedAccesses('Type')">
Type
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Type' && !$ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Type' && $ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="$ctrl.unauthorizeAccess(user)" class="interactive" pagination-id="table_authaccess" dir-paginate="user in $ctrl.authorizedAccesses | filter:$ctrl.state.filterAuthorizedUsers | orderBy:$ctrl.state.sortAuthorizedAccessesBy:$ctrl.state.sortAuthorizedAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_authorizedAccesses">
<td>{{ user.Name }}</td>
<td>
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
{{ user.Type }}
</td>
</tr>
<tr ng-if="!$ctrl.authorizedAccesses">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.authorizedAccesses.length === 0 || (authorizedAccesses | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedAccesses:sortReverseAuthorizedAccesses | itemsPerPage: state.pagination_count_authorizedAccesses).length === 0">
<td colspan="2" class="text-center text-muted">No authorized user or team.</td>
</tr>
</tbody>
</table>
<div ng-if="$ctrl.authorizedAccesses" class="pull-left pagination-controls">
<dir-pagination-controls pagination-id="table_authaccess"></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -0,0 +1,157 @@
angular.module('portainer')
.controller('porAccessManagementController', ['AccessService', 'Pagination', 'Notifications',
function (AccessService, Pagination, Notifications) {
var ctrl = this;
ctrl.state = {
pagination_count_accesses: Pagination.getPaginationCount('access_management_accesses'),
pagination_count_authorizedAccesses: Pagination.getPaginationCount('access_management_AuthorizedAccesses'),
sortAccessesBy: 'Type',
sortAccessesReverse: false,
sortAuthorizedAccessesBy: 'Type',
sortAuthorizedAccessesReverse: false
};
ctrl.orderAccesses = function(sortBy) {
ctrl.state.sortAccessesReverse = (ctrl.state.sortAccessesBy === sortBy) ? !ctrl.state.sortAccessesReverse : false;
ctrl.state.sortAccessesBy = sortBy;
};
ctrl.orderAuthorizedAccesses = function(sortBy) {
ctrl.state.sortAuthorizedAccessesReverse = (ctrl.state.sortAuthorizedAccessesBy === sortBy) ? !ctrl.state.sortAuthorizedAccessesReverse : false;
ctrl.state.sortAuthorizedAccessesBy = sortBy;
};
ctrl.changePaginationCountAuthorizedAccesses = function() {
Pagination.setPaginationCount('access_management_AuthorizedAccesses', ctrl.state.pagination_count_authorizedAccesses);
};
ctrl.changePaginationCountAccesses = function() {
Pagination.setPaginationCount('access_management_accesses', ctrl.state.pagination_count_accesses);
};
function dispatchUserAndTeamIDs(accesses, users, teams) {
angular.forEach(accesses, function (access) {
if (access.Type === 'user') {
users.push(access.Id);
} else if (access.Type === 'team') {
teams.push(access.Id);
}
});
}
function processAuthorizedIDs(accesses, authorizedAccesses) {
var authorizedUserIDs = [];
var authorizedTeamIDs = [];
if (accesses) {
dispatchUserAndTeamIDs(accesses, authorizedUserIDs, authorizedTeamIDs);
}
if (authorizedAccesses) {
dispatchUserAndTeamIDs(authorizedAccesses, authorizedUserIDs, authorizedTeamIDs);
}
return {
userIDs: authorizedUserIDs,
teamIDs: authorizedTeamIDs
};
}
function removeFromAccesses(access, accesses) {
_.remove(accesses, function(n) {
return n.Id === access.Id;
});
}
function removeFromAccessIDs(accessId, accessIDs) {
_.remove(accessIDs, function(n) {
return n === accessId;
});
}
ctrl.authorizeAccess = function(access) {
var accessData = processAuthorizedIDs(null, ctrl.authorizedAccesses);
var authorizedUserIDs = accessData.userIDs;
var authorizedTeamIDs = accessData.teamIDs;
if (access.Type === 'user') {
authorizedUserIDs.push(access.Id);
} else if (access.Type === 'team') {
authorizedTeamIDs.push(access.Id);
}
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
.then(function success(data) {
removeFromAccesses(access, ctrl.accesses);
ctrl.authorizedAccesses.push(access);
Notifications.success('Accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
ctrl.unauthorizeAccess = function(access) {
var accessData = processAuthorizedIDs(null, ctrl.authorizedAccesses);
var authorizedUserIDs = accessData.userIDs;
var authorizedTeamIDs = accessData.teamIDs;
if (access.Type === 'user') {
removeFromAccessIDs(access.Id, authorizedUserIDs);
} else if (access.Type === 'team') {
removeFromAccessIDs(access.Id, authorizedTeamIDs);
}
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
.then(function success(data) {
removeFromAccesses(access, ctrl.authorizedAccesses);
ctrl.accesses.push(access);
Notifications.success('Accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
ctrl.unauthorizeAllAccesses = function() {
ctrl.updateAccess({ userAccesses: [], teamAccesses: [] })
.then(function success(data) {
ctrl.accesses = ctrl.accesses.concat(ctrl.authorizedAccesses);
ctrl.authorizedAccesses = [];
Notifications.success('Accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
ctrl.authorizeAllAccesses = function() {
var accessData = processAuthorizedIDs(ctrl.accesses, ctrl.authorizedAccesses);
var authorizedUserIDs = accessData.userIDs;
var authorizedTeamIDs = accessData.teamIDs;
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
.then(function success(data) {
ctrl.authorizedAccesses = ctrl.authorizedAccesses.concat(ctrl.accesses);
ctrl.accesses = [];
Notifications.success('Accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
function initComponent() {
var entity = ctrl.accessControlledEntity;
AccessService.accesses(entity.AuthorizedUsers, entity.AuthorizedTeams)
.then(function success(data) {
ctrl.accesses = data.accesses;
ctrl.authorizedAccesses = data.authorizedAccesses;
})
.catch(function error(err) {
ctrl.accesses = [];
ctrl.authorizedAccesses = [];
Notifications.error('Failure', err, 'Unable to retrieve accesses');
});
}
initComponent();
}]);

View File

@@ -0,0 +1,8 @@
angular.module('portainer').component('porImageRegistry', {
templateUrl: 'app/directives/imageRegistry/porImageRegistry.html',
controller: 'porImageRegistryController',
bindings: {
'image': '=',
'registry': '='
}
});

View File

@@ -0,0 +1,12 @@
<div>
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="$ctrl.image" id="image_name" placeholder="e.g. myImage:myTag">
</div>
<label for="image_registry" class="col-sm-2 col-md-1 margin-sm-top control-label text-left">
Registry
</label>
<div class="col-sm-10 col-md-4 margin-sm-top">
<select ng-options="registry as registry.Name for registry in $ctrl.availableRegistries" ng-model="$ctrl.registry" id="image_registry" class="form-control"></select>
</div>
</div>

View File

@@ -0,0 +1,23 @@
angular.module('portainer')
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'Notifications',
function ($q, RegistryService, DockerHubService, Notifications) {
var ctrl = this;
function initComponent() {
$q.all({
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub()
})
.then(function success(data) {
var dockerhub = data.dockerhub;
var registries = data.registries;
ctrl.availableRegistries = [dockerhub].concat(registries);
ctrl.registry = dockerhub;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registries');
});
}
initComponent();
}]);

View File

@@ -4,15 +4,14 @@ angular.module('portainer.helpers')
var helper = {};
helper.extractImageAndRegistryFromTag = function(tag) {
var slashCount = _.countBy(tag)['/'];
helper.extractImageAndRegistryFromRepository = function(repository) {
var slashCount = _.countBy(repository)['/'];
var registry = null;
var image = tag;
var image = repository;
if (slashCount > 1) {
// assume something/some/thing[/...]
var registryAndImage = _.split(tag, '/');
registry = registryAndImage[0];
image = registryAndImage[1];
registry = repository.substr(0, repository.indexOf('/'));
image = repository.substr(repository.indexOf('/') + 1);
}
return {

View File

@@ -0,0 +1,18 @@
angular.module('portainer.helpers')
.factory('RegistryHelper', [function RegistryHelperFactory() {
'use strict';
var helper = {};
helper.getRegistryByURL = function(registries, url) {
for (var i = 0; i < registries.length; i++) {
if (registries[i].URL === url) {
return registries[i];
}
}
return null;
};
return helper;
}]);

View File

@@ -21,7 +21,7 @@ angular.module('portainer.helpers')
SecretID: secret.Id,
SecretName: secret.Name,
File: {
Name: secret.Name,
Name: secret.FileName,
UID: '0',
GID: '0',
Mode: 444

View File

@@ -1,10 +1,10 @@
function EndpointAccessUserViewModel(data) {
function UserAccessViewModel(data) {
this.Id = data.Id;
this.Name = data.Username;
this.Type = 'user';
}
function EndpointAccessTeamViewModel(data) {
function TeamAccessViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Type = 'team';

View File

@@ -0,0 +1,7 @@
function DockerHubViewModel(data) {
this.Name = 'DockerHub';
this.URL = '';
this.Authentication = data.Authentication;
this.Username = data.Username;
this.Password = data.Password;
}

View File

@@ -0,0 +1,11 @@
function RegistryViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.URL = data.URL;
this.Authentication = data.Authentication;
this.Username = data.Username;
this.Password = data.Password;
this.AuthorizedUsers = data.AuthorizedUsers;
this.AuthorizedTeams = data.AuthorizedTeams;
this.Checked = false;
}

View File

@@ -3,7 +3,7 @@ function TemplateViewModel(data) {
this.Description = data.description;
this.Note = data.note;
this.Categories = data.categories ? data.categories : [];
this.Platform = data.platform ? data.platform : '';
this.Platform = data.platform ? data.platform : 'undefined';
this.Logo = data.logo;
this.Image = data.image;
this.Registry = data.registry ? data.registry : '';

View File

@@ -0,0 +1,8 @@
angular.module('portainer.rest')
.factory('DockerHub', ['$resource', 'DOCKERHUB_ENDPOINT', function DockerHubFactory($resource, DOCKERHUB_ENDPOINT) {
'use strict';
return $resource(DOCKERHUB_ENDPOINT, {}, {
get: { method: 'GET' },
update: { method: 'PUT' }
});
}]);

12
app/rest/api/registry.js Normal file
View File

@@ -0,0 +1,12 @@
angular.module('portainer.rest')
.factory('Registries', ['$resource', 'REGISTRIES_ENDPOINT', function RegistriesFactory($resource, REGISTRIES_ENDPOINT) {
'use strict';
return $resource(REGISTRIES_ENDPOINT + '/:id/:action', {}, {
create: { method: 'POST' },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
remove: { method: 'DELETE', params: { id: '@id'} }
});
}]);

View File

@@ -1,13 +0,0 @@
angular.module('portainer.rest')
.factory('Events', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function EventFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/events', {
endpointId: EndpointProvider.endpointID
},
{
query: {
method: 'GET', params: {since: '@since', until: '@until'},
isArray: true, transformResponse: jsonObjectsToArrayHandler
}
});
}]);

View File

@@ -1,6 +1,7 @@
angular.module('portainer.rest')
.factory('Image', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ImageFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
.factory('Image', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', 'HttpRequestHelper', function ImageFactory($resource, DOCKER_ENDPOINT, EndpointProvider, HttpRequestHelper) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/images/:id/:action', {
endpointId: EndpointProvider.endpointID
},
@@ -14,11 +15,13 @@ angular.module('portainer.rest')
inspect: {method: 'GET', params: {id: '@id', action: 'json'}},
push: {
method: 'POST', params: {action: 'push', id: '@tag'},
isArray: true, transformResponse: jsonObjectsToArrayHandler
isArray: true, transformResponse: jsonObjectsToArrayHandler,
headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader }
},
create: {
method: 'POST', params: {action: 'create', fromImage: '@fromImage', tag: '@tag'},
isArray: true, transformResponse: jsonObjectsToArrayHandler
isArray: true, transformResponse: jsonObjectsToArrayHandler,
headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader }
},
remove: {
method: 'DELETE', params: {id: '@id', force: '@force'},

View File

@@ -1,7 +0,0 @@
angular.module('portainer.rest')
.factory('Info', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function InfoFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/info', {
endpointId: EndpointProvider.endpointID
});
}]);

View File

@@ -1,5 +1,5 @@
angular.module('portainer.rest')
.factory('Service', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ServiceFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
.factory('Service', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', 'HttpRequestHelper' ,function ServiceFactory($resource, DOCKER_ENDPOINT, EndpointProvider, HttpRequestHelper) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/services/:id/:action', {
endpointId: EndpointProvider.endpointID
@@ -7,7 +7,10 @@ angular.module('portainer.rest')
{
get: { method: 'GET', params: {id: '@id'} },
query: { method: 'GET', isArray: true },
create: { method: 'POST', params: {action: 'create'} },
create: {
method: 'POST', params: {action: 'create'},
headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader }
},
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
remove: { method: 'DELETE', params: {id: '@id'} }
});

17
app/rest/docker/system.js Normal file
View File

@@ -0,0 +1,17 @@
angular.module('portainer.rest')
.factory('System', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function SystemFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/:action', {
name: '@name',
endpointId: EndpointProvider.endpointID
},
{
info: { method: 'GET', params: { action: 'info' } },
version: { method: 'GET', params: { action: 'version' } },
events: {
method: 'GET', params: { action: 'events', since: '@since', until: '@until' },
isArray: true, transformResponse: jsonObjectsToArrayHandler
},
auth: { method: 'POST', params: { action: 'auth' } }
});
}]);

View File

@@ -1,7 +0,0 @@
angular.module('portainer.rest')
.factory('Version', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function VersionFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/version', {
endpointId: EndpointProvider.endpointID
});
}]);

View File

@@ -0,0 +1,58 @@
angular.module('portainer.services')
.factory('AccessService', ['$q', 'UserService', 'TeamService', function AccessServiceFactory($q, UserService, TeamService) {
'use strict';
var service = {};
function mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs) {
var accesses = [];
var authorizedAccesses = [];
angular.forEach(userAccesses, function(access) {
if (_.includes(authorizedUserIDs, access.Id)) {
authorizedAccesses.push(access);
} else {
accesses.push(access);
}
});
angular.forEach(teamAccesses, function(access) {
if (_.includes(authorizedTeamIDs, access.Id)) {
authorizedAccesses.push(access);
} else {
accesses.push(access);
}
});
return {
accesses: accesses,
authorizedAccesses: authorizedAccesses
};
}
service.accesses = function(authorizedUserIDs, authorizedTeamIDs) {
var deferred = $q.defer();
$q.all({
users: UserService.users(false),
teams: TeamService.teams()
})
.then(function success(data) {
var userAccesses = data.users.map(function (user) {
return new UserAccessViewModel(user);
});
var teamAccesses = data.teams.map(function (team) {
return new TeamAccessViewModel(team);
});
var accessData = mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs);
deferred.resolve(accessData);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve users and teams', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@@ -0,0 +1,26 @@
angular.module('portainer.services')
.factory('DockerHubService', ['$q', 'DockerHub', function DockerHubServiceFactory($q, DockerHub) {
'use strict';
var service = {};
service.dockerhub = function() {
var deferred = $q.defer();
DockerHub.get().$promise
.then(function success(data) {
var dockerhub = new DockerHubViewModel(data);
deferred.resolve(dockerhub);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve DockerHub details', err: err });
});
return deferred.promise;
};
service.update = function(dockerhub) {
return DockerHub.update({}, dockerhub).$promise;
};
return service;
}]);

View File

@@ -0,0 +1,92 @@
angular.module('portainer.services')
.factory('RegistryService', ['$q', 'Registries', 'DockerHubService', 'RegistryHelper', 'ImageHelper', function RegistryServiceFactory($q, Registries, DockerHubService, RegistryHelper, ImageHelper) {
'use strict';
var service = {};
service.registries = function() {
var deferred = $q.defer();
Registries.query().$promise
.then(function success(data) {
var registries = data.map(function (item) {
return new RegistryViewModel(item);
});
deferred.resolve(registries);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve registries', err: err});
});
return deferred.promise;
};
service.registry = function(id) {
var deferred = $q.defer();
Registries.get({id: id}).$promise
.then(function success(data) {
var registry = new RegistryViewModel(data);
deferred.resolve(registry);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve registry details', err: err});
});
return deferred.promise;
};
service.encodedCredentials = function(registry) {
var credentials = {
username: registry.Username,
password: registry.Password,
serveraddress: registry.URL
};
return btoa(JSON.stringify(credentials));
};
service.updateAccess = function(id, authorizedUserIDs, authorizedTeamIDs) {
return Registries.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
};
service.deleteRegistry = function(id) {
return Registries.remove({id: id}).$promise;
};
service.updateRegistry = function(registry) {
return Registries.update({ id: registry.Id }, registry).$promise;
};
service.createRegistry = function(name, URL, authentication, username, password) {
var payload = {
Name: name,
URL: URL,
Authentication: authentication
};
if (authentication) {
payload.Username = username;
payload.Password = password;
}
return Registries.create({}, payload).$promise;
};
service.retrieveRegistryFromRepository = function(repository) {
var deferred = $q.defer();
var imageDetails = ImageHelper.extractImageAndRegistryFromRepository(repository);
$q.when(imageDetails.registry ? service.registries() : DockerHubService.dockerhub())
.then(function success(data) {
var registry = data;
if (imageDetails.registry) {
registry = RegistryHelper.getRegistryByURL(data, imageDetails.registry);
}
deferred.resolve(registry);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve the registry associated to the repository', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@@ -8,8 +8,8 @@ angular.module('portainer.services')
Settings.get().$promise
.then(function success(data) {
var status = new SettingsViewModel(data);
deferred.resolve(status);
var settings = new SettingsViewModel(data);
deferred.resolve(settings);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve application settings', err: err });

View File

@@ -1,5 +1,5 @@
angular.module('portainer.services')
.factory('ImageService', ['$q', 'Image', 'ImageHelper', function ImageServiceFactory($q, Image, ImageHelper) {
.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper) {
'use strict';
var service = {};
@@ -35,10 +35,40 @@ angular.module('portainer.services')
return deferred.promise;
};
service.pullImage = function(image, registry) {
service.pushImage = function(tag, registry) {
var deferred = $q.defer();
var imageConfiguration = ImageHelper.createImageConfigForContainer(image, registry);
Image.create(imageConfiguration).$promise
var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
Image.push({tag: tag}).$promise
.then(function success(data) {
if (data[data.length - 1].error) {
deferred.reject({ msg: data[data.length - 1].error });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to push image tag', err: err });
});
return deferred.promise;
};
function pullImageAndIgnoreErrors(imageConfiguration) {
var deferred = $q.defer();
Image.create({}, imageConfiguration).$promise
.finally(function final() {
deferred.resolve();
});
return deferred.promise;
}
function pullImageAndAcknowledgeErrors(imageConfiguration) {
var deferred = $q.defer();
Image.create({}, imageConfiguration).$promise
.then(function success(data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('message');
if (err) {
@@ -51,12 +81,20 @@ angular.module('portainer.services')
.catch(function error(err) {
deferred.reject({ msg: 'Unable to pull image', err: err });
});
return deferred.promise;
};
service.pullTag = function(tag) {
var imageAndRegistry = ImageHelper.extractImageAndRegistryFromTag(tag);
return service.pullImage(imageAndRegistry.image, imageAndRegistry.registry);
return deferred.promise;
}
service.pullImage = function(image, registry, ignoreErrors) {
var imageDetails = ImageHelper.extractImageAndRegistryFromRepository(image);
var imageConfiguration = ImageHelper.createImageConfigForContainer(imageDetails.image, registry.URL);
var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
if (ignoreErrors) {
return pullImageAndIgnoreErrors(imageConfiguration);
}
return pullImageAndAcknowledgeErrors(imageConfiguration);
};
service.tagImage = function(id, image, registry) {
@@ -80,21 +118,5 @@ angular.module('portainer.services')
return deferred.promise;
};
service.pushImage = function(tag) {
var deferred = $q.defer();
Image.push({tag: tag}).$promise
.then(function success(data) {
if (data[data.length - 1].error) {
deferred.reject({ msg: data[data.length - 1].error });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to push image tag', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@@ -1,20 +0,0 @@
angular.module('portainer.services')
.factory('InfoService', ['$q', 'Info', function InfoServiceFactory($q, Info) {
'use strict';
var service = {};
service.getVolumePlugins = function() {
var deferred = $q.defer();
Info.get({}).$promise
.then(function success(data) {
var plugins = data.Plugins.Volume;
deferred.resolve(plugins);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve volume plugin information', err: err});
});
return deferred.promise;
};
return service;
}]);

View File

@@ -0,0 +1,45 @@
angular.module('portainer.services')
.factory('SystemService', ['$q', 'System', function SystemServiceFactory($q, System) {
'use strict';
var service = {};
service.getVolumePlugins = function() {
var deferred = $q.defer();
System.info({}).$promise
.then(function success(data) {
var plugins = data.Plugins.Volume;
deferred.resolve(plugins);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve volume plugin information', err: err});
});
return deferred.promise;
};
service.info = function() {
return System.info({}).$promise;
};
service.version = function() {
return System.version({}).$promise;
};
service.events = function(from, to) {
var deferred = $q.defer();
System.events({since: from, until: to}).$promise
.then(function success(data) {
var events = data.map(function (item) {
return new EventViewModel(item);
});
deferred.resolve(events);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve engine events', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@@ -0,0 +1,17 @@
angular.module('portainer.services')
.factory('HttpRequestHelper', [function HttpRequestHelper() {
'use strict';
var service = {};
var headers = {};
service.registryAuthenticationHeader = function() {
return headers.registryAuthentication;
};
service.setRegistryAuthenticationHeader = function(headerValue) {
headers.registryAuthentication = headerValue;
};
return service;
}]);

View File

@@ -15,8 +15,12 @@ angular.module('portainer.services')
msg = e.message;
} else if (e.data && e.data.length > 0 && e.data[0].message) {
msg = e.data[0].message;
} else if (e.err && e.err.data && e.err.data.length > 0 && e.err.data[0].message) {
msg = e.err.data[0].message;
} else if (e.msg) {
msg = e.msg;
} else if (e.data && e.data.err) {
msg = e.data.err;
}
toastr.error($sanitize(msg), $sanitize(title), {timeOut: 6000});
};

View File

@@ -1,5 +1,5 @@
angular.module('portainer.services')
.factory('StateManager', ['$q', 'Info', 'InfoHelper', 'Version', 'LocalStorage', 'SettingsService', 'StatusService', function StateManagerFactory($q, Info, InfoHelper, Version, LocalStorage, SettingsService, StatusService) {
.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService) {
'use strict';
var manager = {};
@@ -75,19 +75,25 @@ angular.module('portainer.services')
if (loading) {
state.loading = true;
}
$q.all([Info.get({}).$promise, Version.get({}).$promise])
$q.all({
info: SystemService.info(),
version: SystemService.version()
})
.then(function success(data) {
var endpointMode = InfoHelper.determineEndpointMode(data[0]);
var endpointAPIVersion = parseFloat(data[1].ApiVersion);
var endpointMode = InfoHelper.determineEndpointMode(data.info);
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
state.endpoint.mode = endpointMode;
state.endpoint.apiVersion = endpointAPIVersion;
LocalStorage.storeEndpointState(state.endpoint);
state.loading = false;
deferred.resolve();
}, function error(err) {
state.loading = false;
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to connect to the Docker endpoint', err: err});
})
.finally(function final() {
state.loading = false;
});
return deferred.promise;
};

View File

@@ -395,32 +395,32 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
box-shadow: inset 0 0 1px rgba(0,0,0,.5), inset 0 0 40px #337ab7;
}
.ownership_wrapper {
.boxselector_wrapper {
display: flex;
flex-flow: row wrap;
margin: 0.5rem;
}
.ownership_wrapper > div {
.boxselector_wrapper > div {
flex: 1;
padding: 0.5rem;
}
.ownership_wrapper .ownership_header {
.boxselector_wrapper .boxselector_header {
font-size: 14px;
margin-bottom: 5px;
font-weight: bold;
}
.ownership_wrapper input[type="radio"] {
.boxselector_wrapper input[type="radio"] {
display: none;
}
.ownership_wrapper input[type="radio"]:not(:disabled) ~ label {
.boxselector_wrapper input[type="radio"]:not(:disabled) ~ label {
cursor: pointer;
}
.ownership_wrapper label {
.boxselector_wrapper label {
font-weight: normal;
font-size: 12px;
display: block;
@@ -433,14 +433,14 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
position: relative;
}
.ownership_wrapper input[type="radio"]:checked + label {
.boxselector_wrapper input[type="radio"]:checked + label {
background: #337ab7;
color: white;
padding-top: 2rem;
border-color: #337ab7;
}
.ownership_wrapper input[type="radio"]:checked + label::after {
.boxselector_wrapper input[type="radio"]:checked + label::after {
color: #337ab7;
font-family: FontAwesome;
border: 2px solid #337ab7;
@@ -461,7 +461,7 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
}
@media only screen and (max-width: 700px) {
.ownership_wrapper {
.boxselector_wrapper {
flex-direction: column;
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "portainer",
"version": "1.13.2",
"version": "1.13.4",
"homepage": "https://github.com/portainer/portainer",
"authors": [
"Anthony Lapenna <anthony.lapenna at gmail dot com>"

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