Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bccd521f8 | ||
|
|
5e2b3c1d07 | ||
|
|
210bdc8022 | ||
|
|
3cb96235b7 | ||
|
|
d695657711 | ||
|
|
5131c4c10b | ||
|
|
912ebf4672 | ||
|
|
dd0fc6fab8 | ||
|
|
910136ee9b | ||
|
|
61f652da04 | ||
|
|
a2b4cd8050 | ||
|
|
774738110b | ||
|
|
851a1ac64c | ||
|
|
d653391cdd | ||
|
|
f96b70841f | ||
|
|
8d4807c9e7 | ||
|
|
87825f7ebb | ||
|
|
be4f3ec81d | ||
|
|
56604a5445 | ||
|
|
c0d282e85b | ||
|
|
b9b32f0526 | ||
|
|
be4beacdf7 | ||
|
|
bf6b398a27 | ||
|
|
9a0f0a9701 | ||
|
|
ef8edfb67b | ||
|
|
0e8da2db18 | ||
|
|
e65d132b3d | ||
|
|
13b2fcffd2 | ||
|
|
c1e486bf43 | ||
|
|
8c68e92e74 | ||
|
|
a6ef27164c | ||
|
|
d50a650686 | ||
|
|
35dd3916dd | ||
|
|
1a28e1091c | ||
|
|
124458c3d6 | ||
|
|
8e2dbd1775 | ||
|
|
27188f4dff | ||
|
|
ef13f6fb3b | ||
|
|
92391254bc | ||
|
|
d3e87b2435 | ||
|
|
e5666dfdf2 | ||
|
|
e96e615761 | ||
|
|
c85aa0739d | ||
|
|
d814f3aaa4 | ||
|
|
3d5f9a76e4 | ||
|
|
d27528a771 | ||
|
|
04ea81e7cd | ||
|
|
d7769dec33 | ||
|
|
12adeadc94 | ||
|
|
b5429f7504 | ||
|
|
cf5c3ee536 | ||
|
|
86c450bd91 | ||
|
|
0d6ab099ac | ||
|
|
5110f83fae | ||
|
|
252e05e963 | ||
|
|
635ecdef72 | ||
|
|
b08d2b07bc | ||
|
|
3919ad3ccf | ||
|
|
aca4f5c286 | ||
|
|
387b4c66d9 | ||
|
|
7c40d2caa9 | ||
|
|
02203e7ce5 | ||
|
|
53583741ba | ||
|
|
12eb9671de | ||
|
|
29d66bfd97 | ||
|
|
57fde5ae7c | ||
|
|
471f902171 | ||
|
|
2e2aba1bbb | ||
|
|
f2347b2f77 | ||
|
|
a39645a297 | ||
|
|
806a0b92a0 | ||
|
|
a438357b45 | ||
|
|
206eb0513d | ||
|
|
5ad6837547 | ||
|
|
272a040c91 | ||
|
|
c04b9e5340 | ||
|
|
3f085a977c | ||
|
|
a1dd12a947 | ||
|
|
a7df43bd45 | ||
|
|
5d749c2ebf | ||
|
|
536ca15e90 | ||
|
|
703e423e04 | ||
|
|
780fec8e36 | ||
|
|
0a436600f4 | ||
|
|
32c2ce90e2 | ||
|
|
a864641692 | ||
|
|
344eee098d | ||
|
|
bc4b0a0b35 | ||
|
|
b23943e30b | ||
|
|
25ed6a71fb | ||
|
|
8dc6d05ed6 | ||
|
|
fe5a993fc9 | ||
|
|
6df5eb3787 | ||
|
|
bc3d5e97ea | ||
|
|
9909b6d481 | ||
|
|
90a32d1b67 | ||
|
|
472834ac42 | ||
|
|
b3f4c6f751 | ||
|
|
317303fc43 | ||
|
|
b6b579d55d | ||
|
|
6d6f4f092d | ||
|
|
7473681c5b | ||
|
|
54c8872d25 | ||
|
|
c5ce45f588 | ||
|
|
07a0c4dfe3 | ||
|
|
80bb94e745 | ||
|
|
6c89412f39 | ||
|
|
034e29cd74 | ||
|
|
0e0764eff8 | ||
|
|
e47db0b8c9 | ||
|
|
6d401dcd59 | ||
|
|
6609c2e928 | ||
|
|
a161d25d48 | ||
|
|
4adedf9436 | ||
|
|
1168e94534 | ||
|
|
b57bfe3eee | ||
|
|
3592e88e4f | ||
|
|
219cde4733 | ||
|
|
c82cd50d87 | ||
|
|
dae4893fe1 | ||
|
|
1e686f0428 | ||
|
|
08c5a5a4f6 | ||
|
|
9360f24d89 | ||
|
|
d0477b216f | ||
|
|
a812f4729c | ||
|
|
db324998e3 | ||
|
|
4ec65a80df | ||
|
|
f2b9700345 | ||
|
|
d8f8ab785c | ||
|
|
b316efe80b | ||
|
|
14a4587f5e | ||
|
|
afd99d2d68 | ||
|
|
7bba1c9c5e | ||
|
|
fd79afb429 | ||
|
|
d5f00597a5 | ||
|
|
1c4ccfe294 | ||
|
|
f48423d5aa | ||
|
|
5d98d9b54b | ||
|
|
132dd4acc4 | ||
|
|
c7e306841a | ||
|
|
5e74a3993b | ||
|
|
5bf10b89b1 | ||
|
|
bde9dd8b88 | ||
|
|
42d28db47a | ||
|
|
128601bb58 |
@@ -12,7 +12,8 @@ engines:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
- javascript
|
||||
javascript:
|
||||
mass_threshold: 80
|
||||
eslint:
|
||||
enabled: true
|
||||
config:
|
||||
|
||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at anthony.lapenna@portainer.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
16
README.md
16
README.md
@@ -1,24 +1,26 @@
|
||||
|
||||
<p align="center">
|
||||
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
|
||||
<img title="portainer" src='https://portainer.io/images/logo_alt.png' />
|
||||
</p>
|
||||
|
||||
[](https://hub.docker.com/r/portainer/portainer/)
|
||||
[](http://microbadger.com/images/portainer/portainer "Image size")
|
||||
[](http://portainer.readthedocs.io/en/latest/?badge=stable)
|
||||
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
||||
[]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci)
|
||||
[](https://codeclimate.com/github/portainer/portainer)
|
||||
[](https://portainer.io/slack/)
|
||||
[](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
|
||||
|
||||
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
|
||||
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
|
||||
|
||||
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (Docker for Linux and Docker for Windows are supported).
|
||||
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container).
|
||||
|
||||
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm*.
|
||||
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*.
|
||||
|
||||
## Demo
|
||||
|
||||
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
|
||||
<img src="https://portainer.io/images/screenshots/portainer.gif" width="77%"/>
|
||||
|
||||
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
|
||||
|
||||
@@ -33,8 +35,8 @@ Please note that the public demo cluster is **reset every 15min**.
|
||||
|
||||
* Issues: https://github.com/portainer/portainer/issues
|
||||
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
|
||||
* Slack (chat): https://portainer.io/slack/
|
||||
* Gitter (chat): https://gitter.im/portainer/Lobby
|
||||
* Slack: http://portainer.io/slack/
|
||||
|
||||
## Reporting bugs and contributing
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ type Store struct {
|
||||
EndpointService *EndpointService
|
||||
ResourceControlService *ResourceControlService
|
||||
VersionService *VersionService
|
||||
SettingsService *SettingsService
|
||||
RegistryService *RegistryService
|
||||
DockerHubService *DockerHubService
|
||||
|
||||
db *bolt.DB
|
||||
checkForDataMigration bool
|
||||
@@ -35,6 +38,9 @@ const (
|
||||
teamMembershipBucketName = "team_membership"
|
||||
endpointBucketName = "endpoints"
|
||||
resourceControlBucketName = "resource_control"
|
||||
settingsBucketName = "settings"
|
||||
registryBucketName = "registries"
|
||||
dockerhubBucketName = "dockerhub"
|
||||
)
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
@@ -47,6 +53,9 @@ func NewStore(storePath string) (*Store, error) {
|
||||
EndpointService: &EndpointService{},
|
||||
ResourceControlService: &ResourceControlService{},
|
||||
VersionService: &VersionService{},
|
||||
SettingsService: &SettingsService{},
|
||||
RegistryService: &RegistryService{},
|
||||
DockerHubService: &DockerHubService{},
|
||||
}
|
||||
store.UserService.store = store
|
||||
store.TeamService.store = store
|
||||
@@ -54,6 +63,9 @@ func NewStore(storePath string) (*Store, error) {
|
||||
store.EndpointService.store = store
|
||||
store.ResourceControlService.store = store
|
||||
store.VersionService.store = store
|
||||
store.SettingsService.store = store
|
||||
store.RegistryService.store = store
|
||||
store.DockerHubService.store = store
|
||||
|
||||
_, err := os.Stat(storePath + "/" + databaseFileName)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
@@ -70,36 +82,26 @@ func NewStore(storePath string) (*Store, error) {
|
||||
// Open opens and initializes the BoltDB database.
|
||||
func (store *Store) Open() error {
|
||||
path := store.Path + "/" + databaseFileName
|
||||
|
||||
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.db = db
|
||||
|
||||
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
|
||||
resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
|
||||
registryBucketName, dockerhubBucketName}
|
||||
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(versionBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(userBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(teamBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(resourceControlBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(teamMembershipBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
for _, bucket := range bucketsToCreate {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
61
api/bolt/dockerhub_service.go
Normal file
61
api/bolt/dockerhub_service.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// DockerHubService represents a service for managing registries.
|
||||
type DockerHubService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
const (
|
||||
dbDockerHubKey = "DOCKERHUB"
|
||||
)
|
||||
|
||||
// DockerHub returns the DockerHub object.
|
||||
func (service *DockerHubService) DockerHub() (*portainer.DockerHub, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(dockerhubBucketName))
|
||||
value := bucket.Get([]byte(dbDockerHubKey))
|
||||
if value == nil {
|
||||
return portainer.ErrDockerHubNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dockerhub portainer.DockerHub
|
||||
err = internal.UnmarshalDockerHub(data, &dockerhub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dockerhub, nil
|
||||
}
|
||||
|
||||
// StoreDockerHub persists a DockerHub object.
|
||||
func (service *DockerHubService) StoreDockerHub(dockerhub *portainer.DockerHub) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(dockerhubBucketName))
|
||||
|
||||
data, err := internal.MarshalDockerHub(dockerhub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put([]byte(dbDockerHubKey), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// EndpointService represents a service for managing users.
|
||||
// EndpointService represents a service for managing endpoints.
|
||||
type EndpointService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@ func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
|
||||
return json.Unmarshal(data, endpoint)
|
||||
}
|
||||
|
||||
// MarshalRegistry encodes a registry to binary format.
|
||||
func MarshalRegistry(registry *portainer.Registry) ([]byte, error) {
|
||||
return json.Marshal(registry)
|
||||
}
|
||||
|
||||
// UnmarshalRegistry decodes a registry from a binary data.
|
||||
func UnmarshalRegistry(data []byte, registry *portainer.Registry) error {
|
||||
return json.Unmarshal(data, registry)
|
||||
}
|
||||
|
||||
// MarshalResourceControl encodes a resource control object to binary format.
|
||||
func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) {
|
||||
return json.Marshal(rc)
|
||||
@@ -57,6 +67,26 @@ 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.
|
||||
|
||||
25
api/bolt/migrate_dbversion2.go
Normal file
25
api/bolt/migrate_dbversion2.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package bolt
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion3() error {
|
||||
legacySettings, err := m.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.AuthenticationMethod = portainer.AuthenticationInternal
|
||||
legacySettings.LDAPSettings = portainer.LDAPSettings{
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
portainer.LDAPSearchSettings{},
|
||||
},
|
||||
}
|
||||
|
||||
err = m.SettingsService.StoreSettings(legacySettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
27
api/bolt/migrate_dbversion3.go
Normal file
27
api/bolt/migrate_dbversion3.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package bolt
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
func (m *Migrator) updateEndpointsToDBVersion4() error {
|
||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range legacyEndpoints {
|
||||
endpoint.TLSConfig = portainer.TLSConfiguration{}
|
||||
if endpoint.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = endpoint.TLSCACertPath
|
||||
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
|
||||
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
|
||||
}
|
||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ type Migrator struct {
|
||||
UserService *UserService
|
||||
EndpointService *EndpointService
|
||||
ResourceControlService *ResourceControlService
|
||||
SettingsService *SettingsService
|
||||
VersionService *VersionService
|
||||
CurrentDBVersion int
|
||||
store *Store
|
||||
@@ -18,6 +19,7 @@ func NewMigrator(store *Store, version int) *Migrator {
|
||||
UserService: store.UserService,
|
||||
EndpointService: store.EndpointService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
VersionService: store.VersionService,
|
||||
CurrentDBVersion: version,
|
||||
store: store,
|
||||
@@ -28,7 +30,7 @@ func NewMigrator(store *Store, version int) *Migrator {
|
||||
func (m *Migrator) Migrate() error {
|
||||
|
||||
// Portainer < 1.12
|
||||
if m.CurrentDBVersion == 0 {
|
||||
if m.CurrentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -36,7 +38,7 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.CurrentDBVersion == 1 {
|
||||
if m.CurrentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -47,6 +49,22 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
114
api/bolt/registry_service.go
Normal file
114
api/bolt/registry_service.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// RegistryService represents a service for managing registries.
|
||||
type RegistryService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// Registry returns an registry by ID.
|
||||
func (service *RegistryService) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
value := bucket.Get(internal.Itob(int(ID)))
|
||||
if value == nil {
|
||||
return portainer.ErrRegistryNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var registry portainer.Registry
|
||||
err = internal.UnmarshalRegistry(data, ®istry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// Registries returns an array containing all the registries.
|
||||
func (service *RegistryService) Registries() ([]portainer.Registry, error) {
|
||||
var registries = make([]portainer.Registry, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var registry portainer.Registry
|
||||
err := internal.UnmarshalRegistry(v, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
registries = append(registries, registry)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
// CreateRegistry creates a new registry.
|
||||
func (service *RegistryService) CreateRegistry(registry *portainer.Registry) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
registry.ID = portainer.RegistryID(id)
|
||||
|
||||
data, err := internal.MarshalRegistry(registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(int(registry.ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRegistry updates an registry.
|
||||
func (service *RegistryService) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
||||
data, err := internal.MarshalRegistry(registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes an registry.
|
||||
func (service *RegistryService) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
err := bucket.Delete(internal.Itob(int(ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
61
api/bolt/settings_service.go
Normal file
61
api/bolt/settings_service.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
@@ -29,16 +30,13 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
|
||||
flags := &portainer.CLIFlags{
|
||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
|
||||
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
|
||||
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||
@@ -47,6 +45,10 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
// Deprecated flags
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
@@ -79,6 +81,8 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
return errNoAuthExcludeAdminPassword
|
||||
}
|
||||
|
||||
displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,3 +126,15 @@ func validateSyncInterval(syncInterval string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func displayDeprecationWarnings(templates, logo string, labels []portainer.Pair) {
|
||||
if templates != "" {
|
||||
log.Println("Warning: the --templates / -t flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
if logo != "" {
|
||||
log.Println("Warning: the --logo flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
if labels != nil {
|
||||
log.Println("Warning: the --hide-label / -l flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
|
||||
@@ -4,7 +4,6 @@ const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/portainer/portainer/file"
|
||||
"github.com/portainer/portainer/http"
|
||||
"github.com/portainer/portainer/jwt"
|
||||
"github.com/portainer/portainer/ldap"
|
||||
|
||||
"log"
|
||||
)
|
||||
@@ -68,6 +69,10 @@ func initCryptoService() portainer.CryptoService {
|
||||
return &crypto.Service{}
|
||||
}
|
||||
|
||||
func initLDAPService() portainer.LDAPService {
|
||||
return &ldap.Service{}
|
||||
}
|
||||
|
||||
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
|
||||
authorizeEndpointMgmt := true
|
||||
if externalEnpointFile != "" {
|
||||
@@ -82,16 +87,66 @@ func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpo
|
||||
return authorizeEndpointMgmt
|
||||
}
|
||||
|
||||
func initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Settings {
|
||||
return &portainer.Settings{
|
||||
HiddenLabels: *flags.Labels,
|
||||
Logo: *flags.Logo,
|
||||
func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
|
||||
return &portainer.Status{
|
||||
Analytics: !*flags.NoAnalytics,
|
||||
Authentication: !*flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
Version: portainer.APIVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func initDockerHub(dockerHubService portainer.DockerHubService) error {
|
||||
_, err := dockerHubService.DockerHub()
|
||||
if err == portainer.ErrDockerHubNotFound {
|
||||
dockerhub := &portainer.DockerHub{
|
||||
Authentication: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
return dockerHubService.StoreDockerHub(dockerhub)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
|
||||
_, err := settingsService.Settings()
|
||||
if err == portainer.ErrSettingsNotFound {
|
||||
settings := &portainer.Settings{
|
||||
LogoURL: *flags.Logo,
|
||||
DisplayExternalContributors: true,
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
LDAPSettings: portainer.LDAPSettings{
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
portainer.LDAPSearchSettings{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if *flags.Templates != "" {
|
||||
settings.TemplatesURL = *flags.Templates
|
||||
} else {
|
||||
settings.TemplatesURL = portainer.DefaultTemplatesURL
|
||||
}
|
||||
|
||||
if *flags.Labels != nil {
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
} else {
|
||||
settings.BlackListedLabels = make([]portainer.Pair, 0)
|
||||
}
|
||||
|
||||
return settingsService.StoreSettings(settings)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
|
||||
endpoints, err := endpointService.Endpoints()
|
||||
if err != nil {
|
||||
@@ -112,9 +167,21 @@ func main() {
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
|
||||
ldapService := initLDAPService()
|
||||
|
||||
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
|
||||
|
||||
settings := initSettings(authorizeEndpointMgmt, flags)
|
||||
err := initSettings(store.SettingsService, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = initDockerHub(store.DockerHubService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
|
||||
|
||||
if *flags.Endpoint != "" {
|
||||
var endpoints []portainer.Endpoint
|
||||
@@ -124,12 +191,15 @@ func main() {
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSSkipVerify: false,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
@@ -156,10 +226,9 @@ func main() {
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
Settings: settings,
|
||||
TemplatesURL: *flags.Templates,
|
||||
AuthDisabled: *flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
UserService: store.UserService,
|
||||
@@ -167,16 +236,20 @@ func main() {
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
EndpointService: store.EndpointService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
RegistryService: store.RegistryService,
|
||||
DockerHubService: store.DockerHubService,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||
err := server.Start()
|
||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,16 @@ type (
|
||||
endpointsToUpdate []*portainer.Endpoint
|
||||
endpointsToDelete []*portainer.Endpoint
|
||||
}
|
||||
|
||||
fileEndpoint struct {
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
|
||||
TLSCACert string `json:"TLSCACert,omitempty"`
|
||||
TLSCert string `json:"TLSCert,omitempty"`
|
||||
TLSKey string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -55,6 +65,28 @@ func isValidEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint {
|
||||
convertedEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, e := range fileEndpoints {
|
||||
endpoint := portainer.Endpoint{
|
||||
Name: e.Name,
|
||||
URL: e.URL,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
}
|
||||
if e.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify
|
||||
endpoint.TLSConfig.TLSCACertPath = e.TLSCACert
|
||||
endpoint.TLSConfig.TLSCertPath = e.TLSCert
|
||||
endpoint.TLSConfig.TLSKeyPath = e.TLSKey
|
||||
}
|
||||
convertedEndpoints = append(convertedEndpoints, endpoint)
|
||||
}
|
||||
|
||||
return convertedEndpoints
|
||||
}
|
||||
|
||||
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
|
||||
for idx, v := range endpoints {
|
||||
if endpoint.Name == v.Name && isValidEndpoint(&v) {
|
||||
@@ -66,22 +98,25 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint
|
||||
|
||||
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
|
||||
var endpoint *portainer.Endpoint
|
||||
if original.URL != updated.URL || original.TLS != updated.TLS ||
|
||||
(updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) ||
|
||||
(updated.TLS && original.TLSCertPath != updated.TLSCertPath) ||
|
||||
(updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) {
|
||||
if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) {
|
||||
endpoint = original
|
||||
endpoint.URL = updated.URL
|
||||
if updated.TLS {
|
||||
endpoint.TLS = true
|
||||
endpoint.TLSCACertPath = updated.TLSCACertPath
|
||||
endpoint.TLSCertPath = updated.TLSCertPath
|
||||
endpoint.TLSKeyPath = updated.TLSKeyPath
|
||||
if updated.TLSConfig.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify
|
||||
endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath
|
||||
endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath
|
||||
endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath
|
||||
} else {
|
||||
endpoint.TLS = false
|
||||
endpoint.TLSCACertPath = ""
|
||||
endpoint.TLSCertPath = ""
|
||||
endpoint.TLSKeyPath = ""
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
}
|
||||
}
|
||||
return endpoint
|
||||
@@ -117,7 +152,7 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
|
||||
}
|
||||
|
||||
for idx, endpoint := range fileEndpoints {
|
||||
if endpoint.Name == "" || endpoint.URL == "" {
|
||||
if !isValidEndpoint(&endpoint) {
|
||||
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
|
||||
continue
|
||||
}
|
||||
@@ -141,7 +176,7 @@ func (job endpointSyncJob) Sync() error {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileEndpoints []portainer.Endpoint
|
||||
var fileEndpoints []fileEndpoint
|
||||
err = json.Unmarshal(data, &fileEndpoints)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
return err
|
||||
@@ -156,7 +191,9 @@ func (job endpointSyncJob) Sync() error {
|
||||
return err
|
||||
}
|
||||
|
||||
sync := job.prepareSyncData(storedEndpoints, fileEndpoints)
|
||||
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
|
||||
|
||||
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
||||
if sync.requireSync() {
|
||||
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
|
||||
@@ -4,23 +4,38 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func CreateTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) {
|
||||
TLSConfig := &tls.Config{}
|
||||
|
||||
if config.TLS {
|
||||
if config.TLSCertPath != "" && config.TLSKeyPath != "" {
|
||||
cert, err := tls.LoadX509KeyPair(config.TLSCertPath, config.TLSKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
TLSConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
if !config.TLSSkipVerify {
|
||||
caCert, err := ioutil.ReadFile(config.TLSCACertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
TLSConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
TLSConfig.InsecureSkipVerify = config.TLSSkipVerify
|
||||
}
|
||||
caCert, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
return config, nil
|
||||
|
||||
return TLSConfig, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package portainer
|
||||
const (
|
||||
ErrUnauthorized = Error("Unauthorized")
|
||||
ErrResourceAccessDenied = Error("Access denied to resource")
|
||||
ErrResourceNotFound = Error("Unable to find resource")
|
||||
ErrUnsupportedDockerAPI = Error("Unsupported Docker API response")
|
||||
ErrMissingSecurityContext = Error("Unable to find security details in request context")
|
||||
)
|
||||
@@ -12,8 +13,10 @@ const (
|
||||
const (
|
||||
ErrUserNotFound = Error("User not found")
|
||||
ErrUserAlreadyExists = Error("User already exists")
|
||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed.")
|
||||
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
|
||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
||||
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
|
||||
ErrCannotRemoveAdmin = Error("Cannot remove the default administrator account")
|
||||
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
|
||||
)
|
||||
|
||||
// Team errors.
|
||||
@@ -41,11 +44,27 @@ const (
|
||||
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
||||
)
|
||||
|
||||
// Registry errors.
|
||||
const (
|
||||
ErrRegistryNotFound = Error("Registry not found")
|
||||
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
|
||||
)
|
||||
|
||||
// Version errors.
|
||||
const (
|
||||
ErrDBVersionNotFound = Error("DB version not found")
|
||||
)
|
||||
|
||||
// Settings errors.
|
||||
const (
|
||||
ErrSettingsNotFound = Error("Settings not found")
|
||||
)
|
||||
|
||||
// DockerHub errors.
|
||||
const (
|
||||
ErrDockerHubNotFound = Error("Dockerhub not found")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
const (
|
||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||
|
||||
@@ -6,12 +6,13 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
|
||||
TLSStorePath = "tls"
|
||||
// LDAPStorePath represents the subfolder where LDAP TLS files are stored in the TLSStorePath.
|
||||
LDAPStorePath = "ldap"
|
||||
// TLSCACertFile represents the name on disk for a TLS CA file.
|
||||
TLSCACertFile = "ca.pem"
|
||||
// TLSCertFile represents the name on disk for a TLS certificate file.
|
||||
@@ -50,11 +51,10 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r.
|
||||
func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error {
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
endpointStorePath := path.Join(TLSStorePath, ID)
|
||||
err := service.createDirectoryInStoreIfNotExist(endpointStorePath)
|
||||
// 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)
|
||||
err := service.createDirectoryInStoreIfNotExist(storePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType p
|
||||
return portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
tlsFilePath := path.Join(endpointStorePath, fileName)
|
||||
tlsFilePath := path.Join(storePath, fileName)
|
||||
err = service.createFileInStore(tlsFilePath, r)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -80,7 +80,7 @@ func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType p
|
||||
}
|
||||
|
||||
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
|
||||
func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType) (string, error) {
|
||||
func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSFileType) (string, error) {
|
||||
var fileName string
|
||||
switch fileType {
|
||||
case portainer.TLSFileCA:
|
||||
@@ -92,15 +92,36 @@ func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileT
|
||||
default:
|
||||
return "", portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil
|
||||
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
|
||||
}
|
||||
|
||||
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
|
||||
func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID)
|
||||
err := os.RemoveAll(endpointPath)
|
||||
// DeleteTLSFiles deletes a folder in the TLS store path.
|
||||
func (service *Service) DeleteTLSFiles(folder string) error {
|
||||
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
||||
err := os.RemoveAll(storePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTLSFile deletes a specific TLS file from a folder.
|
||||
func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileType) error {
|
||||
var fileName string
|
||||
switch fileType {
|
||||
case portainer.TLSFileCA:
|
||||
fileName = TLSCACertFile
|
||||
case portainer.TLSFileCert:
|
||||
fileName = TLSCertFile
|
||||
case portainer.TLSFileKey:
|
||||
fileName = TLSKeyFile
|
||||
default:
|
||||
return portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
|
||||
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// errorResponse is a generic response for sending a error.
|
||||
@@ -21,10 +20,3 @@ func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log.
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
|
||||
}
|
||||
|
||||
// WriteMethodNotAllowedResponse writes an error message to the response and sets the Allow header.
|
||||
func WriteMethodNotAllowedResponse(w http.ResponseWriter, allowedMethods []string) {
|
||||
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ import (
|
||||
// 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
|
||||
Logger *log.Logger
|
||||
authDisabled bool
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -42,17 +44,23 @@ func NewAuthHandler(bouncer *security.RequestBouncer, authDisabled bool) *AuthHa
|
||||
authDisabled: authDisabled,
|
||||
}
|
||||
h.Handle("/auth",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth)))
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
|
||||
return
|
||||
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
|
||||
@@ -75,24 +83,39 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
u, err := handler.UserService.UserByUsername(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
|
||||
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)
|
||||
@@ -101,12 +124,3 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
|
||||
}
|
||||
|
||||
type postAuthRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
type postAuthResponse struct {
|
||||
JWT string `json:"jwt"`
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.PathPrefix("/{id}/").Handler(
|
||||
h.PathPrefix("/{id}/docker").Handler(
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
|
||||
return h
|
||||
}
|
||||
@@ -90,5 +90,5 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
|
||||
}
|
||||
}
|
||||
|
||||
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
|
||||
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
89
api/http/handler/dockerhub.go
Normal file
89
api/http/handler/dockerhub.go
Normal file
@@ -0,0 +1,89 @@
|
||||
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 NewDockerHubHandler.
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,35 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
TLSSkipClientVerify bool
|
||||
}
|
||||
|
||||
postEndpointsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putEndpointAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"-"`
|
||||
AuthorizedTeams []int `valid:"-"`
|
||||
}
|
||||
|
||||
putEndpointsRequest struct {
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
TLSSkipVerify bool `valid:"-"`
|
||||
TLSSkipClientVerify bool `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetEndpoints handles GET requests on /endpoints
|
||||
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
@@ -98,10 +127,13 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
PublicURL: req.PublicURL,
|
||||
TLS: req.TLS,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
PublicURL: req.PublicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: req.TLS,
|
||||
TLSSkipVerify: req.TLSSkipVerify,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
@@ -113,12 +145,20 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
if req.TLS {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||
endpoint.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
|
||||
if !req.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
@@ -129,17 +169,6 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
||||
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
type postEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool
|
||||
}
|
||||
|
||||
type postEndpointsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
@@ -218,11 +247,6 @@ func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r
|
||||
}
|
||||
}
|
||||
|
||||
type putEndpointAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"-"`
|
||||
AuthorizedTeams []int `valid:"-"`
|
||||
}
|
||||
|
||||
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
@@ -272,20 +296,36 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
||||
endpoint.PublicURL = req.PublicURL
|
||||
}
|
||||
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
if req.TLS {
|
||||
endpoint.TLS = true
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||
endpoint.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
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.TLS = false
|
||||
endpoint.TLSCACertPath = ""
|
||||
endpoint.TLSCertPath = ""
|
||||
endpoint.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = true
|
||||
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
|
||||
@@ -305,13 +345,6 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
||||
}
|
||||
}
|
||||
|
||||
type putEndpointsRequest struct {
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
}
|
||||
|
||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
@@ -346,8 +379,8 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
if endpoint.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
|
||||
if endpoint.TLSConfig.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileHandler represents an HTTP API handler for managing static files.
|
||||
type FileHandler struct {
|
||||
http.Handler
|
||||
Logger *log.Logger
|
||||
allowedDirectories map[string]bool
|
||||
}
|
||||
|
||||
// NewFileHandler returns a new instance of FileHandler.
|
||||
func NewFileHandler(assetPath string) *FileHandler {
|
||||
h := &FileHandler{
|
||||
Handler: http.FileServer(http.Dir(assetPath)),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
allowedDirectories: map[string]bool{
|
||||
"/": true,
|
||||
"/css": true,
|
||||
"/js": true,
|
||||
"/images": true,
|
||||
"/fonts": true,
|
||||
"/ico": true,
|
||||
},
|
||||
}
|
||||
return h
|
||||
}
|
||||
@@ -27,11 +45,18 @@ func isHTML(acceptContent []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (fileHandler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (handler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
requestDirectory := path.Dir(r.URL.Path)
|
||||
if !handler.allowedDirectories[requestDirectory] {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceNotFound, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !isHTML(r.Header["Accept"]) {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
fileHandler.Handler.ServeHTTP(w, r)
|
||||
|
||||
handler.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ type Handler struct {
|
||||
TeamHandler *TeamHandler
|
||||
TeamMembershipHandler *TeamMembershipHandler
|
||||
EndpointHandler *EndpointHandler
|
||||
RegistryHandler *RegistryHandler
|
||||
DockerHubHandler *DockerHubHandler
|
||||
ResourceHandler *ResourceHandler
|
||||
StatusHandler *StatusHandler
|
||||
SettingsHandler *SettingsHandler
|
||||
TemplatesHandler *TemplatesHandler
|
||||
DockerHandler *DockerHandler
|
||||
@@ -33,40 +36,48 @@ const (
|
||||
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
||||
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
|
||||
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
|
||||
// ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse
|
||||
// ErrEmptyResponseBody = portainer.Error("Empty response body")
|
||||
)
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/auth") {
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/auth"):
|
||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
|
||||
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/teams") {
|
||||
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/team_memberships") {
|
||||
http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/endpoints") {
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/resource_controls") {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
||||
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
||||
if strings.Contains(r.URL.Path, "/docker") {
|
||||
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
|
||||
} else {
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/resource_controls"):
|
||||
http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/settings"):
|
||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/status"):
|
||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/upload") {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
||||
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/websocket") {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/users"):
|
||||
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/teams"):
|
||||
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):
|
||||
http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/websocket"):
|
||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
|
||||
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/") {
|
||||
case strings.HasPrefix(r.URL.Path, "/"):
|
||||
h.FileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// encodeJSON encodes v to w in JSON format. Error() is called if encoding fails.
|
||||
// encodeJSON encodes v to w in JSON format. WriteErrorResponse() is called if encoding fails.
|
||||
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, logger)
|
||||
|
||||
314
api/http/handler/registry.go
Normal file
314
api/http/handler/registry.go
Normal file
@@ -0,0 +1,314 @@
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,23 @@ func NewResourceHandler(bouncer *security.RequestBouncer) *ResourceHandler {
|
||||
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
|
||||
@@ -61,6 +78,10 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht
|
||||
resourceControlType = portainer.ServiceResourceControl
|
||||
case "volume":
|
||||
resourceControlType = portainer.VolumeResourceControl
|
||||
case "network":
|
||||
resourceControlType = portainer.NetworkResourceControl
|
||||
case "secret":
|
||||
resourceControlType = portainer.SecretResourceControl
|
||||
default:
|
||||
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
@@ -121,22 +142,13 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht
|
||||
|
||||
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type postResourcesRequest struct {
|
||||
ResourceID string `valid:"required"`
|
||||
Type string `valid:"required"`
|
||||
AdministratorsOnly bool `valid:"-"`
|
||||
Users []int `valid:"-"`
|
||||
Teams []int `valid:"-"`
|
||||
SubResourceIDs []string `valid:"-"`
|
||||
}
|
||||
|
||||
// handlePutResources handles PUT requests on /resources/:id
|
||||
func (handler *ResourceHandler) handlePutResources(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
@@ -210,12 +222,6 @@ func (handler *ResourceHandler) handlePutResources(w http.ResponseWriter, r *htt
|
||||
}
|
||||
}
|
||||
|
||||
type putResourcesRequest struct {
|
||||
AdministratorsOnly bool `valid:"-"`
|
||||
Users []int `valid:"-"`
|
||||
Teams []int `valid:"-"`
|
||||
}
|
||||
|
||||
// handleDeleteResources handles DELETE requests on /resources/:id
|
||||
func (handler *ResourceHandler) handleDeleteResources(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/file"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
@@ -12,32 +16,154 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||
// SettingsHandler represents an HTTP API handler for managing Settings.
|
||||
type SettingsHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
settings *portainer.Settings
|
||||
Logger *log.Logger
|
||||
SettingsService portainer.SettingsService
|
||||
LDAPService portainer.LDAPService
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewSettingsHandler returns a new instance of SettingsHandler.
|
||||
func NewSettingsHandler(bouncer *security.RequestBouncer, settings *portainer.Settings) *SettingsHandler {
|
||||
// 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),
|
||||
settings: settings,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetSettings)))
|
||||
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"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
}
|
||||
|
||||
putSettingsRequest struct {
|
||||
TemplatesURL string `valid:"required"`
|
||||
LogoURL string `valid:""`
|
||||
BlackListedLabels []portainer.Pair `valid:""`
|
||||
DisplayExternalContributors bool `valid:""`
|
||||
AuthenticationMethod int `valid:"required"`
|
||||
LDAPSettings portainer.LDAPSettings `valid:""`
|
||||
}
|
||||
|
||||
putSettingsLDAPCheckRequest struct {
|
||||
LDAPSettings portainer.LDAPSettings `valid:""`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetSettings handles GET requests on /settings
|
||||
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet})
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, handler.settings, handler.Logger)
|
||||
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,
|
||||
DisplayExternalContributors: settings.DisplayExternalContributors,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
}
|
||||
|
||||
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,
|
||||
DisplayExternalContributors: req.DisplayExternalContributors,
|
||||
LDAPSettings: req.LDAPSettings,
|
||||
}
|
||||
|
||||
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(file.LDAPStorePath, portainer.TLSFileCA)
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
|
||||
err := handler.FileService.DeleteTLSFiles(file.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(file.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
|
||||
}
|
||||
}
|
||||
|
||||
38
api/http/handler/status.go
Normal file
38
api/http/handler/status.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// StatusHandler represents an HTTP API handler for managing Status.
|
||||
type StatusHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
Status *portainer.Status
|
||||
}
|
||||
|
||||
// NewStatusHandler returns a new instance of StatusHandler.
|
||||
func NewStatusHandler(bouncer *security.RequestBouncer, status *portainer.Status) *StatusHandler {
|
||||
h := &StatusHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
Status: status,
|
||||
}
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetStatus))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetStatus handles GET requests on /status
|
||||
func (handler *StatusHandler) handleGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
encodeJSON(w, handler.Status, handler.Logger)
|
||||
return
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func NewTeamHandler(bouncer *security.RequestBouncer) *TeamHandler {
|
||||
h.Handle("/teams",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostTeams))).Methods(http.MethodPost)
|
||||
h.Handle("/teams",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeam))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
@@ -47,6 +47,20 @@ func NewTeamHandler(bouncer *security.RequestBouncer) *TeamHandler {
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postTeamsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
}
|
||||
|
||||
postTeamsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putTeamRequest struct {
|
||||
Name string `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostTeams handles POST requests on /teams
|
||||
func (handler *TeamHandler) handlePostTeams(w http.ResponseWriter, r *http.Request) {
|
||||
var req postTeamsRequest
|
||||
@@ -84,23 +98,23 @@ func (handler *TeamHandler) handlePostTeams(w http.ResponseWriter, r *http.Reque
|
||||
encodeJSON(w, &postTeamsResponse{ID: int(team.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
type postTeamsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
type postTeamsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
}
|
||||
|
||||
// handleGetTeams handles GET requests on /teams
|
||||
func (handler *TeamHandler) handleGetTeams(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := handler.TeamService.Teams()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, teams, handler.Logger)
|
||||
filteredTeams := security.FilterUserTeams(teams, securityContext)
|
||||
|
||||
encodeJSON(w, filteredTeams, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetTeam handles GET requests on /teams/:id
|
||||
@@ -181,10 +195,6 @@ func (handler *TeamHandler) handlePutTeam(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
type putTeamRequest struct {
|
||||
Name string `valid:"-"`
|
||||
}
|
||||
|
||||
// handleDeleteTeam handles DELETE requests on /teams/:id
|
||||
func (handler *TeamHandler) handleDeleteTeam(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -42,6 +42,24 @@ func NewTeamMembershipHandler(bouncer *security.RequestBouncer) *TeamMembershipH
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postTeamMembershipsRequest struct {
|
||||
UserID int `valid:"required"`
|
||||
TeamID int `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
postTeamMembershipsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putTeamMembershipRequest struct {
|
||||
UserID int `valid:"required"`
|
||||
TeamID int `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostTeamMemberships handles POST requests on /team_memberships
|
||||
func (handler *TeamMembershipHandler) handlePostTeamMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
@@ -100,16 +118,6 @@ func (handler *TeamMembershipHandler) handlePostTeamMemberships(w http.ResponseW
|
||||
encodeJSON(w, &postTeamMembershipsResponse{ID: int(membership.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
type postTeamMembershipsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
type postTeamMembershipsRequest struct {
|
||||
UserID int `valid:"required"`
|
||||
TeamID int `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
// handleGetTeamsMemberships handles GET requests on /team_memberships
|
||||
func (handler *TeamMembershipHandler) handleGetTeamsMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
@@ -195,12 +203,6 @@ func (handler *TeamMembershipHandler) handlePutTeamMembership(w http.ResponseWri
|
||||
}
|
||||
}
|
||||
|
||||
type putTeamMembershipRequest struct {
|
||||
UserID int `valid:"required"`
|
||||
TeamID int `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
// handleDeleteTeamMembership handles DELETE requests on /team_memberships/:id
|
||||
func (handler *TeamMembershipHandler) handleDeleteTeamMembership(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
@@ -14,33 +15,27 @@ import (
|
||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||
type TemplatesHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
containerTemplatesURL string
|
||||
Logger *log.Logger
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
const (
|
||||
containerTemplatesURLLinuxServerIo = "http://tools.linuxserver.io/portainer.json"
|
||||
containerTemplatesURLLinuxServerIo = "https://tools.linuxserver.io/portainer.json"
|
||||
)
|
||||
|
||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||
func NewTemplatesHandler(bouncer *security.RequestBouncer, containerTemplatesURL string) *TemplatesHandler {
|
||||
func NewTemplatesHandler(bouncer *security.RequestBouncer) *TemplatesHandler {
|
||||
h := &TemplatesHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
containerTemplatesURL: containerTemplatesURL,
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/templates",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates)))
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetTemplates handles GET requests on /templates?key=<key>
|
||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
key := r.FormValue("key")
|
||||
if key == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
@@ -49,7 +44,12 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht
|
||||
|
||||
var templatesURL string
|
||||
if key == "containers" {
|
||||
templatesURL = handler.containerTemplatesURL
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
templatesURL = settings.TemplatesURL
|
||||
} else if key == "linuxserver.io" {
|
||||
templatesURL = containerTemplatesURLLinuxServerIo
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -26,23 +25,19 @@ func NewUploadHandler(bouncer *security.RequestBouncer) *UploadHandler {
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUploadTLS)))
|
||||
h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostUploadTLS))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
// handlePostUploadTLS handles POST requests on /upload/tls/{certificate:(?:ca|cert|key)}?folder=folder
|
||||
func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
endpointID := vars["endpointID"]
|
||||
certificate := vars["certificate"]
|
||||
ID, err := strconv.Atoi(endpointID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
|
||||
folder := r.FormValue("folder")
|
||||
if folder == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,7 +61,7 @@ func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file)
|
||||
err = handler.FileService.StoreTLSFile(folder, fileType, file)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
|
||||
@@ -26,6 +26,7 @@ type UserHandler struct {
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewUserHandler returns a new instance of UserHandler.
|
||||
@@ -46,18 +47,46 @@ func NewUserHandler(bouncer *security.RequestBouncer) *UserHandler {
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/memberships",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetMemberships))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/teams",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUserPasswd)))
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUserPasswd))).Methods(http.MethodPost)
|
||||
h.Handle("/users/admin/check",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetAdminCheck)))
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetAdminCheck))).Methods(http.MethodGet)
|
||||
h.Handle("/users/admin/init",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAdminInit)))
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAdminInit))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postUsersRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:""`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
postUsersResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
postUserPasswdRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
postUserPasswdResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
putUserRequest struct {
|
||||
Password string `valid:"-"`
|
||||
Role int `valid:"-"`
|
||||
}
|
||||
|
||||
postAdminInitRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostUsers handles POST requests on /users
|
||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var req postUsersRequest
|
||||
@@ -93,13 +122,6 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
var role portainer.UserRole
|
||||
if req.Role == 1 {
|
||||
role = portainer.AdministratorRole
|
||||
} else {
|
||||
role = portainer.StandardUserRole
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername(req.Username)
|
||||
if err != nil && err != portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
@@ -110,16 +132,32 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
var role portainer.UserRole
|
||||
if req.Role == 1 {
|
||||
role = portainer.AdministratorRole
|
||||
} else {
|
||||
role = portainer.StandardUserRole
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: req.Username,
|
||||
Role: role,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
@@ -129,16 +167,6 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
|
||||
encodeJSON(w, &postUsersResponse{ID: int(user.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
type postUsersResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
type postUsersRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
// handleGetUsers handles GET requests on /users
|
||||
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
@@ -164,11 +192,6 @@ func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:id/passwd
|
||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
@@ -210,14 +233,6 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
|
||||
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
|
||||
}
|
||||
|
||||
type postUserPasswdRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
type postUserPasswdResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
// handleGetUser handles GET requests on /users/:id
|
||||
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
@@ -317,18 +332,8 @@ func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
type putUserRequest struct {
|
||||
Password string `valid:"-"`
|
||||
Role int `valid:"-"`
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles GET requests on /users/admin/check
|
||||
// handleGetAdminCheck handles GET requests on /users/admin/check
|
||||
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
@@ -342,11 +347,6 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R
|
||||
|
||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
var req postAdminInitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
@@ -359,10 +359,14 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(users) == 0 {
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Username: req.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
@@ -376,18 +380,10 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type postAdminInitRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
// handleDeleteUser handles DELETE requests on /users/:id
|
||||
@@ -401,6 +397,22 @@ func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
if userID == 1 {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveAdmin, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.ID == portainer.UserID(userID) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrAdminCannotRemoveSelf, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||
|
||||
if err == portainer.ErrUserNotFound {
|
||||
@@ -454,37 +466,3 @@ func (handler *UserHandler) handleGetMemberships(w http.ResponseWriter, r *http.
|
||||
|
||||
encodeJSON(w, memberships, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetTeams handles GET requests on /users/:id/teams
|
||||
func (handler *UserHandler) handleGetTeams(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
uid, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
userID := portainer.UserID(uid)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedUserManagement(userID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := handler.TeamService.Teams()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredTeams := security.FilterUserTeams(teams, securityContext)
|
||||
|
||||
encodeJSON(w, filteredTeams, handler.Logger)
|
||||
}
|
||||
|
||||
@@ -71,10 +71,8 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||
|
||||
// Should not be managed here
|
||||
var tlsConfig *tls.Config
|
||||
if endpoint.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath,
|
||||
endpoint.TLSCertPath,
|
||||
endpoint.TLSKeyPath)
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||
return
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
|
||||
// containerListOperation extracts the response as a JSON object, loop through the containers array
|
||||
// decorate and/or filter the containers based on resource controls before rewriting the response
|
||||
func containerListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func containerListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// ContainerList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
@@ -24,22 +24,30 @@ func containerListOperation(request *http.Request, response *http.Response, oper
|
||||
return err
|
||||
}
|
||||
|
||||
if operationContext.isAdmin {
|
||||
responseArray, err = decorateContainerList(responseArray, operationContext.resourceControls)
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterContainerList(responseArray, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
|
||||
responseArray, err = filterContainerList(responseArray, executor.operationContext.resourceControls,
|
||||
executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.labelBlackList != nil {
|
||||
responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// containerInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the container based on resource control (check are done based on the containerID and optional Swarm service ID)
|
||||
// and either rewrite an access denied response or a decorated container.
|
||||
func containerInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func containerInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// ContainerInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
@@ -52,9 +60,10 @@ func containerInspectOperation(request *http.Request, response *http.Response, o
|
||||
}
|
||||
containerID := responseObject[containerIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(containerID, operationContext.resourceControls)
|
||||
resourceControl := getResourceControlByResourceID(containerID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
|
||||
executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
@@ -64,9 +73,10 @@ func containerInspectOperation(request *http.Request, response *http.Response, o
|
||||
containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject)
|
||||
if containerLabels != nil && containerLabels[containerLabelForServiceIdentifier] != nil {
|
||||
serviceID := containerLabels[containerLabelForServiceIdentifier].(string)
|
||||
resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls)
|
||||
resourceControl := getResourceControlByResourceID(serviceID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
|
||||
executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
|
||||
@@ -82,6 +82,54 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer
|
||||
return decoratedServiceData, nil
|
||||
}
|
||||
|
||||
// decorateNetworkList loops through all networks and will decorate any network with an existing resource control.
|
||||
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
func decorateNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||
decoratedNetworkData := make([]interface{}, 0)
|
||||
|
||||
for _, network := range networkData {
|
||||
|
||||
networkObject := network.(map[string]interface{})
|
||||
if networkObject[networkIdentifier] == nil {
|
||||
return nil, ErrDockerNetworkIdentifierNotFound
|
||||
}
|
||||
|
||||
networkID := networkObject[networkIdentifier].(string)
|
||||
resourceControl := getResourceControlByResourceID(networkID, resourceControls)
|
||||
if resourceControl != nil {
|
||||
networkObject = decorateObject(networkObject, resourceControl)
|
||||
}
|
||||
|
||||
decoratedNetworkData = append(decoratedNetworkData, networkObject)
|
||||
}
|
||||
|
||||
return decoratedNetworkData, nil
|
||||
}
|
||||
|
||||
// decorateSecretList loops through all secrets and will decorate any secret with an existing resource control.
|
||||
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||
func decorateSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||
decoratedSecretData := make([]interface{}, 0)
|
||||
|
||||
for _, secret := range secretData {
|
||||
|
||||
secretObject := secret.(map[string]interface{})
|
||||
if secretObject[secretIdentifier] == nil {
|
||||
return nil, ErrDockerSecretIdentifierNotFound
|
||||
}
|
||||
|
||||
secretID := secretObject[secretIdentifier].(string)
|
||||
resourceControl := getResourceControlByResourceID(secretID, resourceControls)
|
||||
if resourceControl != nil {
|
||||
secretObject = decorateObject(secretObject, resourceControl)
|
||||
}
|
||||
|
||||
decoratedSecretData = append(decoratedSecretData, secretObject)
|
||||
}
|
||||
|
||||
return decoratedSecretData, nil
|
||||
}
|
||||
|
||||
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
||||
metadata := make(map[string]interface{})
|
||||
metadata["ResourceControl"] = resourceControl
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
type proxyFactory struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||
@@ -23,7 +24,7 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
u.Scheme = "https"
|
||||
proxy := factory.createReverseProxy(u)
|
||||
config, err := crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
|
||||
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -37,6 +38,7 @@ func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
|
||||
transport := &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
dockerTransport: newSocketTransport(path),
|
||||
}
|
||||
proxy.Transport = transport
|
||||
@@ -48,6 +50,7 @@ func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReversePro
|
||||
transport := &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
dockerTransport: newHTTPTransport(),
|
||||
}
|
||||
proxy.Transport = transport
|
||||
|
||||
@@ -65,6 +65,27 @@ func filterContainerList(containerData []interface{}, resourceControls []portain
|
||||
return filteredContainerData, nil
|
||||
}
|
||||
|
||||
// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains
|
||||
// any labels in the labels black list.
|
||||
func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) {
|
||||
filteredContainerData := make([]interface{}, 0)
|
||||
|
||||
for _, container := range containerData {
|
||||
containerObject := container.(map[string]interface{})
|
||||
|
||||
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
|
||||
if containerLabels != nil {
|
||||
if !containerHasBlackListedLabel(containerLabels, labelBlackList) {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
} else {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredContainerData, nil
|
||||
}
|
||||
|
||||
// filterServiceList loops through all services, filters services without any resource control (public resources) or with
|
||||
// any resource control giving access to the user (these services will be decorated).
|
||||
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
@@ -89,3 +110,76 @@ func filterServiceList(serviceData []interface{}, resourceControls []portainer.R
|
||||
|
||||
return filteredServiceData, nil
|
||||
}
|
||||
|
||||
// filterNetworkList loops through all networks, filters networks without any resource control (public resources) or with
|
||||
// any resource control giving access to the user (these networks will be decorated).
|
||||
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
func filterNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
|
||||
filteredNetworkData := make([]interface{}, 0)
|
||||
|
||||
for _, network := range networkData {
|
||||
networkObject := network.(map[string]interface{})
|
||||
if networkObject[networkIdentifier] == nil {
|
||||
return nil, ErrDockerNetworkIdentifierNotFound
|
||||
}
|
||||
|
||||
networkID := networkObject[networkIdentifier].(string)
|
||||
resourceControl := getResourceControlByResourceID(networkID, resourceControls)
|
||||
if resourceControl == nil {
|
||||
filteredNetworkData = append(filteredNetworkData, networkObject)
|
||||
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
|
||||
networkObject = decorateObject(networkObject, resourceControl)
|
||||
filteredNetworkData = append(filteredNetworkData, networkObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredNetworkData, nil
|
||||
}
|
||||
|
||||
// filterSecretList loops through all secrets, filters secrets without any resource control (public resources) or with
|
||||
// any resource control giving access to the user (these secrets will be decorated).
|
||||
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||
func filterSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
|
||||
filteredSecretData := make([]interface{}, 0)
|
||||
|
||||
for _, secret := range secretData {
|
||||
secretObject := secret.(map[string]interface{})
|
||||
if secretObject[secretIdentifier] == nil {
|
||||
return nil, ErrDockerSecretIdentifierNotFound
|
||||
}
|
||||
|
||||
secretID := secretObject[secretIdentifier].(string)
|
||||
resourceControl := getResourceControlByResourceID(secretID, resourceControls)
|
||||
if resourceControl == nil {
|
||||
filteredSecretData = append(filteredSecretData, secretObject)
|
||||
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
|
||||
secretObject = decorateObject(secretObject, resourceControl)
|
||||
filteredSecretData = append(filteredSecretData, secretObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredSecretData, nil
|
||||
}
|
||||
|
||||
// filterTaskList loops through all tasks, filters tasks without any resource control (public resources) or with
|
||||
// any resource control giving access to the user based on the associated service identifier.
|
||||
// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
func filterTaskList(taskData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
|
||||
filteredTaskData := make([]interface{}, 0)
|
||||
|
||||
for _, task := range taskData {
|
||||
taskObject := task.(map[string]interface{})
|
||||
if taskObject[taskServiceIdentifier] == nil {
|
||||
return nil, ErrDockerTaskServiceIdentifierNotFound
|
||||
}
|
||||
|
||||
serviceID := taskObject[taskServiceIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(serviceID, resourceControls)
|
||||
if resourceControl == nil || (resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl)) {
|
||||
filteredTaskData = append(filteredTaskData, taskObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTaskData, nil
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@ type Manager struct {
|
||||
}
|
||||
|
||||
// NewManager initializes a new proxy Service
|
||||
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService) *Manager {
|
||||
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager {
|
||||
return &Manager{
|
||||
proxies: cmap.New(),
|
||||
proxyFactory: &proxyFactory{
|
||||
ResourceControlService: resourceControlService,
|
||||
TeamMembershipService: teamMembershipService,
|
||||
SettingsService: settingsService,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -36,7 +37,7 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
|
||||
}
|
||||
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLS {
|
||||
if endpoint.TLSConfig.TLS {
|
||||
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
66
api/http/proxy/networks.go
Normal file
66
api/http/proxy/networks.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerNetworkIdentifierNotFound defines an error raised when Portainer is unable to find a network identifier
|
||||
ErrDockerNetworkIdentifierNotFound = portainer.Error("Docker network identifier not found")
|
||||
networkIdentifier = "Id"
|
||||
)
|
||||
|
||||
// networkListOperation extracts the response as a JSON object, loop through the networks array
|
||||
// decorate and/or filter the networks based on resource controls before rewriting the response
|
||||
func networkListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// NetworkList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
responseArray, err := getResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterNetworkList(responseArray, executor.operationContext.resourceControls,
|
||||
executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// networkInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the network based on resource control and either rewrite an access denied response
|
||||
// or a decorated network.
|
||||
func networkInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// NetworkInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if responseObject[networkIdentifier] == nil {
|
||||
return ErrDockerNetworkIdentifierNotFound
|
||||
}
|
||||
networkID := responseObject[networkIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(networkID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
|
||||
executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
@@ -85,6 +85,11 @@ func rewriteResponse(response *http.Response, newResponseData interface{}, statu
|
||||
response.StatusCode = statusCode
|
||||
response.Body = body
|
||||
response.ContentLength = int64(len(jsonData))
|
||||
|
||||
if response.Header == nil {
|
||||
response.Header = make(http.Header)
|
||||
}
|
||||
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
67
api/http/proxy/secrets.go
Normal file
67
api/http/proxy/secrets.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerSecretIdentifierNotFound defines an error raised when Portainer is unable to find a secret identifier
|
||||
ErrDockerSecretIdentifierNotFound = portainer.Error("Docker secret identifier not found")
|
||||
secretIdentifier = "ID"
|
||||
)
|
||||
|
||||
// secretListOperation extracts the response as a JSON object, loop through the secrets array
|
||||
// decorate and/or filter the secrets based on resource controls before rewriting the response
|
||||
func secretListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
|
||||
// SecretList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||
responseArray, err := getResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterSecretList(responseArray, executor.operationContext.resourceControls,
|
||||
executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// secretInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the secret based on resource control (check are done based on the secretID and optional Swarm service ID)
|
||||
// and either rewrite an access denied response or a decorated secret.
|
||||
func secretInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// SecretInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if responseObject[secretIdentifier] == nil {
|
||||
return ErrDockerSecretIdentifierNotFound
|
||||
}
|
||||
secretID := responseObject[secretIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(secretID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
|
||||
executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const (
|
||||
|
||||
// serviceListOperation extracts the response as a JSON array, loop through the service array
|
||||
// decorate and/or filter the services based on resource controls before rewriting the response
|
||||
func serviceListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func serviceListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// ServiceList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
@@ -23,10 +23,10 @@ func serviceListOperation(request *http.Request, response *http.Response, operat
|
||||
return err
|
||||
}
|
||||
|
||||
if operationContext.isAdmin {
|
||||
responseArray, err = decorateServiceList(responseArray, operationContext.resourceControls)
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterServiceList(responseArray, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
|
||||
responseArray, err = filterServiceList(responseArray, executor.operationContext.resourceControls, executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -38,7 +38,7 @@ func serviceListOperation(request *http.Request, response *http.Response, operat
|
||||
// serviceInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the service based on resource control and either rewrite an access denied response
|
||||
// or a decorated service.
|
||||
func serviceInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func serviceInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// ServiceInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
@@ -51,9 +51,9 @@ func serviceInspectOperation(request *http.Request, response *http.Response, ope
|
||||
}
|
||||
serviceID := responseObject[serviceIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls)
|
||||
resourceControl := getResourceControlByResourceID(serviceID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
|
||||
@@ -34,6 +34,9 @@ func (proxy *socketProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
|
||||
36
api/http/proxy/tasks.go
Normal file
36
api/http/proxy/tasks.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerTaskServiceIdentifierNotFound defines an error raised when Portainer is unable to find the service identifier associated to a task
|
||||
ErrDockerTaskServiceIdentifierNotFound = portainer.Error("Docker task service identifier not found")
|
||||
taskServiceIdentifier = "ServiceID"
|
||||
)
|
||||
|
||||
// taskListOperation extracts the response as a JSON object, loop through the tasks array
|
||||
// and filter the tasks based on resource controls before rewriting the response
|
||||
func taskListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
|
||||
// TaskList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
responseArray, err := getResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !executor.operationContext.isAdmin {
|
||||
responseArray, err = filterTaskList(responseArray, executor.operationContext.resourceControls,
|
||||
executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type (
|
||||
dockerTransport *http.Transport
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
restrictedOperationContext struct {
|
||||
isAdmin bool
|
||||
@@ -22,7 +23,11 @@ type (
|
||||
userTeamIDs []portainer.TeamID
|
||||
resourceControls []portainer.ResourceControl
|
||||
}
|
||||
restrictedOperationRequest func(*http.Request, *http.Response, *restrictedOperationContext) error
|
||||
operationExecutor struct {
|
||||
operationContext *restrictedOperationContext
|
||||
labelBlackList []portainer.Pair
|
||||
}
|
||||
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
|
||||
)
|
||||
|
||||
func newSocketTransport(socketPath string) *http.Transport {
|
||||
@@ -48,19 +53,29 @@ func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Resp
|
||||
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||
path := request.URL.Path
|
||||
|
||||
if strings.HasPrefix(path, "/containers") {
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/containers"):
|
||||
return p.proxyContainerRequest(request)
|
||||
} else if strings.HasPrefix(path, "/services") {
|
||||
case strings.HasPrefix(path, "/services"):
|
||||
return p.proxyServiceRequest(request)
|
||||
} else if strings.HasPrefix(path, "/volumes") {
|
||||
case strings.HasPrefix(path, "/volumes"):
|
||||
return p.proxyVolumeRequest(request)
|
||||
case strings.HasPrefix(path, "/networks"):
|
||||
return p.proxyNetworkRequest(request)
|
||||
case strings.HasPrefix(path, "/secrets"):
|
||||
return p.proxySecretRequest(request)
|
||||
case strings.HasPrefix(path, "/swarm"):
|
||||
return p.proxySwarmRequest(request)
|
||||
case strings.HasPrefix(path, "/nodes"):
|
||||
return p.proxyNodeRequest(request)
|
||||
case strings.HasPrefix(path, "/tasks"):
|
||||
return p.proxyTaskRequest(request)
|
||||
default:
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
||||
// return p.executeDockerRequest(request)
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/containers/create":
|
||||
return p.executeDockerRequest(request)
|
||||
@@ -69,7 +84,7 @@ func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Res
|
||||
return p.administratorOperation(request)
|
||||
|
||||
case "/containers/json":
|
||||
return p.rewriteOperation(request, containerListOperation)
|
||||
return p.rewriteOperationWithLabelFiltering(request, containerListOperation)
|
||||
|
||||
default:
|
||||
// This section assumes /containers/**
|
||||
@@ -96,9 +111,6 @@ func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Respo
|
||||
case "/services/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/volumes/prune":
|
||||
return p.administratorOperation(request)
|
||||
|
||||
case "/services":
|
||||
return p.rewriteOperation(request, serviceListOperation)
|
||||
|
||||
@@ -142,6 +154,67 @@ func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Respon
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/networks/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/networks":
|
||||
return p.rewriteOperation(request, networkListOperation)
|
||||
|
||||
default:
|
||||
// assume /networks/{id}
|
||||
if request.Method == http.MethodGet {
|
||||
return p.rewriteOperation(request, networkInspectOperation)
|
||||
}
|
||||
networkID := path.Base(requestPath)
|
||||
return p.restrictedOperation(request, networkID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/secrets/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/secrets":
|
||||
return p.rewriteOperation(request, secretListOperation)
|
||||
|
||||
default:
|
||||
// assume /secrets/{id}
|
||||
if request.Method == http.MethodGet {
|
||||
return p.rewriteOperation(request, secretInspectOperation)
|
||||
}
|
||||
secretID := path.Base(requestPath)
|
||||
return p.restrictedOperation(request, secretID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
// assume /nodes/{id}
|
||||
if path.Base(requestPath) != "nodes" {
|
||||
return p.administratorOperation(request)
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
|
||||
return p.administratorOperation(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/tasks":
|
||||
return p.rewriteOperation(request, taskListOperation)
|
||||
default:
|
||||
// assume /tasks/{id}
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
// restrictedOperation ensures that the current user has the required authorizations
|
||||
// before executing the original request.
|
||||
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
||||
@@ -177,9 +250,69 @@ func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID s
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
// rewriteOperation will create a new operation context with data that will be used
|
||||
// to decorate the original request's response as well as retrieve all the black listed labels
|
||||
// to filter the resources.
|
||||
func (p *proxyTransport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||
operationContext, err := p.createOperationContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings, err := p.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executor := &operationExecutor{
|
||||
operationContext: operationContext,
|
||||
labelBlackList: settings.BlackListedLabels,
|
||||
}
|
||||
|
||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
||||
}
|
||||
|
||||
// rewriteOperation will create a new operation context with data that will be used
|
||||
// to decorate the original request's response.
|
||||
func (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||
operationContext, err := p.createOperationContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executor := &operationExecutor{
|
||||
operationContext: operationContext,
|
||||
}
|
||||
|
||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
||||
response, err := p.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = operation(request, response, executor)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// administratorOperation ensures that the user has administrator privileges
|
||||
// before executing the original request.
|
||||
func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse()
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) {
|
||||
var err error
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
@@ -212,26 +345,5 @@ func (p *proxyTransport) rewriteOperation(request *http.Request, operation restr
|
||||
operationContext.userTeamIDs = userTeamIDs
|
||||
}
|
||||
|
||||
response, err := p.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = operation(request, response, operationContext)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// administratorOperation ensures that the user has administrator privileges
|
||||
// before executing the original request.
|
||||
func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse()
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
return operationContext, nil
|
||||
}
|
||||
|
||||
@@ -15,3 +15,18 @@ func getResourceControlByResourceID(resourceID string, resourceControls []portai
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool {
|
||||
for key, value := range containerLabels {
|
||||
labelName := key
|
||||
labelValue := value.(string)
|
||||
|
||||
for _, blackListedLabel := range labelBlackList {
|
||||
if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const (
|
||||
|
||||
// volumeListOperation extracts the response as a JSON object, loop through the volume array
|
||||
// decorate and/or filter the volumes based on resource controls before rewriting the response
|
||||
func volumeListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func volumeListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// VolumeList response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
@@ -28,10 +28,10 @@ func volumeListOperation(request *http.Request, response *http.Response, operati
|
||||
if responseObject["Volumes"] != nil {
|
||||
volumeData := responseObject["Volumes"].([]interface{})
|
||||
|
||||
if operationContext.isAdmin {
|
||||
volumeData, err = decorateVolumeList(volumeData, operationContext.resourceControls)
|
||||
if executor.operationContext.isAdmin {
|
||||
volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
volumeData, err = filterVolumeList(volumeData, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
|
||||
volumeData, err = filterVolumeList(volumeData, executor.operationContext.resourceControls, executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -47,7 +47,7 @@ func volumeListOperation(request *http.Request, response *http.Response, operati
|
||||
// volumeInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the volume based on resource control and either rewrite an access denied response
|
||||
// or a decorated volume.
|
||||
func volumeInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func volumeInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// VolumeInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
@@ -60,9 +60,9 @@ func volumeInspectOperation(request *http.Request, response *http.Response, oper
|
||||
}
|
||||
volumeID := responseObject[volumeIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(volumeID, operationContext.resourceControls)
|
||||
resourceControl := getResourceControlByResourceID(volumeID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
|
||||
@@ -22,7 +22,7 @@ func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceContro
|
||||
if teamAccessesCount > 0 {
|
||||
for _, access := range resourceControl.TeamAccesses {
|
||||
for _, membership := range context.UserMemberships {
|
||||
if membership.TeamID == access.TeamID && membership.Role == portainer.TeamLeader {
|
||||
if membership.TeamID == access.TeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler
|
||||
return h
|
||||
}
|
||||
|
||||
// RestrictedAccess defines defines a security check for restricted endpoints.
|
||||
// RestrictedAccess defines a security check for restricted endpoints.
|
||||
// Authentication is required to access these endpoints.
|
||||
// The request context will be enhanced with a RestrictedRequestContext object
|
||||
// that might be used later to authorize/filter access to resources.
|
||||
|
||||
@@ -60,6 +60,24 @@ func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []po
|
||||
return filteredUsers
|
||||
}
|
||||
|
||||
// FilterRegistries filters registries based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized endpoints.
|
||||
func FilterRegistries(registries []portainer.Registry, context *RestrictedRequestContext) ([]portainer.Registry, error) {
|
||||
|
||||
filteredRegistries := registries
|
||||
if !context.IsAdmin {
|
||||
filteredRegistries = make([]portainer.Registry, 0)
|
||||
|
||||
for _, registry := range registries {
|
||||
if isRegistryAccessAuthorized(®istry, context.UserID, context.UserMemberships) {
|
||||
filteredRegistries = append(filteredRegistries, registry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRegistries, nil
|
||||
}
|
||||
|
||||
// FilterEndpoints filters endpoints based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized endpoints.
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestContext) ([]portainer.Endpoint, error) {
|
||||
@@ -78,6 +96,22 @@ func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestC
|
||||
return filteredEndpoints, nil
|
||||
}
|
||||
|
||||
func isRegistryAccessAuthorized(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
for _, authorizedUserID := range registry.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, membership := range memberships {
|
||||
for _, authorizedTeamID := range registry.AuthorizedTeams {
|
||||
if membership.TeamID == authorizedTeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEndpointAccessAuthorized(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
|
||||
@@ -15,16 +15,19 @@ type Server struct {
|
||||
AssetsPath string
|
||||
AuthDisabled bool
|
||||
EndpointManagement bool
|
||||
Status *portainer.Status
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
EndpointService portainer.EndpointService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
SettingsService portainer.SettingsService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
LDAPService portainer.LDAPService
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
SSLCert string
|
||||
@@ -34,25 +37,33 @@ type Server struct {
|
||||
// Start starts the HTTP server
|
||||
func (server *Server) Start() error {
|
||||
requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled)
|
||||
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService)
|
||||
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService)
|
||||
|
||||
var authHandler = handler.NewAuthHandler(requestBouncer, server.AuthDisabled)
|
||||
authHandler.UserService = server.UserService
|
||||
authHandler.CryptoService = server.CryptoService
|
||||
authHandler.JWTService = server.JWTService
|
||||
authHandler.LDAPService = server.LDAPService
|
||||
authHandler.SettingsService = server.SettingsService
|
||||
var userHandler = handler.NewUserHandler(requestBouncer)
|
||||
userHandler.UserService = server.UserService
|
||||
userHandler.TeamService = server.TeamService
|
||||
userHandler.TeamMembershipService = server.TeamMembershipService
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
userHandler.ResourceControlService = server.ResourceControlService
|
||||
userHandler.SettingsService = server.SettingsService
|
||||
var teamHandler = handler.NewTeamHandler(requestBouncer)
|
||||
teamHandler.TeamService = server.TeamService
|
||||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var teamMembershipHandler = handler.NewTeamMembershipHandler(requestBouncer)
|
||||
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var settingsHandler = handler.NewSettingsHandler(requestBouncer, server.Settings)
|
||||
var templatesHandler = handler.NewTemplatesHandler(requestBouncer, server.TemplatesURL)
|
||||
var statusHandler = handler.NewStatusHandler(requestBouncer, server.Status)
|
||||
var settingsHandler = handler.NewSettingsHandler(requestBouncer)
|
||||
settingsHandler.SettingsService = server.SettingsService
|
||||
settingsHandler.LDAPService = server.LDAPService
|
||||
settingsHandler.FileService = server.FileService
|
||||
var templatesHandler = handler.NewTemplatesHandler(requestBouncer)
|
||||
templatesHandler.SettingsService = server.SettingsService
|
||||
var dockerHandler = handler.NewDockerHandler(requestBouncer)
|
||||
dockerHandler.EndpointService = server.EndpointService
|
||||
dockerHandler.TeamMembershipService = server.TeamMembershipService
|
||||
@@ -63,6 +74,10 @@ func (server *Server) Start() error {
|
||||
endpointHandler.EndpointService = server.EndpointService
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
var registryHandler = handler.NewRegistryHandler(requestBouncer)
|
||||
registryHandler.RegistryService = server.RegistryService
|
||||
var dockerHubHandler = handler.NewDockerHubHandler(requestBouncer)
|
||||
dockerHubHandler.DockerHubService = server.DockerHubService
|
||||
var resourceHandler = handler.NewResourceHandler(requestBouncer)
|
||||
resourceHandler.ResourceControlService = server.ResourceControlService
|
||||
var uploadHandler = handler.NewUploadHandler(requestBouncer)
|
||||
@@ -75,8 +90,11 @@ func (server *Server) Start() error {
|
||||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
EndpointHandler: endpointHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
DockerHubHandler: dockerHubHandler,
|
||||
ResourceHandler: resourceHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
StatusHandler: statusHandler,
|
||||
TemplatesHandler: templatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
|
||||
123
api/ldap/ldap.go
Normal file
123
api/ldap/ldap.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
|
||||
"gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrUserNotFound defines an error raised when the user is not found via LDAP search
|
||||
// or that too many entries (> 1) are returned.
|
||||
ErrUserNotFound = portainer.Error("User not found or too many entries returned")
|
||||
)
|
||||
|
||||
// Service represents a service used to authenticate users against a LDAP/AD.
|
||||
type Service struct{}
|
||||
|
||||
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
|
||||
var userDN string
|
||||
found := false
|
||||
for _, searchSettings := range settings {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
searchSettings.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, username),
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
// Deliberately skip errors on the search request so that we can jump to other search settings
|
||||
// if any issue arise with the current one.
|
||||
sr, _ := conn.Search(searchRequest)
|
||||
|
||||
if len(sr.Entries) == 1 {
|
||||
found = true
|
||||
userDN = sr.Entries[0].DN
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", ErrUserNotFound
|
||||
}
|
||||
|
||||
return userDN, nil
|
||||
}
|
||||
|
||||
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
|
||||
|
||||
if settings.TLSConfig.TLS || settings.StartTLS {
|
||||
config, err := crypto.CreateTLSConfiguration(&settings.TLSConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.ServerName = strings.Split(settings.URL, ":")[0]
|
||||
|
||||
if settings.TLSConfig.TLS {
|
||||
return ldap.DialTLS("tcp", settings.URL, config)
|
||||
}
|
||||
|
||||
conn, err := ldap.Dial("tcp", settings.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conn.StartTLS(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
return ldap.Dial("tcp", settings.URL)
|
||||
}
|
||||
|
||||
// AuthenticateUser is used to authenticate a user against a LDAP/AD.
|
||||
func (*Service) AuthenticateUser(username, password string, settings *portainer.LDAPSettings) error {
|
||||
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userDN, err := searchUser(username, connection, settings.SearchSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = connection.Bind(userDN, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestConnectivity is used to test a connection against the LDAP server using the credentials
|
||||
// specified in the LDAPSettings.
|
||||
func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error {
|
||||
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
179
api/portainer.go
179
api/portainer.go
@@ -17,9 +17,6 @@ type (
|
||||
ExternalEndpoints *string
|
||||
SyncInterval *string
|
||||
Endpoint *string
|
||||
Labels *[]Pair
|
||||
Logo *string
|
||||
Templates *string
|
||||
NoAuth *bool
|
||||
NoAnalytics *bool
|
||||
TLSVerify *bool
|
||||
@@ -30,15 +27,54 @@ type (
|
||||
SSLCert *string
|
||||
SSLKey *string
|
||||
AdminPassword *string
|
||||
// Deprecated fields
|
||||
Logo *string
|
||||
Templates *string
|
||||
Labels *[]Pair
|
||||
}
|
||||
|
||||
// Settings represents Portainer settings.
|
||||
// Status represents the application status.
|
||||
Status struct {
|
||||
Authentication bool `json:"Authentication"`
|
||||
EndpointManagement bool `json:"EndpointManagement"`
|
||||
Analytics bool `json:"Analytics"`
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
|
||||
// LDAPSettings represents the settings used to connect to a LDAP server.
|
||||
LDAPSettings struct {
|
||||
ReaderDN string `json:"ReaderDN"`
|
||||
Password string `json:"Password"`
|
||||
URL string `json:"URL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
StartTLS bool `json:"StartTLS"`
|
||||
SearchSettings []LDAPSearchSettings `json:"SearchSettings"`
|
||||
}
|
||||
|
||||
// TLSConfiguration represents a TLS configuration.
|
||||
TLSConfiguration struct {
|
||||
TLS bool `json:"TLS"`
|
||||
TLSSkipVerify bool `json:"TLSSkipVerify"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
|
||||
// LDAPSearchSettings represents settings used to search for users in a LDAP server.
|
||||
LDAPSearchSettings struct {
|
||||
BaseDN string `json:"BaseDN"`
|
||||
Filter string `json:"Filter"`
|
||||
UserNameAttribute string `json:"UserNameAttribute"`
|
||||
}
|
||||
|
||||
// Settings represents the application settings.
|
||||
Settings struct {
|
||||
HiddenLabels []Pair `json:"hiddenLabels"`
|
||||
Logo string `json:"logo"`
|
||||
Authentication bool `json:"authentication"`
|
||||
Analytics bool `json:"analytics"`
|
||||
EndpointManagement bool `json:"endpointManagement"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
}
|
||||
|
||||
// User represents a user account.
|
||||
@@ -56,6 +92,9 @@ type (
|
||||
// or a regular user
|
||||
UserRole int
|
||||
|
||||
// AuthenticationMethod represents the authentication method used to authenticate a user.
|
||||
AuthenticationMethod int
|
||||
|
||||
// Team represents a list of user accounts.
|
||||
Team struct {
|
||||
ID TeamID `json:"Id"`
|
||||
@@ -86,22 +125,50 @@ type (
|
||||
Role UserRole
|
||||
}
|
||||
|
||||
// RegistryID represents a registry identifier.
|
||||
RegistryID int
|
||||
|
||||
// Registry represents a Docker registry with all the info required
|
||||
// to connect to it.
|
||||
Registry struct {
|
||||
ID RegistryID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
}
|
||||
|
||||
// DockerHub represents all the required information to connect and use the
|
||||
// Docker Hub.
|
||||
DockerHub struct {
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
}
|
||||
|
||||
// EndpointID represents an endpoint identifier.
|
||||
EndpointID int
|
||||
|
||||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it.
|
||||
Endpoint struct {
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
TLS bool `json:"TLS"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceControlID represents a resource control identifier.
|
||||
@@ -109,20 +176,18 @@ type (
|
||||
|
||||
// ResourceControl represent a reference to a Docker resource with specific access controls
|
||||
ResourceControl struct {
|
||||
ID ResourceControlID `json:"Id"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
SubResourceIDs []string `json:"SubResourceIds"`
|
||||
Type ResourceControlType `json:"Type"`
|
||||
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||
|
||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||
ID ResourceControlID `json:"Id"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
SubResourceIDs []string `json:"SubResourceIds"`
|
||||
Type ResourceControlType `json:"Type"`
|
||||
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated: OwnerID field is deprecated in DBVersion == 2
|
||||
OwnerID UserID `json:"OwnerId"`
|
||||
// Deprecated: AccessLevel field is deprecated in DBVersion == 2
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
||||
// Deprecated in DBVersion == 2
|
||||
OwnerID UserID `json:"OwnerId,omitempty"`
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service).
|
||||
@@ -209,6 +274,27 @@ type (
|
||||
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
|
||||
}
|
||||
|
||||
// RegistryService represents a service for managing registry data.
|
||||
RegistryService interface {
|
||||
Registry(ID RegistryID) (*Registry, error)
|
||||
Registries() ([]Registry, error)
|
||||
CreateRegistry(registry *Registry) error
|
||||
UpdateRegistry(ID RegistryID, registry *Registry) error
|
||||
DeleteRegistry(ID RegistryID) error
|
||||
}
|
||||
|
||||
// DockerHubService represents a service for managing the DockerHub object.
|
||||
DockerHubService interface {
|
||||
DockerHub() (*DockerHub, error)
|
||||
StoreDockerHub(registry *DockerHub) error
|
||||
}
|
||||
|
||||
// SettingsService represents a service for managing application settings.
|
||||
SettingsService interface {
|
||||
Settings() (*Settings, error)
|
||||
StoreSettings(settings *Settings) error
|
||||
}
|
||||
|
||||
// VersionService represents a service for managing version data.
|
||||
VersionService interface {
|
||||
DBVersion() (int, error)
|
||||
@@ -239,22 +325,31 @@ type (
|
||||
|
||||
// FileService represents a service for managing files.
|
||||
FileService interface {
|
||||
StoreTLSFile(endpointID EndpointID, fileType TLSFileType, r io.Reader) error
|
||||
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
|
||||
DeleteTLSFiles(endpointID EndpointID) error
|
||||
StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error
|
||||
GetPathForTLSFile(folder string, fileType TLSFileType) (string, error)
|
||||
DeleteTLSFile(folder string, fileType TLSFileType) error
|
||||
DeleteTLSFiles(folder string) error
|
||||
}
|
||||
|
||||
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
|
||||
EndpointWatcher interface {
|
||||
WatchEndpointFile(endpointFilePath string) error
|
||||
}
|
||||
|
||||
// LDAPService represents a service used to authenticate users against a LDAP/AD.
|
||||
LDAPService interface {
|
||||
AuthenticateUser(username, password string, settings *LDAPSettings) error
|
||||
TestConnectivity(settings *LDAPSettings) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API.
|
||||
APIVersion = "1.13.1"
|
||||
APIVersion = "1.14.1"
|
||||
// DBVersion is the version number of the Portainer database.
|
||||
DBVersion = 2
|
||||
DBVersion = 4
|
||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -282,6 +377,14 @@ const (
|
||||
StandardUserRole
|
||||
)
|
||||
|
||||
const (
|
||||
_ AuthenticationMethod = iota
|
||||
// AuthenticationInternal represents the internal authentication method (authentication against Portainer API)
|
||||
AuthenticationInternal
|
||||
// AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server)
|
||||
AuthenticationLDAP
|
||||
)
|
||||
|
||||
const (
|
||||
_ ResourceAccessLevel = iota
|
||||
// ReadWriteAccessLevel represents an access level with read-write permissions on a resource
|
||||
@@ -296,4 +399,8 @@ const (
|
||||
ServiceResourceControl
|
||||
// VolumeResourceControl represents a resource control associated to a Docker volume
|
||||
VolumeResourceControl
|
||||
// NetworkResourceControl represents a resource control associated to a Docker network
|
||||
NetworkResourceControl
|
||||
// SecretResourceControl represents a resource control associated to a Docker secret
|
||||
SecretResourceControl
|
||||
)
|
||||
|
||||
2575
api/swagger.yaml
Normal file
2575
api/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
250
app/app.js
250
app/app.js
@@ -20,42 +20,53 @@ angular.module('portainer', [
|
||||
'portainer.services',
|
||||
'auth',
|
||||
'dashboard',
|
||||
'common.accesscontrol.panel',
|
||||
'common.accesscontrol.form',
|
||||
'container',
|
||||
'containerConsole',
|
||||
'containerLogs',
|
||||
'containerStats',
|
||||
'serviceLogs',
|
||||
'containers',
|
||||
'createContainer',
|
||||
'createNetwork',
|
||||
'createRegistry',
|
||||
'createSecret',
|
||||
'createService',
|
||||
'createVolume',
|
||||
'docker',
|
||||
'engine',
|
||||
'endpoint',
|
||||
'endpointAccess',
|
||||
'endpointInit',
|
||||
'endpoints',
|
||||
'events',
|
||||
'image',
|
||||
'images',
|
||||
'initAdmin',
|
||||
'initEndpoint',
|
||||
'main',
|
||||
'network',
|
||||
'networks',
|
||||
'node',
|
||||
'registries',
|
||||
'registry',
|
||||
'registryAccess',
|
||||
'secrets',
|
||||
'secret',
|
||||
'service',
|
||||
'services',
|
||||
'settings',
|
||||
'settingsAuthentication',
|
||||
'sidebar',
|
||||
'stats',
|
||||
'swarm',
|
||||
'swarmVisualizer',
|
||||
'task',
|
||||
'team',
|
||||
'teams',
|
||||
'templates',
|
||||
'user',
|
||||
'users',
|
||||
'userSettings',
|
||||
'volume',
|
||||
'volumes'])
|
||||
'volumes',
|
||||
'rzModule'])
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
|
||||
'use strict';
|
||||
|
||||
@@ -65,7 +76,6 @@ angular.module('portainer', [
|
||||
}
|
||||
|
||||
localStorageServiceProvider
|
||||
.setStorageType('sessionStorage')
|
||||
.setPrefix('portainer');
|
||||
|
||||
jwtOptionsProvider.config({
|
||||
@@ -150,8 +160,8 @@ angular.module('portainer', [
|
||||
url: '^/containers/:id/stats',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/stats/stats.html',
|
||||
controller: 'StatsController'
|
||||
templateUrl: 'app/components/containerStats/containerStats.html',
|
||||
controller: 'ContainerStatsController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
@@ -159,7 +169,7 @@ angular.module('portainer', [
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('logs', {
|
||||
.state('containerlogs', {
|
||||
url: '^/containers/:id/logs',
|
||||
views: {
|
||||
'content@': {
|
||||
@@ -172,6 +182,19 @@ angular.module('portainer', [
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('servicelogs', {
|
||||
url: '^/services/:id/logs',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/serviceLogs/servicelogs.html',
|
||||
controller: 'ServiceLogsController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('console', {
|
||||
url: '^/containers/:id/console',
|
||||
views: {
|
||||
@@ -224,7 +247,7 @@ angular.module('portainer', [
|
||||
}
|
||||
})
|
||||
.state('actions.create.container', {
|
||||
url: '/container',
|
||||
url: '/container/:from',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/createContainer/createcontainer.html',
|
||||
@@ -249,6 +272,32 @@ angular.module('portainer', [
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.registry', {
|
||||
url: '/registry',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/createRegistry/createregistry.html',
|
||||
controller: 'CreateRegistryController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.secret', {
|
||||
url: '/secret',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/createSecret/createsecret.html',
|
||||
controller: 'CreateSecretController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.service', {
|
||||
url: '/service',
|
||||
views: {
|
||||
@@ -275,12 +324,39 @@ angular.module('portainer', [
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('docker', {
|
||||
url: '/docker/',
|
||||
.state('init', {
|
||||
abstract: true,
|
||||
url: '/init',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/docker/docker.html',
|
||||
controller: 'DockerController'
|
||||
template: '<div ui-view="content@"></div>'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('init.endpoint', {
|
||||
url: '/endpoint',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/initEndpoint/initEndpoint.html',
|
||||
controller: 'InitEndpointController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('init.admin', {
|
||||
url: '/admin',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/initAdmin/initAdmin.html',
|
||||
controller: 'InitAdminController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('engine', {
|
||||
url: '/engine/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/engine/engine.html',
|
||||
controller: 'EngineController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
@@ -327,15 +403,6 @@ angular.module('portainer', [
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('endpointInit', {
|
||||
url: '/init/endpoint',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/endpointInit/endpointInit.html',
|
||||
controller: 'EndpointInitController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('events', {
|
||||
url: '/events/',
|
||||
views: {
|
||||
@@ -414,6 +481,71 @@ angular.module('portainer', [
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('registries', {
|
||||
url: '/registries/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/registries/registries.html',
|
||||
controller: 'RegistriesController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('registry', {
|
||||
url: '^/registries/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/registry/registry.html',
|
||||
controller: 'RegistryController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('registry.access', {
|
||||
url: '^/registries/:id/access',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/registryAccess/registryAccess.html',
|
||||
controller: 'RegistryAccessController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('secrets', {
|
||||
url: '^/secrets/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/secrets/secrets.html',
|
||||
controller: 'SecretsController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('secret', {
|
||||
url: '^/secret/:id/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/secret/secret.html',
|
||||
controller: 'SecretController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('services', {
|
||||
url: '/services/',
|
||||
views: {
|
||||
@@ -453,6 +585,19 @@ angular.module('portainer', [
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('settings_authentication', {
|
||||
url: '^/settings/authentication',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/settingsAuthentication/settingsAuthentication.html',
|
||||
controller: 'SettingsAuthenticationController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('task', {
|
||||
url: '^/task/:id',
|
||||
views: {
|
||||
@@ -552,6 +697,19 @@ angular.module('portainer', [
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('userSettings', {
|
||||
url: '/userSettings/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/userSettings/userSettings.html',
|
||||
controller: 'UserSettingsController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('teams', {
|
||||
url: '/teams/',
|
||||
views: {
|
||||
@@ -579,7 +737,7 @@ angular.module('portainer', [
|
||||
}
|
||||
})
|
||||
.state('swarm', {
|
||||
url: '/swarm/',
|
||||
url: '/swarm',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/swarm/swarm.html',
|
||||
@@ -590,7 +748,21 @@ angular.module('portainer', [
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.state('swarm.visualizer', {
|
||||
url: '/visualizer',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/swarmVisualizer/swarmVisualizer.html',
|
||||
controller: 'SwarmVisualizerController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
;
|
||||
}])
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) {
|
||||
EndpointProvider.initialize();
|
||||
@@ -620,15 +792,17 @@ angular.module('portainer', [
|
||||
}])
|
||||
// This is your docker url that the api will use to make requests
|
||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('DOCKER_ENDPOINT', 'api/docker')
|
||||
.constant('CONFIG_ENDPOINT', 'api/settings')
|
||||
.constant('AUTH_ENDPOINT', 'api/auth')
|
||||
.constant('USERS_ENDPOINT', 'api/users')
|
||||
.constant('TEAMS_ENDPOINT', 'api/teams')
|
||||
.constant('TEAM_MEMBERSHIPS_ENDPOINT', 'api/team_memberships')
|
||||
.constant('RESOURCE_CONTROL_ENDPOINT', 'api/resource_controls')
|
||||
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
|
||||
.constant('TEMPLATES_ENDPOINT', 'api/templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.13.1');
|
||||
// .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('API_ENDPOINT_AUTH', 'api/auth')
|
||||
.constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub')
|
||||
.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints')
|
||||
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
|
||||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
|
||||
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
|
||||
.constant('API_ENDPOINT_STATUS', 'api/status')
|
||||
.constant('API_ENDPOINT_USERS', 'api/users')
|
||||
.constant('API_ENDPOINT_TEAMS', 'api/teams')
|
||||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
|
||||
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
|
||||
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10);
|
||||
|
||||
@@ -1,92 +1,38 @@
|
||||
<div class="page-wrapper">
|
||||
<!-- login box -->
|
||||
<div class="container simple-box">
|
||||
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
||||
<div class="col-sm-6 col-sm-offset-3">
|
||||
<!-- login box logo -->
|
||||
<div class="row">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
</div>
|
||||
<!-- !login box logo -->
|
||||
<!-- init password panel -->
|
||||
<div class="panel panel-default" ng-if="initPassword">
|
||||
<div class="panel-body">
|
||||
<!-- init password form -->
|
||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
Please specify a password for the <b>admin</b> user account.
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password.length >= 8]" aria-hidden="true"></i>
|
||||
Your password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- password input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="admin_password" type="password" class="form-control" name="password" ng-model="initPasswordData.password" autofocus>
|
||||
</div>
|
||||
<!-- !password input -->
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password !== '' && initPasswordData.password === initPasswordData.password_confirmation]" aria-hidden="true"></i>
|
||||
Confirm your password
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- password confirmation input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="password_confirmation" type="password" class="form-control" name="password" ng-model="initPasswordData.password_confirmation">
|
||||
</div>
|
||||
<!-- !password confirmation input -->
|
||||
<!-- validate button -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="initPasswordData.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Unable to create default user
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-disabled="initPasswordData.password.length < 8 || initPasswordData.password !== initPasswordData.password_confirmation" ng-click="createAdminUser()"><i class="fa fa-key" aria-hidden="true"></i> Validate</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !validate button -->
|
||||
</form>
|
||||
<!-- !init password form -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !init password panel -->
|
||||
<!-- login panel -->
|
||||
<div class="panel panel-default" ng-if="!initPassword">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- login form -->
|
||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
||||
<form class="simple-box-form form-horizontal">
|
||||
<!-- username input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
|
||||
<input id="username" type="text" class="form-control" name="username" ng-model="authData.username" placeholder="Username">
|
||||
<input id="username" type="text" class="form-control" name="username" ng-model="formValues.Username" auto-focus>
|
||||
</div>
|
||||
<!-- !username input -->
|
||||
<!-- password input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="password" type="password" class="form-control" name="password" ng-model="authData.password" autofocus>
|
||||
<input id="password" type="password" class="form-control" name="password" ng-model="formValues.Password">
|
||||
</div>
|
||||
<!-- !password input -->
|
||||
<!-- login button -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }}
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-primary btn-sm pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
||||
<span class="pull-left" style="margin: 5px;" ng-if="state.AuthenticationError">
|
||||
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<span class="small text-danger">{{ state.AuthenticationError }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !login button -->
|
||||
|
||||
@@ -1,115 +1,110 @@
|
||||
angular.module('auth', [])
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) {
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) {
|
||||
|
||||
$scope.authData = {
|
||||
username: 'admin',
|
||||
password: '',
|
||||
error: ''
|
||||
};
|
||||
$scope.initPasswordData = {
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
error: false
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
||||
$scope.formValues = {
|
||||
Username: '',
|
||||
Password: ''
|
||||
};
|
||||
|
||||
if (!$scope.applicationState.application.authentication) {
|
||||
$scope.state = {
|
||||
AuthenticationError: ''
|
||||
};
|
||||
|
||||
function setActiveEndpointAndRedirectToDashboard(endpoint) {
|
||||
var endpointID = EndpointProvider.endpointID();
|
||||
if (!endpointID) {
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
}
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success(data) {
|
||||
$state.go('dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
|
||||
function unauthenticatedFlow() {
|
||||
EndpointService.endpoints()
|
||||
.then(function success(data) {
|
||||
if (data.length > 0) {
|
||||
endpointID = EndpointProvider.endpointID();
|
||||
if (!endpointID) {
|
||||
endpointID = data[0].Id;
|
||||
EndpointProvider.setEndpointID(endpointID);
|
||||
}
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
else {
|
||||
$state.go('endpointInit');
|
||||
}
|
||||
}, function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
});
|
||||
} else {
|
||||
Users.checkAdminUser({}, function () {},
|
||||
function (e) {
|
||||
if (e.status === 404) {
|
||||
$scope.initPassword = true;
|
||||
var endpoints = data;
|
||||
if (endpoints.length > 0) {
|
||||
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||
} else {
|
||||
Notifications.error('Failure', e, 'Unable to verify administrator account existence');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($stateParams.logout) {
|
||||
Authentication.logout();
|
||||
}
|
||||
|
||||
if ($stateParams.error) {
|
||||
$scope.authData.error = $stateParams.error;
|
||||
Authentication.logout();
|
||||
}
|
||||
|
||||
if (Authentication.isAuthenticated()) {
|
||||
$state.go('dashboard');
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
});
|
||||
|
||||
$scope.createAdminUser = function() {
|
||||
var password = $sanitize($scope.initPasswordData.password);
|
||||
Users.initAdminUser({password: password}, function (d) {
|
||||
$scope.initPassword = false;
|
||||
$timeout(function() {
|
||||
var element = $window.document.getElementById('password');
|
||||
if(element) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
}, function (e) {
|
||||
$scope.initPassword.error = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.authenticateUser = function() {
|
||||
$scope.authenticationError = false;
|
||||
var username = $sanitize($scope.authData.username);
|
||||
var password = $sanitize($scope.authData.password);
|
||||
Authentication.login(username, password)
|
||||
.then(function success(data) {
|
||||
return EndpointService.endpoints();
|
||||
})
|
||||
.then(function success(data) {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
if (data.length > 0) {
|
||||
endpointID = EndpointProvider.endpointID();
|
||||
if (!endpointID) {
|
||||
endpointID = data[0].Id;
|
||||
EndpointProvider.setEndpointID(endpointID);
|
||||
}
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
else if (data.length === 0 && userDetails.role === 1) {
|
||||
$state.go('endpointInit');
|
||||
} else if (data.length === 0 && userDetails.role === 2) {
|
||||
Authentication.logout();
|
||||
$scope.authData.error = 'User not allowed. Please contact your administrator.';
|
||||
$state.go('init.endpoint');
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.authData.error = 'Authentication error';
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
});
|
||||
}
|
||||
|
||||
function authenticatedFlow() {
|
||||
UserService.administratorExists()
|
||||
.then(function success(exists) {
|
||||
if (!exists) {
|
||||
$state.go('init.admin');
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to verify administrator account existence');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.authenticateUser = function() {
|
||||
var username = $scope.formValues.Username;
|
||||
var password = $scope.formValues.Password;
|
||||
|
||||
SettingsService.publicSettings()
|
||||
.then(function success(data) {
|
||||
var settings = data;
|
||||
if (settings.AuthenticationMethod === 1) {
|
||||
username = $sanitize(username);
|
||||
password = $sanitize(password);
|
||||
}
|
||||
return Authentication.login(username, password);
|
||||
})
|
||||
.then(function success() {
|
||||
return EndpointService.endpoints();
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoints = data;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
if (endpoints.length > 0) {
|
||||
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||
} else if (endpoints.length === 0 && userDetails.role === 1) {
|
||||
$state.go('init.endpoint');
|
||||
} else if (endpoints.length === 0 && userDetails.role === 2) {
|
||||
Authentication.logout();
|
||||
$scope.state.AuthenticationError = 'User not allowed. Please contact your administrator.';
|
||||
}
|
||||
})
|
||||
.catch(function error() {
|
||||
$scope.state.AuthenticationError = 'Invalid credentials';
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
if ($stateParams.logout || $stateParams.error) {
|
||||
Authentication.logout();
|
||||
$scope.state.AuthenticationError = $stateParams.error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Authentication.isAuthenticated()) {
|
||||
$state.go('dashboard');
|
||||
}
|
||||
|
||||
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||
if (!authenticationEnabled) {
|
||||
unauthenticatedFlow();
|
||||
} else {
|
||||
authenticatedFlow();
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
<div ng-controller="AccessControlFormController">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Access control
|
||||
</div>
|
||||
<!-- access-control-switch -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="ownership" class="control-label text-left">
|
||||
Enable access control
|
||||
<portainer-tooltip position="bottom" message="When enabled, you can restrict the access and management of this resource."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="ownership" type="checkbox" ng-model="formValues.enableAccessControl" ng-click="synchronizeFormData()"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !access-control-switch -->
|
||||
<!-- restricted-access -->
|
||||
<div class="form-group" ng-if="formValues.enableAccessControl" style="margin-bottom: 0">
|
||||
<div class="ownership_wrapper">
|
||||
<div ng-if="isAdmin">
|
||||
<input type="radio" id="access_administrators" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="administrators">
|
||||
<label for="access_administrators">
|
||||
<div class="ownership_header">
|
||||
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Administrators
|
||||
</div>
|
||||
<p>I want to restrict the management of this resource to administrators only</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="isAdmin">
|
||||
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="restricted">
|
||||
<label for="access_restricted">
|
||||
<div class="ownership_header">
|
||||
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Restricted
|
||||
</div>
|
||||
<p>
|
||||
I want to restrict the management of this resource to a set of users and/or teams
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="!isAdmin">
|
||||
<input type="radio" id="access_private" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="private">
|
||||
<label for="access_private">
|
||||
<div class="ownership_header">
|
||||
<i ng-class="'private' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Private
|
||||
</div>
|
||||
<p>
|
||||
I want to this resource to be manageable by myself only
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="!isAdmin && availableTeams.length > 0">
|
||||
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="restricted">
|
||||
<label for="access_restricted">
|
||||
<div class="ownership_header">
|
||||
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Restricted
|
||||
</div>
|
||||
<p ng-if="availableTeams.length === 1">
|
||||
I want any member of my team (<b>{{ availableTeams[0].Name }}</b>) to be able to manage this resource
|
||||
</p>
|
||||
<p ng-if="availableTeams.length > 1">
|
||||
I want to restrict the management of this resource to one or more of my teams
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- restricted-access -->
|
||||
<!-- authorized-teams -->
|
||||
<div class="form-group" ng-if="formValues.enableAccessControl && formValues.Ownership === 'restricted' && (isAdmin || (!isAdmin && availableTeams.length > 1))" >
|
||||
<div class="col-sm-12">
|
||||
<label for="group-access" class="control-label text-left">
|
||||
Authorized teams
|
||||
<portainer-tooltip ng-if="isAdmin && availableTeams.length > 0" position="bottom" message="You can select which teams(s) will be able to manage this resource."></portainer-tooltip>
|
||||
<portainer-tooltip ng-if="!isAdmin && availableTeams.length > 1" position="bottom" message="As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource."></portainer-tooltip>
|
||||
</label>
|
||||
<span ng-if="isAdmin && availableTeams.length === 0" class="small text-muted" style="margin-left: 20px;">
|
||||
You have not yet created any team. Head over the <a ui-sref="teams">teams view</a> to manage user teams.</span>
|
||||
</span>
|
||||
<span isteven-multi-select
|
||||
ng-if="(isAdmin && availableTeams.length > 0) || (!isAdmin && availableTeams.length > 1)"
|
||||
input-model="availableTeams"
|
||||
output-model="formValues.Ownership_Teams"
|
||||
button-label="Name"
|
||||
item-label="Name"
|
||||
tick-property="ticked"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
on-item-click="synchronizeFormData()"
|
||||
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
|
||||
style="margin-left: 20px;"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !authorized-teams -->
|
||||
<!-- authorized-users -->
|
||||
<div class="form-group" ng-if="formValues.enableAccessControl && formValues.Ownership === 'restricted' && isAdmin">
|
||||
<div class="col-sm-12">
|
||||
<label for="group-access" class="control-label text-left">
|
||||
Authorized users
|
||||
<portainer-tooltip ng-if="isAdmin && availableUsers.length > 0" position="bottom" message="You can select which user(s) will be able to manage this resource."></portainer-tooltip>
|
||||
</label>
|
||||
<span ng-if="availableUsers.length === 0" class="small text-muted" style="margin-left: 20px;">
|
||||
You have not yet created any user. Head over the <a ui-sref="users">users view</a> to manage users.</span>
|
||||
</span>
|
||||
<span isteven-multi-select
|
||||
ng-if="availableUsers.length > 0"
|
||||
input-model="availableUsers"
|
||||
output-model="formValues.Ownership_Users"
|
||||
button-label="Username"
|
||||
item-label="Username"
|
||||
tick-property="ticked"
|
||||
helper-elements="filter"
|
||||
search-property="Username"
|
||||
on-item-click="synchronizeFormData()"
|
||||
translation="{nothingSelected: 'Select one or more users', search: 'Search...'}"
|
||||
style="margin-left: 20px;"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !authorized-users -->
|
||||
</div>
|
||||
@@ -1,55 +0,0 @@
|
||||
angular.module('common.accesscontrol.form', [])
|
||||
.controller('AccessControlFormController', ['$q', '$scope', '$state', 'UserService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'ControllerDataPipeline',
|
||||
function ($q, $scope, $state, UserService, ResourceControlService, Notifications, Authentication, ModalService, ControllerDataPipeline) {
|
||||
|
||||
$scope.availableTeams = [];
|
||||
$scope.availableUsers = [];
|
||||
|
||||
$scope.formValues = {
|
||||
enableAccessControl: true,
|
||||
Ownership_Teams: [],
|
||||
Ownership_Users: [],
|
||||
Ownership: 'private'
|
||||
};
|
||||
|
||||
$scope.synchronizeFormData = function() {
|
||||
ControllerDataPipeline.setAccessControlFormData($scope.formValues.enableAccessControl,
|
||||
$scope.formValues.Ownership, $scope.formValues.Ownership_Users, $scope.formValues.Ownership_Teams);
|
||||
};
|
||||
|
||||
function initAccessControlForm() {
|
||||
$('#loadingViewSpinner').show();
|
||||
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true: false;
|
||||
$scope.isAdmin = isAdmin;
|
||||
|
||||
if (isAdmin) {
|
||||
$scope.formValues.Ownership = 'administrators';
|
||||
}
|
||||
|
||||
$q.all({
|
||||
availableTeams: UserService.userTeams(userDetails.ID),
|
||||
availableUsers: isAdmin ? UserService.users(false) : []
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.availableUsers = data.availableUsers;
|
||||
|
||||
var availableTeams = data.availableTeams;
|
||||
$scope.availableTeams = availableTeams;
|
||||
if (!isAdmin && availableTeams.length === 1) {
|
||||
$scope.formValues.Ownership_Teams = availableTeams;
|
||||
}
|
||||
|
||||
$scope.synchronizeFormData();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve access control information');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
initAccessControlForm();
|
||||
}]);
|
||||
@@ -1,158 +0,0 @@
|
||||
angular.module('common.accesscontrol.panel', [])
|
||||
.controller('AccessControlPanelController', ['$q', '$scope', '$state', 'UserService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'ControllerDataPipeline', 'FormValidator',
|
||||
function ($q, $scope, $state, UserService, ResourceControlService, Notifications, Authentication, ModalService, ControllerDataPipeline, FormValidator) {
|
||||
|
||||
$scope.state = {
|
||||
displayAccessControlPanel: false,
|
||||
canEditOwnership: false,
|
||||
editOwnership: false,
|
||||
formValidationError: ''
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Ownership: 'public',
|
||||
Ownership_Users: [],
|
||||
Ownership_Teams: []
|
||||
};
|
||||
|
||||
$scope.authorizedUsers = [];
|
||||
$scope.availableUsers = [];
|
||||
$scope.authorizedTeams = [];
|
||||
$scope.availableTeams = [];
|
||||
|
||||
$scope.confirmUpdateOwnership = function (force) {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
ModalService.confirmAccessControlUpdate(function (confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
updateOwnership();
|
||||
});
|
||||
};
|
||||
|
||||
function processOwnershipFormValues() {
|
||||
var userIds = [];
|
||||
angular.forEach($scope.formValues.Ownership_Users, function(user) {
|
||||
userIds.push(user.Id);
|
||||
});
|
||||
var teamIds = [];
|
||||
angular.forEach($scope.formValues.Ownership_Teams, function(team) {
|
||||
teamIds.push(team.Id);
|
||||
});
|
||||
var administratorsOnly = $scope.formValues.Ownership === 'administrators' ? true : false;
|
||||
|
||||
return {
|
||||
ownership: $scope.formValues.Ownership,
|
||||
authorizedUserIds: administratorsOnly ? [] : userIds,
|
||||
authorizedTeamIds: administratorsOnly ? [] : teamIds,
|
||||
administratorsOnly: administratorsOnly
|
||||
};
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
|
||||
var accessControlData = {
|
||||
ownership: $scope.formValues.Ownership,
|
||||
authorizedUsers: $scope.formValues.Ownership_Users,
|
||||
authorizedTeams: $scope.formValues.Ownership_Teams
|
||||
};
|
||||
var isAdmin = $scope.isAdmin;
|
||||
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
if (error) {
|
||||
$scope.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateOwnership() {
|
||||
$('#loadingViewSpinner').show();
|
||||
|
||||
var accessControlData = ControllerDataPipeline.getAccessControlData();
|
||||
var resourceId = accessControlData.resourceId;
|
||||
var ownershipParameters = processOwnershipFormValues();
|
||||
|
||||
ResourceControlService.applyResourceControlChange(accessControlData.resourceType, resourceId,
|
||||
$scope.resourceControl, ownershipParameters)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Access control successfully updated');
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update access control');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
function initAccessControlPanel() {
|
||||
$('#loadingViewSpinner').show();
|
||||
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true: false;
|
||||
var userId = userDetails.ID;
|
||||
$scope.isAdmin = isAdmin;
|
||||
|
||||
var accessControlData = ControllerDataPipeline.getAccessControlData();
|
||||
var resourceControl = accessControlData.resourceControl;
|
||||
$scope.resourceType = accessControlData.resourceType;
|
||||
$scope.resourceControl = resourceControl;
|
||||
|
||||
if (isAdmin) {
|
||||
if (resourceControl) {
|
||||
$scope.formValues.Ownership = resourceControl.Ownership === 'private' ? 'restricted' : resourceControl.Ownership;
|
||||
} else {
|
||||
$scope.formValues.Ownership = 'public';
|
||||
}
|
||||
} else {
|
||||
$scope.formValues.Ownership = 'public';
|
||||
}
|
||||
|
||||
ResourceControlService.retrieveOwnershipDetails(resourceControl)
|
||||
.then(function success(data) {
|
||||
$scope.authorizedUsers = data.authorizedUsers;
|
||||
$scope.authorizedTeams = data.authorizedTeams;
|
||||
return ResourceControlService.retrieveUserPermissionsOnResource(userId, isAdmin, resourceControl);
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.state.canEditOwnership = data.isPartOfRestrictedUsers || data.isLeaderOfAnyRestrictedTeams;
|
||||
$scope.state.canChangeOwnershipToTeam = data.isPartOfRestrictedUsers;
|
||||
|
||||
return $q.all({
|
||||
availableUsers: isAdmin ? UserService.users(false) : [],
|
||||
availableTeams: isAdmin || data.isPartOfRestrictedUsers ? UserService.userTeams(userId) : []
|
||||
});
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.availableUsers = data.availableUsers;
|
||||
angular.forEach($scope.availableUsers, function(user) {
|
||||
var found = _.find($scope.authorizedUsers, { Id: user.Id });
|
||||
if (found) {
|
||||
user.selected = true;
|
||||
}
|
||||
});
|
||||
$scope.availableTeams = data.availableTeams;
|
||||
angular.forEach(data.availableTeams, function(team) {
|
||||
var found = _.find($scope.authorizedTeams, { Id: team.Id });
|
||||
if (found) {
|
||||
team.selected = true;
|
||||
}
|
||||
});
|
||||
if (data.availableTeams.length === 1) {
|
||||
$scope.formValues.Ownership_Teams.push(data.availableTeams[0]);
|
||||
}
|
||||
$scope.state.displayAccessControlPanel = true;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve access control information');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
initAccessControlPanel();
|
||||
}]);
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||
<button class="btn btn-danger" ng-click="confirmRemove()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
<button class="btn btn-danger" ng-click="recreate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Recreate</button>
|
||||
<button class="btn btn-primary" ng-click="duplicate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-files-o space-right" aria-hidden="true"></i>Duplicate/Edit</button>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
@@ -33,6 +35,10 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{{ container.Id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td ng-if="!container.edit">
|
||||
@@ -75,7 +81,7 @@
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="containerlogs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
|
||||
</div>
|
||||
</td>
|
||||
@@ -87,7 +93,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-include="'app/components/common/accessControlPanel/accessControlPanel.html'" ng-if="container && applicationState.application.authentication"></div>
|
||||
<!-- access-control-panel -->
|
||||
<por-access-control-panel
|
||||
ng-if="container && applicationState.application.authentication"
|
||||
resource-id="container.Id"
|
||||
resource-control="container.ResourceControl"
|
||||
resource-type="'container'">
|
||||
</por-access-control-panel>
|
||||
<!-- !access-control-panel -->
|
||||
|
||||
<div ng-if="container.State.Health" class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
@@ -114,7 +127,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widge>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,21 +147,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tag-description -->
|
||||
<!-- name-and-registry-inputs -->
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11 col-md-6">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
|
||||
Registry
|
||||
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10 col-md-3 margin-sm-top">
|
||||
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional">
|
||||
</div>
|
||||
<por-image-registry image="config.Image" registry="config.Registry"></por-image-registry>
|
||||
</div>
|
||||
<!-- !name-and-registry-inputs -->
|
||||
<!-- !image-and-registry -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
@@ -261,7 +264,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="!(container.NetworkSettings.Networks | emptyobject)">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Connected networks">
|
||||
@@ -295,11 +298,30 @@
|
||||
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(container, value.NetworkID)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Leave Network</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="(container.NetworkSettings.Networks | emptyobject)">
|
||||
<td colspan="5" class="text-center text-muted">No networks connected.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
<hr />
|
||||
<form class="form-horizontal">
|
||||
<!-- network-input -->
|
||||
<div class="row">
|
||||
<label for="container_network" class="col-sm-3 col-lg-2 control-label text-left">Join a Network</label>
|
||||
<div class="col-sm-5 col-lg-4">
|
||||
<select class="form-control" ng-model="selectedNetwork" id="container_network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Id">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!selectedNetwork" ng-click="containerJoinNetwork(container, selectedNetwork)">Join Network</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('container', [])
|
||||
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
|
||||
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, Notifications, Pagination, ModalService, ControllerDataPipeline) {
|
||||
.controller('ContainerController', ['$q', '$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
|
||||
function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService, ResourceControlService, RegistryService, ImageService) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
$scope.config = {
|
||||
@@ -19,7 +19,6 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
var container = new ContainerDetailsViewModel(d);
|
||||
$scope.container = container;
|
||||
ControllerDataPipeline.setAccessControlData('container', $stateParams.id, container.ResourceControl);
|
||||
$scope.container.edit = false;
|
||||
$scope.container.newContainerName = $filter('trimcontainername')(container.Name);
|
||||
|
||||
@@ -86,7 +85,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con
|
||||
$('#createImageSpinner').show();
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
|
||||
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
$('#createImageSpinner').hide();
|
||||
update();
|
||||
@@ -165,13 +164,14 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con
|
||||
};
|
||||
|
||||
$scope.renameContainer = function () {
|
||||
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
|
||||
if (container.message) {
|
||||
$scope.container.newContainerName = $scope.container.Name;
|
||||
Notifications.error('Unable to rename container', {}, container.message);
|
||||
var container = $scope.container;
|
||||
Container.rename({id: $stateParams.id, 'name': container.newContainerName}, function (d) {
|
||||
if (d.message) {
|
||||
container.newContainerName = container.Name;
|
||||
Notifications.error('Unable to rename container', {}, d.message);
|
||||
} else {
|
||||
$scope.container.Name = $scope.container.newContainerName;
|
||||
Notifications.success('Container successfully renamed', container.name);
|
||||
container.Name = container.newContainerName;
|
||||
Notifications.success('Container successfully renamed', container.Name);
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to rename container');
|
||||
@@ -196,5 +196,120 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con
|
||||
});
|
||||
};
|
||||
|
||||
$scope.duplicate = function() {
|
||||
ModalService.confirmExperimentalFeature(function (experimental) {
|
||||
if(!experimental) { return; }
|
||||
$state.go('actions.create.container', {from: $stateParams.id}, {reload: true});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.confirmRemove = function () {
|
||||
var title = 'You are about to remove a container.';
|
||||
if ($scope.container.State.Running) {
|
||||
title = 'You are about to remove a running container.';
|
||||
}
|
||||
ModalService.confirmContainerDeletion(
|
||||
title,
|
||||
function (result) {
|
||||
if(!result) { return; }
|
||||
var cleanAssociatedVolumes = false;
|
||||
if (result[0]) {
|
||||
cleanAssociatedVolumes = true;
|
||||
}
|
||||
$scope.remove(cleanAssociatedVolumes);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function recreateContainer(pullImage) {
|
||||
$('#loadingViewSpinner').show();
|
||||
var container = $scope.container;
|
||||
var config = ContainerHelper.configFromContainer(container.Model);
|
||||
ContainerService.remove(container, true)
|
||||
.then(function success() {
|
||||
return RegistryService.retrieveRegistryFromRepository(container.Config.Image);
|
||||
})
|
||||
.then(function success(data) {
|
||||
return $q.when(!pullImage || ImageService.pullImage(container.Config.Image, data, true));
|
||||
})
|
||||
.then(function success() {
|
||||
return ContainerService.createAndStartContainer(config);
|
||||
})
|
||||
.then(function success(data) {
|
||||
if (!container.ResourceControl) {
|
||||
return true;
|
||||
} else {
|
||||
var containerIdentifier = data.Id;
|
||||
var resourceControl = container.ResourceControl;
|
||||
var users = resourceControl.UserAccesses.map(function(u) {
|
||||
return u.UserId;
|
||||
});
|
||||
var teams = resourceControl.TeamAccesses.map(function(t) {
|
||||
return t.TeamId;
|
||||
});
|
||||
return ResourceControlService.createResourceControl(resourceControl.AdministratorsOnly,
|
||||
users, teams, containerIdentifier, 'container', []);
|
||||
}
|
||||
})
|
||||
.then(function success(data) {
|
||||
Notifications.success('Container successfully re-created');
|
||||
$state.go('containers', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to re-create container');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.recreate = function() {
|
||||
ModalService.confirmExperimentalFeature(function (experimental) {
|
||||
if(!experimental) { return; }
|
||||
|
||||
ModalService.confirmContainerRecreation(function (result) {
|
||||
if(!result) { return; }
|
||||
var pullImage = false;
|
||||
if (result[0]) {
|
||||
pullImage = true;
|
||||
}
|
||||
recreateContainer(pullImage);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) {
|
||||
$('#joinNetworkSpinner').show();
|
||||
Network.connect({id: networkId}, { Container: $stateParams.id }, function (d) {
|
||||
if (container.message) {
|
||||
$('#joinNetworkSpinner').hide();
|
||||
Notifications.error('Error', d, 'Unable to connect container to network');
|
||||
} else {
|
||||
$('#joinNetworkSpinner').hide();
|
||||
Notifications.success('Container joined network', $stateParams.id);
|
||||
$state.go('container', {id: $stateParams.id}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#joinNetworkSpinner').hide();
|
||||
Notifications.error('Failure', e, 'Unable to connect container to network');
|
||||
});
|
||||
};
|
||||
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
NetworkService.networks(
|
||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||
false,
|
||||
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25,
|
||||
provider === 'DOCKER_SWARM'
|
||||
)
|
||||
.then(function success(data) {
|
||||
var networks = data;
|
||||
$scope.availableNetworks = networks;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve networks');
|
||||
});
|
||||
|
||||
update();
|
||||
}]);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content ng-if="state.loaded">
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -16,29 +16,53 @@
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form>
|
||||
<div class="row">
|
||||
<form class="form-horizontal">
|
||||
<div ng-if="!state.connected">
|
||||
<!-- command-list -->
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
|
||||
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
|
||||
</span>
|
||||
<select class="form-control" ng-model="state.command" id="command">
|
||||
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
|
||||
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
|
||||
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
|
||||
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
|
||||
</select>
|
||||
<div class="form-group">
|
||||
<label for="command" class="col-lg-1 text-left col-sm-2 control-label">Command</label>
|
||||
<div class="col-lg-11 col-sm-10">
|
||||
<div class="input-group" ng-if="!formValues.isCustomCommand">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
|
||||
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
|
||||
</span>
|
||||
<select class="form-control" ng-model="formValues.command" id="command">
|
||||
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
|
||||
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
|
||||
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
|
||||
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
|
||||
</select>
|
||||
</div>
|
||||
<input class="form-control" ng-if="formValues.isCustomCommand" type="text" name="custom-command" ng-model="formValues.customCommand" placeholder="e.g. ps aux">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command-list -->
|
||||
<div class="form-group col-lg-12">
|
||||
<label for="command" class="text-left control-label">Use custom command</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.isCustomCommand"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username" class="col-lg-1 text-left col-sm-2 control-label">
|
||||
User
|
||||
<portainer-tooltip position="bottom" message="Format is one of: user, user:group, uid or uid:gid"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-lg-11 col-sm-10">
|
||||
<input class="form-control" type="text" name="username" ng-model="formValues.user" placeholder="root">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command-list -->
|
||||
<div class="col-sm-8">
|
||||
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="state.connected">Connect</button>
|
||||
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!state.connected">Disconnect</button>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-1 col-sm-offset-2 col-lg-11 col-sm-10">
|
||||
<button type="button" class="btn btn-primary" ng-click="connect()">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="state.connected">
|
||||
<label>Exec into container as <code>{{ ::formValues.user || 'default user' }}</code> using command <code>{{ formValues.isCustomCommand ? formValues.customCommand : formValues.command }}</code></label>
|
||||
<button type="button" class="btn btn-default" ng-click="disconnect()">Disconnect</button>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
angular.module('containerConsole', [])
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications',
|
||||
function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, EndpointProvider, Notifications) {
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ContainerService', 'ExecService',
|
||||
function ($scope, $stateParams, Container, Image, EndpointProvider, Notifications, ContainerHelper, ContainerService, ExecService) {
|
||||
$scope.state = {};
|
||||
$scope.state.loaded = false;
|
||||
$scope.state.connected = false;
|
||||
$scope.formValues = {};
|
||||
|
||||
var socket, term;
|
||||
|
||||
@@ -22,7 +23,7 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
|
||||
} else {
|
||||
Image.get({id: d.Image}, function(imgData) {
|
||||
$scope.imageOS = imgData.Os;
|
||||
$scope.state.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
|
||||
$scope.formValues.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
|
||||
$scope.state.loaded = true;
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
@@ -37,35 +38,38 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
|
||||
|
||||
$scope.connect = function() {
|
||||
$('#loadConsoleSpinner').show();
|
||||
var termWidth = Math.round($('#terminal-container').width() / 8.2);
|
||||
var termWidth = Math.floor(($('#terminal-container').width() - 20) / 8.39);
|
||||
var termHeight = 30;
|
||||
var command = $scope.formValues.isCustomCommand ?
|
||||
$scope.formValues.customCommand : $scope.formValues.command;
|
||||
var execConfig = {
|
||||
id: $stateParams.id,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: $scope.state.command.replace(' ', ',').split(',')
|
||||
User: $scope.formValues.user,
|
||||
Cmd: ContainerHelper.commandStringToArray(command)
|
||||
};
|
||||
|
||||
Container.exec(execConfig, function(d) {
|
||||
if (d.message) {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
Notifications.error('Error', {}, d.message);
|
||||
var execId;
|
||||
ContainerService.createExec(execConfig)
|
||||
.then(function success(data) {
|
||||
execId = data.Id;
|
||||
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
var execId = d.Id;
|
||||
resizeTTY(execId, termHeight, termWidth);
|
||||
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
url = url.replace('http://', 'ws://');
|
||||
}
|
||||
initTerm(url, termHeight, termWidth);
|
||||
url = url.replace('http://', 'ws://');
|
||||
}
|
||||
}, function (e) {
|
||||
initTerm(url, termHeight, termWidth);
|
||||
return ExecService.resizeTTY(execId, termHeight, termWidth, 2000);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to exec into container');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
Notifications.error('Failure', e, 'Unable to start an exec instance');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,19 +83,6 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
|
||||
}
|
||||
};
|
||||
|
||||
function resizeTTY(execId, height, width) {
|
||||
$timeout(function() {
|
||||
Exec.resize({id: execId, height: height, width: width}, function (d) {
|
||||
if (d.message) {
|
||||
Notifications.error('Error', {}, 'Unable to resize TTY');
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', {}, 'Unable to resize TTY');
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
|
||||
function initTerm(url, height, width) {
|
||||
socket = new WebSocket(url);
|
||||
|
||||
@@ -103,9 +94,14 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
|
||||
term.on('data', function (data) {
|
||||
socket.send(data);
|
||||
});
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
term.open(document.getElementById('terminal-container'), true);
|
||||
term.resize(width, height);
|
||||
term.setOption('cursorBlink', true);
|
||||
term.fit();
|
||||
|
||||
window.onresize = function() {
|
||||
term.fit();
|
||||
};
|
||||
|
||||
socket.onmessage = function (e) {
|
||||
term.write(e.data);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
||||
123
app/components/containerStats/containerStats.html
Normal file
123
app/components/containerStats/containerStats.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Container statistics">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-info-circle" title="About statistics">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
This view displays real-time statistics about the container <b>{{ container.Name|trimcontainername }}</b> as well as a list of the running processes
|
||||
inside this container.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left">
|
||||
Refresh rate
|
||||
</label>
|
||||
<div class="col-sm-3 col-md-2">
|
||||
<select id="refreshRate" ng-model="state.refreshRate" ng-change="changeUpdateRepeater()" class="form-control">
|
||||
<option value="5">5s</option>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">60s</option>
|
||||
</select>
|
||||
</div>
|
||||
<span>
|
||||
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-md-6 col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="memoryChart" width="770" height="300"></canvas>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="cpuChart" width="770" height="300"></canvas>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-12 col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="networkChart" width="770" height="300"></canvas>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-sm-12" ng-if="applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Processes">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-repeat="title in processInfo.Titles">
|
||||
<a ng-click="order(title)">
|
||||
{{ title }}
|
||||
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="processDetails in state.filteredProcesses = (processInfo.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
||||
<td ng-repeat="procInfo in processDetails track by $index">{{ procInfo }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!processInfo.Processes">
|
||||
<td colspan="processInfo.Titles.length" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="state.filteredProcesses.length === 0">
|
||||
<td colspan="processInfo.Titles.length" class="text-center text-muted">No processes available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="processInfo.Processes" class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
159
app/components/containerStats/containerStatsController.js
Normal file
159
app/components/containerStats/containerStatsController.js
Normal file
@@ -0,0 +1,159 @@
|
||||
angular.module('containerStats', [])
|
||||
.controller('ContainerStatsController', ['$q', '$scope', '$stateParams', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'Pagination',
|
||||
function ($q, $scope, $stateParams, $document, $interval, ContainerService, ChartService, Notifications, Pagination) {
|
||||
|
||||
$scope.state = {
|
||||
refreshRate: '5'
|
||||
};
|
||||
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
|
||||
$scope.sortType = 'CMD';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
stopRepeater();
|
||||
});
|
||||
|
||||
function stopRepeater() {
|
||||
var repeater = $scope.repeater;
|
||||
if (angular.isDefined(repeater)) {
|
||||
$interval.cancel(repeater);
|
||||
repeater = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateNetworkChart(stats, chart) {
|
||||
var rx = stats.Networks[0].rx_bytes;
|
||||
var tx = stats.Networks[0].tx_bytes;
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
|
||||
ChartService.UpdateNetworkChart(label, rx, tx, chart);
|
||||
}
|
||||
|
||||
function updateMemoryChart(stats, chart) {
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
var value = stats.MemoryUsage;
|
||||
|
||||
ChartService.UpdateMemoryChart(label, value, chart);
|
||||
}
|
||||
|
||||
function updateCPUChart(stats, chart) {
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
var value = calculateCPUPercentUnix(stats);
|
||||
|
||||
ChartService.UpdateCPUChart(label, value, chart);
|
||||
}
|
||||
|
||||
function calculateCPUPercentUnix(stats) {
|
||||
var cpuPercent = 0.0;
|
||||
var cpuDelta = stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage;
|
||||
var systemDelta = stats.CurrentCPUSystemUsage - stats.PreviousCPUSystemUsage;
|
||||
|
||||
if (systemDelta > 0.0 && cpuDelta > 0.0) {
|
||||
cpuPercent = (cpuDelta / systemDelta) * stats.CPUCores * 100.0;
|
||||
}
|
||||
|
||||
return cpuPercent;
|
||||
}
|
||||
|
||||
$scope.changeUpdateRepeater = function() {
|
||||
var networkChart = $scope.networkChart;
|
||||
var cpuChart = $scope.cpuChart;
|
||||
var memoryChart = $scope.memoryChart;
|
||||
|
||||
stopRepeater();
|
||||
setUpdateRepeater(networkChart, cpuChart, memoryChart);
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(1500);
|
||||
};
|
||||
|
||||
function startChartUpdate(networkChart, cpuChart, memoryChart) {
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all({
|
||||
stats: ContainerService.containerStats($stateParams.id),
|
||||
top: ContainerService.containerTop($stateParams.id)
|
||||
})
|
||||
.then(function success(data) {
|
||||
var stats = data.stats;
|
||||
$scope.processInfo = data.top;
|
||||
updateNetworkChart(stats, networkChart);
|
||||
updateMemoryChart(stats, memoryChart);
|
||||
updateCPUChart(stats, cpuChart);
|
||||
setUpdateRepeater(networkChart, cpuChart, memoryChart);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
stopRepeater();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
function setUpdateRepeater(networkChart, cpuChart, memoryChart) {
|
||||
var refreshRate = $scope.state.refreshRate;
|
||||
$scope.repeater = $interval(function() {
|
||||
$q.all({
|
||||
stats: ContainerService.containerStats($stateParams.id),
|
||||
top: ContainerService.containerTop($stateParams.id)
|
||||
})
|
||||
.then(function success(data) {
|
||||
var stats = data.stats;
|
||||
$scope.processInfo = data.top;
|
||||
updateNetworkChart(stats, networkChart);
|
||||
updateMemoryChart(stats, memoryChart);
|
||||
updateCPUChart(stats, cpuChart);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
stopRepeater();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
|
||||
});
|
||||
}, refreshRate * 1000);
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
var networkChartCtx = $('#networkChart');
|
||||
var networkChart = ChartService.CreateNetworkChart(networkChartCtx);
|
||||
$scope.networkChart = networkChart;
|
||||
|
||||
var cpuChartCtx = $('#cpuChart');
|
||||
var cpuChart = ChartService.CreateCPUChart(cpuChartCtx);
|
||||
$scope.cpuChart = cpuChart;
|
||||
|
||||
var memoryChartCtx = $('#memoryChart');
|
||||
var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx);
|
||||
$scope.memoryChart = memoryChart;
|
||||
|
||||
startChartUpdate(networkChart, cpuChart, memoryChart);
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
|
||||
ContainerService.container($stateParams.id)
|
||||
.then(function success(data) {
|
||||
$scope.container = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container information');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
|
||||
$document.ready(function() {
|
||||
initCharts();
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
@@ -25,12 +25,12 @@
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button type="button" class="btn btn-success btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
||||
<button type="button" class="btn btn-danger btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
||||
<button type="button" class="btn btn-success btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount || state.noStoppedItemsSelected"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
||||
<button type="button" class="btn btn-danger btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount || state.noRunningItemsSelected"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
||||
<button type="button" class="btn btn-danger btn-responsive" ng-click="killAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="restartAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount || state.noRunningItemsSelected"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount || state.noPausedItemsSelected"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||
<button type="button" class="btn btn-danger btn-responsive" ng-click="confirmRemoveAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<a class="btn btn-primary" type="button" ui-sref="actions.create.container"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add container</a>
|
||||
@@ -61,6 +61,9 @@
|
||||
<span ng-show="sortType == 'Names' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
<a data-toggle="tooltip" title="More" ng-click="truncateMore();" ng-show="showMore">
|
||||
<i class="fa fa-plus-square" aria-hidden="true"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Image')">
|
||||
@@ -106,8 +109,8 @@
|
||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) !== -1" class="label label-{{ container.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ container.Status }}</span>
|
||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) === -1" class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span>
|
||||
</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername|truncate: 40}}</a></td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: 40}}</a></td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername|truncate: truncate_size}}</a></td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: truncate_size}}</a></td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></td>
|
||||
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
|
||||
@@ -137,5 +140,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
angular.module('containers', [])
|
||||
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'Info', 'Settings', 'Notifications', 'Config', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider',
|
||||
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, Info, Settings, Notifications, Config, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
|
||||
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'SystemService', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider', 'LocalStorage',
|
||||
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, SystemService, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider, LocalStorage) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
|
||||
$scope.state.displayAll = Settings.displayAll;
|
||||
$scope.state.displayAll = LocalStorage.getFilterContainerShowAll();
|
||||
$scope.state.displayIP = false;
|
||||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = false;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.truncate_size = 40;
|
||||
$scope.showMore = true;
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
@@ -25,9 +28,6 @@ angular.module('containers', [])
|
||||
$scope.state.selectedItemCount = 0;
|
||||
Container.query(data, function (d) {
|
||||
var containers = d;
|
||||
if ($scope.containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
|
||||
}
|
||||
$scope.containers = containers.map(function (container) {
|
||||
var model = new ContainerViewModel(container);
|
||||
model.Status = $filter('containerstatus')(model.Status);
|
||||
@@ -44,6 +44,7 @@ angular.module('containers', [])
|
||||
}
|
||||
return model;
|
||||
});
|
||||
updateSelectionFlags();
|
||||
$('#loadContainersSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadContainersSpinner').hide();
|
||||
@@ -59,7 +60,7 @@ angular.module('containers', [])
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadContainersSpinner').hide();
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
update({all: $scope.state.displayAll ? 1 : 0});
|
||||
}
|
||||
};
|
||||
angular.forEach(items, function (c) {
|
||||
@@ -120,22 +121,20 @@ angular.module('containers', [])
|
||||
angular.forEach($scope.state.filteredContainers, function (container) {
|
||||
if (container.Checked !== allSelected) {
|
||||
container.Checked = allSelected;
|
||||
$scope.selectItem(container);
|
||||
toggleItemSelection(container);
|
||||
}
|
||||
});
|
||||
updateSelectionFlags();
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
toggleItemSelection(item);
|
||||
updateSelectionFlags();
|
||||
};
|
||||
|
||||
$scope.toggleGetAll = function () {
|
||||
Settings.displayAll = $scope.state.displayAll;
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
LocalStorage.storeFilterContainerShowAll($scope.state.displayAll);
|
||||
update({all: $scope.state.displayAll ? 1 : 0});
|
||||
};
|
||||
|
||||
$scope.startAction = function () {
|
||||
@@ -166,6 +165,12 @@ angular.module('containers', [])
|
||||
batch($scope.containers, Container.remove, 'Removed');
|
||||
};
|
||||
|
||||
|
||||
$scope.truncateMore = function(size) {
|
||||
$scope.truncate_size = 80;
|
||||
$scope.showMore = false;
|
||||
};
|
||||
|
||||
$scope.confirmRemoveAction = function () {
|
||||
var isOneContainerRunning = false;
|
||||
angular.forEach($scope.containers, function (c) {
|
||||
@@ -191,6 +196,34 @@ angular.module('containers', [])
|
||||
);
|
||||
};
|
||||
|
||||
function toggleItemSelection(item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectionFlags() {
|
||||
$scope.state.noStoppedItemsSelected = true;
|
||||
$scope.state.noRunningItemsSelected = true;
|
||||
$scope.state.noPausedItemsSelected = true;
|
||||
$scope.containers.forEach(function(container) {
|
||||
if(!container.Checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(container.Status === 'paused') {
|
||||
$scope.state.noPausedItemsSelected = false;
|
||||
} else if(container.Status === 'stopped' ||
|
||||
container.Status === 'created') {
|
||||
$scope.state.noStoppedItemsSelected = false;
|
||||
} else if(container.Status === 'running') {
|
||||
$scope.state.noRunningItemsSelected = false;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function retrieveSwarmHostsInfo(data) {
|
||||
var swarm_hosts = {};
|
||||
var systemStatus = data.SystemStatus;
|
||||
@@ -206,15 +239,19 @@ angular.module('containers', [])
|
||||
return swarm_hosts;
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
|
||||
Info.get({}, function (d) {
|
||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
});
|
||||
} else {
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
}
|
||||
});
|
||||
function initView() {
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
$q.when(provider !== 'DOCKER_SWARM' || SystemService.info())
|
||||
.then(function success(data) {
|
||||
if (provider === 'DOCKER_SWARM') {
|
||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(data);
|
||||
}
|
||||
update({all: $scope.state.displayAll ? 1 : 0});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve cluster information');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
|
||||
// See app/components/templates/templatesController.js as a reference.
|
||||
angular.module('createContainer', [])
|
||||
.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator',
|
||||
function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) {
|
||||
.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService',
|
||||
function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService) {
|
||||
|
||||
$scope.formValues = {
|
||||
alwaysPull: true,
|
||||
Console: 'none',
|
||||
Volumes: [],
|
||||
Registry: '',
|
||||
NetworkContainer: '',
|
||||
Labels: [],
|
||||
ExtraHosts: [],
|
||||
IPv4: '',
|
||||
IPv6: ''
|
||||
IPv6: '',
|
||||
AccessControlData: new AccessControlFormData()
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
@@ -91,10 +91,12 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
|
||||
$scope.config.HostConfig.Devices.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.fromContainerMultipleNetworks = false;
|
||||
|
||||
function prepareImageConfig(config) {
|
||||
var image = config.Image;
|
||||
var registry = $scope.formValues.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry.URL);
|
||||
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
$scope.imageConfig = imageConfig;
|
||||
}
|
||||
@@ -178,6 +180,7 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
|
||||
var networkMode = mode;
|
||||
if (containerName) {
|
||||
networkMode += ':' + containerName;
|
||||
config.Hostname = '';
|
||||
}
|
||||
config.HostConfig.NetworkMode = networkMode;
|
||||
|
||||
@@ -232,48 +235,253 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
|
||||
return config;
|
||||
}
|
||||
|
||||
function initView() {
|
||||
Config.$promise.then(function (c) {
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve volumes');
|
||||
});
|
||||
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
function confirmCreateContainer() {
|
||||
var deferred = $q.defer();
|
||||
Container.query({ all: 1, filters: {name: ['^/' + $scope.config.name + '$'] }}).$promise
|
||||
.then(function success(data) {
|
||||
var existingContainer = data[0];
|
||||
if (existingContainer) {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure ?',
|
||||
message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Replace',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
});
|
||||
$scope.globalNetworkCount = networks.length;
|
||||
networks.push({Name: 'bridge'});
|
||||
networks.push({Name: 'host'});
|
||||
networks.push({Name: 'none'});
|
||||
}
|
||||
networks.push({Name: 'container'});
|
||||
$scope.availableNetworks = networks;
|
||||
if (!_.find(networks, {'Name': 'bridge'})) {
|
||||
$scope.config.HostConfig.NetworkMode = 'nat';
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve networks');
|
||||
});
|
||||
|
||||
Container.query({}, function (d) {
|
||||
var containers = d;
|
||||
if (containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
|
||||
}
|
||||
$scope.runningContainers = containers;
|
||||
}, function(e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve running containers');
|
||||
});
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { deferred.resolve(false); }
|
||||
else {
|
||||
// Remove old container
|
||||
ContainerService.remove(existingContainer, true)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Container Removed', existingContainer.Id);
|
||||
deferred.resolve(true);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to remove container', err: err });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deferred.resolve(true);
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
||||
return undefined;
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function loadFromContainerCmd(d) {
|
||||
if ($scope.config.Cmd) {
|
||||
$scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd);
|
||||
} else {
|
||||
$scope.config.Cmd = '';
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromContainerPortBindings(d) {
|
||||
var bindings = [];
|
||||
for (var p in $scope.config.HostConfig.PortBindings) {
|
||||
if ({}.hasOwnProperty.call($scope.config.HostConfig.PortBindings, p)) {
|
||||
var hostPort = '';
|
||||
if ($scope.config.HostConfig.PortBindings[p][0].HostIp) {
|
||||
hostPort = $scope.config.HostConfig.PortBindings[p][0].HostIp + ':';
|
||||
}
|
||||
hostPort += $scope.config.HostConfig.PortBindings[p][0].HostPort;
|
||||
var b = {
|
||||
'hostPort': hostPort,
|
||||
'containerPort': p.split('/')[0],
|
||||
'protocol': p.split('/')[1]
|
||||
};
|
||||
bindings.push(b);
|
||||
}
|
||||
}
|
||||
$scope.config.HostConfig.PortBindings = bindings;
|
||||
}
|
||||
|
||||
function loadFromContainerVolumes(d) {
|
||||
for (var v in d.Mounts) {
|
||||
if ({}.hasOwnProperty.call(d.Mounts, v)) {
|
||||
var mount = d.Mounts[v];
|
||||
var volume = {
|
||||
'type': mount.Type,
|
||||
'name': mount.Name || mount.Source,
|
||||
'containerPath': mount.Destination,
|
||||
'readOnly': mount.RW === false
|
||||
};
|
||||
$scope.formValues.Volumes.push(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromContainerNetworkConfig(d) {
|
||||
$scope.config.NetworkingConfig = {
|
||||
EndpointsConfig: {}
|
||||
};
|
||||
var networkMode = d.HostConfig.NetworkMode;
|
||||
if (networkMode === 'default') {
|
||||
$scope.config.HostConfig.NetworkMode = 'bridge';
|
||||
if (!_.find($scope.availableNetworks, {'Name': 'bridge'})) {
|
||||
$scope.config.HostConfig.NetworkMode = 'nat';
|
||||
}
|
||||
}
|
||||
if ($scope.config.HostConfig.NetworkMode.indexOf('container:') === 0) {
|
||||
var netContainer = $scope.config.HostConfig.NetworkMode.split(/^container:/)[1];
|
||||
$scope.config.HostConfig.NetworkMode = 'container';
|
||||
for (var c in $scope.runningContainers) {
|
||||
if ($scope.runningContainers[c].Names && $scope.runningContainers[c].Names[0] === '/' + netContainer) {
|
||||
$scope.formValues.NetworkContainer = $scope.runningContainers[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.fromContainerMultipleNetworks = Object.keys(d.NetworkSettings.Networks).length >= 2;
|
||||
if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]) {
|
||||
if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig) {
|
||||
if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address) {
|
||||
$scope.formValues.IPv4 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address;
|
||||
}
|
||||
if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address) {
|
||||
$scope.formValues.IPv6 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address;
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode];
|
||||
// ExtraHosts
|
||||
for (var h in $scope.config.HostConfig.ExtraHosts) {
|
||||
if ({}.hasOwnProperty.call($scope.config.HostConfig.ExtraHosts, h)) {
|
||||
$scope.formValues.ExtraHosts.push({'value': $scope.config.HostConfig.ExtraHosts[h]});
|
||||
$scope.config.HostConfig.ExtraHosts = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromContainerEnvrionmentVariables(d) {
|
||||
var envArr = [];
|
||||
for (var e in $scope.config.Env) {
|
||||
if ({}.hasOwnProperty.call($scope.config.Env, e)) {
|
||||
var arr = $scope.config.Env[e].split(/\=(.+)/);
|
||||
envArr.push({'name': arr[0], 'value': arr[1]});
|
||||
}
|
||||
}
|
||||
$scope.config.Env = envArr;
|
||||
}
|
||||
|
||||
function loadFromContainerLabels(d) {
|
||||
for (var l in $scope.config.Labels) {
|
||||
if ({}.hasOwnProperty.call($scope.config.Labels, l)) {
|
||||
$scope.formValues.Labels.push({ name: l, value: $scope.config.Labels[l]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromContainerConsole(d) {
|
||||
if ($scope.config.OpenStdin && $scope.config.Tty) {
|
||||
$scope.formValues.Console = 'both';
|
||||
} else if (!$scope.config.OpenStdin && $scope.config.Tty) {
|
||||
$scope.formValues.Console = 'tty';
|
||||
} else if ($scope.config.OpenStdin && !$scope.config.Tty) {
|
||||
$scope.formValues.Console = 'interactive';
|
||||
} else if (!$scope.config.OpenStdin && !$scope.config.Tty) {
|
||||
$scope.formValues.Console = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromContainerDevices(d) {
|
||||
var path = [];
|
||||
for (var dev in $scope.config.HostConfig.Devices) {
|
||||
if ({}.hasOwnProperty.call($scope.config.HostConfig.Devices, dev)) {
|
||||
var device = $scope.config.HostConfig.Devices[dev];
|
||||
path.push({'pathOnHost': device.PathOnHost, 'pathInContainer': device.PathInContainer});
|
||||
}
|
||||
}
|
||||
$scope.config.HostConfig.Devices = path;
|
||||
}
|
||||
|
||||
function loadFromContainerImageConfig(d) {
|
||||
var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image);
|
||||
RegistryService.retrieveRegistryFromRepository($scope.config.Image)
|
||||
.then(function success(data) {
|
||||
if (data) {
|
||||
$scope.config.Image = imageInfo.image;
|
||||
$scope.formValues.Registry = data;
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrive registry');
|
||||
});
|
||||
}
|
||||
|
||||
function loadFromContainerSpec() {
|
||||
// Get container
|
||||
Container.get({ id: $stateParams.from }).$promise
|
||||
.then(function success(d) {
|
||||
var fromContainer = new ContainerDetailsViewModel(d);
|
||||
if (!fromContainer.ResourceControl) {
|
||||
$scope.formValues.AccessControlData.AccessControlEnabled = false;
|
||||
}
|
||||
$scope.fromContainer = fromContainer;
|
||||
$scope.config = ContainerHelper.configFromContainer(fromContainer.Model);
|
||||
loadFromContainerCmd(d);
|
||||
loadFromContainerPortBindings(d);
|
||||
loadFromContainerVolumes(d);
|
||||
loadFromContainerNetworkConfig(d);
|
||||
loadFromContainerEnvrionmentVariables(d);
|
||||
loadFromContainerLabels(d);
|
||||
loadFromContainerConsole(d);
|
||||
loadFromContainerDevices(d);
|
||||
loadFromContainerImageConfig(d);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container');
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve volumes');
|
||||
});
|
||||
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
NetworkService.networks(
|
||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||
false,
|
||||
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25,
|
||||
provider === 'DOCKER_SWARM'
|
||||
)
|
||||
.then(function success(data) {
|
||||
var networks = data;
|
||||
networks.push({ Name: 'container' });
|
||||
$scope.availableNetworks = networks;
|
||||
|
||||
if (_.find(networks, {'Name': 'nat'})) {
|
||||
$scope.config.HostConfig.NetworkMode = 'nat';
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve networks');
|
||||
});
|
||||
|
||||
Container.query({}, function (d) {
|
||||
var containers = d;
|
||||
$scope.runningContainers = containers;
|
||||
if ($stateParams.from !== '') {
|
||||
loadFromContainerSpec();
|
||||
} else {
|
||||
$scope.fromContainer = {};
|
||||
$scope.formValues.Registry = {};
|
||||
}
|
||||
}, function(e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve running containers');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
@@ -289,43 +497,50 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
$('#createContainerSpinner').show();
|
||||
confirmCreateContainer()
|
||||
.then(function success(confirm) {
|
||||
if (!confirm) {
|
||||
return false;
|
||||
}
|
||||
$('#createContainerSpinner').show();
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true : false;
|
||||
|
||||
var accessControlData = ControllerDataPipeline.getAccessControlFormData();
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true : false;
|
||||
if (!validateForm(accessControlData, isAdmin)) {
|
||||
$('#createContainerSpinner').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateForm(accessControlData, isAdmin)) {
|
||||
$('#createContainerSpinner').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var config = prepareConfiguration();
|
||||
createContainer(config, accessControlData);
|
||||
};
|
||||
|
||||
function createContainer(config, accessControlData) {
|
||||
$q.when($scope.formValues.alwaysPull ? ImageService.pullImage($scope.config.Image, $scope.formValues.Registry) : null)
|
||||
.then(function success() {
|
||||
return ContainerService.createAndStartContainer(config);
|
||||
})
|
||||
.then(function success(data) {
|
||||
var containerIdentifier = data.Id;
|
||||
var userId = Authentication.getUserDetails().ID;
|
||||
return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, []);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Container successfully created');
|
||||
$state.go('containers', {}, {reload: true});
|
||||
var config = prepareConfiguration();
|
||||
createContainer(config, accessControlData);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create container');
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
function createContainer(config, accessControlData) {
|
||||
$q.when(!$scope.formValues.alwaysPull || ImageService.pullImage($scope.config.Image, $scope.formValues.Registry, true))
|
||||
.finally(function final() {
|
||||
$('#createContainerSpinner').hide();
|
||||
ContainerService.createAndStartContainer(config)
|
||||
.then(function success(data) {
|
||||
var containerIdentifier = data.Id;
|
||||
var userId = Authentication.getUserDetails().ID;
|
||||
return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, []);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Container successfully created');
|
||||
$state.go('containers', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create container');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#createContainerSpinner').hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
}]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create container"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="containers">Containers</a> > Add container
|
||||
<a ui-sref="containers">Containers</a> > Add container
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -21,34 +21,30 @@
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Image configuration
|
||||
</div>
|
||||
<!-- image-and-registry-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-11 col-md-6">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
|
||||
Registry
|
||||
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10 col-md-3 margin-sm-top">
|
||||
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
|
||||
</div>
|
||||
<div ng-if="!formValues.Registry && fromContainer">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
|
||||
<span class="small text-danger" style="margin-left: 5px;">The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register that registry first.</span>
|
||||
</div>
|
||||
<!-- !image-and-registry-inputs -->
|
||||
<!-- always-pull -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="ownership" class="control-label text-left">
|
||||
Always pull the image
|
||||
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically try to pull the specified image before creating the container."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
|
||||
</label>
|
||||
<div ng-if="formValues.Registry || !fromContainer">
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry>
|
||||
</div>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- always-pull -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="ownership" class="control-label text-left">
|
||||
Always pull the image
|
||||
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically try to pull the specified image before creating the container."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !always-pull -->
|
||||
</div>
|
||||
<!-- !always-pull -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ports configuration
|
||||
</div>
|
||||
@@ -108,7 +104,7 @@
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- access-control -->
|
||||
<div ng-include="'app/components/common/accessControlForm/accessControlForm.html'" ng-if="applicationState.application.authentication"></div>
|
||||
<por-access-control-form form-data="formValues.AccessControlData" resource-control="fromContainer.ResourceControl" ng-if="applicationState.application.authentication && fromContainer"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
@@ -116,10 +112,14 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="create()">Start container</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image || (!formValues.Registry && fromContainer)" ng-click="create()">Start container</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="containers">Cancel</a>
|
||||
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
<span ng-if="fromContainerMultipleNetworks" style="margin-left: 10px">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span class="small text-muted" style="margin-left: 5px;">This container is connected to multiple networks, only one network will be kept at creation time.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
angular.module('createNetwork', [])
|
||||
.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network',
|
||||
function ($scope, $state, Notifications, Network) {
|
||||
.controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
|
||||
function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
|
||||
|
||||
$scope.formValues = {
|
||||
DriverOptions: [],
|
||||
Subnet: '',
|
||||
Gateway: '',
|
||||
Labels: []
|
||||
Labels: [],
|
||||
AccessControlData: new AccessControlFormData()
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
formValidationError: ''
|
||||
};
|
||||
|
||||
$scope.availableNetworkDrivers = [];
|
||||
|
||||
$scope.config = {
|
||||
Driver: 'bridge',
|
||||
CheckDuplicate: true,
|
||||
@@ -30,30 +38,13 @@ function ($scope, $state, Notifications, Network) {
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
$scope.formValues.Labels.push({ key: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
function createNetwork(config) {
|
||||
$('#createNetworkSpinner').show();
|
||||
Network.create(config, function (d) {
|
||||
if (d.message) {
|
||||
$('#createNetworkSpinner').hide();
|
||||
Notifications.error('Unable to create network', {}, d.message);
|
||||
} else {
|
||||
Notifications.success('Network created', d.Id);
|
||||
$('#createNetworkSpinner').hide();
|
||||
$state.go('networks', {}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createNetworkSpinner').hide();
|
||||
Notifications.error('Failure', e, 'Unable to create network');
|
||||
});
|
||||
}
|
||||
|
||||
function prepareIPAMConfiguration(config) {
|
||||
if ($scope.formValues.Subnet) {
|
||||
var ipamConfig = {};
|
||||
@@ -74,13 +65,7 @@ function ($scope, $state, Notifications, Network) {
|
||||
}
|
||||
|
||||
function prepareLabelsConfig(config) {
|
||||
var labels = {};
|
||||
$scope.formValues.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
config.Labels = labels;
|
||||
config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels);
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
@@ -91,8 +76,66 @@ function ($scope, $state, Notifications, Network) {
|
||||
return config;
|
||||
}
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
$scope.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
var config = prepareConfiguration();
|
||||
createNetwork(config);
|
||||
$('#createResourceSpinner').show();
|
||||
|
||||
var networkConfiguration = prepareConfiguration();
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true : false;
|
||||
|
||||
if (!validateForm(accessControlData, isAdmin)) {
|
||||
$('#createResourceSpinner').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkService.create(networkConfiguration)
|
||||
.then(function success(data) {
|
||||
var networkIdentifier = data.Id;
|
||||
var userId = userDetails.ID;
|
||||
return ResourceControlService.applyResourceControl('network', networkIdentifier, userId, accessControlData, []);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Network successfully created');
|
||||
$state.go('networks', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'An error occured during network creation');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#createResourceSpinner').hide();
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
if(endpointProvider !== 'DOCKER_SWARM') {
|
||||
PluginService.networkPlugins(apiVersion < 1.25)
|
||||
.then(function success(data){
|
||||
$scope.availableNetworkDrivers = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve network drivers');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create network"></rd-header-title>
|
||||
<rd-header-title title="Create network">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="networks">Networks</a> > Add network
|
||||
<a ui-sref="networks">Networks</a> > Add network
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -39,8 +41,11 @@
|
||||
<!-- driver-input -->
|
||||
<div class="form-group">
|
||||
<label for="network_driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName">
|
||||
<div class="col-sm-11">
|
||||
<select class="form-control" ng-options="driver for driver in availableNetworkDrivers" ng-model="config.Driver" ng-if="availableNetworkDrivers.length > 0">
|
||||
<option disabled hidden value="">Select a driver</option>
|
||||
</select>
|
||||
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName" ng-if="availableNetworkDrivers.length === 0">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !driver-input -->
|
||||
@@ -90,7 +95,7 @@
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
@@ -116,6 +121,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !internal -->
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
@@ -124,7 +132,8 @@
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="create()">Create network</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="networks">Cancel</a>
|
||||
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
|
||||
49
app/components/createRegistry/createRegistryController.js
Normal file
49
app/components/createRegistry/createRegistryController.js
Normal file
@@ -0,0 +1,49 @@
|
||||
angular.module('createRegistry', [])
|
||||
.controller('CreateRegistryController', ['$scope', '$state', 'RegistryService', 'Notifications',
|
||||
function ($scope, $state, RegistryService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
RegistryType: 'quay'
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Name: 'Quay',
|
||||
URL: 'quay.io',
|
||||
Authentication: true,
|
||||
Username: '',
|
||||
Password: ''
|
||||
};
|
||||
|
||||
$scope.selectQuayRegistry = function() {
|
||||
$scope.formValues.Name = 'Quay';
|
||||
$scope.formValues.URL = 'quay.io';
|
||||
$scope.formValues.Authentication = true;
|
||||
};
|
||||
|
||||
$scope.selectCustomRegistry = function() {
|
||||
$scope.formValues.Name = '';
|
||||
$scope.formValues.URL = '';
|
||||
$scope.formValues.Authentication = false;
|
||||
};
|
||||
|
||||
$scope.addRegistry = function() {
|
||||
$('#createRegistrySpinner').show();
|
||||
var registryName = $scope.formValues.Name;
|
||||
var registryURL = $scope.formValues.URL.replace(/^https?\:\/\//i, '');
|
||||
var authentication = $scope.formValues.Authentication;
|
||||
var username = $scope.formValues.Username;
|
||||
var password = $scope.formValues.Password;
|
||||
|
||||
RegistryService.createRegistry(registryName, registryURL, authentication, username, password)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Registry successfully created');
|
||||
$state.go('registries');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create registry');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#createRegistrySpinner').hide();
|
||||
});
|
||||
};
|
||||
}]);
|
||||
117
app/components/createRegistry/createregistry.html
Normal file
117
app/components/createRegistry/createregistry.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create registry">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="display:none"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="registries">Registries</a> > Add registry
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Registry provider
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div ng-click="selectQuayRegistry()">
|
||||
<input type="radio" id="registry_quay" ng-model="state.RegistryType" value="quay">
|
||||
<label for="registry_quay">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Quay.io
|
||||
</div>
|
||||
<p>Quay container registry</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-click="selectCustomRegistry()">
|
||||
<input type="radio" id="registry_custom" ng-model="state.RegistryType" value="custom">
|
||||
<label for="registry_custom">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Custom registry
|
||||
</div>
|
||||
<p>Define your own registry</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title" ng-if="state.RegistryType === 'custom'">
|
||||
Important notice
|
||||
</div>
|
||||
<div class="form-group" ng-if="state.RegistryType === 'custom'">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Docker requires you to connect to a <a href="https://docs.docker.com/registry/deploying/#running-a-domain-registry" target="_blank">secure registry</a>.
|
||||
You can find more information about how to connect to an insecure registry <a href="https://docs.docker.com/registry/insecure/" target="_blank">in the Docker documentation</a>.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Registry details
|
||||
</div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group" ng-if="state.RegistryType === 'custom'">
|
||||
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="registry_name" ng-model="formValues.Name" placeholder="e.g. my-registry">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- registry-url-input -->
|
||||
<div class="form-group" ng-if="state.RegistryType === 'custom'">
|
||||
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Registry URL
|
||||
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry. Any protocol will be stripped."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="registry_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:5000 or myregistry.domain.tld">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !registry-url-input -->
|
||||
<!-- authentication-checkbox -->
|
||||
<div class="form-group" ng-if="state.RegistryType === 'custom'">
|
||||
<div class="col-sm-12">
|
||||
<label for="registry_auth" class="control-label text-left">
|
||||
Authentication
|
||||
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.Authentication"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !authentication-checkbox -->
|
||||
<!-- authentication-credentials -->
|
||||
<div ng-if="formValues.Authentication || state.RegistryType === 'quay'">
|
||||
<!-- credentials-user -->
|
||||
<div class="form-group">
|
||||
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="credentials_username" ng-model="formValues.Username">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !credentials-user -->
|
||||
<!-- credentials-password -->
|
||||
<div class="form-group">
|
||||
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="password" class="form-control" id="credentials_password" ng-model="formValues.Password">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !credentials-password -->
|
||||
</div>
|
||||
<!-- !authentication-credentials -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.Authentication && (!formValues.Username || !formValues.Password))" ng-click="addRegistry()">Add registry</button>
|
||||
<i id="createRegistrySpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
87
app/components/createSecret/createSecretController.js
Normal file
87
app/components/createSecret/createSecretController.js
Normal file
@@ -0,0 +1,87 @@
|
||||
angular.module('createSecret', [])
|
||||
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
|
||||
function ($scope, $state, Notifications, SecretService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
Data: '',
|
||||
Labels: [],
|
||||
encodeSecret: true,
|
||||
AccessControlData: new AccessControlFormData()
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
formValidationError: ''
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ key: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
function prepareLabelsConfig(config) {
|
||||
config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels);
|
||||
}
|
||||
|
||||
function prepareSecretData(config) {
|
||||
if ($scope.formValues.encodeSecret) {
|
||||
config.Data = btoa(unescape(encodeURIComponent($scope.formValues.Data)));
|
||||
} else {
|
||||
config.Data = $scope.formValues.Data;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = {};
|
||||
config.Name = $scope.formValues.Name;
|
||||
prepareSecretData(config);
|
||||
prepareLabelsConfig(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
$scope.state.formValidationError = '';
|
||||
var error = '';
|
||||
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
$scope.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
$('#createResourceSpinner').show();
|
||||
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true : false;
|
||||
|
||||
if (!validateForm(accessControlData, isAdmin)) {
|
||||
$('#createResourceSpinner').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var secretConfiguration = prepareConfiguration();
|
||||
SecretService.create(secretConfiguration)
|
||||
.then(function success(data) {
|
||||
var secretIdentifier = data.ID;
|
||||
var userId = userDetails.ID;
|
||||
return ResourceControlService.applyResourceControl('secret', secretIdentifier, userId, accessControlData, []);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Secret successfully created');
|
||||
$state.go('secrets', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create secret');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#createResourceSpinner').hide();
|
||||
});
|
||||
};
|
||||
}]);
|
||||
89
app/components/createSecret/createsecret.html
Normal file
89
app/components/createSecret/createsecret.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create secret"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="secrets">Secrets</a> > Add secret
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="secret_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" id="secret_name" placeholder="e.g. mySecret">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- secret-data -->
|
||||
<div class="form-group">
|
||||
<label for="secret_data" class="col-sm-1 control-label text-left">Secret</label>
|
||||
<div class="col-sm-11">
|
||||
<textarea class="form-control" rows="5" ng-model="formValues.Data"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !secret-data -->
|
||||
<!-- encode-secret -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="encode_secret" class="control-label text-left">
|
||||
Encode secret
|
||||
<portainer-tooltip position="bottom" message="Secrets need to be base64 encoded. Disable this if your secret is already base64 encoded."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="encode_secret" ng-model="formValues.encodeSecret"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !encode-secret -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels-->
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.Data" ng-click="create()">Create secret</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="secrets">Cancel</a>
|
||||
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +1,13 @@
|
||||
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
|
||||
// See app/components/templates/templatesController.js as a reference.
|
||||
angular.module('createService', [])
|
||||
.controller('CreateServiceController', ['$scope', '$state', 'Service', 'ServiceHelper', 'Volume', 'Network', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
|
||||
function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator) {
|
||||
.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', 'NodeService',
|
||||
function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService) {
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
Image: '',
|
||||
Registry: '',
|
||||
Registry: {},
|
||||
Mode: 'replicated',
|
||||
Replicas: 1,
|
||||
Command: '',
|
||||
@@ -23,14 +23,30 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
Ports: [],
|
||||
Parallelism: 1,
|
||||
PlacementConstraints: [],
|
||||
PlacementPreferences: [],
|
||||
UpdateDelay: 0,
|
||||
FailureAction: 'pause'
|
||||
UpdateOrder: 'stop-first',
|
||||
FailureAction: 'pause',
|
||||
Secrets: [],
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
CpuLimit: 0,
|
||||
CpuReservation: 0,
|
||||
MemoryLimit: 0,
|
||||
MemoryReservation: 0,
|
||||
MemoryLimitUnit: 'MB',
|
||||
MemoryReservationUnit: 'MB'
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
formValidationError: ''
|
||||
};
|
||||
|
||||
$scope.refreshSlider = function () {
|
||||
$timeout(function () {
|
||||
$scope.$broadcast('rzSliderForceRender');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addPortBinding = function() {
|
||||
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
|
||||
};
|
||||
@@ -55,6 +71,14 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
$scope.formValues.Volumes.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addSecret = function() {
|
||||
$scope.formValues.Secrets.push({});
|
||||
};
|
||||
|
||||
$scope.removeSecret = function(index) {
|
||||
$scope.formValues.Secrets.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addEnvironmentVariable = function() {
|
||||
$scope.formValues.Env.push({ name: '', value: ''});
|
||||
};
|
||||
@@ -62,20 +86,25 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
$scope.removeEnvironmentVariable = function(index) {
|
||||
$scope.formValues.Env.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addPlacementConstraint = function() {
|
||||
$scope.formValues.PlacementConstraints.push({ key: '', operator: '==', value: '' });
|
||||
};
|
||||
|
||||
$scope.removePlacementConstraint = function(index) {
|
||||
$scope.formValues.PlacementConstraints.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addPlacementPreference = function() {
|
||||
$scope.formValues.PlacementPreferences.push({ key: '', operator: '==', value: '' });
|
||||
$scope.formValues.PlacementPreferences.push({ strategy: 'spread', value: '' });
|
||||
};
|
||||
|
||||
$scope.removePlacementPreference = function(index) {
|
||||
$scope.formValues.PlacementPreferences.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
$scope.formValues.Labels.push({ key: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
@@ -83,7 +112,7 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
};
|
||||
|
||||
$scope.addContainerLabel = function() {
|
||||
$scope.formValues.ContainerLabels.push({ name: '', value: ''});
|
||||
$scope.formValues.ContainerLabels.push({ key: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeContainerLabel = function(index) {
|
||||
@@ -91,7 +120,7 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
};
|
||||
|
||||
function prepareImageConfig(config, input) {
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry);
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry.URL);
|
||||
config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
}
|
||||
|
||||
@@ -156,21 +185,8 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
}
|
||||
|
||||
function prepareLabelsConfig(config, input) {
|
||||
var labels = {};
|
||||
input.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
config.Labels = labels;
|
||||
|
||||
var containerLabels = {};
|
||||
input.ContainerLabels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
containerLabels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
config.TaskTemplate.ContainerSpec.Labels = containerLabels;
|
||||
config.Labels = LabelHelper.fromKeyValueToLabelHash(input.Labels);
|
||||
config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(input.ContainerLabels);
|
||||
}
|
||||
|
||||
function prepareVolumes(config, input) {
|
||||
@@ -196,11 +212,60 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
config.UpdateConfig = {
|
||||
Parallelism: input.Parallelism || 0,
|
||||
Delay: input.UpdateDelay || 0,
|
||||
FailureAction: input.FailureAction
|
||||
FailureAction: input.FailureAction,
|
||||
Order: input.UpdateOrder
|
||||
};
|
||||
}
|
||||
|
||||
function preparePlacementConfig(config, input) {
|
||||
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints);
|
||||
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(input.PlacementPreferences);
|
||||
}
|
||||
|
||||
function prepareSecretConfig(config, input) {
|
||||
if (input.Secrets) {
|
||||
var secrets = [];
|
||||
angular.forEach(input.Secrets, function(secret) {
|
||||
if (secret.model) {
|
||||
var s = SecretHelper.secretConfig(secret.model);
|
||||
s.File.Name = s.SecretName;
|
||||
secrets.push(s);
|
||||
}
|
||||
});
|
||||
config.TaskTemplate.ContainerSpec.Secrets = secrets;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareResourcesCpuConfig(config, input) {
|
||||
// CPU Limit
|
||||
if (input.CpuLimit > 0) {
|
||||
config.TaskTemplate.Resources.Limits.NanoCPUs = input.CpuLimit * 1000000000;
|
||||
}
|
||||
// CPU Reservation
|
||||
if (input.CpuReservation > 0) {
|
||||
config.TaskTemplate.Resources.Reservations.NanoCPUs = input.CpuReservation * 1000000000;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareResourcesMemoryConfig(config, input) {
|
||||
// Memory Limit - Round to 0.125
|
||||
var memoryLimit = (Math.round(input.MemoryLimit * 8) / 8).toFixed(3);
|
||||
memoryLimit *= 1024 * 1024;
|
||||
if (input.MemoryLimitUnit === 'GB') {
|
||||
memoryLimit *= 1024;
|
||||
}
|
||||
if (memoryLimit > 0) {
|
||||
config.TaskTemplate.Resources.Limits.MemoryBytes = memoryLimit;
|
||||
}
|
||||
// Memory Resevation - Round to 0.125
|
||||
var memoryReservation = (Math.round(input.MemoryReservation * 8) / 8).toFixed(3);
|
||||
memoryReservation *= 1024 * 1024;
|
||||
if (input.MemoryReservationUnit === 'GB') {
|
||||
memoryReservation *= 1024;
|
||||
}
|
||||
if (memoryReservation > 0) {
|
||||
config.TaskTemplate.Resources.Reservations.MemoryBytes = memoryReservation;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
@@ -211,7 +276,11 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
ContainerSpec: {
|
||||
Mounts: []
|
||||
},
|
||||
Placement: {}
|
||||
Placement: {},
|
||||
Resources: {
|
||||
Limits: {},
|
||||
Reservations: {}
|
||||
}
|
||||
},
|
||||
Mode: {},
|
||||
EndpointSpec: {}
|
||||
@@ -225,11 +294,18 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
prepareVolumes(config, input);
|
||||
prepareNetworks(config, input);
|
||||
prepareUpdateConfig(config, input);
|
||||
prepareSecretConfig(config, input);
|
||||
preparePlacementConfig(config, input);
|
||||
prepareResourcesCpuConfig(config, input);
|
||||
prepareResourcesMemoryConfig(config, input);
|
||||
return config;
|
||||
}
|
||||
|
||||
function createNewService(config, accessControlData) {
|
||||
|
||||
var registry = $scope.formValues.Registry;
|
||||
var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : '';
|
||||
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
|
||||
Service.create(config).$promise
|
||||
.then(function success(data) {
|
||||
var serviceIdentifier = data.ID;
|
||||
@@ -263,7 +339,7 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
$scope.create = function createService() {
|
||||
$('#createServiceSpinner').show();
|
||||
|
||||
var accessControlData = ControllerDataPipeline.getAccessControlFormData();
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true : false;
|
||||
|
||||
@@ -277,20 +353,38 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
||||
};
|
||||
|
||||
function initView() {
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve volumes');
|
||||
});
|
||||
$('#loadingViewSpinner').show();
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
|
||||
Network.query({}, function (d) {
|
||||
$scope.availableNetworks = d.filter(function (network) {
|
||||
if (network.Scope === 'swarm') {
|
||||
return network;
|
||||
$q.all({
|
||||
volumes: VolumeService.volumes(),
|
||||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||
networks: NetworkService.networks(true, true, false, false),
|
||||
nodes: NodeService.nodes()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.availableVolumes = data.volumes;
|
||||
$scope.availableNetworks = data.networks;
|
||||
$scope.availableSecrets = data.secrets;
|
||||
// Set max cpu value
|
||||
var maxCpus = 0;
|
||||
for (var n in data.nodes) {
|
||||
if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) {
|
||||
maxCpus = data.nodes[n].CPUs;
|
||||
}
|
||||
});
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve networks');
|
||||
}
|
||||
if (maxCpus > 0) {
|
||||
$scope.state.sliderMaxCpu = maxCpus / 1000000000;
|
||||
} else {
|
||||
$scope.state.sliderMaxCpu = 32;
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to initialize view');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create service"></rd-header-title>
|
||||
<rd-header-title title="Create service">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="services">Services</a> > Add service
|
||||
<a ui-sref="services">Services</a> > Add service
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -21,21 +23,11 @@
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Image configuration
|
||||
</div>
|
||||
<!-- image-and-registry-inputs -->
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<label for="service_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-11 col-md-6">
|
||||
<input type="text" class="form-control" ng-model="formValues.Image" id="service_image" placeholder="e.g. nginx:latest">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
|
||||
Registry
|
||||
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10 col-md-3 margin-sm-top">
|
||||
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
|
||||
</div>
|
||||
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry>
|
||||
</div>
|
||||
<!-- !image-and-registry-inputs -->
|
||||
<!-- !image-and-registry -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Scheduling
|
||||
</div>
|
||||
@@ -109,7 +101,7 @@
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- access-control -->
|
||||
<div ng-include="'app/components/common/accessControlForm/accessControlForm.html'" ng-if="applicationState.application.authentication"></div>
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
@@ -140,7 +132,8 @@
|
||||
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li>
|
||||
<li class="interactive"><a data-target="#placement" data-toggle="tab">Placement</a></li>
|
||||
<li class="interactive" ng-if="applicationState.endpoint.apiVersion >= 1.25"><a data-target="#secrets" data-toggle="tab">Secrets</a></li>
|
||||
<li class="interactive"><a data-target="#resources-placement" data-toggle="tab" ng-click="refreshSlider()">Resources & Placement</a></li>
|
||||
</ul>
|
||||
<!-- tab-content -->
|
||||
<div class="tab-content">
|
||||
@@ -249,7 +242,7 @@
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select class="form-control" ng-model="volume.Source">
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Id">{{ vol.Id|truncate:30 }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- !volume -->
|
||||
@@ -335,7 +328,7 @@
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
@@ -362,7 +355,7 @@
|
||||
<div ng-repeat="label in formValues.ContainerLabels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
@@ -384,12 +377,12 @@
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- parallelism-input -->
|
||||
<div class="form-group">
|
||||
<label for="parallelism" class="col-sm-2 col-lg-1 control-label text-left">Parallelism</label>
|
||||
<div class="col-sm-2">
|
||||
<label for="parallelism" class="col-sm-3 col-lg-1 control-label text-left">Parallelism</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<input type="number" class="form-control" ng-model="formValues.Parallelism" id="parallelism" placeholder="e.g. 1">
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Maximum number of tasks to be updated simultaneously (0 to update all at once).
|
||||
</p>
|
||||
</div>
|
||||
@@ -397,12 +390,12 @@
|
||||
<!-- !parallelism-input -->
|
||||
<!-- delay-input -->
|
||||
<div class="form-group">
|
||||
<label for="update-delay" class="col-sm-2 col-lg-1 control-label text-left">Delay</label>
|
||||
<div class="col-sm-2">
|
||||
<label for="update-delay" class="col-sm-3 col-lg-1 control-label text-left">Delay</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<input type="number" class="form-control" ng-model="formValues.UpdateDelay" id="update-delay" placeholder="e.g. 10">
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Amount of time between updates.
|
||||
</p>
|
||||
</div>
|
||||
@@ -410,22 +403,48 @@
|
||||
<!-- !delay-input -->
|
||||
<!-- failureAction-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Failure action</label>
|
||||
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
|
||||
<label for="failure-action" class="col-sm-3 col-lg-1 control-label text-left">Failure action</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="formValues.FailureAction" uib-btn-radio="'continue'">Continue</label>
|
||||
<label class="btn btn-primary" ng-model="formValues.FailureAction" uib-btn-radio="'pause'">Pause</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Action taken on failure to start after update.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- !failureAction-input -->
|
||||
<!-- order-input -->
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.29">
|
||||
|
||||
<label for="update-order" class="col-sm-3 col-lg-1 control-label text-left">Order</label>
|
||||
<div class="col-sm-4 col-lg-3">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="formValues.UpdateOrder" uib-btn-radio="'start-first'">start-first</label>
|
||||
<label class="btn btn-primary" ng-model="formValues.UpdateOrder" uib-btn-radio="'stop-first'">stop-first</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<p class="small text-muted">
|
||||
Operation order on failure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- !order-input -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-update-config -->
|
||||
|
||||
<!-- tab-placement -->
|
||||
<div class="tab-pane" id="placement" ng-include="'app/components/createService/includes/placement.html'"></div>
|
||||
<!-- !tab-placement -->
|
||||
<!-- tab-secrets -->
|
||||
<div class="tab-pane" id="secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" ng-include="'app/components/createService/includes/secret.html'"></div>
|
||||
<!-- !tab-secrets -->
|
||||
<!-- tab-resources-placement -->
|
||||
<div class="tab-pane" id="resources-placement" ng-include="'app/components/createService/includes/resources-placement.html'"></div>
|
||||
<!-- !tab-resources-placement -->
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Placement constraints</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint(service)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="constraint in formValues.PlacementConstraints" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="constraint.key" placeholder="e.g. node.role">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select name="constraintOperator" class="form-control" ng-model="constraint.operator">
|
||||
<option value="==">==</option>
|
||||
<option value="!=">!=</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="constraint.value" placeholder="e.g. manager">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementConstraint($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
136
app/components/createService/includes/resources-placement.html
Normal file
136
app/components/createService/includes/resources-placement.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resources
|
||||
</div>
|
||||
<!-- memory-reservation-input -->
|
||||
<div class="form-group">
|
||||
<label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Memory reservation
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" step="0.125" min="0" class="form-control" ng-model="formValues.MemoryReservation" id="memory-reservation" placeholder="e.g. 64">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<select class="form-control" ng-model="formValues.MemoryReservationUnit">
|
||||
<option value="MB">MB</option>
|
||||
<option value="GB">GB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted">
|
||||
Minimum memory available on a node to run a task
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !memory-reservation-input -->
|
||||
<!-- memory-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Memory limit
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" step="0.125" min="0" class="form-control" ng-model="formValues.MemoryLimit" id="memory-limit" placeholder="e.g. 128">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<select class="form-control" ng-model="formValues.MemoryLimitUnit">
|
||||
<option value="MB">MB</option>
|
||||
<option value="GB">GB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted">
|
||||
Maximum memory usage per task (set to 0 for unlimited)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !memory-limit-input -->
|
||||
<!-- cpu-reservation-input -->
|
||||
<div class="form-group">
|
||||
<label for="cpu-reservation" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
CPU reservation
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<por-slider model="formValues.CpuReservation" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="state.sliderMaxCpu"></por-slider>
|
||||
</div>
|
||||
<div class="col-sm-4" style="margin-top: 20px;">
|
||||
<p class="small text-muted">
|
||||
Minimum CPU available on a node to run a task
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cpu-reservation-input -->
|
||||
<!-- cpu-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
CPU limit
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<por-slider model="formValues.CpuLimit" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="state.sliderMaxCpu"></por-slider>
|
||||
</div>
|
||||
<div class="col-sm-4" style="margin-top: 20px;">
|
||||
<p class="small text-muted">
|
||||
Maximum CPU usage per task
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cpu-limit-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Placement
|
||||
</div>
|
||||
<!-- placement-constraints -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Placement constraints</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="constraint in formValues.PlacementConstraints" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="constraint.key" placeholder="e.g. node.role">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select name="constraintOperator" class="form-control" ng-model="constraint.operator">
|
||||
<option value="==">==</option>
|
||||
<option value="!=">!=</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="constraint.value" placeholder="e.g. manager">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementConstraint($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !placement-constraints -->
|
||||
<!-- placement-preferences -->
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.30">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Placement preferences</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementPreference()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement preference
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="preference in formValues.PlacementPreferences" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">strategy</span>
|
||||
<input type="text" class="form-control" ng-model="preference.strategy" placeholder="e.g. spread">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="preference.value" placeholder="e.g. node.labels.datacenter">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementPreference($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !placement-preferences -->
|
||||
</form>
|
||||
28
app/components/createService/includes/secret.html
Normal file
28
app/components/createService/includes/secret.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
Secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Secrets</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSecret()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add a secret
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="secret in formValues.Secrets" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">secret</span>
|
||||
<select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets">
|
||||
<option value="" selected="selected">Select a secret</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSecret($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,10 +1,11 @@
|
||||
angular.module('createVolume', [])
|
||||
.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'InfoService', 'ResourceControlService', 'Authentication', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
|
||||
function ($scope, $state, VolumeService, InfoService, ResourceControlService, Authentication, Notifications, ControllerDataPipeline, FormValidator) {
|
||||
.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator',
|
||||
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator) {
|
||||
|
||||
$scope.formValues = {
|
||||
Driver: 'local',
|
||||
DriverOptions: []
|
||||
DriverOptions: [],
|
||||
AccessControlData: new AccessControlFormData()
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
@@ -40,8 +41,8 @@ function ($scope, $state, VolumeService, InfoService, ResourceControlService, Au
|
||||
var driver = $scope.formValues.Driver;
|
||||
var driverOptions = $scope.formValues.DriverOptions;
|
||||
var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions);
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var accessControlData = ControllerDataPipeline.getAccessControlFormData();
|
||||
var isAdmin = userDetails.role === 1 ? true : false;
|
||||
|
||||
if (!validateForm(accessControlData, isAdmin)) {
|
||||
@@ -69,16 +70,20 @@ function ($scope, $state, VolumeService, InfoService, ResourceControlService, Au
|
||||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
InfoService.getVolumePlugins()
|
||||
.then(function success(data) {
|
||||
$scope.availableVolumeDrivers = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve volume drivers');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
if (endpointProvider !== 'DOCKER_SWARM') {
|
||||
PluginService.volumePlugins(apiVersion < 1.25 || endpointProvider === 'VMWARE_VIC')
|
||||
.then(function success(data) {
|
||||
$scope.availableVolumeDrivers = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve volume drivers');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="volumes">Volumes</a> > Add volume
|
||||
<a ui-sref="volumes">Volumes</a> > Add volume
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
<!-- !driver-options -->
|
||||
<!-- access-control -->
|
||||
<div ng-include="'app/components/common/accessControlForm/accessControlForm.html'" ng-if="applicationState.application.authentication"></div>
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
||||
@@ -125,9 +125,6 @@
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-cubes"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-if="infoData.Driver">
|
||||
<div><i class="fa fa-hdd-o space-right"></i>{{ infoData.Driver }} driver</div>
|
||||
</div>
|
||||
<div class="title">{{ volumeData.total }}</div>
|
||||
<div class="comment">Volumes</div>
|
||||
</rd-widget-body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('dashboard', [])
|
||||
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications',
|
||||
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) {
|
||||
.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'SystemService', 'Notifications',
|
||||
function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, SystemService, Notifications) {
|
||||
|
||||
$scope.containerData = {
|
||||
total: 0
|
||||
@@ -15,14 +15,10 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
||||
total: 0
|
||||
};
|
||||
|
||||
function prepareContainerData(d, containersToHideLabels) {
|
||||
function prepareContainerData(d) {
|
||||
var running = 0;
|
||||
var stopped = 0;
|
||||
|
||||
var containers = d;
|
||||
if (containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
|
||||
}
|
||||
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var item = containers[i];
|
||||
@@ -65,16 +61,16 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
||||
$scope.infoData = info;
|
||||
}
|
||||
|
||||
function fetchDashboardData(containersToHideLabels) {
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all([
|
||||
Container.query({all: 1}).$promise,
|
||||
Image.query({}).$promise,
|
||||
Volume.query({}).$promise,
|
||||
Network.query({}).$promise,
|
||||
Info.get({}).$promise
|
||||
SystemService.info()
|
||||
]).then(function (d) {
|
||||
prepareContainerData(d[0], containersToHideLabels);
|
||||
prepareContainerData(d[0]);
|
||||
prepareImageData(d[1]);
|
||||
prepareVolumeData(d[2]);
|
||||
prepareNetworkData(d[3]);
|
||||
@@ -86,7 +82,5 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
||||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
fetchDashboardData(c.hiddenLabels);
|
||||
});
|
||||
initView();
|
||||
}]);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
angular.module('docker', [])
|
||||
.controller('DockerController', ['$scope', 'Info', 'Version', 'Notifications',
|
||||
function ($scope, Info, Version, Notifications) {
|
||||
$scope.state = {
|
||||
loaded: false
|
||||
};
|
||||
$scope.info = {};
|
||||
$scope.version = {};
|
||||
|
||||
Info.get({}, function (infoData) {
|
||||
$scope.info = infoData;
|
||||
Version.get({}, function (versionData) {
|
||||
$scope.version = versionData;
|
||||
$scope.state.loaded = true;
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve engine details');
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve engine information');
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}]);
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
|
||||
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Configuration
|
||||
</div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||
@@ -38,77 +41,23 @@
|
||||
<portainer-tooltip position="bottom" message="URL or IP address where exposed containers will be reachable. This field is optional and will default to the endpoint URL."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input ng-disabled="endpointType === 'local'" type="text" class="form-control" id="endpoint_public_url" ng-model="endpoint.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
|
||||
<input type="text" class="form-control" id="endpoint_public_url" ng-model="endpoint.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-public-url-input -->
|
||||
<!-- tls-checkbox -->
|
||||
<div class="form-group" ng-if="endpointType === 'remote'">
|
||||
<div class="col-sm-12">
|
||||
<label for="tls" class="control-label text-left">
|
||||
TLS
|
||||
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="endpoint.TLS"><i></i>
|
||||
</label>
|
||||
<!-- endpoint-security -->
|
||||
<div ng-if="endpointType === 'remote'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Security
|
||||
</div>
|
||||
<por-endpoint-security form-data="formValues.SecurityFormData" endpoint="endpoint"></por-endpoint-security>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-certs -->
|
||||
<div ng-if="endpoint.TLS">
|
||||
<!-- ca-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSCACert !== endpoint.TLSCACert">{{ formValues.TLSCACert.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSCACert && formValues.TLSCACert === endpoint.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ca-input -->
|
||||
<!-- cert-input -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSCert !== endpoint.TLSCert">{{ formValues.TLSCert.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSCert && formValues.TLSCert === endpoint.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cert-input -->
|
||||
<!-- key-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSKey !== endpoint.TLSKey">{{ formValues.TLSKey.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSKey && formValues.TLSKey === endpoint.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !key-input -->
|
||||
</div>
|
||||
<!-- !tls-certs -->
|
||||
<!-- !endpoint-security -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="updateEndpoint()">Update endpoint</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && ((endpoint.TLSVerify && !formValues.TLSCACert) || (endpoint.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="updateEndpoint()">Update endpoint</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="endpoints">Cancel</a>
|
||||
<i id="updateEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</span>
|
||||
<i id="updateResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -7,35 +7,41 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications)
|
||||
}
|
||||
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
TLSCACert: null,
|
||||
TLSCert: null,
|
||||
TLSKey: null
|
||||
SecurityFormData: new EndpointSecurityFormData()
|
||||
};
|
||||
|
||||
$scope.updateEndpoint = function() {
|
||||
var ID = $scope.endpoint.Id;
|
||||
var endpoint = $scope.endpoint;
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
var TLSMode = securityData.TLSMode;
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
|
||||
var endpointParams = {
|
||||
name: $scope.endpoint.Name,
|
||||
URL: $scope.endpoint.URL,
|
||||
PublicURL: $scope.endpoint.PublicURL,
|
||||
TLS: $scope.endpoint.TLS,
|
||||
TLSCACert: $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null,
|
||||
TLSCert: $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null,
|
||||
TLSKey: $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null,
|
||||
name: endpoint.Name,
|
||||
URL: endpoint.URL,
|
||||
PublicURL: endpoint.PublicURL,
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||
TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert,
|
||||
TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert,
|
||||
TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey,
|
||||
type: $scope.endpointType
|
||||
};
|
||||
|
||||
EndpointService.updateEndpoint(ID, endpointParams)
|
||||
$('updateResourceSpinner').show();
|
||||
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||
$state.go('endpoints');
|
||||
}, function error(err) {
|
||||
$scope.state.error = err.msg;
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint');
|
||||
}, function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
@@ -43,25 +49,27 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications)
|
||||
});
|
||||
};
|
||||
|
||||
function getEndpoint(endpointID) {
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
EndpointService.endpoint($stateParams.id).then(function success(data) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
$scope.endpoint = data;
|
||||
if (data.URL.indexOf('unix://') === 0) {
|
||||
EndpointService.endpoint($stateParams.id)
|
||||
.then(function success(data) {
|
||||
var endpoint = data;
|
||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||
$scope.endpoint = endpoint;
|
||||
|
||||
if (endpoint.URL.indexOf('unix://') === 0) {
|
||||
$scope.endpointType = 'local';
|
||||
} else {
|
||||
$scope.endpointType = 'remote';
|
||||
}
|
||||
$scope.endpoint.URL = $filter('stripprotocol')(data.URL);
|
||||
$scope.formValues.TLSCACert = data.TLSCACert;
|
||||
$scope.formValues.TLSCert = data.TLSCert;
|
||||
$scope.formValues.TLSKey = data.TLSKey;
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
getEndpoint($stateParams.id);
|
||||
initView();
|
||||
}]);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a> > Access management
|
||||
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a> > Access management
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -41,137 +41,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="endpoint">
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users and groups">
|
||||
<div class="pull-md-right pull-lg-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count_accesses" ng-change="changePaginationCountAccesses()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<button class="btn btn-primary btn-sm" ng-click="authorizeAllAccesses()" ng-disabled="accesses.length === 0 || filteredUsers.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all</button>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<input type="text" id="filter" ng-model="state.filterUsers" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAccesses('Name')">
|
||||
Name
|
||||
<span ng-show="sortTypeAccesses == 'Name' && !sortReverseAccesses" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortTypeAccesses == 'Name' && sortReverseAccesses" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAccesses('Type')">
|
||||
Type
|
||||
<span ng-show="sortTypeAccesses == 'Type' && !sortReverseAccesses" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortTypeAccesses == 'Type' && sortReverseAccesses" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-click="authorizeAccess(user)" class="interactive" dir-paginate="user in accesses | filter:state.filterUsers | orderBy:sortTypeAccesses:sortReverseAccesses | itemsPerPage: state.pagination_count_accesses">
|
||||
<td>{{ user.Name }}</td>
|
||||
<td>
|
||||
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ user.Type }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!accesses">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="accesses.length === 0 || (accesses | filter:state.filterUsers | orderBy:sortTypeAccesses:sortReverseAccesses | itemsPerPage: state.pagination_count_accesses).length === 0">
|
||||
<td colspan="2" class="text-center text-muted">No user or team available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="accesses" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users and groups">
|
||||
<div class="pull-md-right pull-lg-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count_authorizedAccesses" ng-change="changePaginationCountAuthorizedAccesses()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<button class="btn btn-primary btn-sm" ng-click="unauthorizeAllAccesses()" ng-disabled="authorizedAccesses.length === 0 || filteredAuthorizedUsers.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all</button>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<input type="text" id="filter" ng-model="state.filterAuthorizedUsers" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAuthorizedAccesses('Name')">
|
||||
Name
|
||||
<span ng-show="sortTypeAuthorizedAccesses == 'Name' && !sortReverseAuthorizedAccesses" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortTypeAuthorizedAccesses == 'Name' && sortReverseAuthorizedAccesses" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAuthorizedAccesses('Type')">
|
||||
Type
|
||||
<span ng-show="sortTypeAuthorizedAccesses == 'Type' && !sortReverseAuthorizedAccesses" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortTypeAuthorizedAccesses == 'Type' && sortReverseAuthorizedAccesses" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-click="unauthorizeAccess(user)" class="interactive" pagination-id="table_authaccess" dir-paginate="user in authorizedAccesses | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedAccesses:sortReverseAuthorizedAccesses | itemsPerPage: state.pagination_count_authorizedAccesses">
|
||||
<td>{{ user.Name }}</td>
|
||||
<td>
|
||||
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ user.Type }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!authorizedAccesses">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="authorizedAccesses.length === 0 || (authorizedAccesses | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedAccesses:sortReverseAuthorizedAccesses | itemsPerPage: state.pagination_count_authorizedAccesses).length === 0">
|
||||
<td colspan="2" class="text-center text-muted">No authorized user or team.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="authorizedAccesses" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls pagination-id="table_authaccess"></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<por-access-management ng-if="endpoint" access-controlled-entity="endpoint" update-access="updateAccess(userAccesses, teamAccesses)">
|
||||
</por-access-management>
|
||||
|
||||
@@ -1,177 +1,18 @@
|
||||
angular.module('endpointAccess', [])
|
||||
.controller('EndpointAccessController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'UserService', 'TeamService', 'Pagination', 'Notifications',
|
||||
function ($q, $scope, $state, $stateParams, $filter, EndpointService, UserService, TeamService, Pagination, Notifications) {
|
||||
.controller('EndpointAccessController', ['$scope', '$stateParams', 'EndpointService', 'Notifications',
|
||||
function ($scope, $stateParams, EndpointService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
pagination_count_accesses: Pagination.getPaginationCount('endpoint_access_accesses'),
|
||||
pagination_count_authorizedAccesses: Pagination.getPaginationCount('endpoint_access_authorizedAccesses')
|
||||
};
|
||||
|
||||
$scope.sortTypeAccesses = 'Type';
|
||||
$scope.sortReverseAccesses = false;
|
||||
|
||||
$scope.orderAccesses = function(sortType) {
|
||||
$scope.sortReverseAccesses = ($scope.sortTypeAccesses === sortType) ? !$scope.sortReverseAccesses : false;
|
||||
$scope.sortTypeAccesses = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCountAccesses = function() {
|
||||
Pagination.setPaginationCount('endpoint_access_accesses', $scope.state.pagination_count_accesses);
|
||||
};
|
||||
|
||||
$scope.sortTypeAuthorizedAccesses = 'Type';
|
||||
$scope.sortReverseAuthorizedAccesses = false;
|
||||
|
||||
$scope.orderAuthorizedAccesses = function(sortType) {
|
||||
$scope.sortReverseAuthorizedAccesses = ($scope.sortTypeAuthorizedAccesses === sortType) ? !$scope.sortReverseAuthorizedAccesses : false;
|
||||
$scope.sortTypeAuthorizedAccesses = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCountAuthorizedAccesses = function() {
|
||||
Pagination.setPaginationCount('endpoint_access_authorizedAccesses', $scope.state.pagination_count_authorizedAccesses);
|
||||
};
|
||||
|
||||
$scope.authorizeAllAccesses = function() {
|
||||
var authorizedUsers = [];
|
||||
var authorizedTeams = [];
|
||||
angular.forEach($scope.authorizedAccesses, function (a) {
|
||||
if (a.Type === 'user') {
|
||||
authorizedUsers.push(a.Id);
|
||||
} else if (a.Type === 'team') {
|
||||
authorizedTeams.push(a.Id);
|
||||
}
|
||||
});
|
||||
angular.forEach($scope.accesses, function (a) {
|
||||
if (a.Type === 'user') {
|
||||
authorizedUsers.push(a.Id);
|
||||
} else if (a.Type === 'team') {
|
||||
authorizedTeams.push(a.Id);
|
||||
}
|
||||
});
|
||||
|
||||
EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams)
|
||||
.then(function success(data) {
|
||||
$scope.authorizedAccesses = $scope.authorizedAccesses.concat($scope.accesses);
|
||||
$scope.accesses = [];
|
||||
Notifications.success('Endpoint accesses successfully updated');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint accesses');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.unauthorizeAllAccesses = function() {
|
||||
EndpointService.updateAccess($stateParams.id, [], [])
|
||||
.then(function success(data) {
|
||||
$scope.accesses = $scope.accesses.concat($scope.authorizedAccesses);
|
||||
$scope.authorizedAccesses = [];
|
||||
Notifications.success('Endpoint accesses successfully updated');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint accesses');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.authorizeAccess = function(access) {
|
||||
var authorizedUsers = [];
|
||||
var authorizedTeams = [];
|
||||
angular.forEach($scope.authorizedAccesses, function (a) {
|
||||
if (a.Type === 'user') {
|
||||
authorizedUsers.push(a.Id);
|
||||
} else if (a.Type === 'team') {
|
||||
authorizedTeams.push(a.Id);
|
||||
}
|
||||
});
|
||||
|
||||
if (access.Type === 'user') {
|
||||
authorizedUsers.push(access.Id);
|
||||
} else if (access.Type === 'team') {
|
||||
authorizedTeams.push(access.Id);
|
||||
}
|
||||
|
||||
EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams)
|
||||
.then(function success(data) {
|
||||
removeAccessFromArray(access, $scope.accesses);
|
||||
$scope.authorizedAccesses.push(access);
|
||||
Notifications.success('Endpoint accesses successfully updated', access.Name);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint accesses');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.unauthorizeAccess = function(access) {
|
||||
var authorizedUsers = [];
|
||||
var authorizedTeams = [];
|
||||
angular.forEach($scope.authorizedAccesses, function (a) {
|
||||
if (a.Type === 'user') {
|
||||
authorizedUsers.push(a.Id);
|
||||
} else if (a.Type === 'team') {
|
||||
authorizedTeams.push(a.Id);
|
||||
}
|
||||
});
|
||||
|
||||
if (access.Type === 'user') {
|
||||
_.remove(authorizedUsers, function(n) {
|
||||
return n === access.Id;
|
||||
});
|
||||
} else if (access.Type === 'team') {
|
||||
_.remove(authorizedTeams, function(n) {
|
||||
return n === access.Id;
|
||||
});
|
||||
}
|
||||
|
||||
EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams)
|
||||
.then(function success(data) {
|
||||
removeAccessFromArray(access, $scope.authorizedAccesses);
|
||||
$scope.accesses.push(access);
|
||||
Notifications.success('Endpoint accesses successfully updated', access.Name);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint accesses');
|
||||
});
|
||||
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
|
||||
return EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams);
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all({
|
||||
endpoint: EndpointService.endpoint($stateParams.id),
|
||||
users: UserService.users(false),
|
||||
teams: TeamService.teams()
|
||||
})
|
||||
EndpointService.endpoint($stateParams.id)
|
||||
.then(function success(data) {
|
||||
$scope.endpoint = data.endpoint;
|
||||
$scope.accesses = [];
|
||||
var users = data.users.map(function (user) {
|
||||
return new EndpointAccessUserViewModel(user);
|
||||
});
|
||||
var teams = data.teams.map(function (team) {
|
||||
return new EndpointAccessTeamViewModel(team);
|
||||
});
|
||||
$scope.accesses = $scope.accesses.concat(users, teams);
|
||||
$scope.authorizedAccesses = [];
|
||||
angular.forEach($scope.endpoint.AuthorizedUsers, function(userID) {
|
||||
for (var i = 0, l = $scope.accesses.length; i < l; i++) {
|
||||
if ($scope.accesses[i].Type === 'user' && $scope.accesses[i].Id === userID) {
|
||||
$scope.authorizedAccesses.push($scope.accesses[i]);
|
||||
$scope.accesses.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
angular.forEach($scope.endpoint.AuthorizedTeams, function(teamID) {
|
||||
for (var i = 0, l = $scope.accesses.length; i < l; i++) {
|
||||
if ($scope.accesses[i].Type === 'team' && $scope.accesses[i].Id === teamID) {
|
||||
$scope.authorizedAccesses.push($scope.accesses[i]);
|
||||
$scope.accesses.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
$scope.endpoint = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.accesses = [];
|
||||
$scope.authorizedAccesses = [];
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
})
|
||||
.finally(function final(){
|
||||
@@ -179,14 +20,5 @@ function ($q, $scope, $state, $stateParams, $filter, EndpointService, UserServic
|
||||
});
|
||||
}
|
||||
|
||||
function removeAccessFromArray(access, accesses) {
|
||||
for (var i = 0, l = accesses.length; i < l; i++) {
|
||||
if (access.Type === accesses[i].Type && access.Id === accesses[i].Id) {
|
||||
accesses.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
<div class="page-wrapper">
|
||||
<!-- simple box -->
|
||||
<div class="container simple-box">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
|
||||
<!-- simple box logo -->
|
||||
<div class="row">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||
</div>
|
||||
<!-- !simple box logo -->
|
||||
<!-- init-endpoint panel -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- init-endpoint form -->
|
||||
<form class="form-horizontal" style="margin: 20px;" enctype="multipart/form-data" method="POST">
|
||||
<!-- comment -->
|
||||
<div class="form-group" style="text-align: center;">
|
||||
<h4>Connect Portainer to a Docker engine or Swarm cluster endpoint</h4>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- endpoin-type radio -->
|
||||
<div class="form-group">
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType" ng-click="resetErrorMessage()">Manage the Docker instance where Portainer is running</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType" ng-click="resetErrorMessage()">Manage a remote Docker instance</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- endpoint-type radio -->
|
||||
<!-- local-endpoint -->
|
||||
<div ng-if="formValues.endpointType === 'local'" style="margin-top: 25px;">
|
||||
<div class="form-group">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 5px;"></i>
|
||||
<span class="small text-primary">This feature is not yet available for native Docker Windows containers.</span>
|
||||
<div class="small text-primary">On Linux and when using Docker for Mac or Docker for Windows or Docker Toolbox, ensure that you have started Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code></div>
|
||||
</div>
|
||||
<!-- connect button -->
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</p>
|
||||
<span class="pull-right">
|
||||
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
|
||||
<button type="submit" class="btn btn-primary" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !connect button -->
|
||||
</div>
|
||||
<!-- !local-endpoint -->
|
||||
<!-- remote-endpoint -->
|
||||
<div ng-if="formValues.endpointType === 'remote'" style="margin-top: 25px;">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-4 col-lg-3 control-label text-left">Name</label>
|
||||
<div class="col-sm-8 col-lg-9">
|
||||
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- endpoint-url-input -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_url" class="col-sm-4 col-lg-3 control-label text-left">
|
||||
Endpoint URL
|
||||
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8 col-lg-9">
|
||||
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-url-input -->
|
||||
<!-- tls-checkbox -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="tls" class="control-label text-left">
|
||||
TLS
|
||||
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.TLS"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-certs -->
|
||||
<div ng-if="formValues.TLS">
|
||||
<!-- ca-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-9">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCACert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ca-input -->
|
||||
<!-- cert-input -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-3 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-9">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cert-input -->
|
||||
<!-- key-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-9">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSKey.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !key-input -->
|
||||
</div>
|
||||
<!-- !tls-certs -->
|
||||
<!-- connect button -->
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</p>
|
||||
<span class="pull-right">
|
||||
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !connect button -->
|
||||
</div>
|
||||
<!-- !remote-endpoint -->
|
||||
</form>
|
||||
<!-- !init-endpoint form -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !init-endpoint panel -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !simple box -->
|
||||
</div>
|
||||
@@ -1,91 +0,0 @@
|
||||
angular.module('endpointInit', [])
|
||||
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
|
||||
function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications) {
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
endpointType: 'remote',
|
||||
Name: '',
|
||||
URL: '',
|
||||
TLS: false,
|
||||
TLSCACert: null,
|
||||
TLSCert: null,
|
||||
TLSKey: null
|
||||
};
|
||||
|
||||
if (!_.isEmpty($scope.applicationState.endpoint)) {
|
||||
$state.go('dashboard');
|
||||
}
|
||||
|
||||
$scope.resetErrorMessage = function() {
|
||||
$scope.state.error = '';
|
||||
};
|
||||
|
||||
function showErrorMessage(message) {
|
||||
$scope.state.uploadInProgress = false;
|
||||
$scope.state.error = message;
|
||||
}
|
||||
|
||||
function updateEndpointState(endpointID) {
|
||||
EndpointProvider.setEndpointID(endpointID);
|
||||
StateManager.updateEndpointState(false)
|
||||
.then(function success(data) {
|
||||
$state.go('dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
EndpointService.deleteEndpoint(endpointID)
|
||||
.then(function success() {
|
||||
showErrorMessage('Unable to connect to the Docker endpoint');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.createLocalEndpoint = function() {
|
||||
$('#initEndpointSpinner').show();
|
||||
$scope.state.error = '';
|
||||
var name = 'local';
|
||||
var URL = 'unix:///var/run/docker.sock';
|
||||
var TLS = false;
|
||||
|
||||
EndpointService.createLocalEndpoint(name, URL, TLS, true)
|
||||
.then(function success(data) {
|
||||
var endpointID = data.Id;
|
||||
updateEndpointState(data.Id);
|
||||
}, function error() {
|
||||
$scope.state.error = 'Unable to create endpoint';
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#initEndpointSpinner').hide();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createRemoteEndpoint = function() {
|
||||
$('#initEndpointSpinner').show();
|
||||
$scope.state.error = '';
|
||||
var name = $scope.formValues.Name;
|
||||
var URL = $scope.formValues.URL;
|
||||
var PublicURL = URL.split(':')[0];
|
||||
var TLS = $scope.formValues.TLS;
|
||||
var TLSCAFile = $scope.formValues.TLSCACert;
|
||||
var TLSCertFile = $scope.formValues.TLSCert;
|
||||
var TLSKeyFile = $scope.formValues.TLSKey;
|
||||
|
||||
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
.then(function success(data) {
|
||||
var endpointID = data.Id;
|
||||
updateEndpointState(endpointID);
|
||||
}, function error(err) {
|
||||
showErrorMessage(err.msg);
|
||||
}, function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
}
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#initEndpointSpinner').hide();
|
||||
});
|
||||
};
|
||||
}]);
|
||||
@@ -14,8 +14,11 @@
|
||||
<rd-widget-header icon="fa-exclamation-triangle" title="Endpoint management is not available">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag. Endpoint management via the UI is disabled. You can still manage endpoint access.</span>
|
||||
</rd-wigdet-body>
|
||||
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag.
|
||||
Endpoint management via the UI is disabled.
|
||||
<span ng-if="applicationState.application.authentication">You can still manage endpoint access.</span>
|
||||
</span>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,75 +60,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-public-url-input -->
|
||||
<!-- tls-checkbox -->
|
||||
<!-- endpoint-security -->
|
||||
<por-endpoint-security form-data="formValues.SecurityFormData"></por-endpoint-security>
|
||||
<!-- !endpoint-security -->
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="tls" class="control-label text-left">
|
||||
TLS
|
||||
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.TLS"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && ((formValues.TLSVerify && !formValues.TLSCACert) || (formValues.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="addEndpoint()"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</button>
|
||||
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-certs -->
|
||||
<div ng-if="formValues.TLS">
|
||||
<!-- ca-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCACert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ca-input -->
|
||||
<!-- cert-input -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cert-input -->
|
||||
<!-- key-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSKey.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !key-input -->
|
||||
</div>
|
||||
<!-- !tls-certs -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="addEndpoint()">Add endpoint</button>
|
||||
<i id="createEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -183,17 +132,12 @@
|
||||
<td>{{ endpoint.URL | stripprotocol }}</td>
|
||||
<td>
|
||||
<span ng-if="applicationState.application.endpointManagement">
|
||||
<span ng-if="endpoint.Id !== activeEndpointID">
|
||||
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
|
||||
</span>
|
||||
<span class="small text-muted" ng-if="endpoint.Id === activeEndpointID">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i> You cannot edit the active endpoint
|
||||
</span>
|
||||
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
|
||||
</span>
|
||||
<span ng-if="applicationState.application.authentication">
|
||||
<a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
|
||||
</span>
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!endpoints">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
@@ -208,6 +152,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ angular.module('endpoints', [])
|
||||
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination',
|
||||
function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagination) {
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false,
|
||||
selectedItemCount: 0,
|
||||
pagination_count: Pagination.getPaginationCount('endpoints')
|
||||
@@ -14,10 +13,7 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
|
||||
Name: '',
|
||||
URL: '',
|
||||
PublicURL: '',
|
||||
TLS: false,
|
||||
TLSCACert: null,
|
||||
TLSCert: null,
|
||||
TLSKey: null
|
||||
SecurityFormData: new EndpointSecurityFormData()
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
@@ -47,23 +43,28 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
|
||||
};
|
||||
|
||||
$scope.addEndpoint = function() {
|
||||
$scope.state.error = '';
|
||||
var name = $scope.formValues.Name;
|
||||
var URL = $scope.formValues.URL;
|
||||
var PublicURL = $scope.formValues.PublicURL;
|
||||
if (PublicURL === '') {
|
||||
PublicURL = URL.split(':')[0];
|
||||
}
|
||||
var TLS = $scope.formValues.TLS;
|
||||
var TLSCAFile = $scope.formValues.TLSCACert;
|
||||
var TLSCertFile = $scope.formValues.TLSCert;
|
||||
var TLSKeyFile = $scope.formValues.TLSKey;
|
||||
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, false).then(function success(data) {
|
||||
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
var TLSMode = securityData.TLSMode;
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
var TLSCAFile = TLSSkipVerify ? null : securityData.TLSCACert;
|
||||
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
||||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||
|
||||
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile).then(function success(data) {
|
||||
Notifications.success('Endpoint created', name);
|
||||
$state.reload();
|
||||
}, function error(err) {
|
||||
$scope.state.uploadInProgress = false;
|
||||
$scope.state.error = err.msg;
|
||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||
}, function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
@@ -101,7 +102,6 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
|
||||
EndpointService.endpoints()
|
||||
.then(function success(data) {
|
||||
$scope.endpoints = data;
|
||||
$scope.activeEndpointID = EndpointProvider.endpointID();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Engine overview">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker" ui-sref-opts="{reload: true}">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="engine" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
@@ -8,7 +8,7 @@
|
||||
<rd-header-content>Docker</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="state.loaded">
|
||||
<div class="row" ng-if="info && version">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-code" title="Engine version"></rd-widget-header>
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="state.loaded">
|
||||
<div class="row" ng-if="info && version">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-th" title="Engine status"></rd-widget-header>
|
||||
@@ -92,7 +92,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="state.loaded && info.Plugins">
|
||||
<div class="row" ng-if="info && info.Plugins">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plug" title="Engine plugins"></rd-widget-header>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user