Merge branch 'release/1.18.0'

pull/2000/head 1.18.0
Anthony Lapenna 2018-06-21 14:28:14 +03:00
commit 7f9644b55e
425 changed files with 13072 additions and 8219 deletions

46
LICENSE
View File

@ -1,4 +1,4 @@
Portainer: Copyright (c) 2016 Portainer.io
Copyright (c) 2018 Portainer.io
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
@ -14,46 +14,4 @@ freely, subject to the following restrictions:
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Portainer contains code which was originally under this license:
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3. This notice may not be removed or altered from any source distribution.

View File

@ -56,8 +56,18 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
**_Portainer_** has full support for the following Docker versions:
* Docker 1.10 to the latest version
* Docker Swarm >= 1.2.3
* Standalone Docker Swarm >= 1.2.3 _(**NOTE:** Use of Standalone Docker Swarm is being discouraged since the introduction of built-in Swarm Mode in Docker. While older versions of Portainer had support for Standalone Docker Swarm, Portainer 1.17.0 and newer **do not** support it. However, the built-in Swarm Mode of Docker is fully supported.)_
Partial support for the following Docker versions (some features may not be available):
* Docker 1.9
## Licensing
Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference.
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014] [Elliot Hesp]

View File

@ -2,84 +2,66 @@ package bolt
import (
"log"
"os"
"path"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/dockerhub"
"github.com/portainer/portainer/bolt/endpoint"
"github.com/portainer/portainer/bolt/endpointgroup"
"github.com/portainer/portainer/bolt/migrator"
"github.com/portainer/portainer/bolt/registry"
"github.com/portainer/portainer/bolt/resourcecontrol"
"github.com/portainer/portainer/bolt/settings"
"github.com/portainer/portainer/bolt/stack"
"github.com/portainer/portainer/bolt/tag"
"github.com/portainer/portainer/bolt/team"
"github.com/portainer/portainer/bolt/teammembership"
"github.com/portainer/portainer/bolt/user"
"github.com/portainer/portainer/bolt/version"
)
const (
databaseFileName = "portainer.db"
)
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
// Path where is stored the BoltDB database.
Path string
// Services
UserService *UserService
TeamService *TeamService
TeamMembershipService *TeamMembershipService
EndpointService *EndpointService
EndpointGroupService *EndpointGroupService
ResourceControlService *ResourceControlService
VersionService *VersionService
SettingsService *SettingsService
RegistryService *RegistryService
DockerHubService *DockerHubService
StackService *StackService
db *bolt.DB
checkForDataMigration bool
path string
db *bolt.DB
checkForDataMigration bool
fileService portainer.FileService
DockerHubService *dockerhub.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
UserService *user.Service
VersionService *version.Service
}
const (
databaseFileName = "portainer.db"
versionBucketName = "version"
userBucketName = "users"
teamBucketName = "teams"
teamMembershipBucketName = "team_membership"
endpointBucketName = "endpoints"
endpointGroupBucketName = "endpoint_groups"
resourceControlBucketName = "resource_control"
settingsBucketName = "settings"
registryBucketName = "registries"
dockerhubBucketName = "dockerhub"
stackBucketName = "stacks"
)
// NewStore initializes a new Store and the associated services
func NewStore(storePath string) (*Store, error) {
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
store := &Store{
Path: storePath,
UserService: &UserService{},
TeamService: &TeamService{},
TeamMembershipService: &TeamMembershipService{},
EndpointService: &EndpointService{},
EndpointGroupService: &EndpointGroupService{},
ResourceControlService: &ResourceControlService{},
VersionService: &VersionService{},
SettingsService: &SettingsService{},
RegistryService: &RegistryService{},
DockerHubService: &DockerHubService{},
StackService: &StackService{},
path: storePath,
fileService: fileService,
}
store.UserService.store = store
store.TeamService.store = store
store.TeamMembershipService.store = store
store.EndpointService.store = store
store.EndpointGroupService.store = store
store.ResourceControlService.store = store
store.VersionService.store = store
store.SettingsService.store = store
store.RegistryService.store = store
store.DockerHubService.store = store
store.StackService.store = store
_, err := os.Stat(storePath + "/" + databaseFileName)
if err != nil && os.IsNotExist(err) {
store.checkForDataMigration = false
} else if err != nil {
databasePath := path.Join(storePath, databaseFileName)
databaseFileExists, err := fileService.FileExists(databasePath)
if err != nil {
return nil, err
}
if !databaseFileExists {
store.checkForDataMigration = false
} else {
store.checkForDataMigration = true
}
@ -89,29 +71,14 @@ 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})
databasePath := path.Join(store.path, databaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.db = db
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
endpointGroupBucketName, resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
registryBucketName, dockerhubBucketName, stackBucketName}
return db.Update(func(tx *bolt.Tx) error {
for _, bucket := range bucketsToCreate {
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
if err != nil {
return err
}
}
return nil
})
return store.initServices()
}
// Init creates the default data set.
@ -128,6 +95,7 @@ func (store *Store) Init() error {
Labels: []portainer.Pair{},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Tags: []string{},
}
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
@ -147,28 +115,114 @@ func (store *Store) Close() error {
// MigrateData automatically migrate the data based on the DBVersion.
func (store *Store) MigrateData() error {
if !store.checkForDataMigration {
err := store.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
}
return nil
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
if err == portainer.ErrDBVersionNotFound {
if err == portainer.ErrObjectNotFound {
version = 0
} else if err != nil {
return err
}
if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{
DB: store.db,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
StackService: store.StackService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
}
migrator := migrator.NewMigrator(migratorParams)
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
migrator := NewMigrator(store, version)
err = migrator.Migrate()
if err != nil {
log.Printf("An error occurred during database migration: %s\n", err)
return err
}
}
return nil
}
func (store *Store) initServices() error {
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.db)
if err != nil {
return err
}
store.EndpointService = endpointService
registryService, err := registry.NewService(store.db)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.db)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.db)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.db)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.db)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.db)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.db)
if err != nil {
return err
}
store.TeamService = teamService
userService, err := user.NewService(store.db)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.db)
if err != nil {
return err
}
store.VersionService = versionService
return nil
}

View File

@ -0,0 +1,48 @@
package dockerhub
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "dockerhub"
dockerHubKey = "DOCKERHUB"
)
// Service represents a service for managing Dockerhub data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// DockerHub returns the DockerHub object.
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
err := internal.GetObject(service.db, BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil {
return nil, err
}
return &dockerhub, nil
}
// UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return internal.UpdateObject(service.db, BucketName, []byte(dockerHubKey), dockerhub)
}

View File

@ -1,61 +0,0 @@
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

@ -0,0 +1,138 @@
package endpoint
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoints"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Endpoint returns an endpoint by ID.
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// UpdateEndpoint updates an endpoint.
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpoint)
}
// DeleteEndpoint deletes an endpoint.
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// Endpoints return an array containing all the endpoints.
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpoint portainer.Endpoint
err := internal.UnmarshalObject(v, &endpoint)
if err != nil {
return err
}
endpoints = append(endpoints, endpoint)
}
return nil
})
return endpoints, err
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpoint.ID)), data)
})
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *Service) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
for _, endpoint := range toCreate {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return nil
})
}

View File

@ -1,114 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// EndpointGroupService represents a service for managing endpoint groups.
type EndpointGroupService struct {
store *Store
}
// EndpointGroup returns an endpoint group by ID.
func (service *EndpointGroupService) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointGroupBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrEndpointGroupNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var endpointGroup portainer.EndpointGroup
err = internal.UnmarshalEndpointGroup(data, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// EndpointGroups return an array containing all the endpoint groups.
func (service *EndpointGroupService) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointGroupBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpointGroup portainer.EndpointGroup
err := internal.UnmarshalEndpointGroup(v, &endpointGroup)
if err != nil {
return err
}
endpointGroups = append(endpointGroups, endpointGroup)
}
return nil
})
if err != nil {
return nil, err
}
return endpointGroups, nil
}
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *EndpointGroupService) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointGroupBucketName))
id, _ := bucket.NextSequence()
endpointGroup.ID = portainer.EndpointGroupID(id)
data, err := internal.MarshalEndpointGroup(endpointGroup)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpointGroup.ID)), data)
if err != nil {
return err
}
return nil
})
}
// UpdateEndpointGroup updates an endpoint group.
func (service *EndpointGroupService) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
data, err := internal.MarshalEndpointGroup(endpointGroup)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointGroupBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteEndpointGroup deletes an endpoint group.
func (service *EndpointGroupService) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointGroupBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}

View File

@ -1,154 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// EndpointService represents a service for managing endpoints.
type EndpointService struct {
store *Store
}
// Endpoint returns an endpoint by ID.
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrEndpointNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var endpoint portainer.Endpoint
err = internal.UnmarshalEndpoint(data, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// Endpoints return an array containing all the endpoints.
func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpoint portainer.Endpoint
err := internal.UnmarshalEndpoint(v, &endpoint)
if err != nil {
return err
}
endpoints = append(endpoints, endpoint)
}
return nil
})
if err != nil {
return nil, err
}
return endpoints, nil
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *EndpointService) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
for _, endpoint := range toCreate {
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
err := marshalAndStoreEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return nil
})
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
return nil
})
}
// UpdateEndpoint updates an endpoint.
func (service *EndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteEndpoint deletes an endpoint.
func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
return nil
}
func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
return marshalAndStoreEndpoint(endpoint, bucket)
}

View File

@ -0,0 +1,95 @@
package endpointgroup
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_groups"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EndpointGroup returns an endpoint group by ID.
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// UpdateEndpointGroup updates an endpoint group.
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an endpoint group.
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// EndpointGroups return an array containing all the endpoint groups.
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpointGroup portainer.EndpointGroup
err := internal.UnmarshalObject(v, &endpointGroup)
if err != nil {
return err
}
endpointGroups = append(endpointGroups, endpointGroup)
}
return nil
})
return endpointGroups, err
}
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
endpointGroup.ID = portainer.EndpointGroupID(id)
data, err := internal.MarshalObject(endpointGroup)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointGroup.ID)), data)
})
}

94
api/bolt/internal/db.go Normal file
View File

@ -0,0 +1,94 @@
package internal
import (
"encoding/binary"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
)
// 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.
func Itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
// CreateBucket is a generic function used to create a bucket inside a bolt database.
func CreateBucket(db *bolt.DB, bucketName string) error {
return db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return nil
})
}
// GetObject is a generic function used to retrieve an unmarshalled object from a bolt database.
func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
var data []byte
err := db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
if value == nil {
return portainer.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return err
}
return UnmarshalObject(data, object)
}
// UpdateObject is a generic function used to update an object inside a bolt database.
func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
return db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := MarshalObject(object)
if err != nil {
return err
}
err = bucket.Put(key, data)
if err != nil {
return err
}
return nil
})
}
// DeleteObject is a generic function used to delete an object inside a bolt database.
func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
return db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
})
}
// GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1.
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
var identifier int
db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id := bucket.Sequence()
identifier = int(id)
return nil
})
identifier++
return identifier
}

View File

@ -1,117 +0,0 @@
package internal
import (
"github.com/portainer/portainer"
"encoding/binary"
"encoding/json"
)
// MarshalUser encodes a user to binary format.
func MarshalUser(user *portainer.User) ([]byte, error) {
return json.Marshal(user)
}
// UnmarshalUser decodes a user from a binary data.
func UnmarshalUser(data []byte, user *portainer.User) error {
return json.Unmarshal(data, user)
}
// MarshalTeam encodes a team to binary format.
func MarshalTeam(team *portainer.Team) ([]byte, error) {
return json.Marshal(team)
}
// UnmarshalTeam decodes a team from a binary data.
func UnmarshalTeam(data []byte, team *portainer.Team) error {
return json.Unmarshal(data, team)
}
// MarshalTeamMembership encodes a team membership to binary format.
func MarshalTeamMembership(membership *portainer.TeamMembership) ([]byte, error) {
return json.Marshal(membership)
}
// UnmarshalTeamMembership decodes a team membership from a binary data.
func UnmarshalTeamMembership(data []byte, membership *portainer.TeamMembership) error {
return json.Unmarshal(data, membership)
}
// MarshalEndpoint encodes an endpoint to binary format.
func MarshalEndpoint(endpoint *portainer.Endpoint) ([]byte, error) {
return json.Marshal(endpoint)
}
// UnmarshalEndpoint decodes an endpoint from a binary data.
func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
return json.Unmarshal(data, endpoint)
}
// MarshalEndpointGroup encodes an endpoint group to binary format.
func MarshalEndpointGroup(group *portainer.EndpointGroup) ([]byte, error) {
return json.Marshal(group)
}
// UnmarshalEndpointGroup decodes an endpoint group from a binary data.
func UnmarshalEndpointGroup(data []byte, group *portainer.EndpointGroup) error {
return json.Unmarshal(data, group)
}
// MarshalStack encodes a stack to binary format.
func MarshalStack(stack *portainer.Stack) ([]byte, error) {
return json.Marshal(stack)
}
// UnmarshalStack decodes a stack from a binary data.
func UnmarshalStack(data []byte, stack *portainer.Stack) error {
return json.Unmarshal(data, stack)
}
// 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)
}
// UnmarshalResourceControl decodes a resource control object from a binary data.
func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error {
return json.Unmarshal(data, rc)
}
// MarshalSettings encodes a settings object to binary format.
func MarshalSettings(settings *portainer.Settings) ([]byte, error) {
return json.Marshal(settings)
}
// UnmarshalSettings decodes a settings object from a binary data.
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.
func Itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}

15
api/bolt/internal/json.go Normal file
View File

@ -0,0 +1,15 @@
package internal
import (
"encoding/json"
)
// MarshalObject encodes an object to binary format
func MarshalObject(object interface{}) ([]byte, error) {
return json.Marshal(object)
}
// UnmarshalObject decodes an object from binary data
func UnmarshalObject(data []byte, object interface{}) error {
return json.Unmarshal(data, object)
}

View File

@ -1,16 +0,0 @@
package bolt
func (m *Migrator) updateSettingsToVersion5() error {
legacySettings, err := m.SettingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowBindMountsForRegularUsers = true
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
}

View File

@ -1,16 +0,0 @@
package bolt
func (m *Migrator) updateSettingsToVersion6() error {
legacySettings, err := m.SettingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowPrivilegedModeForRegularUsers = true
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
}

View File

@ -1,16 +0,0 @@
package bolt
func (m *Migrator) updateSettingsToVersion7() error {
legacySettings, err := m.SettingsService.Settings()
if err != nil {
return err
}
legacySettings.DisplayDonationHeader = true
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
}

View File

@ -1,128 +0,0 @@
package bolt
import "github.com/portainer/portainer"
// Migrator defines a service to migrate data after a Portainer version update.
type Migrator struct {
UserService *UserService
EndpointService *EndpointService
ResourceControlService *ResourceControlService
SettingsService *SettingsService
VersionService *VersionService
CurrentDBVersion int
store *Store
}
// NewMigrator creates a new Migrator.
func NewMigrator(store *Store, version int) *Migrator {
return &Migrator{
UserService: store.UserService,
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
VersionService: store.VersionService,
CurrentDBVersion: version,
store: store,
}
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.CurrentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.CurrentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
// Portainer 1.13.x
if m.CurrentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.CurrentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1235
if m.CurrentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1236
if m.CurrentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1449
if m.CurrentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
if m.CurrentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
// https: //github.com/portainer/portainer/issues/1396
if m.CurrentDBVersion < 9 {
err := m.updateEndpointsToVersion9()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/461
if m.CurrentDBVersion < 10 {
err := m.updateEndpointsToVersion10()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1906
if m.CurrentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return err
}
}
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
}
return nil
}

View File

@ -1,19 +1,20 @@
package bolt
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/user"
)
func (m *Migrator) updateAdminUserToDBVersion1() error {
u, err := m.UserService.UserByUsername("admin")
u, err := m.userService.UserByUsername("admin")
if err == nil {
admin := &portainer.User{
Username: "admin",
Password: u.Password,
Role: portainer.AdministratorRole,
}
err = m.UserService.CreateUser(admin)
err = m.userService.CreateUser(admin)
if err != nil {
return err
}
@ -21,19 +22,15 @@ func (m *Migrator) updateAdminUserToDBVersion1() error {
if err != nil {
return err
}
} else if err != nil && err != portainer.ErrUserNotFound {
} else if err != nil && err != portainer.ErrObjectNotFound {
return err
}
return nil
}
func (m *Migrator) removeLegacyAdminUser() error {
return m.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err := bucket.Delete([]byte("admin"))
if err != nil {
return err
}
return nil
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(user.BucketName))
return bucket.Delete([]byte("admin"))
})
}

View File

@ -1,4 +1,4 @@
package bolt
package migrator
import (
"github.com/boltdb/bolt"
@ -16,7 +16,7 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
resourceControl.SubResourceIDs = []string{}
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
owner, err := m.UserService.User(resourceControl.OwnerID)
owner, err := m.userService.User(resourceControl.OwnerID)
if err != nil {
return err
}
@ -33,7 +33,7 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
}
err = m.ResourceControlService.CreateResourceControl(&resourceControl)
err = m.resourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
return err
}
@ -43,14 +43,14 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
}
func (m *Migrator) updateEndpointsToDBVersion2() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.AuthorizedTeams = []portainer.TeamID{}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
@ -61,12 +61,12 @@ func (m *Migrator) updateEndpointsToDBVersion2() error {
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
legacyResourceControls := make([]portainer.ResourceControl, 0)
err := m.store.db.View(func(tx *bolt.Tx) error {
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("containerResourceControl"))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
@ -78,7 +78,7 @@ func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
@ -90,7 +90,7 @@ func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}

View File

@ -1,9 +1,9 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion11() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
@ -18,7 +18,7 @@ func (m *Migrator) updateEndpointsToVersion11() error {
}
}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}

View File

@ -0,0 +1,127 @@
package migrator
import (
"strconv"
"strings"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/portainer/portainer/bolt/stack"
)
func (m *Migrator) updateEndpointsToVersion12() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Tags = []string{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointGroupsToVersion12() error {
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, group := range legacyEndpointGroups {
group.Tags = []string{}
err = m.endpointGroupService.UpdateEndpointGroup(group.ID, &group)
if err != nil {
return err
}
}
return nil
}
type legacyStack struct {
ID string `json:"Id"`
Name string `json:"Name"`
EndpointID portainer.EndpointID `json:"EndpointId"`
SwarmID string `json:"SwarmId"`
EntryPoint string `json:"EntryPoint"`
Env []portainer.Pair `json:"Env"`
ProjectPath string
}
func (m *Migrator) updateStacksToVersion12() error {
legacyStacks, err := m.retrieveLegacyStacks()
if err != nil {
return err
}
for _, legacyStack := range legacyStacks {
err := m.convertLegacyStack(&legacyStack)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) convertLegacyStack(s *legacyStack) error {
stackID := m.stackService.GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: s.Name,
Type: portainer.DockerSwarmStack,
SwarmID: s.SwarmID,
EndpointID: 0,
EntryPoint: s.EntryPoint,
Env: s.Env,
}
stack.ProjectPath = strings.Replace(s.ProjectPath, s.ID, strconv.Itoa(stackID), 1)
err := m.fileService.Rename(s.ProjectPath, stack.ProjectPath)
if err != nil {
return err
}
err = m.deleteLegacyStack(s.ID)
if err != nil {
return err
}
return m.stackService.CreateStack(stack)
}
func (m *Migrator) deleteLegacyStack(legacyID string) error {
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
return bucket.Delete([]byte(legacyID))
})
}
func (m *Migrator) retrieveLegacyStacks() ([]legacyStack, error) {
var legacyStacks = make([]legacyStack, 0)
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack legacyStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
legacyStacks = append(legacyStacks, stack)
}
return nil
})
return legacyStacks, err
}

View File

@ -1,9 +1,9 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateSettingsToDBVersion3() error {
legacySettings, err := m.SettingsService.Settings()
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
@ -16,10 +16,5 @@ func (m *Migrator) updateSettingsToDBVersion3() error {
},
}
err = m.SettingsService.StoreSettings(legacySettings)
if err != nil {
return err
}
return nil
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@ -1,9 +1,9 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToDBVersion4() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
@ -17,7 +17,8 @@ func (m *Migrator) updateEndpointsToDBVersion4() error {
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}

View File

@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion5() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowBindMountsForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion6() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowPrivilegedModeForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion7() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.DisplayDonationHeader = true
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@ -1,16 +1,16 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion8() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Extensions = []portainer.EndpointExtension{}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}

View File

@ -1,16 +1,16 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion9() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.GroupID = portainer.EndpointGroupID(1)
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}

View File

@ -1,16 +1,16 @@
package bolt
package migrator
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToVersion10() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Type = portainer.DockerEnvironment
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}

View File

@ -0,0 +1,174 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/endpoint"
"github.com/portainer/portainer/bolt/endpointgroup"
"github.com/portainer/portainer/bolt/resourcecontrol"
"github.com/portainer/portainer/bolt/settings"
"github.com/portainer/portainer/bolt/stack"
"github.com/portainer/portainer/bolt/user"
"github.com/portainer/portainer/bolt/version"
)
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
db *bolt.DB
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
resourceControlService *resourcecontrol.Service
settingsService *settings.Service
stackService *stack.Service
userService *user.Service
versionService *version.Service
fileService portainer.FileService
}
// Parameters represents the required parameters to create a new Migrator instance.
Parameters struct {
DB *bolt.DB
DatabaseVersion int
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
}
)
// NewMigrator creates a new Migrator.
func NewMigrator(parameters *Parameters) *Migrator {
return &Migrator{
db: parameters.DB,
currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
resourceControlService: parameters.ResourceControlService,
settingsService: parameters.SettingsService,
stackService: parameters.StackService,
userService: parameters.UserService,
versionService: parameters.VersionService,
fileService: parameters.FileService,
}
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.currentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.currentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
// Portainer 1.13.x
if m.currentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.currentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1235
if m.currentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1236
if m.currentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1449
if m.currentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
if m.currentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
// https: //github.com/portainer/portainer/issues/1396
if m.currentDBVersion < 9 {
err := m.updateEndpointsToVersion9()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/461
if m.currentDBVersion < 10 {
err := m.updateEndpointsToVersion10()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1906
if m.currentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return err
}
}
// Portainer 1.18.0
if m.currentDBVersion < 12 {
err := m.updateEndpointsToVersion12()
if err != nil {
return err
}
err = m.updateEndpointGroupsToVersion12()
if err != nil {
return err
}
err = m.updateStacksToVersion12()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@ -0,0 +1,95 @@
package registry
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "registries"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Registry returns an registry by ID.
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
var registry portainer.Registry
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &registry)
if err != nil {
return nil, err
}
return &registry, nil
}
// Registries returns an array containing all the registries.
func (service *Service) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var registry portainer.Registry
err := internal.UnmarshalObject(v, &registry)
if err != nil {
return err
}
registries = append(registries, registry)
}
return nil
})
return registries, err
}
// CreateRegistry creates a new registry.
func (service *Service) CreateRegistry(registry *portainer.Registry) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
registry.ID = portainer.RegistryID(id)
data, err := internal.MarshalObject(registry)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(registry.ID)), data)
})
}
// UpdateRegistry updates an registry.
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, registry)
}
// DeleteRegistry deletes an registry.
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@ -1,114 +0,0 @@
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

@ -1,148 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// ResourceControlService represents a service for managing resource controls.
type ResourceControlService struct {
store *Store
}
// ResourceControl returns a ResourceControl object by ID
func (service *ResourceControlService) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrResourceControlNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var resourceControl portainer.ResourceControl
err = internal.UnmarshalResourceControl(data, &resourceControl)
if err != nil {
return nil, err
}
return &resourceControl, nil
}
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs
func (service *ResourceControlService) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var rc portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &rc)
if err != nil {
return err
}
if rc.ResourceID == resourceID {
resourceControl = &rc
}
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = &rc
}
}
}
if resourceControl == nil {
return portainer.ErrResourceControlNotFound
}
return nil
})
if err != nil {
return nil, err
}
return resourceControl, nil
}
// ResourceControls returns all the ResourceControl objects
func (service *ResourceControlService) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &resourceControl)
if err != nil {
return err
}
rcs = append(rcs, resourceControl)
}
return nil
})
if err != nil {
return nil, err
}
return rcs, nil
}
// CreateResourceControl creates a new ResourceControl object
func (service *ResourceControlService) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
id, _ := bucket.NextSequence()
resourceControl.ID = portainer.ResourceControlID(id)
data, err := internal.MarshalResourceControl(resourceControl)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(resourceControl.ID)), data)
if err != nil {
return err
}
return nil
})
}
// UpdateResourceControl saves a ResourceControl object.
func (service *ResourceControlService) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
data, err := internal.MarshalResourceControl(resourceControl)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *ResourceControlService) DeleteResourceControl(ID portainer.ResourceControlID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(resourceControlBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}

View File

@ -0,0 +1,134 @@
package resourcecontrol
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "resource_control"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// ResourceControl returns a ResourceControl object by ID
func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
var resourceControl portainer.ResourceControl
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &resourceControl)
if err != nil {
return nil, err
}
return &resourceControl, nil
}
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var rc portainer.ResourceControl
err := internal.UnmarshalObject(v, &rc)
if err != nil {
return err
}
if rc.ResourceID == resourceID {
resourceControl = &rc
break
}
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = &rc
break
}
}
}
if resourceControl == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return resourceControl, err
}
// ResourceControls returns all the ResourceControl objects
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
rcs = append(rcs, resourceControl)
}
return nil
})
return rcs, err
}
// CreateResourceControl creates a new ResourceControl object
func (service *Service) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
resourceControl.ID = portainer.ResourceControlID(id)
data, err := internal.MarshalObject(resourceControl)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(resourceControl.ID)), data)
})
}
// UpdateResourceControl saves a ResourceControl object.
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, resourceControl)
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@ -0,0 +1,48 @@
package settings
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "settings"
settingsKey = "SETTINGS"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Settings retrieve the settings object.
func (service *Service) Settings() (*portainer.Settings, error) {
var settings portainer.Settings
err := internal.GetObject(service.db, BucketName, []byte(settingsKey), &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return internal.UpdateObject(service.db, BucketName, []byte(settingsKey), settings)
}

View File

@ -1,61 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// SettingsService represents a service to manage application settings.
type SettingsService struct {
store *Store
}
const (
dbSettingsKey = "SETTINGS"
)
// Settings retrieve the settings object.
func (service *SettingsService) Settings() (*portainer.Settings, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(settingsBucketName))
value := bucket.Get([]byte(dbSettingsKey))
if value == nil {
return portainer.ErrSettingsNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var settings portainer.Settings
err = internal.UnmarshalSettings(data, &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// StoreSettings persists a Settings object.
func (service *SettingsService) StoreSettings(settings *portainer.Settings) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(settingsBucketName))
data, err := internal.MarshalSettings(settings)
if err != nil {
return err
}
err = bucket.Put([]byte(dbSettingsKey), data)
if err != nil {
return err
}
return nil
})
}

134
api/bolt/stack/stack.go Normal file
View File

@ -0,0 +1,134 @@
package stack
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "stacks"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Stack returns a stack object by ID.
func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var stack portainer.Stack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// StackByName returns a stack object by name.
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
var stack *portainer.Stack
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Stack
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
stack = &t
break
}
}
if stack == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return stack, err
}
// Stacks returns an array containing all the stacks.
func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack portainer.Stack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
return stacks, err
}
// GetNextIdentifier returns the next identifier for a stack.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}
// CreateStack creates a new stack.
func (service *Service) CreateStack(stack *portainer.Stack) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for stacks
err := bucket.SetSequence(uint64(stack.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(stack)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(stack.ID)), data)
})
}
// UpdateStack updates a stack.
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, stack)
}
// DeleteStack deletes a stack.
func (service *Service) DeleteStack(ID portainer.StackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@ -1,138 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// StackService represents a service for managing stacks.
type StackService struct {
store *Store
}
// Stack returns a stack object by ID.
func (service *StackService) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
value := bucket.Get([]byte(ID))
if value == nil {
return portainer.ErrStackNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var stack portainer.Stack
err = internal.UnmarshalStack(data, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// Stacks returns an array containing all the stacks.
func (service *StackService) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack portainer.Stack
err := internal.UnmarshalStack(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
if err != nil {
return nil, err
}
return stacks, nil
}
// StacksBySwarmID return an array containing all the stacks related to the specified Swarm ID.
func (service *StackService) StacksBySwarmID(id string) ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack portainer.Stack
err := internal.UnmarshalStack(v, &stack)
if err != nil {
return err
}
if stack.SwarmID == id {
stacks = append(stacks, stack)
}
}
return nil
})
if err != nil {
return nil, err
}
return stacks, nil
}
// CreateStack creates a new stack.
func (service *StackService) CreateStack(stack *portainer.Stack) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
data, err := internal.MarshalStack(stack)
if err != nil {
return err
}
err = bucket.Put([]byte(stack.ID), data)
if err != nil {
return err
}
return nil
})
}
// UpdateStack updates an stack.
func (service *StackService) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
data, err := internal.MarshalStack(stack)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
err = bucket.Put([]byte(ID), data)
if err != nil {
return err
}
return nil
})
}
// DeleteStack deletes an stack.
func (service *StackService) DeleteStack(ID portainer.StackID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stackBucketName))
err := bucket.Delete([]byte(ID))
if err != nil {
return err
}
return nil
})
}

76
api/bolt/tag/tag.go Normal file
View File

@ -0,0 +1,76 @@
package tag
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "tags"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Tags return an array containing all the tags.
func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var tag portainer.Tag
err := internal.UnmarshalObject(v, &tag)
if err != nil {
return err
}
tags = append(tags, tag)
}
return nil
})
return tags, err
}
// CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
tag.ID = portainer.TagID(id)
data, err := internal.MarshalObject(tag)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(tag.ID)), data)
})
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

126
api/bolt/team/team.go Normal file
View File

@ -0,0 +1,126 @@
package team
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "teams"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Team returns a Team by ID
func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
var team portainer.Team
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &team)
if err != nil {
return nil, err
}
return &team, nil
}
// TeamByName returns a team by name.
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Team
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
team = &t
break
}
}
if team == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return team, err
}
// Teams return an array containing all the teams.
func (service *Service) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var team portainer.Team
err := internal.UnmarshalObject(v, &team)
if err != nil {
return err
}
teams = append(teams, team)
}
return nil
})
return teams, err
}
// UpdateTeam saves a Team.
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, team)
}
// CreateTeam creates a new Team.
func (service *Service) CreateTeam(team *portainer.Team) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
team.ID = portainer.TeamID(id)
data, err := internal.MarshalObject(team)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(team.ID)), data)
})
}
// DeleteTeam deletes a Team.
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@ -1,217 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// TeamMembershipService represents a service for managing TeamMembership objects.
type TeamMembershipService struct {
store *Store
}
// TeamMembership returns a TeamMembership object by ID
func (service *TeamMembershipService) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrTeamMembershipNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var membership portainer.TeamMembership
err = internal.UnmarshalTeamMembership(data, &membership)
if err != nil {
return nil, err
}
return &membership, nil
}
// TeamMemberships return an array containing all the TeamMembership objects.
func (service *TeamMembershipService) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
memberships = append(memberships, membership)
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
func (service *TeamMembershipService) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
memberships = append(memberships, membership)
}
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
func (service *TeamMembershipService) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
memberships = append(memberships, membership)
}
}
return nil
})
if err != nil {
return nil, err
}
return memberships, nil
}
// UpdateTeamMembership saves a TeamMembership object.
func (service *TeamMembershipService) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
data, err := internal.MarshalTeamMembership(membership)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *TeamMembershipService) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
id, _ := bucket.NextSequence()
membership.ID = portainer.TeamMembershipID(id)
data, err := internal.MarshalTeamMembership(membership)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(membership.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteTeamMembership deletes a TeamMembership object.
func (service *TeamMembershipService) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *TeamMembershipService) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *TeamMembershipService) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamMembershipBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalTeamMembership(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}

View File

@ -1,144 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// TeamService represents a service for managing teams.
type TeamService struct {
store *Store
}
// Team returns a Team by ID
func (service *TeamService) Team(ID portainer.TeamID) (*portainer.Team, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrTeamNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var team portainer.Team
err = internal.UnmarshalTeam(data, &team)
if err != nil {
return nil, err
}
return &team, nil
}
// TeamByName returns a team by name.
func (service *TeamService) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Team
err := internal.UnmarshalTeam(v, &t)
if err != nil {
return err
}
if t.Name == name {
team = &t
}
}
if team == nil {
return portainer.ErrTeamNotFound
}
return nil
})
if err != nil {
return nil, err
}
return team, nil
}
// Teams return an array containing all the teams.
func (service *TeamService) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var team portainer.Team
err := internal.UnmarshalTeam(v, &team)
if err != nil {
return err
}
teams = append(teams, team)
}
return nil
})
if err != nil {
return nil, err
}
return teams, nil
}
// UpdateTeam saves a Team.
func (service *TeamService) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
data, err := internal.MarshalTeam(team)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateTeam creates a new Team.
func (service *TeamService) CreateTeam(team *portainer.Team) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
id, _ := bucket.NextSequence()
team.ID = portainer.TeamID(id)
data, err := internal.MarshalTeam(team)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(team.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteTeam deletes a Team.
func (service *TeamService) DeleteTeam(ID portainer.TeamID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(teamBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}

View File

@ -0,0 +1,197 @@
package teammembership
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "team_membership"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// TeamMembership returns a TeamMembership object by ID
func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
var membership portainer.TeamMembership
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &membership)
if err != nil {
return nil, err
}
return &membership, nil
}
// TeamMemberships return an array containing all the TeamMembership objects.
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
memberships = append(memberships, membership)
}
return nil
})
return memberships, err
}
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
memberships = append(memberships, membership)
}
}
return nil
})
return memberships, err
}
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
memberships = append(memberships, membership)
}
}
return nil
})
return memberships, err
}
// UpdateTeamMembership saves a TeamMembership object.
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, membership)
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *Service) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
membership.ID = portainer.TeamMembershipID(id)
data, err := internal.MarshalObject(membership)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(membership.ID)), data)
})
}
// DeleteTeamMembership deletes a TeamMembership object.
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}

149
api/bolt/user/user.go Normal file
View File

@ -0,0 +1,149 @@
package user
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "users"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// User returns a user by ID
func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
var user portainer.User
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UserByUsername returns a user by username.
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var u portainer.User
err := internal.UnmarshalObject(v, &u)
if err != nil {
return err
}
if u.Username == username {
user = &u
break
}
}
if user == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return user, err
}
// Users return an array containing all the users.
func (service *Service) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalObject(v, &user)
if err != nil {
return err
}
users = append(users, user)
}
return nil
})
return users, err
}
// UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalObject(v, &user)
if err != nil {
return err
}
if user.Role == role {
users = append(users, user)
}
}
return nil
})
return users, err
}
// UpdateUser saves a user.
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, user)
}
// CreateUser creates a new user.
func (service *Service) CreateUser(user *portainer.User) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
data, err := internal.MarshalObject(user)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(user.ID)), data)
})
}
// DeleteUser deletes a user.
func (service *Service) DeleteUser(ID portainer.UserID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@ -1,170 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// UserService represents a service for managing users.
type UserService struct {
store *Store
}
// User returns a user by ID
func (service *UserService) User(ID portainer.UserID) (*portainer.User, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get(internal.Itob(int(ID)))
if value == nil {
return portainer.ErrUserNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var user portainer.User
err = internal.UnmarshalUser(data, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UserByUsername returns a user by username.
func (service *UserService) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var u portainer.User
err := internal.UnmarshalUser(v, &u)
if err != nil {
return err
}
if u.Username == username {
user = &u
}
}
if user == nil {
return portainer.ErrUserNotFound
}
return nil
})
if err != nil {
return nil, err
}
return user, nil
}
// Users return an array containing all the users.
func (service *UserService) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalUser(v, &user)
if err != nil {
return err
}
users = append(users, user)
}
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
// UsersByRole return an array containing all the users with the specified role.
func (service *UserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalUser(v, &user)
if err != nil {
return err
}
if user.Role == role {
users = append(users, user)
}
}
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
// UpdateUser saves a user.
func (service *UserService) UpdateUser(ID portainer.UserID, user *portainer.User) error {
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err = bucket.Put(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateUser creates a new user.
func (service *UserService) CreateUser(user *portainer.User) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(user.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteUser deletes a user.
func (service *UserService) DeleteUser(ID portainer.UserID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}

View File

@ -0,0 +1,66 @@
package version
import (
"strconv"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "version"
versionKey = "DB_VERSION"
)
// Service represents a service to manage stored versions.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// DBVersion retrieves the stored database version.
func (service *Service) DBVersion() (int, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(versionKey))
if value == nil {
return portainer.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return 0, err
}
return strconv.Atoi(string(data))
}
// StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(version int) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(strconv.Itoa(version))
return bucket.Put([]byte(versionKey), data)
})
}

View File

@ -1,58 +0,0 @@
package bolt
import (
"strconv"
"github.com/portainer/portainer"
"github.com/boltdb/bolt"
)
// VersionService represents a service to manage stored versions.
type VersionService struct {
store *Store
}
const (
dBVersionKey = "DB_VERSION"
)
// DBVersion retrieves the stored database version.
func (service *VersionService) DBVersion() (int, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(versionBucketName))
value := bucket.Get([]byte(dBVersionKey))
if value == nil {
return portainer.ErrDBVersionNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return 0, err
}
dbVersion, err := strconv.Atoi(string(data))
if err != nil {
return 0, err
}
return dbVersion, nil
}
// StoreDBVersion store the database version.
func (service *VersionService) StoreDBVersion(version int) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(versionBucketName))
data := []byte(strconv.Itoa(version))
err := bucket.Put([]byte(dBVersionKey), data)
if err != nil {
return err
}
return nil
})
}

View File

@ -15,6 +15,7 @@ import (
"github.com/portainer/portainer/http/client"
"github.com/portainer/portainer/jwt"
"github.com/portainer/portainer/ldap"
"github.com/portainer/portainer/libcompose"
"log"
)
@ -41,8 +42,8 @@ func initFileService(dataStorePath string) portainer.FileService {
return fileService
}
func initStore(dataStorePath string) *bolt.Store {
store, err := bolt.NewStore(dataStorePath)
func initStore(dataStorePath string, fileService portainer.FileService) *bolt.Store {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatal(err)
}
@ -64,8 +65,12 @@ func initStore(dataStorePath string) *bolt.Store {
return store
}
func initStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.StackManager, error) {
return exec.NewStackManager(assetsPath, dataStorePath, signatureService, fileService)
func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager {
return libcompose.NewComposeStackManager(dataStorePath)
}
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService)
}
func initJWTService(authenticationEnabled bool) portainer.JWTService {
@ -120,13 +125,13 @@ func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portaine
func initDockerHub(dockerHubService portainer.DockerHubService) error {
_, err := dockerHubService.DockerHub()
if err == portainer.ErrDockerHubNotFound {
if err == portainer.ErrObjectNotFound {
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
return dockerHubService.StoreDockerHub(dockerhub)
return dockerHubService.UpdateDockerHub(dockerhub)
} else if err != nil {
return err
}
@ -136,10 +141,9 @@ func initDockerHub(dockerHubService portainer.DockerHubService) error {
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
_, err := settingsService.Settings()
if err == portainer.ErrSettingsNotFound {
if err == portainer.ErrObjectNotFound {
settings := &portainer.Settings{
LogoURL: *flags.Logo,
DisplayDonationHeader: true,
DisplayExternalContributors: false,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{
@ -164,7 +168,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
settings.BlackListedLabels = make([]portainer.Pair, 0)
}
return settingsService.StoreSettings(settings)
return settingsService.UpdateSettings(settings)
} else if err != nil {
return err
}
@ -232,6 +236,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
}
if strings.HasPrefix(endpoint.URL, "tcp://") {
@ -270,6 +275,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
}
return endpointService.CreateEndpoint(endpoint)
@ -301,7 +307,7 @@ func main() {
fileService := initFileService(*flags.Data)
store := initStore(*flags.Data)
store := initStore(*flags.Data, fileService)
defer store.Close()
jwtService := initJWTService(!*flags.NoAuth)
@ -321,11 +327,13 @@ func main() {
log.Fatal(err)
}
stackManager, err := initStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
if err != nil {
log.Fatal(err)
}
composeStackManager := initComposeStackManager(*flags.Data)
err = initSettings(store.SettingsService, flags)
if err != nil {
log.Fatal(err)
@ -395,7 +403,9 @@ func main() {
RegistryService: store.RegistryService,
DockerHubService: store.DockerHubService,
StackService: store.StackService,
StackManager: stackManager,
TagService: store.TagService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,

View File

@ -4,63 +4,64 @@ package portainer
const (
ErrUnauthorized = Error("Unauthorized")
ErrResourceAccessDenied = Error("Access denied to resource")
ErrResourceNotFound = Error("Unable to find resource")
ErrUnsupportedDockerAPI = Error("Unsupported Docker API response")
ErrObjectNotFound = Error("Object not found inside the database")
ErrMissingSecurityContext = Error("Unable to find security details in request context")
)
// User errors.
const (
ErrUserNotFound = Error("User not found")
ErrUserAlreadyExists = Error("User already exists")
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
ErrCannotRemoveAdmin = Error("Cannot remove the default administrator account")
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
)
// Team errors.
const (
ErrTeamNotFound = Error("Team not found")
ErrTeamAlreadyExists = Error("Team already exists")
)
// TeamMembership errors.
const (
ErrTeamMembershipNotFound = Error("Team membership not found")
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team")
)
// ResourceControl errors.
const (
ErrResourceControlNotFound = Error("Resource control not found")
ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource")
ErrInvalidResourceControlType = Error("Unsupported resource control type")
)
// Endpoint errors.
const (
ErrEndpointNotFound = Error("Endpoint not found")
ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Azure environment errors
const (
ErrAzureInvalidCredentials = Error("Invalid Azure credentials")
)
// Endpoint group errors.
const (
ErrEndpointGroupNotFound = Error("Endpoint group not found")
ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group")
)
// Registry errors.
const (
ErrRegistryNotFound = Error("Registry not found")
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
)
// Stack errors
const (
ErrStackNotFound = Error("Stack not found")
ErrStackAlreadyExists = Error("A stack already exists with this name")
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
ErrStackNotExternal = Error("Not an external stack")
)
// Tag errors
const (
ErrTagAlreadyExists = Error("A tag already exists with this name")
)
// Endpoint extensions error
@ -69,21 +70,6 @@ const (
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
)
// Version errors.
const (
ErrDBVersionNotFound = Error("DB version not found")
)
// Settings errors.
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

@ -11,18 +11,18 @@ import (
"github.com/portainer/portainer"
)
// StackManager represents a service for managing stacks.
type StackManager struct {
// SwarmStackManager represents a service for managing stacks.
type SwarmStackManager struct {
binaryPath string
dataPath string
signatureService portainer.DigitalSignatureService
fileService portainer.FileService
}
// NewStackManager initializes a new StackManager service.
// NewSwarmStackManager initializes a new SwarmStackManager service.
// It also updates the configuration of the Docker CLI binary.
func NewStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*StackManager, error) {
manager := &StackManager{
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*SwarmStackManager, error) {
manager := &SwarmStackManager{
binaryPath: binaryPath,
dataPath: dataPath,
signatureService: signatureService,
@ -38,7 +38,7 @@ func NewStackManager(binaryPath, dataPath string, signatureService portainer.Dig
}
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
for _, registry := range registries {
if registry.Authentication {
@ -54,14 +54,14 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []
}
// Logout executes the docker logout command.
func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil, "")
}
// Deploy executes the docker stack deploy command.
func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
@ -81,7 +81,7 @@ func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint
}
// Remove executes the docker stack rm command.
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "")
@ -133,7 +133,7 @@ func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portaine
return command, args
}
func (manager *StackManager) updateDockerCLIConfiguration(dataPath string) error {
func (manager *SwarmStackManager) updateDockerCLIConfiguration(dataPath string) error {
configFilePath := path.Join(dataPath, "config.json")
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
if err != nil {
@ -161,7 +161,7 @@ func (manager *StackManager) updateDockerCLIConfiguration(dataPath string) error
return nil
}
func (manager *StackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
var config map[string]interface{}
raw, err := manager.fileService.GetFileContent(path)

View File

@ -77,9 +77,9 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string {
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
}
// StoreStackFileFromString creates a subfolder in the ComposeStorePath and stores a new file using the content from a string.
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stackFileContent string) (string, error) {
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
@ -87,7 +87,6 @@ func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stac
}
composeFilePath := path.Join(stackStorePath, fileName)
data := []byte(stackFileContent)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
@ -98,31 +97,13 @@ func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stac
return path.Join(service.fileStorePath, stackStorePath), nil
}
// StoreStackFileFromReader creates a subfolder in the ComposeStorePath and stores a new file using the content from an io.Reader.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreStackFileFromReader(stackIdentifier, fileName string, r io.Reader) (string, error) {
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := path.Join(stackStorePath, fileName)
err = service.createFileInStore(composeFilePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, stackStorePath), nil
}
// StoreTLSFile creates a folder in the TLSStorePath and stores a new file with the content from r.
func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileType, r io.Reader) error {
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
// It returns the path to the newly created file.
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (string, error) {
storePath := path.Join(TLSStorePath, folder)
err := service.createDirectoryInStore(storePath)
if err != nil {
return err
return "", err
}
var fileName string
@ -134,15 +115,16 @@ func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileTy
case portainer.TLSFileKey:
fileName = TLSKeyFile
default:
return portainer.ErrUndefinedTLSFileType
return "", portainer.ErrUndefinedTLSFileType
}
tlsFilePath := path.Join(storePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(tlsFilePath, r)
if err != nil {
return err
return "", err
}
return nil
return path.Join(service.fileStorePath, tlsFilePath), nil
}
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
@ -204,6 +186,11 @@ func (service *Service) GetFileContent(filePath string) (string, error) {
return string(content), nil
}
// Rename renames a file or directory
func (service *Service) Rename(oldPath, newPath string) error {
return os.Rename(oldPath, newPath)
}
// WriteJSONToFile writes JSON to the specified file.
func (service *Service) WriteJSONToFile(path string, content interface{}) error {
jsonContent, err := json.Marshal(content)
@ -214,10 +201,21 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
return ioutil.WriteFile(path, jsonContent, 0644)
}
// FileExists checks for the existence of the specified file.
func (service *Service) FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// KeyPairFilesExist checks for the existence of the key files.
func (service *Service) KeyPairFilesExist() (bool, error) {
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
exists, err := fileExists(privateKeyPath)
exists, err := service.FileExists(privateKeyPath)
if err != nil {
return false, err
}
@ -226,7 +224,7 @@ func (service *Service) KeyPairFilesExist() (bool, error) {
}
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
exists, err = fileExists(publicKeyPath)
exists, err = service.FileExists(publicKeyPath)
if err != nil {
return false, err
}
@ -320,13 +318,3 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
block, _ := pem.Decode(fileContent)
return block.Bytes, nil
}
func fileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}

View File

@ -2,15 +2,68 @@ package client
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/portainer/portainer"
)
// HTTPClient represents a client to send HTTP requests.
type HTTPClient struct {
*http.Client
}
// NewHTTPClient is used to build a new HTTPClient.
func NewHTTPClient() *HTTPClient {
return &HTTPClient{
&http.Client{
Timeout: time.Second * 5,
},
}
}
// AzureAuthenticationResponse represents an Azure API authentication response.
type AzureAuthenticationResponse struct {
AccessToken string `json:"access_token"`
ExpiresOn string `json:"expires_on"`
}
// ExecuteAzureAuthenticationRequest is used to execute an authentication request
// against the Azure API. It re-uses the same http.Client.
func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portainer.AzureCredentials) (*AzureAuthenticationResponse, error) {
loginURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", credentials.TenantID)
params := url.Values{
"grant_type": {"client_credentials"},
"client_id": {credentials.ApplicationID},
"client_secret": {credentials.AuthenticationKey},
"resource": {"https://management.azure.com/"},
}
response, err := client.PostForm(loginURL, params)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
return nil, portainer.ErrAzureInvalidCredentials
}
var token AzureAuthenticationResponse
err = json.NewDecoder(response.Body).Decode(&token)
if err != nil {
return nil, err
}
return &token, nil
}
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
// using the specified host and optional TLS configuration.
// It uses a new Http.Client for each operation.
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
transport := &http.Transport{}

View File

@ -6,18 +6,36 @@ import (
"net/http"
)
// errorResponse is a generic response for sending a error.
type errorResponse struct {
Err string `json:"err,omitempty"`
}
// WriteErrorResponse writes an error message to the response and logger.
func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log.Logger) {
if logger != nil {
logger.Printf("http error: %s (code=%d)", err, code)
type (
// LoggerHandler defines a HTTP handler that includes a HandlerError return pointer
LoggerHandler func(http.ResponseWriter, *http.Request) *HandlerError
// HandlerError represents an error raised inside a HTTP handler
HandlerError struct {
StatusCode int
Message string
Err error
}
errorResponse struct {
Err string `json:"err,omitempty"`
}
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
func (handler LoggerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
err := handler(rw, r)
if err != nil {
writeErrorResponse(rw, err)
}
}
func writeErrorResponse(rw http.ResponseWriter, err *HandlerError) {
log.Printf("http error: %s (err=%s) (code=%d)\n", err.Message, err.Err, err.StatusCode)
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(err.StatusCode)
json.NewEncoder(rw).Encode(&errorResponse{Err: err.Message})
}
// WriteError is a convenience function that creates a new HandlerError before calling writeErrorResponse.
// For use outside of the standard http handlers.
func WriteError(rw http.ResponseWriter, code int, message string, err error) {
writeErrorResponse(rw, &HandlerError{code, message, err})
}

View File

@ -1,126 +0,0 @@
package handler
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"os"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
// AuthHandler represents an HTTP API handler for managing authentication.
type AuthHandler struct {
*mux.Router
Logger *log.Logger
authDisabled bool
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
SettingsService portainer.SettingsService
}
const (
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
// when the server has been started with the --no-auth flag
ErrAuthDisabled = portainer.Error("Authentication is disabled")
)
// NewAuthHandler returns a new instance of AuthHandler.
func NewAuthHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, authDisabled bool) *AuthHandler {
h := &AuthHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
authDisabled: authDisabled,
}
h.Handle("/auth",
rateLimiter.LimitAccess(bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth)))).Methods(http.MethodPost)
return h
}
type (
postAuthRequest struct {
Username string `valid:"required"`
Password string `valid:"required"`
}
postAuthResponse struct {
JWT string `json:"jwt"`
}
)
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
if handler.authDisabled {
httperror.WriteErrorResponse(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
var req postAuthRequest
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, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
return
}
var username = req.Username
var password = req.Password
u, err := handler.UserService.UserByUsername(username)
if err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusBadRequest, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
settings, err := handler.SettingsService.Settings()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP && u.ID != 1 {
err = handler.LDAPService.AuthenticateUser(username, password, &settings.LDAPSettings)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
} else {
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
return
}
}
tokenData := &portainer.TokenData{
ID: u.ID,
Username: u.Username,
Role: u.Role,
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
}

View File

@ -0,0 +1,79 @@
package auth
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type authenticatePayload struct {
Username string
Password string
}
type authenticateResponse struct {
JWT string `json:"jwt"`
}
func (payload *authenticatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Username) {
return portainer.Error("Invalid username")
}
if govalidator.IsNull(payload.Password) {
return portainer.Error("Invalid password")
}
return nil
}
func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if handler.authDisabled {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Cannot authenticate user. Portainer was started with the --no-auth flag", ErrAuthDisabled}
}
var payload authenticatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
u, err := handler.UserService.UserByUsername(payload.Username)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid credentials", ErrInvalidCredentials}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
settings, err := handler.SettingsService.Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP && u.ID != 1 {
err = handler.LDAPService.AuthenticateUser(payload.Username, payload.Password, &settings.LDAPSettings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate user via LDAP/AD", err}
}
} else {
err = handler.CryptoService.CompareHashAndData(u.Password, payload.Password)
if err != nil {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", ErrInvalidCredentials}
}
}
tokenData := &portainer.TokenData{
ID: u.ID,
Username: u.Username,
Role: u.Role,
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
}
return response.JSON(w, &authenticateResponse{JWT: token})
}

View File

@ -0,0 +1,41 @@
package auth
import (
"net/http"
"github.com/gorilla/mux"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
const (
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
// when the server has been started with the --no-auth flag
ErrAuthDisabled = portainer.Error("Authentication is disabled")
)
// Handler is the HTTP handler used to handle authentication operations.
type Handler struct {
*mux.Router
authDisabled bool
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
SettingsService portainer.SettingsService
}
// NewHandler creates a handler to manage authentication operations.
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, authDisabled bool) *Handler {
h := &Handler{
Router: mux.NewRouter(),
authDisabled: authDisabled,
}
h.Handle("/auth",
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
return h
}

View File

@ -1,92 +0,0 @@
package handler
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
TeamMembershipService portainer.TeamMembershipService
ProxyManager *proxy.Manager
}
// NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
h := &DockerHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.PathPrefix("/{id}/docker").Handler(
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
return h
}
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole {
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
}

View File

@ -1,91 +0,0 @@
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 DockerHubHandler.
func NewDockerHubHandler(bouncer *security.RequestBouncer) *DockerHubHandler {
h := &DockerHubHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/dockerhub",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetDockerHub))).Methods(http.MethodGet)
h.Handle("/dockerhub",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutDockerHub))).Methods(http.MethodPut)
return h
}
type (
putDockerHubRequest struct {
Authentication bool `valid:""`
Username string `valid:""`
Password string `valid:""`
}
)
// 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
}
dockerhub.Password = ""
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)
}
}

View File

@ -0,0 +1,19 @@
package dockerhub
import (
"net/http"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/response"
)
// GET request on /api/dockerhub
func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
dockerhub, err := handler.DockerHubService.DockerHub()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
}
hideFields(dockerhub)
return response.JSON(w, dockerhub)
}

View File

@ -0,0 +1,52 @@
package dockerhub
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type dockerhubUpdatePayload struct {
Authentication bool
Username string
Password string
}
func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error {
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
// PUT request on /api/dockerhub
func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload dockerhubUpdatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
dockerhub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
if payload.Authentication {
dockerhub.Authentication = true
dockerhub.Username = payload.Username
dockerhub.Password = payload.Password
}
err = handler.DockerHubService.UpdateDockerHub(dockerhub)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err}
}
return response.Empty(w)
}

View File

@ -0,0 +1,33 @@
package dockerhub
import (
"net/http"
"github.com/gorilla/mux"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
func hideFields(dockerHub *portainer.DockerHub) {
dockerHub.Password = ""
}
// Handler is the HTTP handler used to handle DockerHub operations.
type Handler struct {
*mux.Router
DockerHubService portainer.DockerHubService
}
// NewHandler creates a handler to manage Dockerhub operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/dockerhub",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
h.Handle("/dockerhub",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
return h
}

View File

@ -1,541 +0,0 @@
package handler
import (
"bytes"
"strings"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/http/client"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
)
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
type EndpointHandler struct {
*mux.Router
Logger *log.Logger
authorizeEndpointManagement bool
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
FileService portainer.FileService
ProxyManager *proxy.Manager
}
const (
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
// when the server has been started with the --external-endpoints flag
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
)
// NewEndpointHandler returns a new instance of EndpointHandler.
func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *EndpointHandler {
h := &EndpointHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
authorizeEndpointManagement: authorizeEndpointManagement,
}
h.Handle("/endpoints",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
h.Handle("/endpoints",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}/access",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
return h
}
type (
putEndpointAccessRequest struct {
AuthorizedUsers []int `valid:"-"`
AuthorizedTeams []int `valid:"-"`
}
putEndpointsRequest struct {
Name string `valid:"-"`
URL string `valid:"-"`
PublicURL string `valid:"-"`
GroupID int `valid:"-"`
TLS bool `valid:"-"`
TLSSkipVerify bool `valid:"-"`
TLSSkipClientVerify bool `valid:"-"`
}
postEndpointPayload struct {
name string
url string
publicURL string
groupID int
useTLS bool
skipTLSServerVerification bool
skipTLSClientVerification bool
caCert []byte
cert []byte
key []byte
}
)
// handleGetEndpoints handles GET requests on /endpoints
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
groups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
filteredEndpoints, err := security.FilterEndpoints(endpoints, groups, securityContext)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, filteredEndpoints, handler.Logger)
}
func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
if err != nil {
return nil, err
}
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.url, tlsConfig)
if err != nil {
return nil, err
}
endpointType := portainer.DockerEnvironment
if agentOnDockerEnvironment {
endpointType = portainer.AgentOnDockerEnvironment
}
endpoint := &portainer.Endpoint{
Name: payload.name,
URL: payload.url,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.groupID),
PublicURL: payload.publicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.useTLS,
TLSSkipVerify: payload.skipTLSServerVerification,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, err
}
folder := strconv.Itoa(int(endpoint.ID))
if !payload.skipTLSServerVerification {
r := bytes.NewReader(payload.caCert)
// TODO: review the API exposed by the FileService to store
// a file from a byte slice and return the path to the stored file instead
// of using multiple legacy calls (StoreTLSFile, GetPathForTLSFile) here.
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileCA, r)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return nil, err
}
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSConfig.TLSCACertPath = caCertPath
}
if !payload.skipTLSClientVerification {
r := bytes.NewReader(payload.cert)
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileCert, r)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return nil, err
}
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
r = bytes.NewReader(payload.key)
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileKey, r)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return nil, err
}
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return nil, err
}
return endpoint, nil
}
func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
endpointType := portainer.DockerEnvironment
if !strings.HasPrefix(payload.url, "unix://") {
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.url, nil)
if err != nil {
return nil, err
}
if agentOnDockerEnvironment {
endpointType = portainer.AgentOnDockerEnvironment
}
}
endpoint := &portainer.Endpoint{
Name: payload.name,
URL: payload.url,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.groupID),
PublicURL: payload.publicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
err := handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, err
}
return endpoint, nil
}
func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
if payload.useTLS {
return handler.createTLSSecuredEndpoint(payload)
}
return handler.createUnsecuredEndpoint(payload)
}
func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload, error) {
payload := &postEndpointPayload{}
payload.name = r.FormValue("Name")
payload.url = r.FormValue("URL")
payload.publicURL = r.FormValue("PublicURL")
if payload.name == "" || payload.url == "" {
return nil, ErrInvalidRequestFormat
}
rawGroupID := r.FormValue("GroupID")
if rawGroupID == "" {
payload.groupID = 1
} else {
groupID, err := strconv.Atoi(rawGroupID)
if err != nil {
return nil, err
}
payload.groupID = groupID
}
payload.useTLS = r.FormValue("TLS") == "true"
if payload.useTLS {
payload.skipTLSServerVerification = r.FormValue("TLSSkipVerify") == "true"
payload.skipTLSClientVerification = r.FormValue("TLSSkipClientVerify") == "true"
if !payload.skipTLSServerVerification {
caCert, err := getUploadedFileContent(r, "TLSCACertFile")
if err != nil {
return nil, err
}
payload.caCert = caCert
}
if !payload.skipTLSClientVerification {
cert, err := getUploadedFileContent(r, "TLSCertFile")
if err != nil {
return nil, err
}
payload.cert = cert
key, err := getUploadedFileContent(r, "TLSKeyFile")
if err != nil {
return nil, err
}
payload.key = key
}
}
return payload, nil
}
// handlePostEndpoints handles POST requests on /endpoints
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
payload, err := convertPostEndpointRequestToPayload(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.createEndpoint(payload)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &endpoint, handler.Logger)
}
// handleGetEndpoint handles GET requests on /endpoints/:id
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, endpoint, handler.Logger)
}
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointAccessRequest
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
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range req.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpoint.AuthorizedUsers = authorizedUserIDs
}
if req.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range req.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpoint.AuthorizedTeams = authorizedTeamIDs
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointsRequest
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
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Name != "" {
endpoint.Name = req.Name
}
if req.URL != "" {
endpoint.URL = req.URL
}
if req.PublicURL != "" {
endpoint.PublicURL = req.PublicURL
}
if req.GroupID != 0 {
endpoint.GroupID = portainer.EndpointGroupID(req.GroupID)
}
folder := strconv.Itoa(int(endpoint.ID))
if req.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = req.TLSSkipVerify
if !req.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSConfig.TLSCACertPath = caCertPath
} else {
endpoint.TLSConfig.TLSCACertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
}
if !req.TLSSkipClientVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
} else {
endpoint.TLSConfig.TLSCertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSKeyPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
}
} else {
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(folder)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
vars := mux.Vars(r)
id := vars["id"]
endpointID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
handler.ProxyManager.DeleteProxy(string(endpointID))
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if endpoint.TLSConfig.TLS {
err = handler.FileService.DeleteTLSFiles(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
}

View File

@ -1,364 +0,0 @@
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"
)
// EndpointGroupHandler represents an HTTP API handler for managing endpoint groups.
type EndpointGroupHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
}
// NewEndpointGroupHandler returns a new instance of EndpointGroupHandler.
func NewEndpointGroupHandler(bouncer *security.RequestBouncer) *EndpointGroupHandler {
h := &EndpointGroupHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/endpoint_groups",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpointGroups))).Methods(http.MethodPost)
h.Handle("/endpoint_groups",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpointGroups))).Methods(http.MethodGet)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpointGroup))).Methods(http.MethodGet)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointGroup))).Methods(http.MethodPut)
h.Handle("/endpoint_groups/{id}/access",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointGroupAccess))).Methods(http.MethodPut)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpointGroup))).Methods(http.MethodDelete)
return h
}
type (
postEndpointGroupsResponse struct {
ID int `json:"Id"`
}
postEndpointGroupsRequest struct {
Name string `valid:"required"`
Description string `valid:"-"`
Labels []portainer.Pair `valid:""`
AssociatedEndpoints []portainer.EndpointID `valid:""`
}
putEndpointGroupAccessRequest struct {
AuthorizedUsers []int `valid:"-"`
AuthorizedTeams []int `valid:"-"`
}
putEndpointGroupsRequest struct {
Name string `valid:"-"`
Description string `valid:"-"`
Labels []portainer.Pair `valid:""`
AssociatedEndpoints []portainer.EndpointID `valid:""`
}
)
// handleGetEndpointGroups handles GET requests on /endpoint_groups
func (handler *EndpointGroupHandler) handleGetEndpointGroups(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
filteredEndpointGroups, err := security.FilterEndpointGroups(endpointGroups, securityContext)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, filteredEndpointGroups, handler.Logger)
}
// handlePostEndpointGroups handles POST requests on /endpoint_groups
func (handler *EndpointGroupHandler) handlePostEndpointGroups(w http.ResponseWriter, r *http.Request) {
var req postEndpointGroupsRequest
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
}
endpointGroup := &portainer.EndpointGroup{
Name: req.Name,
Description: req.Description,
Labels: req.Labels,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
}
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
for _, endpoint := range endpoints {
if endpoint.GroupID == portainer.EndpointGroupID(1) {
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, req.AssociatedEndpoints)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
}
encodeJSON(w, &postEndpointGroupsResponse{ID: int(endpointGroup.ID)}, handler.Logger)
}
// handleGetEndpointGroup handles GET requests on /endpoint_groups/:id
func (handler *EndpointGroupHandler) handleGetEndpointGroup(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointGroupID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrEndpointGroupNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, endpointGroup, handler.Logger)
}
// handlePutEndpointGroupAccess handles PUT requests on /endpoint_groups/:id/access
func (handler *EndpointGroupHandler) handlePutEndpointGroupAccess(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointGroupID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointGroupAccessRequest
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
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrEndpointGroupNotFound {
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))
}
endpointGroup.AuthorizedUsers = authorizedUserIDs
}
if req.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range req.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpointGroup.AuthorizedTeams = authorizedTeamIDs
}
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handlePutEndpointGroup handles PUT requests on /endpoint_groups/:id
func (handler *EndpointGroupHandler) handlePutEndpointGroup(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointGroupID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putEndpointGroupsRequest
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
}
groupID := portainer.EndpointGroupID(endpointGroupID)
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(groupID)
if err == portainer.ErrEndpointGroupNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Name != "" {
endpointGroup.Name = req.Name
}
if req.Description != "" {
endpointGroup.Description = req.Description
}
endpointGroup.Labels = req.Labels
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
for _, endpoint := range endpoints {
err = handler.updateEndpointGroup(endpoint, groupID, req.AssociatedEndpoints)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
}
func (handler *EndpointGroupHandler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
if endpoint.GroupID == groupID {
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
}
return nil
}
func (handler *EndpointGroupHandler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
for _, id := range associatedEndpoints {
if id == endpoint.ID {
return nil
}
}
endpoint.GroupID = portainer.EndpointGroupID(1)
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
}
func (handler *EndpointGroupHandler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
for _, id := range associatedEndpoints {
if id == endpoint.ID {
endpoint.GroupID = groupID
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
}
}
return nil
}
// handleDeleteEndpointGroup handles DELETE requests on /endpoint_groups/:id
func (handler *EndpointGroupHandler) handleDeleteEndpointGroup(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
endpointGroupID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
if endpointGroupID == 1 {
httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveDefaultGroup, http.StatusForbidden, handler.Logger)
return
}
groupID := portainer.EndpointGroupID(endpointGroupID)
_, err = handler.EndpointGroupService.EndpointGroup(groupID)
if err == portainer.ErrEndpointGroupNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.EndpointGroupService.DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
for _, endpoint := range endpoints {
if endpoint.GroupID == groupID {
endpoint.GroupID = portainer.EndpointGroupID(1)
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
}
}

View File

@ -0,0 +1,66 @@
package endpointgroups
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointGroupCreatePayload struct {
Name string
Description string
AssociatedEndpoints []portainer.EndpointID
Tags []string
}
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid endpoint group name")
}
if payload.Tags == nil {
payload.Tags = []string{}
}
return nil
}
// POST request on /api/endpoint_groups
func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload endpointGroupCreatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpointGroup := &portainer.EndpointGroup{
Name: payload.Name,
Description: payload.Description,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Tags: payload.Tags,
}
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the endpoint group inside the database", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
if endpoint.GroupID == portainer.EndpointGroupID(1) {
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, payload.AssociatedEndpoints)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
}
return response.JSON(w, endpointGroup)
}

View File

@ -0,0 +1,51 @@
package endpointgroups
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// DELETE request on /api/endpoint_groups/:id
func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
}
if endpointGroupID == 1 {
return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", portainer.ErrCannotRemoveDefaultGroup}
}
_, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
}
err = handler.EndpointGroupService.DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the endpoint group from the database", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
endpoint.GroupID = portainer.EndpointGroupID(1)
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
}
return response.Empty(w)
}

View File

@ -0,0 +1,27 @@
package endpointgroups
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// GET request on /api/endpoint_groups/:id
func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
}
return response.JSON(w, endpointGroup)
}

View File

@ -0,0 +1,25 @@
package endpointgroups
import (
"net/http"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/response"
"github.com/portainer/portainer/http/security"
)
// GET request on /api/endpoint_groups
func (handler *Handler) endpointGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
endpointGroups = security.FilterEndpointGroups(endpointGroups, securityContext)
return response.JSON(w, endpointGroups)
}

View File

@ -0,0 +1,73 @@
package endpointgroups
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointGroupUpdatePayload struct {
Name string
Description string
AssociatedEndpoints []portainer.EndpointID
Tags []string
}
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoint_groups/:id
func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
}
var payload endpointGroupUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
}
if payload.Name != "" {
endpointGroup.Name = payload.Name
}
if payload.Description != "" {
endpointGroup.Description = payload.Description
}
if payload.Tags != nil {
endpointGroup.Tags = payload.Tags
}
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
return response.JSON(w, endpointGroup)
}

View File

@ -0,0 +1,63 @@
package endpointgroups
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointGroupUpdateAccessPayload struct {
AuthorizedUsers []int
AuthorizedTeams []int
}
func (payload *endpointGroupUpdateAccessPayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoint_groups/:id/access
func (handler *Handler) endpointGroupUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
}
var payload endpointGroupUpdateAccessPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
}
if payload.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range payload.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpointGroup.AuthorizedUsers = authorizedUserIDs
}
if payload.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range payload.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpointGroup.AuthorizedTeams = authorizedTeamIDs
}
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
}
return response.JSON(w, endpointGroup)
}

View File

@ -0,0 +1,69 @@
package endpointgroups
import (
"net/http"
"github.com/gorilla/mux"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/endpoint_groups",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
h.Handle("/endpoint_groups",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
h.Handle("/endpoint_groups/{id}/access",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdateAccess))).Methods(http.MethodPut)
h.Handle("/endpoint_groups/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
return h
}
func (handler *Handler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
for _, id := range associatedEndpoints {
if id == endpoint.ID {
return nil
}
}
endpoint.GroupID = portainer.EndpointGroupID(1)
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
}
func (handler *Handler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
for _, id := range associatedEndpoints {
if id == endpoint.ID {
endpoint.GroupID = groupID
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
}
}
return nil
}
func (handler *Handler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
if endpoint.GroupID == groupID {
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
}
return nil
}

View File

@ -0,0 +1,32 @@
package endpointproxy
import (
"github.com/gorilla/mux"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
)
// Handler is the HTTP handler used to proxy requests to external APIs.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
EndpointService portainer.EndpointService
ProxyManager *proxy.Manager
}
// NewHandler creates a handler to proxy requests to external APIs.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.PathPrefix("/{id}/azure").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
h.PathPrefix("/{id}/docker").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
h.PathPrefix("/{id}/extensions/storidge").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
return h
}

View File

@ -0,0 +1,43 @@
package endpointproxy
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"net/http"
)
func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.EndpointAccess(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
}
}
id := strconv.Itoa(endpointID)
http.StripPrefix("/"+id+"/azure", proxy).ServeHTTP(w, r)
return nil
}

View File

@ -0,0 +1,43 @@
package endpointproxy
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"net/http"
)
func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.EndpointAccess(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(string(endpointID))
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
}
}
id := strconv.Itoa(endpointID)
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
return nil
}

View File

@ -0,0 +1,56 @@
package endpointproxy
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"net/http"
)
func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.EndpointAccess(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
}
var storidgeExtension *portainer.EndpointExtension
for _, extension := range endpoint.Extensions {
if extension.Type == portainer.StoridgeEndpointExtension {
storidgeExtension = &extension
}
}
if storidgeExtension == nil {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", portainer.ErrEndpointExtensionNotSupported}
}
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
var proxy http.Handler
proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy", err}
}
}
id := strconv.Itoa(endpointID)
http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r)
return nil
}

View File

@ -0,0 +1,306 @@
package endpoints
import (
"net/http"
"strconv"
"strings"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/http/client"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointCreatePayload struct {
Name string
URL string
EndpointType int
PublicURL string
GroupID int
TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
TLSCACertFile []byte
TLSCertFile []byte
TLSKeyFile []byte
AzureApplicationID string
AzureTenantID string
AzureAuthenticationKey string
Tags []string
}
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
if err != nil {
return portainer.Error("Invalid stack name")
}
payload.Name = name
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
if err != nil || endpointType == 0 {
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 3 (Azure environment)")
}
payload.EndpointType = endpointType
groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true)
if groupID == 0 {
groupID = 1
}
payload.GroupID = groupID
var tags []string
err = request.RetrieveMultiPartFormJSONValue(r, "Tags", &tags, true)
if err != nil {
return portainer.Error("Invalid Tags parameter")
}
payload.Tags = tags
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
payload.TLS = useTLS
if payload.TLS {
skipTLSServerVerification, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLSSkipVerify", true)
payload.TLSSkipVerify = skipTLSServerVerification
skipTLSClientVerification, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLSSkipClientVerify", true)
payload.TLSSkipClientVerify = skipTLSClientVerification
if !payload.TLSSkipVerify {
caCert, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
if err != nil {
return portainer.Error("Invalid CA certificate file. Ensure that the file is uploaded correctly")
}
payload.TLSCACertFile = caCert
}
if !payload.TLSSkipClientVerify {
cert, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
if err != nil {
return portainer.Error("Invalid certificate file. Ensure that the file is uploaded correctly")
}
payload.TLSCertFile = cert
key, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
if err != nil {
return portainer.Error("Invalid key file. Ensure that the file is uploaded correctly")
}
payload.TLSKeyFile = key
}
}
switch portainer.EndpointType(payload.EndpointType) {
case portainer.AzureEnvironment:
azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false)
if err != nil {
return portainer.Error("Invalid Azure application ID")
}
payload.AzureApplicationID = azureApplicationID
azureTenantID, err := request.RetrieveMultiPartFormValue(r, "AzureTenantID", false)
if err != nil {
return portainer.Error("Invalid Azure tenant ID")
}
payload.AzureTenantID = azureTenantID
azureAuthenticationKey, err := request.RetrieveMultiPartFormValue(r, "AzureAuthenticationKey", false)
if err != nil {
return portainer.Error("Invalid Azure authentication key")
}
payload.AzureAuthenticationKey = azureAuthenticationKey
default:
url, err := request.RetrieveMultiPartFormValue(r, "URL", false)
if err != nil {
return portainer.Error("Invalid endpoint URL")
}
payload.URL = url
publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true)
payload.PublicURL = publicURL
}
return nil
}
// POST request on /api/endpoints
func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
payload := &endpointCreatePayload{}
err := payload.Validate(r)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, endpointCreationError := handler.createEndpoint(payload)
if endpointCreationError != nil {
return endpointCreationError
}
return response.JSON(w, endpoint)
}
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
return handler.createAzureEndpoint(payload)
}
if payload.TLS {
return handler.createTLSSecuredEndpoint(payload)
}
return handler.createUnsecuredEndpoint(payload)
}
func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
credentials := portainer.AzureCredentials{
ApplicationID: payload.AzureApplicationID,
TenantID: payload.AzureTenantID,
AuthenticationKey: payload.AzureAuthenticationKey,
}
httpClient := client.NewHTTPClient()
_, err := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", err}
}
endpoint := &portainer.Endpoint{
Name: payload.Name,
URL: payload.URL,
Type: portainer.AzureEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
AzureCredentials: credentials,
Tags: payload.Tags,
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
}
return endpoint, nil
}
func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
endpointType := portainer.DockerEnvironment
if !strings.HasPrefix(payload.URL, "unix://") {
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, nil)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err}
}
if agentOnDockerEnvironment {
endpointType = portainer.AgentOnDockerEnvironment
}
}
endpoint := &portainer.Endpoint{
Name: payload.Name,
URL: payload.URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
}
err := handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
}
return endpoint, nil
}
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipClientVerify, payload.TLSSkipVerify)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to create TLS configuration", err}
}
agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, tlsConfig)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err}
}
endpointType := portainer.DockerEnvironment
if agentOnDockerEnvironment {
endpointType = portainer.AgentOnDockerEnvironment
}
endpoint := &portainer.Endpoint{
Name: payload.Name,
URL: payload.URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
}
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
}
filesystemError := handler.storeTLSFiles(endpoint, payload)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return nil, filesystemError
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return endpoint, nil
}
func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *endpointCreatePayload) *httperror.HandlerError {
folder := strconv.Itoa(int(endpoint.ID))
if !payload.TLSSkipVerify {
caCertPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileCA, payload.TLSCACertFile)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS CA certificate file on disk", err}
}
endpoint.TLSConfig.TLSCACertPath = caCertPath
}
if !payload.TLSSkipClientVerify {
certPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileCert, payload.TLSCertFile)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS certificate file on disk", err}
}
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, err := handler.FileService.StoreTLSFileFromBytes(folder, portainer.TLSFileKey, payload.TLSKeyFile)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS key file on disk", err}
}
endpoint.TLSConfig.TLSKeyPath = keyPath
}
return nil
}

View File

@ -0,0 +1,48 @@
package endpoints
import (
"net/http"
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// DELETE request on /api/endpoints/:id
func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if endpoint.TLSConfig.TLS {
folder := strconv.Itoa(endpointID)
err = handler.FileService.DeleteTLSFiles(folder)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
}
}
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
}
handler.ProxyManager.DeleteProxy(string(endpointID))
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
return response.Empty(w)
}

View File

@ -0,0 +1,73 @@
package endpoints
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointExtensionAddPayload struct {
Type int
URL string
}
func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error {
if payload.Type != 1 {
return portainer.Error("Invalid type value. Value must be one of: 1 (Storidge)")
}
if payload.Type == 1 && govalidator.IsNull(payload.URL) {
return portainer.Error("Invalid extension URL")
}
return nil
}
// POST request on /api/endpoints/:id/extensions
func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
var payload endpointExtensionAddPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
extensionType := portainer.EndpointExtensionType(payload.Type)
var extension *portainer.EndpointExtension
for _, ext := range endpoint.Extensions {
if ext.Type == extensionType {
extension = &ext
}
}
if extension != nil {
extension.URL = payload.URL
} else {
extension = &portainer.EndpointExtension{
Type: extensionType,
URL: payload.URL,
}
endpoint.Extensions = append(endpoint.Extensions, *extension)
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return response.JSON(w, extension)
}

View File

@ -0,0 +1,43 @@
package endpoints
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// DELETE request on /api/endpoints/:id/extensions/:extensionType
func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
extensionType, err := request.RetrieveNumericRouteVariableValue(r, "extensionType")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension type route variable", err}
}
for idx, ext := range endpoint.Extensions {
if ext.Type == portainer.EndpointExtensionType(extensionType) {
endpoint.Extensions = append(endpoint.Extensions[:idx], endpoint.Extensions[idx+1:]...)
}
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return response.Empty(w)
}

View File

@ -0,0 +1,27 @@
package endpoints
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// GET request on /api/endpoints/:id
func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
return response.JSON(w, endpoint)
}

View File

@ -0,0 +1,34 @@
package endpoints
import (
"net/http"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/response"
"github.com/portainer/portainer/http/security"
)
// GET request on /api/endpoints
func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
for _, endpoint := range filteredEndpoints {
hideFields(&endpoint)
}
return response.JSON(w, filteredEndpoints)
}

View File

@ -0,0 +1,142 @@
package endpoints
import (
"net/http"
"strconv"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/client"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointUpdatePayload struct {
Name string
URL string
PublicURL string
GroupID int
TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
AzureApplicationID string
AzureTenantID string
AzureAuthenticationKey string
Tags []string
}
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoints/:id
func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
var payload endpointUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if payload.Name != "" {
endpoint.Name = payload.Name
}
if payload.URL != "" {
endpoint.URL = payload.URL
}
if payload.PublicURL != "" {
endpoint.PublicURL = payload.PublicURL
}
if payload.GroupID != 0 {
endpoint.GroupID = portainer.EndpointGroupID(payload.GroupID)
}
if payload.Tags != nil {
endpoint.Tags = payload.Tags
}
if endpoint.Type == portainer.AzureEnvironment {
credentials := endpoint.AzureCredentials
if payload.AzureApplicationID != "" {
credentials.ApplicationID = payload.AzureApplicationID
}
if payload.AzureTenantID != "" {
credentials.TenantID = payload.AzureTenantID
}
if payload.AzureAuthenticationKey != "" {
credentials.AuthenticationKey = payload.AzureAuthenticationKey
}
httpClient := client.NewHTTPClient()
_, authErr := httpClient.ExecuteAzureAuthenticationRequest(&credentials)
if authErr != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", authErr}
}
endpoint.AzureCredentials = credentials
}
folder := strconv.Itoa(endpointID)
if payload.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = payload.TLSSkipVerify
if !payload.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSConfig.TLSCACertPath = caCertPath
} else {
endpoint.TLSConfig.TLSCACertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
}
if !payload.TLSSkipClientVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
} else {
endpoint.TLSConfig.TLSCertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSKeyPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
}
} else {
endpoint.TLSConfig.TLS = false
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(folder)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
}
}
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return response.JSON(w, endpoint)
}

View File

@ -0,0 +1,67 @@
package endpoints
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type endpointUpdateAccessPayload struct {
AuthorizedUsers []int
AuthorizedTeams []int
}
func (payload *endpointUpdateAccessPayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoints/:id/access
func (handler *Handler) endpointUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
if !handler.authorizeEndpointManagement {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
}
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
var payload endpointUpdateAccessPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if payload.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range payload.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpoint.AuthorizedUsers = authorizedUserIDs
}
if payload.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range payload.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
endpoint.AuthorizedTeams = authorizedTeamIDs
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
}
return response.JSON(w, endpoint)
}

View File

@ -0,0 +1,59 @@
package endpoints
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"net/http"
"github.com/gorilla/mux"
)
const (
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
// when the server has been started with the --external-endpoints flag
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
)
func hideFields(endpoint *portainer.Endpoint) {
endpoint.AzureCredentials = portainer.AzureCredentials{}
}
// Handler is the HTTP handler used to handle endpoint operations.
type Handler struct {
*mux.Router
authorizeEndpointManagement bool
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
FileService portainer.FileService
ProxyManager *proxy.Manager
}
// NewHandler creates a handler to manage endpoint operations.
func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *Handler {
h := &Handler{
Router: mux.NewRouter(),
authorizeEndpointManagement: authorizeEndpointManagement,
}
h.Handle("/endpoints",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
h.Handle("/endpoints",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}/access",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdateAccess))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/extensions",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/extensions/{extensionType}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
return h
}

View File

@ -1,143 +0,0 @@
package handler
import (
"encoding/json"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// ExtensionHandler represents an HTTP API handler for managing Settings.
type ExtensionHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
ProxyManager *proxy.Manager
}
// NewExtensionHandler returns a new instance of ExtensionHandler.
func NewExtensionHandler(bouncer *security.RequestBouncer) *ExtensionHandler {
h := &ExtensionHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/{endpointId}/extensions",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostExtensions))).Methods(http.MethodPost)
h.Handle("/{endpointId}/extensions/{extensionType}",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleDeleteExtensions))).Methods(http.MethodDelete)
return h
}
type (
postExtensionRequest struct {
Type int `valid:"required"`
URL string `valid:"required"`
}
)
func (handler *ExtensionHandler) handlePostExtensions(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["endpointId"])
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(id)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
var req postExtensionRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
extensionType := portainer.EndpointExtensionType(req.Type)
var extension *portainer.EndpointExtension
for _, ext := range endpoint.Extensions {
if ext.Type == extensionType {
extension = &ext
}
}
if extension != nil {
extension.URL = req.URL
} else {
extension = &portainer.EndpointExtension{
Type: extensionType,
URL: req.URL,
}
endpoint.Extensions = append(endpoint.Extensions, *extension)
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, extension, handler.Logger)
}
func (handler *ExtensionHandler) handleDeleteExtensions(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["endpointId"])
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(id)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err == portainer.ErrEndpointNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
extType, err := strconv.Atoi(vars["extensionType"])
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
extensionType := portainer.EndpointExtensionType(extType)
for idx, ext := range endpoint.Extensions {
if ext.Type == extensionType {
endpoint.Extensions = append(endpoint.Extensions[:idx], endpoint.Extensions[idx+1:]...)
}
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}

View File

@ -1,106 +0,0 @@
package extensions
import (
"strconv"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// StoridgeHandler represents an HTTP API handler for proxying requests to the Docker API.
type StoridgeHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
TeamMembershipService portainer.TeamMembershipService
ProxyManager *proxy.Manager
}
// NewStoridgeHandler returns a new instance of StoridgeHandler.
func NewStoridgeHandler(bouncer *security.RequestBouncer) *StoridgeHandler {
h := &StoridgeHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.PathPrefix("/{id}/extensions/storidge").Handler(
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToStoridgeAPI)))
return h
}
func (handler *StoridgeHandler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.Role != portainer.AdministratorRole {
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
}
var storidgeExtension *portainer.EndpointExtension
for _, extension := range endpoint.Extensions {
if extension.Type == portainer.StoridgeEndpointExtension {
storidgeExtension = &extension
}
}
if storidgeExtension == nil {
httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionNotSupported, http.StatusInternalServerError, handler.Logger)
return
}
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
var proxy http.Handler
proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r)
}

View File

@ -1,24 +1,19 @@
package handler
package file
import (
"os"
"log"
"net/http"
"strings"
)
// FileHandler represents an HTTP API handler for managing static files.
type FileHandler struct {
// Handler represents an HTTP API handler for managing static files.
type Handler struct {
http.Handler
Logger *log.Logger
}
// NewFileHandler returns a new instance of FileHandler.
func NewFileHandler(assetPublicPath string) *FileHandler {
h := &FileHandler{
// NewHandler creates a handler to serve static files.
func NewHandler(assetPublicPath string) *Handler {
h := &Handler{
Handler: http.FileServer(http.Dir(assetPublicPath)),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
return h
}
@ -32,7 +27,7 @@ func isHTML(acceptContent []string) bool {
return false
}
func (handler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !isHTML(r.Header["Accept"]) {
w.Header().Set("Cache-Control", "max-age=31536000")
} else {

View File

@ -1,52 +1,54 @@
package handler
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/handler/extensions"
"github.com/portainer/portainer/http/handler/auth"
"github.com/portainer/portainer/http/handler/dockerhub"
"github.com/portainer/portainer/http/handler/endpointgroups"
"github.com/portainer/portainer/http/handler/endpointproxy"
"github.com/portainer/portainer/http/handler/endpoints"
"github.com/portainer/portainer/http/handler/file"
"github.com/portainer/portainer/http/handler/registries"
"github.com/portainer/portainer/http/handler/resourcecontrols"
"github.com/portainer/portainer/http/handler/settings"
"github.com/portainer/portainer/http/handler/stacks"
"github.com/portainer/portainer/http/handler/status"
"github.com/portainer/portainer/http/handler/tags"
"github.com/portainer/portainer/http/handler/teammemberships"
"github.com/portainer/portainer/http/handler/teams"
"github.com/portainer/portainer/http/handler/templates"
"github.com/portainer/portainer/http/handler/upload"
"github.com/portainer/portainer/http/handler/users"
"github.com/portainer/portainer/http/handler/websocket"
)
// Handler is a collection of all the service handlers.
type Handler struct {
AuthHandler *AuthHandler
UserHandler *UserHandler
TeamHandler *TeamHandler
TeamMembershipHandler *TeamMembershipHandler
EndpointHandler *EndpointHandler
EndpointGroupHandler *EndpointGroupHandler
RegistryHandler *RegistryHandler
DockerHubHandler *DockerHubHandler
ExtensionHandler *ExtensionHandler
StoridgeHandler *extensions.StoridgeHandler
ResourceHandler *ResourceHandler
StackHandler *StackHandler
StatusHandler *StatusHandler
SettingsHandler *SettingsHandler
TemplatesHandler *TemplatesHandler
DockerHandler *DockerHandler
WebSocketHandler *WebSocketHandler
UploadHandler *UploadHandler
FileHandler *FileHandler
}
AuthHandler *auth.Handler
const (
// ErrInvalidJSON defines an error raised the app is unable to parse request data
ErrInvalidJSON = portainer.Error("Invalid JSON")
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
)
DockerHubHandler *dockerhub.Handler
EndpointGroupHandler *endpointgroups.Handler
EndpointHandler *endpoints.Handler
EndpointProxyHandler *endpointproxy.Handler
FileHandler *file.Handler
RegistryHandler *registries.Handler
ResourceControlHandler *resourcecontrols.Handler
SettingsHandler *settings.Handler
StackHandler *stacks.Handler
StatusHandler *status.Handler
TagHandler *tags.Handler
TeamMembershipHandler *teammemberships.Handler
TeamHandler *teams.Handler
TemplatesHandler *templates.Handler
UploadHandler *upload.Handler
UserHandler *users.Handler
WebSocketHandler *websocket.Handler
}
// ServeHTTP delegates a request to the appropriate subhandler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, "/api/auth"):
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
@ -57,24 +59,26 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
switch {
case strings.Contains(r.URL.Path, "/docker/"):
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/stacks"):
http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r)
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/extensions/storidge"):
http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/extensions"):
http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r)
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/azure/"):
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
}
case strings.HasPrefix(r.URL.Path, "/api/registries"):
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/resource_controls"):
http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r)
http.StripPrefix("/api", h.ResourceControlHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/settings"):
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/stacks"):
http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/status"):
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/tags"):
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/templates"):
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/upload"):
@ -91,27 +95,3 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.FileHandler.ServeHTTP(w, r)
}
}
// encodeJSON encodes v to w in JSON format. WriteErrorResponse() is called if encoding fails.
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(v); err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, logger)
}
}
// getUploadedFileContent retrieve the content of a file uploaded in the request.
// Uses requestParameter as the key to retrieve the file in the request payload.
func getUploadedFileContent(request *http.Request, requestParameter string) ([]byte, error) {
file, _, err := request.FormFile(requestParameter)
if err != nil {
return nil, err
}
defer file.Close()
fileContent, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
return fileContent, nil
}

View File

@ -0,0 +1,43 @@
package registries
import (
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"net/http"
"github.com/gorilla/mux"
)
func hideFields(registry *portainer.Registry) {
registry.Password = ""
}
// Handler is the HTTP handler used to handle registry operations.
type Handler struct {
*mux.Router
RegistryService portainer.RegistryService
}
// NewHandler creates a handler to manage registry operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/registries",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost)
h.Handle("/registries",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
h.Handle("/registries/{id}/access",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdateAccess))).Methods(http.MethodPut)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
return h
}

View File

@ -0,0 +1,68 @@
package registries
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type registryCreatePayload struct {
Name string
URL string
Authentication bool
Username string
Password string
}
func (payload *registryCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid registry name")
}
if govalidator.IsNull(payload.URL) {
return portainer.Error("Invalid registry URL")
}
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload registryCreatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
registries, err := handler.RegistryService.Registries()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
for _, r := range registries {
if r.URL == payload.URL {
return &httperror.HandlerError{http.StatusConflict, "A registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
}
}
registry := &portainer.Registry{
Name: payload.Name,
URL: payload.URL,
Authentication: payload.Authentication,
Username: payload.Username,
Password: payload.Password,
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
}
err = handler.RegistryService.CreateRegistry(registry)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the registry inside the database", err}
}
hideFields(registry)
return response.JSON(w, registry)
}

View File

@ -0,0 +1,32 @@
package registries
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// DELETE request on /api/registries/:id
func (handler *Handler) registryDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
}
_, err = handler.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
err = handler.RegistryService.DeleteRegistry(portainer.RegistryID(registryID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the registry from the database", err}
}
return response.Empty(w)
}

View File

@ -0,0 +1,28 @@
package registries
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
// GET request on /api/registries/:id
func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
}
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
hideFields(registry)
return response.JSON(w, registry)
}

View File

@ -0,0 +1,29 @@
package registries
import (
"net/http"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/response"
"github.com/portainer/portainer/http/security"
)
// GET request on /api/registries
func (handler *Handler) registryList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
registries, err := handler.RegistryService.Registries()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
filteredRegistries := security.FilterRegistries(registries, securityContext)
for _, registry := range filteredRegistries {
hideFields(&registry)
}
return response.JSON(w, registries)
}

View File

@ -0,0 +1,82 @@
package registries
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type registryUpdatePayload struct {
Name string
URL string
Authentication bool
Username string
Password string
}
func (payload *registryUpdatePayload) Validate(r *http.Request) error {
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
}
return nil
}
// PUT request on /api/registries/:id
func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
}
var payload registryUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
registries, err := handler.RegistryService.Registries()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
for _, r := range registries {
if r.URL == payload.URL && r.ID != registry.ID {
return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
}
}
if payload.Name != "" {
registry.Name = payload.Name
}
if payload.URL != "" {
registry.URL = payload.URL
}
if payload.Authentication {
registry.Authentication = true
registry.Username = payload.Username
registry.Password = payload.Password
} else {
registry.Authentication = false
registry.Username = ""
registry.Password = ""
}
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}
}
return response.JSON(w, registry)
}

View File

@ -0,0 +1,63 @@
package registries
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
)
type registryUpdateAccessPayload struct {
AuthorizedUsers []int
AuthorizedTeams []int
}
func (payload *registryUpdateAccessPayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/registries/:id/access
func (handler *Handler) registryUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
}
var payload registryUpdateAccessPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
if payload.AuthorizedUsers != nil {
authorizedUserIDs := []portainer.UserID{}
for _, value := range payload.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
registry.AuthorizedUsers = authorizedUserIDs
}
if payload.AuthorizedTeams != nil {
authorizedTeamIDs := []portainer.TeamID{}
for _, value := range payload.AuthorizedTeams {
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
}
registry.AuthorizedTeams = authorizedTeamIDs
}
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}
}
return response.JSON(w, registry)
}

View File

@ -1,320 +0,0 @@
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
}
type (
postRegistriesRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
Authentication bool `valid:""`
Username string `valid:""`
Password string `valid:""`
}
postRegistriesResponse struct {
ID int `json:"Id"`
}
putRegistryAccessRequest struct {
AuthorizedUsers []int `valid:"-"`
AuthorizedTeams []int `valid:"-"`
}
putRegistriesRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
Authentication bool `valid:""`
Username string `valid:""`
Password string `valid:""`
}
)
// 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
}
for i := range filteredRegistries {
filteredRegistries[i].Password = ""
}
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)
}
// 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
}
registry.Password = ""
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
}
}
// 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
}
}
// 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

@ -1,266 +0,0 @@
package handler
import (
"encoding/json"
"strconv"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// ResourceHandler represents an HTTP API handler for managing resource controls.
type ResourceHandler struct {
*mux.Router
Logger *log.Logger
ResourceControlService portainer.ResourceControlService
}
// NewResourceHandler returns a new instance of ResourceHandler.
func NewResourceHandler(bouncer *security.RequestBouncer) *ResourceHandler {
h := &ResourceHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/resource_controls",
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostResources))).Methods(http.MethodPost)
h.Handle("/resource_controls/{id}",
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutResources))).Methods(http.MethodPut)
h.Handle("/resource_controls/{id}",
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteResources))).Methods(http.MethodDelete)
return h
}
type (
postResourcesRequest struct {
ResourceID string `valid:"required"`
Type string `valid:"required"`
AdministratorsOnly bool `valid:"-"`
Users []int `valid:"-"`
Teams []int `valid:"-"`
SubResourceIDs []string `valid:"-"`
}
putResourcesRequest struct {
AdministratorsOnly bool `valid:"-"`
Users []int `valid:"-"`
Teams []int `valid:"-"`
}
)
// handlePostResources handles POST requests on /resources
func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *http.Request) {
var req postResourcesRequest
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
}
var resourceControlType portainer.ResourceControlType
switch req.Type {
case "container":
resourceControlType = portainer.ContainerResourceControl
case "service":
resourceControlType = portainer.ServiceResourceControl
case "volume":
resourceControlType = portainer.VolumeResourceControl
case "network":
resourceControlType = portainer.NetworkResourceControl
case "secret":
resourceControlType = portainer.SecretResourceControl
case "stack":
resourceControlType = portainer.StackResourceControl
case "config":
resourceControlType = portainer.ConfigResourceControl
default:
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
return
}
if len(req.Users) == 0 && len(req.Teams) == 0 && !req.AdministratorsOnly {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
rc, err := handler.ResourceControlService.ResourceControlByResourceID(req.ResourceID)
if err != nil && err != portainer.ErrResourceControlNotFound {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if rc != nil {
httperror.WriteErrorResponse(w, portainer.ErrResourceControlAlreadyExists, http.StatusConflict, handler.Logger)
return
}
var userAccesses = make([]portainer.UserResourceAccess, 0)
for _, v := range req.Users {
userAccess := portainer.UserResourceAccess{
UserID: portainer.UserID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
userAccesses = append(userAccesses, userAccess)
}
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
for _, v := range req.Teams {
teamAccess := portainer.TeamResourceAccess{
TeamID: portainer.TeamID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
teamAccesses = append(teamAccesses, teamAccess)
}
resourceControl := portainer.ResourceControl{
ResourceID: req.ResourceID,
SubResourceIDs: req.SubResourceIDs,
Type: resourceControlType,
AdministratorsOnly: req.AdministratorsOnly,
UserAccesses: userAccesses,
TeamAccesses: teamAccesses,
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
return
}
// handlePutResources handles PUT requests on /resources/:id
func (handler *ResourceHandler) handlePutResources(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
resourceControlID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req putResourcesRequest
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
}
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
if err == portainer.ErrResourceControlNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
resourceControl.AdministratorsOnly = req.AdministratorsOnly
var userAccesses = make([]portainer.UserResourceAccess, 0)
for _, v := range req.Users {
userAccess := portainer.UserResourceAccess{
UserID: portainer.UserID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
userAccesses = append(userAccesses, userAccess)
}
resourceControl.UserAccesses = userAccesses
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
for _, v := range req.Teams {
teamAccess := portainer.TeamResourceAccess{
TeamID: portainer.TeamID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
teamAccesses = append(teamAccesses, teamAccess)
}
resourceControl.TeamAccesses = teamAccesses
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedResourceControlUpdate(resourceControl, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.ResourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handleDeleteResources handles DELETE requests on /resources/:id
func (handler *ResourceHandler) handleDeleteResources(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
resourceControlID, err := strconv.Atoi(id)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
if err == portainer.ErrResourceControlNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}

View File

@ -0,0 +1,31 @@
package resourcecontrols
import (
"net/http"
"github.com/gorilla/mux"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
// Handler is the HTTP handler used to handle resource control operations.
type Handler struct {
*mux.Router
ResourceControlService portainer.ResourceControlService
}
// NewHandler creates a handler to manage resource control operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/resource_controls",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
h.Handle("/resource_controls/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
h.Handle("/resource_controls/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)
return h
}

View File

@ -0,0 +1,116 @@
package resourcecontrols
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
"github.com/portainer/portainer/http/security"
)
type resourceControlCreatePayload struct {
ResourceID string
Type string
AdministratorsOnly bool
Users []int
Teams []int
SubResourceIDs []string
}
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.ResourceID) {
return portainer.Error("Invalid resource identifier")
}
if govalidator.IsNull(payload.Type) {
return portainer.Error("Invalid type")
}
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.AdministratorsOnly {
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or AdministratorOnly")
}
return nil
}
// POST request on /api/resource_controls
func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload resourceControlCreatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
var resourceControlType portainer.ResourceControlType
switch payload.Type {
case "container":
resourceControlType = portainer.ContainerResourceControl
case "service":
resourceControlType = portainer.ServiceResourceControl
case "volume":
resourceControlType = portainer.VolumeResourceControl
case "network":
resourceControlType = portainer.NetworkResourceControl
case "secret":
resourceControlType = portainer.SecretResourceControl
case "stack":
resourceControlType = portainer.StackResourceControl
case "config":
resourceControlType = portainer.ConfigResourceControl
default:
return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", portainer.ErrInvalidResourceControlType}
}
rc, err := handler.ResourceControlService.ResourceControlByResourceID(payload.ResourceID)
if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
}
if rc != nil {
return &httperror.HandlerError{http.StatusConflict, "A resource control is already associated to this resource", portainer.ErrResourceControlAlreadyExists}
}
var userAccesses = make([]portainer.UserResourceAccess, 0)
for _, v := range payload.Users {
userAccess := portainer.UserResourceAccess{
UserID: portainer.UserID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
userAccesses = append(userAccesses, userAccess)
}
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
for _, v := range payload.Teams {
teamAccess := portainer.TeamResourceAccess{
TeamID: portainer.TeamID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
teamAccesses = append(teamAccesses, teamAccess)
}
resourceControl := portainer.ResourceControl{
ResourceID: payload.ResourceID,
SubResourceIDs: payload.SubResourceIDs,
Type: resourceControlType,
AdministratorsOnly: payload.AdministratorsOnly,
UserAccesses: userAccesses,
TeamAccesses: teamAccesses,
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create a resource control for the specified resource", portainer.ErrResourceAccessDenied}
}
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the resource control inside the database", err}
}
return response.JSON(w, resourceControl)
}

View File

@ -0,0 +1,42 @@
package resourcecontrols
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
"github.com/portainer/portainer/http/security"
)
// DELETE request on /api/resource_controls/:id
func (handler *Handler) resourceControlDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
resourceControlID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err}
}
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the resource control", portainer.ErrResourceAccessDenied}
}
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err}
}
return response.Empty(w)
}

View File

@ -0,0 +1,87 @@
package resourcecontrols
import (
"net/http"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/request"
"github.com/portainer/portainer/http/response"
"github.com/portainer/portainer/http/security"
)
type resourceControlUpdatePayload struct {
AdministratorsOnly bool
Users []int
Teams []int
}
func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.AdministratorsOnly {
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or AdministratorOnly")
}
return nil
}
// PUT request on /api/resource_controls/:id
func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
resourceControlID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err}
}
var payload resourceControlUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if !security.AuthorizedResourceControlAccess(resourceControl, securityContext) {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied}
}
resourceControl.AdministratorsOnly = payload.AdministratorsOnly
var userAccesses = make([]portainer.UserResourceAccess, 0)
for _, v := range payload.Users {
userAccess := portainer.UserResourceAccess{
UserID: portainer.UserID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
userAccesses = append(userAccesses, userAccess)
}
resourceControl.UserAccesses = userAccesses
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
for _, v := range payload.Teams {
teamAccess := portainer.TeamResourceAccess{
TeamID: portainer.TeamID(v),
AccessLevel: portainer.ReadWriteAccessLevel,
}
teamAccesses = append(teamAccesses, teamAccess)
}
resourceControl.TeamAccesses = teamAccesses
if !security.AuthorizedResourceControlUpdate(resourceControl, securityContext) {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied}
}
err = handler.ResourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control changes inside the database", err}
}
return response.JSON(w, resourceControl)
}

View File

@ -1,181 +0,0 @@
package handler
import (
"encoding/json"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
"github.com/portainer/portainer/filesystem"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
)
// SettingsHandler represents an HTTP API handler for managing Settings.
type SettingsHandler struct {
*mux.Router
Logger *log.Logger
SettingsService portainer.SettingsService
LDAPService portainer.LDAPService
FileService portainer.FileService
}
// NewSettingsHandler returns a new instance of OldSettingsHandler.
func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
h := &SettingsHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/settings",
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet)
h.Handle("/settings",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettings))).Methods(http.MethodPut)
h.Handle("/settings/public",
bouncer.PublicAccess(http.HandlerFunc(h.handleGetPublicSettings))).Methods(http.MethodGet)
h.Handle("/settings/authentication/checkLDAP",
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettingsLDAPCheck))).Methods(http.MethodPut)
return h
}
type (
publicSettingsResponse struct {
LogoURL string `json:"LogoURL"`
DisplayDonationHeader bool `json:"DisplayDonationHeader"`
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
}
putSettingsRequest struct {
TemplatesURL string `valid:"required"`
LogoURL string `valid:""`
BlackListedLabels []portainer.Pair `valid:""`
DisplayDonationHeader bool `valid:""`
DisplayExternalContributors bool `valid:""`
AuthenticationMethod int `valid:"required"`
LDAPSettings portainer.LDAPSettings `valid:""`
AllowBindMountsForRegularUsers bool `valid:""`
AllowPrivilegedModeForRegularUsers bool `valid:""`
}
putSettingsLDAPCheckRequest struct {
LDAPSettings portainer.LDAPSettings `valid:""`
}
)
// handleGetSettings handles GET requests on /settings
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
settings, err := handler.SettingsService.Settings()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, settings, handler.Logger)
return
}
// handleGetPublicSettings handles GET requests on /settings/public
func (handler *SettingsHandler) handleGetPublicSettings(w http.ResponseWriter, r *http.Request) {
settings, err := handler.SettingsService.Settings()
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
publicSettings := &publicSettingsResponse{
LogoURL: settings.LogoURL,
DisplayDonationHeader: settings.DisplayDonationHeader,
DisplayExternalContributors: settings.DisplayExternalContributors,
AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
}
encodeJSON(w, publicSettings, handler.Logger)
return
}
// handlePutSettings handles PUT requests on /settings
func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) {
var req putSettingsRequest
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
}
settings := &portainer.Settings{
TemplatesURL: req.TemplatesURL,
LogoURL: req.LogoURL,
BlackListedLabels: req.BlackListedLabels,
DisplayDonationHeader: req.DisplayDonationHeader,
DisplayExternalContributors: req.DisplayExternalContributors,
LDAPSettings: req.LDAPSettings,
AllowBindMountsForRegularUsers: req.AllowBindMountsForRegularUsers,
AllowPrivilegedModeForRegularUsers: req.AllowPrivilegedModeForRegularUsers,
}
if req.AuthenticationMethod == 1 {
settings.AuthenticationMethod = portainer.AuthenticationInternal
} else if req.AuthenticationMethod == 2 {
settings.AuthenticationMethod = portainer.AuthenticationLDAP
} else {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
} else {
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
err := handler.FileService.DeleteTLSFiles(filesystem.LDAPStorePath)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
}
}
err = handler.SettingsService.StoreSettings(settings)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
}
}
// handlePutSettingsLDAPCheck handles PUT requests on /settings/ldap/check
func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter, r *http.Request) {
var req putSettingsLDAPCheckRequest
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
}
if (req.LDAPSettings.TLSConfig.TLS || req.LDAPSettings.StartTLS) && !req.LDAPSettings.TLSConfig.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
req.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
}
err = handler.LDAPService.TestConnectivity(&req.LDAPSettings)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}

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