mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(registries): add registry management (#930)
This commit is contained in:
parent
9360f24d89
commit
08c5a5a4f6
75 changed files with 2317 additions and 621 deletions
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
61
api/bolt/dockerhub_service.go
Normal file
61
api/bolt/dockerhub_service.go
Normal 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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
114
api/bolt/registry_service.go
Normal file
114
api/bolt/registry_service.go
Normal 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, ®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
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
87
api/http/handler/dockerhub.go
Normal file
87
api/http/handler/dockerhub.go
Normal 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:""`
|
||||
}
|
|
@ -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") {
|
||||
|
|
312
api/http/handler/registry.go
Normal file
312
api/http/handler/registry.go
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue