diff --git a/.codeclimate.yml b/.codeclimate.yml
index b6deabfde..84d9c8eda 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -12,7 +12,8 @@ engines:
enabled: true
config:
languages:
- - javascript
+ javascript:
+ mass_threshold: 80
eslint:
enabled: true
config:
diff --git a/README.md b/README.md
index 4312eb017..a3483f6f9 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
-
+
[](https://hub.docker.com/r/portainer/portainer/)
[](http://microbadger.com/images/portainer/portainer "Image size")
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
[]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci)
-[](http://portainer.io/slack/)
+[](https://portainer.io/slack/)
[](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
@@ -19,7 +19,7 @@
## Demo
-
+
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
diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go
index 3c56ede26..f0357a2a4 100644
--- a/api/bolt/datastore.go
+++ b/api/bolt/datastore.go
@@ -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
})
}
diff --git a/api/bolt/dockerhub_service.go b/api/bolt/dockerhub_service.go
new file mode 100644
index 000000000..34acd5594
--- /dev/null
+++ b/api/bolt/dockerhub_service.go
@@ -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
+ })
+}
diff --git a/api/bolt/endpoint_service.go b/api/bolt/endpoint_service.go
index d7c230e9a..a92048d7a 100644
--- a/api/bolt/endpoint_service.go
+++ b/api/bolt/endpoint_service.go
@@ -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
}
diff --git a/api/bolt/internal/internal.go b/api/bolt/internal/internal.go
index f55f72118..3378f93b7 100644
--- a/api/bolt/internal/internal.go
+++ b/api/bolt/internal/internal.go
@@ -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.
diff --git a/api/bolt/registry_service.go b/api/bolt/registry_service.go
new file mode 100644
index 000000000..4c0c393ae
--- /dev/null
+++ b/api/bolt/registry_service.go
@@ -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, ®istry)
+ if err != nil {
+ return nil, err
+ }
+ return ®istry, 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, ®istry)
+ 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
+ })
+}
diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go
index 9876de6aa..a3e265544 100644
--- a/api/cmd/portainer/main.go
+++ b/api/cmd/portainer/main.go
@@ -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,
diff --git a/api/errors.go b/api/errors.go
index 0be2338d8..bf5f5517a 100644
--- a/api/errors.go
+++ b/api/errors.go
@@ -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")
diff --git a/api/http/handler/dockerhub.go b/api/http/handler/dockerhub.go
new file mode 100644
index 000000000..56b8eed4e
--- /dev/null
+++ b/api/http/handler/dockerhub.go
@@ -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:""`
+}
diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go
index 0692dc6de..7fcb58c56 100644
--- a/api/http/handler/handler.go
+++ b/api/http/handler/handler.go
@@ -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") {
diff --git a/api/http/handler/registry.go b/api/http/handler/registry.go
new file mode 100644
index 000000000..164a5f3c1
--- /dev/null
+++ b/api/http/handler/registry.go
@@ -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
+ }
+}
diff --git a/api/http/security/filter.go b/api/http/security/filter.go
index ec83a1ebc..7e7f56c7c 100644
--- a/api/http/security/filter.go
+++ b/api/http/security/filter.go
@@ -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(®istry, 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 {
diff --git a/api/http/server.go b/api/http/server.go
index c183e81a5..14a069eae 100644
--- a/api/http/server.go
+++ b/api/http/server.go
@@ -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,
diff --git a/api/portainer.go b/api/portainer.go
index ec651c3d6..3bb862c98 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -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.3"
// DBVersion is the version number of the Portainer database.
DBVersion = 2
// DefaultTemplatesURL represents the default URL for the templates definitions.
diff --git a/app/app.js b/app/app.js
index 446e3dd18..bba4ad835 100644
--- a/app/app.js
+++ b/app/app.js
@@ -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');
diff --git a/app/components/common/accessControlForm/accessControlForm.html b/app/components/common/accessControlForm/accessControlForm.html
index 70961858e..21e4c3869 100644
--- a/app/components/common/accessControlForm/accessControlForm.html
+++ b/app/components/common/accessControlForm/accessControlForm.html
@@ -17,11 +17,11 @@