mirror of https://github.com/portainer/portainer
commit
7f9644b55e
44
LICENSE
44
LICENSE
|
@ -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
|
This software is provided 'as-is', without any express or implied
|
||||||
warranty. In no event will the authors be held liable for any damages
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
@ -15,45 +15,3 @@ freely, subject to the following restrictions:
|
||||||
2. Altered source versions must be plainly marked as such, and must not be
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
misrepresented as being the original software.
|
misrepresented as being the original software.
|
||||||
3. This notice may not be removed or altered from any source distribution.
|
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.
|
|
||||||
|
|
12
README.md
12
README.md
|
@ -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:
|
**_Portainer_** has full support for the following Docker versions:
|
||||||
|
|
||||||
* Docker 1.10 to the latest version
|
* 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):
|
Partial support for the following Docker versions (some features may not be available):
|
||||||
|
|
||||||
* Docker 1.9
|
* 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]
|
||||||
|
|
|
@ -2,84 +2,66 @@ package bolt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
"github.com/portainer/portainer"
|
"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
|
// Store defines the implementation of portainer.DataStore using
|
||||||
// BoltDB as the storage system.
|
// BoltDB as the storage system.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
// Path where is stored the BoltDB database.
|
path string
|
||||||
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
|
db *bolt.DB
|
||||||
checkForDataMigration bool
|
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
|
// 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{
|
store := &Store{
|
||||||
Path: storePath,
|
path: storePath,
|
||||||
UserService: &UserService{},
|
fileService: fileService,
|
||||||
TeamService: &TeamService{},
|
|
||||||
TeamMembershipService: &TeamMembershipService{},
|
|
||||||
EndpointService: &EndpointService{},
|
|
||||||
EndpointGroupService: &EndpointGroupService{},
|
|
||||||
ResourceControlService: &ResourceControlService{},
|
|
||||||
VersionService: &VersionService{},
|
|
||||||
SettingsService: &SettingsService{},
|
|
||||||
RegistryService: &RegistryService{},
|
|
||||||
DockerHubService: &DockerHubService{},
|
|
||||||
StackService: &StackService{},
|
|
||||||
}
|
}
|
||||||
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)
|
databasePath := path.Join(storePath, databaseFileName)
|
||||||
if err != nil && os.IsNotExist(err) {
|
databaseFileExists, err := fileService.FileExists(databasePath)
|
||||||
store.checkForDataMigration = false
|
if err != nil {
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !databaseFileExists {
|
||||||
|
store.checkForDataMigration = false
|
||||||
} else {
|
} else {
|
||||||
store.checkForDataMigration = true
|
store.checkForDataMigration = true
|
||||||
}
|
}
|
||||||
|
@ -89,29 +71,14 @@ func NewStore(storePath string) (*Store, error) {
|
||||||
|
|
||||||
// Open opens and initializes the BoltDB database.
|
// Open opens and initializes the BoltDB database.
|
||||||
func (store *Store) Open() error {
|
func (store *Store) Open() error {
|
||||||
path := store.Path + "/" + databaseFileName
|
databasePath := path.Join(store.path, databaseFileName)
|
||||||
|
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
store.db = db
|
store.db = db
|
||||||
|
|
||||||
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
|
return store.initServices()
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init creates the default data set.
|
// Init creates the default data set.
|
||||||
|
@ -128,6 +95,7 @@ func (store *Store) Init() error {
|
||||||
Labels: []portainer.Pair{},
|
Labels: []portainer.Pair{},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
|
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
|
||||||
|
@ -147,28 +115,114 @@ func (store *Store) Close() error {
|
||||||
// MigrateData automatically migrate the data based on the DBVersion.
|
// MigrateData automatically migrate the data based on the DBVersion.
|
||||||
func (store *Store) MigrateData() error {
|
func (store *Store) MigrateData() error {
|
||||||
if !store.checkForDataMigration {
|
if !store.checkForDataMigration {
|
||||||
err := store.VersionService.StoreDBVersion(portainer.DBVersion)
|
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := store.VersionService.DBVersion()
|
version, err := store.VersionService.DBVersion()
|
||||||
if err == portainer.ErrDBVersionNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
version = 0
|
version = 0
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if version < portainer.DBVersion {
|
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)
|
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
|
||||||
migrator := NewMigrator(store, version)
|
|
||||||
err = migrator.Migrate()
|
err = migrator.Migrate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("An error occurred during database migration: %s\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,19 +1,20 @@
|
||||||
package bolt
|
package migrator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/bolt/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Migrator) updateAdminUserToDBVersion1() error {
|
func (m *Migrator) updateAdminUserToDBVersion1() error {
|
||||||
u, err := m.UserService.UserByUsername("admin")
|
u, err := m.userService.UserByUsername("admin")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
admin := &portainer.User{
|
admin := &portainer.User{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Password: u.Password,
|
Password: u.Password,
|
||||||
Role: portainer.AdministratorRole,
|
Role: portainer.AdministratorRole,
|
||||||
}
|
}
|
||||||
err = m.UserService.CreateUser(admin)
|
err = m.userService.CreateUser(admin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -21,19 +22,15 @@ func (m *Migrator) updateAdminUserToDBVersion1() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if err != nil && err != portainer.ErrUserNotFound {
|
} else if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Migrator) removeLegacyAdminUser() error {
|
func (m *Migrator) removeLegacyAdminUser() error {
|
||||||
return m.store.db.Update(func(tx *bolt.Tx) error {
|
return m.db.Update(func(tx *bolt.Tx) error {
|
||||||
bucket := tx.Bucket([]byte(userBucketName))
|
bucket := tx.Bucket([]byte(user.BucketName))
|
||||||
err := bucket.Delete([]byte("admin"))
|
return bucket.Delete([]byte("admin"))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package bolt
|
package migrator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
|
@ -16,7 +16,7 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
|
||||||
resourceControl.SubResourceIDs = []string{}
|
resourceControl.SubResourceIDs = []string{}
|
||||||
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
|
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
|
||||||
|
|
||||||
owner, err := m.UserService.User(resourceControl.OwnerID)
|
owner, err := m.userService.User(resourceControl.OwnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
|
||||||
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
|
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.ResourceControlService.CreateResourceControl(&resourceControl)
|
err = m.resourceControlService.CreateResourceControl(&resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -43,14 +43,14 @@ func (m *Migrator) updateResourceControlsToDBVersion2() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Migrator) updateEndpointsToDBVersion2() error {
|
func (m *Migrator) updateEndpointsToDBVersion2() error {
|
||||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
legacyEndpoints, err := m.endpointService.Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, endpoint := range legacyEndpoints {
|
for _, endpoint := range legacyEndpoints {
|
||||||
endpoint.AuthorizedTeams = []portainer.TeamID{}
|
endpoint.AuthorizedTeams = []portainer.TeamID{}
|
||||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -61,12 +61,12 @@ func (m *Migrator) updateEndpointsToDBVersion2() error {
|
||||||
|
|
||||||
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
|
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
|
||||||
legacyResourceControls := make([]portainer.ResourceControl, 0)
|
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"))
|
bucket := tx.Bucket([]byte("containerResourceControl"))
|
||||||
cursor := bucket.Cursor()
|
cursor := bucket.Cursor()
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
var resourceControl portainer.ResourceControl
|
var resourceControl portainer.ResourceControl
|
||||||
err := internal.UnmarshalResourceControl(v, &resourceControl)
|
err := internal.UnmarshalObject(v, &resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl
|
||||||
cursor = bucket.Cursor()
|
cursor = bucket.Cursor()
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
var resourceControl portainer.ResourceControl
|
var resourceControl portainer.ResourceControl
|
||||||
err := internal.UnmarshalResourceControl(v, &resourceControl)
|
err := internal.UnmarshalObject(v, &resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl
|
||||||
cursor = bucket.Cursor()
|
cursor = bucket.Cursor()
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
var resourceControl portainer.ResourceControl
|
var resourceControl portainer.ResourceControl
|
||||||
err := internal.UnmarshalResourceControl(v, &resourceControl)
|
err := internal.UnmarshalObject(v, &resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package bolt
|
package migrator
|
||||||
|
|
||||||
import "github.com/portainer/portainer"
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
func (m *Migrator) updateEndpointsToVersion11() error {
|
func (m *Migrator) updateEndpointsToVersion11() error {
|
||||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
legacyEndpoints, err := m.endpointService.Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package bolt
|
package migrator
|
||||||
|
|
||||||
import "github.com/portainer/portainer"
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
func (m *Migrator) updateSettingsToDBVersion3() error {
|
func (m *Migrator) updateSettingsToDBVersion3() error {
|
||||||
legacySettings, err := m.SettingsService.Settings()
|
legacySettings, err := m.settingsService.Settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,5 @@ func (m *Migrator) updateSettingsToDBVersion3() error {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.SettingsService.StoreSettings(legacySettings)
|
return m.settingsService.UpdateSettings(legacySettings)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package bolt
|
package migrator
|
||||||
|
|
||||||
import "github.com/portainer/portainer"
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
func (m *Migrator) updateEndpointsToDBVersion4() error {
|
func (m *Migrator) updateEndpointsToDBVersion4() error {
|
||||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
legacyEndpoints, err := m.endpointService.Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@ func (m *Migrator) updateEndpointsToDBVersion4() error {
|
||||||
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
|
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
|
||||||
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
|
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
|
||||||
}
|
}
|
||||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
|
||||||
|
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
package bolt
|
package migrator
|
||||||
|
|
||||||
import "github.com/portainer/portainer"
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
func (m *Migrator) updateEndpointsToVersion8() error {
|
func (m *Migrator) updateEndpointsToVersion8() error {
|
||||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
legacyEndpoints, err := m.endpointService.Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, endpoint := range legacyEndpoints {
|
for _, endpoint := range legacyEndpoints {
|
||||||
endpoint.Extensions = []portainer.EndpointExtension{}
|
endpoint.Extensions = []portainer.EndpointExtension{}
|
||||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
package bolt
|
package migrator
|
||||||
|
|
||||||
import "github.com/portainer/portainer"
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
func (m *Migrator) updateEndpointsToVersion9() error {
|
func (m *Migrator) updateEndpointsToVersion9() error {
|
||||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
legacyEndpoints, err := m.endpointService.Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, endpoint := range legacyEndpoints {
|
for _, endpoint := range legacyEndpoints {
|
||||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
package bolt
|
package migrator
|
||||||
|
|
||||||
import "github.com/portainer/portainer"
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
func (m *Migrator) updateEndpointsToVersion10() error {
|
func (m *Migrator) updateEndpointsToVersion10() error {
|
||||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
legacyEndpoints, err := m.endpointService.Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, endpoint := range legacyEndpoints {
|
for _, endpoint := range legacyEndpoints {
|
||||||
endpoint.Type = portainer.DockerEnvironment
|
endpoint.Type = portainer.DockerEnvironment
|
||||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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, ®istry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ®istry, 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, ®istry)
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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, ®istry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ®istry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registries returns an array containing all the registries.
|
|
||||||
func (service *RegistryService) Registries() ([]portainer.Registry, error) {
|
|
||||||
var registries = make([]portainer.Registry, 0)
|
|
||||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(registryBucketName))
|
|
||||||
|
|
||||||
cursor := bucket.Cursor()
|
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
|
||||||
var registry portainer.Registry
|
|
||||||
err := internal.UnmarshalRegistry(v, ®istry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
registries = append(registries, registry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return registries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRegistry creates a new registry.
|
|
||||||
func (service *RegistryService) CreateRegistry(registry *portainer.Registry) error {
|
|
||||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(registryBucketName))
|
|
||||||
|
|
||||||
id, _ := bucket.NextSequence()
|
|
||||||
registry.ID = portainer.RegistryID(id)
|
|
||||||
|
|
||||||
data, err := internal.MarshalRegistry(registry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = bucket.Put(internal.Itob(int(registry.ID)), data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateRegistry updates an registry.
|
|
||||||
func (service *RegistryService) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
|
||||||
data, err := internal.MarshalRegistry(registry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(registryBucketName))
|
|
||||||
err = bucket.Put(internal.Itob(int(ID)), data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRegistry deletes an registry.
|
|
||||||
func (service *RegistryService) DeleteRegistry(ID portainer.RegistryID) error {
|
|
||||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(registryBucketName))
|
|
||||||
err := bucket.Delete(internal.Itob(int(ID)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/portainer/portainer/http/client"
|
"github.com/portainer/portainer/http/client"
|
||||||
"github.com/portainer/portainer/jwt"
|
"github.com/portainer/portainer/jwt"
|
||||||
"github.com/portainer/portainer/ldap"
|
"github.com/portainer/portainer/ldap"
|
||||||
|
"github.com/portainer/portainer/libcompose"
|
||||||
|
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
@ -41,8 +42,8 @@ func initFileService(dataStorePath string) portainer.FileService {
|
||||||
return fileService
|
return fileService
|
||||||
}
|
}
|
||||||
|
|
||||||
func initStore(dataStorePath string) *bolt.Store {
|
func initStore(dataStorePath string, fileService portainer.FileService) *bolt.Store {
|
||||||
store, err := bolt.NewStore(dataStorePath)
|
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -64,8 +65,12 @@ func initStore(dataStorePath string) *bolt.Store {
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
func initStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.StackManager, error) {
|
func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager {
|
||||||
return exec.NewStackManager(assetsPath, dataStorePath, signatureService, fileService)
|
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 {
|
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||||
|
@ -120,13 +125,13 @@ func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portaine
|
||||||
|
|
||||||
func initDockerHub(dockerHubService portainer.DockerHubService) error {
|
func initDockerHub(dockerHubService portainer.DockerHubService) error {
|
||||||
_, err := dockerHubService.DockerHub()
|
_, err := dockerHubService.DockerHub()
|
||||||
if err == portainer.ErrDockerHubNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
dockerhub := &portainer.DockerHub{
|
dockerhub := &portainer.DockerHub{
|
||||||
Authentication: false,
|
Authentication: false,
|
||||||
Username: "",
|
Username: "",
|
||||||
Password: "",
|
Password: "",
|
||||||
}
|
}
|
||||||
return dockerHubService.StoreDockerHub(dockerhub)
|
return dockerHubService.UpdateDockerHub(dockerhub)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -136,10 +141,9 @@ func initDockerHub(dockerHubService portainer.DockerHubService) error {
|
||||||
|
|
||||||
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
|
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
|
||||||
_, err := settingsService.Settings()
|
_, err := settingsService.Settings()
|
||||||
if err == portainer.ErrSettingsNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
settings := &portainer.Settings{
|
settings := &portainer.Settings{
|
||||||
LogoURL: *flags.Logo,
|
LogoURL: *flags.Logo,
|
||||||
DisplayDonationHeader: true,
|
|
||||||
DisplayExternalContributors: false,
|
DisplayExternalContributors: false,
|
||||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||||
LDAPSettings: portainer.LDAPSettings{
|
LDAPSettings: portainer.LDAPSettings{
|
||||||
|
@ -164,7 +168,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||||
settings.BlackListedLabels = make([]portainer.Pair, 0)
|
settings.BlackListedLabels = make([]portainer.Pair, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return settingsService.StoreSettings(settings)
|
return settingsService.UpdateSettings(settings)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -232,6 +236,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
||||||
|
@ -270,6 +275,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpointService.CreateEndpoint(endpoint)
|
return endpointService.CreateEndpoint(endpoint)
|
||||||
|
@ -301,7 +307,7 @@ func main() {
|
||||||
|
|
||||||
fileService := initFileService(*flags.Data)
|
fileService := initFileService(*flags.Data)
|
||||||
|
|
||||||
store := initStore(*flags.Data)
|
store := initStore(*flags.Data, fileService)
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
jwtService := initJWTService(!*flags.NoAuth)
|
jwtService := initJWTService(!*flags.NoAuth)
|
||||||
|
@ -321,11 +327,13 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stackManager, err := initStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
|
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composeStackManager := initComposeStackManager(*flags.Data)
|
||||||
|
|
||||||
err = initSettings(store.SettingsService, flags)
|
err = initSettings(store.SettingsService, flags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -395,7 +403,9 @@ func main() {
|
||||||
RegistryService: store.RegistryService,
|
RegistryService: store.RegistryService,
|
||||||
DockerHubService: store.DockerHubService,
|
DockerHubService: store.DockerHubService,
|
||||||
StackService: store.StackService,
|
StackService: store.StackService,
|
||||||
StackManager: stackManager,
|
TagService: store.TagService,
|
||||||
|
SwarmStackManager: swarmStackManager,
|
||||||
|
ComposeStackManager: composeStackManager,
|
||||||
CryptoService: cryptoService,
|
CryptoService: cryptoService,
|
||||||
JWTService: jwtService,
|
JWTService: jwtService,
|
||||||
FileService: fileService,
|
FileService: fileService,
|
||||||
|
|
|
@ -4,63 +4,64 @@ package portainer
|
||||||
const (
|
const (
|
||||||
ErrUnauthorized = Error("Unauthorized")
|
ErrUnauthorized = Error("Unauthorized")
|
||||||
ErrResourceAccessDenied = Error("Access denied to resource")
|
ErrResourceAccessDenied = Error("Access denied to resource")
|
||||||
ErrResourceNotFound = Error("Unable to find resource")
|
ErrObjectNotFound = Error("Object not found inside the database")
|
||||||
ErrUnsupportedDockerAPI = Error("Unsupported Docker API response")
|
|
||||||
ErrMissingSecurityContext = Error("Unable to find security details in request context")
|
ErrMissingSecurityContext = Error("Unable to find security details in request context")
|
||||||
)
|
)
|
||||||
|
|
||||||
// User errors.
|
// User errors.
|
||||||
const (
|
const (
|
||||||
ErrUserNotFound = Error("User not found")
|
|
||||||
ErrUserAlreadyExists = Error("User already exists")
|
ErrUserAlreadyExists = Error("User already exists")
|
||||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
||||||
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
|
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")
|
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Team errors.
|
// Team errors.
|
||||||
const (
|
const (
|
||||||
ErrTeamNotFound = Error("Team not found")
|
|
||||||
ErrTeamAlreadyExists = Error("Team already exists")
|
ErrTeamAlreadyExists = Error("Team already exists")
|
||||||
)
|
)
|
||||||
|
|
||||||
// TeamMembership errors.
|
// TeamMembership errors.
|
||||||
const (
|
const (
|
||||||
ErrTeamMembershipNotFound = Error("Team membership not found")
|
|
||||||
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team")
|
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResourceControl errors.
|
// ResourceControl errors.
|
||||||
const (
|
const (
|
||||||
ErrResourceControlNotFound = Error("Resource control not found")
|
|
||||||
ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource")
|
ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource")
|
||||||
ErrInvalidResourceControlType = Error("Unsupported resource control type")
|
ErrInvalidResourceControlType = Error("Unsupported resource control type")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Endpoint errors.
|
// Endpoint errors.
|
||||||
const (
|
const (
|
||||||
ErrEndpointNotFound = Error("Endpoint not found")
|
|
||||||
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Azure environment errors
|
||||||
|
const (
|
||||||
|
ErrAzureInvalidCredentials = Error("Invalid Azure credentials")
|
||||||
|
)
|
||||||
|
|
||||||
// Endpoint group errors.
|
// Endpoint group errors.
|
||||||
const (
|
const (
|
||||||
ErrEndpointGroupNotFound = Error("Endpoint group not found")
|
|
||||||
ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group")
|
ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Registry errors.
|
// Registry errors.
|
||||||
const (
|
const (
|
||||||
ErrRegistryNotFound = Error("Registry not found")
|
|
||||||
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
|
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stack errors
|
// Stack errors
|
||||||
const (
|
const (
|
||||||
ErrStackNotFound = Error("Stack not found")
|
|
||||||
ErrStackAlreadyExists = Error("A stack already exists with this name")
|
ErrStackAlreadyExists = Error("A stack already exists with this name")
|
||||||
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
|
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
|
// Endpoint extensions error
|
||||||
|
@ -69,21 +70,6 @@ const (
|
||||||
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
|
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.
|
// Crypto errors.
|
||||||
const (
|
const (
|
||||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||||
|
|
|
@ -11,18 +11,18 @@ import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StackManager represents a service for managing stacks.
|
// SwarmStackManager represents a service for managing stacks.
|
||||||
type StackManager struct {
|
type SwarmStackManager struct {
|
||||||
binaryPath string
|
binaryPath string
|
||||||
dataPath string
|
dataPath string
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
fileService portainer.FileService
|
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.
|
// It also updates the configuration of the Docker CLI binary.
|
||||||
func NewStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*StackManager, error) {
|
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*SwarmStackManager, error) {
|
||||||
manager := &StackManager{
|
manager := &SwarmStackManager{
|
||||||
binaryPath: binaryPath,
|
binaryPath: binaryPath,
|
||||||
dataPath: dataPath,
|
dataPath: dataPath,
|
||||||
signatureService: signatureService,
|
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).
|
// 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)
|
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
for _, registry := range registries {
|
for _, registry := range registries {
|
||||||
if registry.Authentication {
|
if registry.Authentication {
|
||||||
|
@ -54,14 +54,14 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout executes the docker logout command.
|
// 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)
|
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
args = append(args, "logout")
|
args = append(args, "logout")
|
||||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy executes the docker stack deploy command.
|
// 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)
|
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
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.
|
// 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)
|
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
args = append(args, "stack", "rm", stack.Name)
|
args = append(args, "stack", "rm", stack.Name)
|
||||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
|
@ -133,7 +133,7 @@ func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portaine
|
||||||
return command, args
|
return command, args
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *StackManager) updateDockerCLIConfiguration(dataPath string) error {
|
func (manager *SwarmStackManager) updateDockerCLIConfiguration(dataPath string) error {
|
||||||
configFilePath := path.Join(dataPath, "config.json")
|
configFilePath := path.Join(dataPath, "config.json")
|
||||||
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
|
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -161,7 +161,7 @@ func (manager *StackManager) updateDockerCLIConfiguration(dataPath string) error
|
||||||
return nil
|
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{}
|
var config map[string]interface{}
|
||||||
|
|
||||||
raw, err := manager.fileService.GetFileContent(path)
|
raw, err := manager.fileService.GetFileContent(path)
|
|
@ -77,9 +77,9 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string {
|
||||||
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
|
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.
|
// 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)
|
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
|
||||||
err := service.createDirectoryInStore(stackStorePath)
|
err := service.createDirectoryInStore(stackStorePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -87,7 +87,6 @@ func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stac
|
||||||
}
|
}
|
||||||
|
|
||||||
composeFilePath := path.Join(stackStorePath, fileName)
|
composeFilePath := path.Join(stackStorePath, fileName)
|
||||||
data := []byte(stackFileContent)
|
|
||||||
r := bytes.NewReader(data)
|
r := bytes.NewReader(data)
|
||||||
|
|
||||||
err = service.createFileInStore(composeFilePath, r)
|
err = service.createFileInStore(composeFilePath, r)
|
||||||
|
@ -98,31 +97,13 @@ func (service *Service) StoreStackFileFromString(stackIdentifier, fileName, stac
|
||||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
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.
|
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
|
||||||
// It returns the path to the folder where the file is stored.
|
// It returns the path to the newly created file.
|
||||||
func (service *Service) StoreStackFileFromReader(stackIdentifier, fileName string, r io.Reader) (string, error) {
|
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (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 {
|
|
||||||
storePath := path.Join(TLSStorePath, folder)
|
storePath := path.Join(TLSStorePath, folder)
|
||||||
err := service.createDirectoryInStore(storePath)
|
err := service.createDirectoryInStore(storePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileName string
|
var fileName string
|
||||||
|
@ -134,15 +115,16 @@ func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileTy
|
||||||
case portainer.TLSFileKey:
|
case portainer.TLSFileKey:
|
||||||
fileName = TLSKeyFile
|
fileName = TLSKeyFile
|
||||||
default:
|
default:
|
||||||
return portainer.ErrUndefinedTLSFileType
|
return "", portainer.ErrUndefinedTLSFileType
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsFilePath := path.Join(storePath, fileName)
|
tlsFilePath := path.Join(storePath, fileName)
|
||||||
|
r := bytes.NewReader(data)
|
||||||
err = service.createFileInStore(tlsFilePath, r)
|
err = service.createFileInStore(tlsFilePath, r)
|
||||||
if err != nil {
|
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.
|
// 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
|
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.
|
// WriteJSONToFile writes JSON to the specified file.
|
||||||
func (service *Service) WriteJSONToFile(path string, content interface{}) error {
|
func (service *Service) WriteJSONToFile(path string, content interface{}) error {
|
||||||
jsonContent, err := json.Marshal(content)
|
jsonContent, err := json.Marshal(content)
|
||||||
|
@ -214,10 +201,21 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
|
||||||
return ioutil.WriteFile(path, jsonContent, 0644)
|
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.
|
// KeyPairFilesExist checks for the existence of the key files.
|
||||||
func (service *Service) KeyPairFilesExist() (bool, error) {
|
func (service *Service) KeyPairFilesExist() (bool, error) {
|
||||||
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
|
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
|
||||||
exists, err := fileExists(privateKeyPath)
|
exists, err := service.FileExists(privateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -226,7 +224,7 @@ func (service *Service) KeyPairFilesExist() (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
|
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
|
||||||
exists, err = fileExists(publicKeyPath)
|
exists, err = service.FileExists(publicKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -320,13 +318,3 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||||
block, _ := pem.Decode(fileContent)
|
block, _ := pem.Decode(fileContent)
|
||||||
return block.Bytes, nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,15 +2,68 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/portainer/portainer"
|
"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
|
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
|
||||||
// using the specified host and optional TLS configuration.
|
// 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) {
|
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
|
||||||
transport := &http.Transport{}
|
transport := &http.Transport{}
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,36 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// errorResponse is a generic response for sending a error.
|
type (
|
||||||
type errorResponse struct {
|
// 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"`
|
Err string `json:"err,omitempty"`
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// WriteErrorResponse writes an error message to the response and logger.
|
func (handler LoggerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log.Logger) {
|
err := handler(rw, r)
|
||||||
if logger != nil {
|
if err != nil {
|
||||||
logger.Printf("http error: %s (code=%d)", err, code)
|
writeErrorResponse(rw, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
func writeErrorResponse(rw http.ResponseWriter, err *HandlerError) {
|
||||||
w.WriteHeader(code)
|
log.Printf("http error: %s (err=%s) (code=%d)\n", err.Message, err.Err, err.StatusCode)
|
||||||
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
|
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})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -1,24 +1,19 @@
|
||||||
package handler
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileHandler represents an HTTP API handler for managing static files.
|
// Handler represents an HTTP API handler for managing static files.
|
||||||
type FileHandler struct {
|
type Handler struct {
|
||||||
http.Handler
|
http.Handler
|
||||||
Logger *log.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileHandler returns a new instance of FileHandler.
|
// NewHandler creates a handler to serve static files.
|
||||||
func NewFileHandler(assetPublicPath string) *FileHandler {
|
func NewHandler(assetPublicPath string) *Handler {
|
||||||
h := &FileHandler{
|
h := &Handler{
|
||||||
Handler: http.FileServer(http.Dir(assetPublicPath)),
|
Handler: http.FileServer(http.Dir(assetPublicPath)),
|
||||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
|
||||||
}
|
}
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
@ -32,7 +27,7 @@ func isHTML(acceptContent []string) bool {
|
||||||
return false
|
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"]) {
|
if !isHTML(r.Header["Accept"]) {
|
||||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||||
} else {
|
} else {
|
|
@ -1,52 +1,54 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer/http/handler/auth"
|
||||||
httperror "github.com/portainer/portainer/http/error"
|
"github.com/portainer/portainer/http/handler/dockerhub"
|
||||||
"github.com/portainer/portainer/http/handler/extensions"
|
"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.
|
// Handler is a collection of all the service handlers.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
AuthHandler *AuthHandler
|
AuthHandler *auth.Handler
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
DockerHubHandler *dockerhub.Handler
|
||||||
// ErrInvalidJSON defines an error raised the app is unable to parse request data
|
EndpointGroupHandler *endpointgroups.Handler
|
||||||
ErrInvalidJSON = portainer.Error("Invalid JSON")
|
EndpointHandler *endpoints.Handler
|
||||||
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
|
EndpointProxyHandler *endpointproxy.Handler
|
||||||
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
FileHandler *file.Handler
|
||||||
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
|
RegistryHandler *registries.Handler
|
||||||
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
|
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.
|
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/auth"):
|
case strings.HasPrefix(r.URL.Path, "/api/auth"):
|
||||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
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"):
|
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(r.URL.Path, "/docker/"):
|
case strings.Contains(r.URL.Path, "/docker/"):
|
||||||
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||||
case strings.Contains(r.URL.Path, "/stacks"):
|
|
||||||
http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r)
|
|
||||||
case strings.Contains(r.URL.Path, "/extensions/storidge"):
|
case strings.Contains(r.URL.Path, "/extensions/storidge"):
|
||||||
http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||||
case strings.Contains(r.URL.Path, "/extensions"):
|
case strings.Contains(r.URL.Path, "/azure/"):
|
||||||
http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||||
default:
|
default:
|
||||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||||
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/resource_controls"):
|
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"):
|
case strings.HasPrefix(r.URL.Path, "/api/settings"):
|
||||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
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"):
|
case strings.HasPrefix(r.URL.Path, "/api/status"):
|
||||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
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"):
|
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
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)
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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(®istry)
|
||||||
|
}
|
||||||
|
return response.JSON(w, registries)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
Loading…
Reference in New Issue