Compare commits
14 Commits
fix-ce#394
...
feat/GH/31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
785c4e0627 | ||
|
|
aae9ecc49f | ||
|
|
a99ff34c05 | ||
|
|
f2ec7605c2 | ||
|
|
81b4672076 | ||
|
|
0cfa912d77 | ||
|
|
fc0de913c3 | ||
|
|
f7e6ba544e | ||
|
|
24b1894a84 | ||
|
|
46dec01fe3 | ||
|
|
e401724d43 | ||
|
|
d2d7f6fdb9 | ||
|
|
b747f5f81e | ||
|
|
afbd353808 |
@@ -28,7 +28,7 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://www.portainer.io/installation/)
|
||||
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
|
||||
- [Documentation](https://documentation.portainer.io)
|
||||
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
|
||||
|
||||
|
||||
@@ -40,18 +40,11 @@ func (store *Store) Init() error {
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
EnableHostManagementFeatures: false,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
|
||||
51
api/bolt/migrator/migrate_dbversion25.go
Normal file
51
api/bolt/migrator/migrate_dbversion25.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateEndpointSettingsToDB25() error {
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
endpoint := endpoints[i]
|
||||
|
||||
securitySettings := portainer.EndpointSecuritySettings{}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.DockerEnvironment {
|
||||
|
||||
securitySettings = portainer.EndpointSecuritySettings{
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
|
||||
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
|
||||
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers
|
||||
securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures
|
||||
}
|
||||
}
|
||||
|
||||
endpoint.SecuritySettings = securitySettings
|
||||
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -342,5 +342,13 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"strings"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
@@ -47,6 +48,8 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
|
||||
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
|
||||
var user *portainer.User
|
||||
|
||||
username = strings.ToLower(username)
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
cursor := bucket.Cursor()
|
||||
@@ -58,7 +61,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Username == username {
|
||||
if strings.ToLower(u.Username) == username {
|
||||
user = &u
|
||||
break
|
||||
}
|
||||
@@ -123,6 +126,7 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
|
||||
// UpdateUser saves a user.
|
||||
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
user.Username = strings.ToLower(user.Username)
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, user)
|
||||
}
|
||||
|
||||
@@ -133,6 +137,7 @@ func (service *Service) CreateUser(user *portainer.User) error {
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
user.ID = portainer.UserID(id)
|
||||
user.Username = strings.ToLower(user.Username)
|
||||
|
||||
data, err := internal.MarshalObject(user)
|
||||
if err != nil {
|
||||
|
||||
@@ -248,6 +248,18 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
||||
@@ -297,6 +309,18 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
@@ -440,6 +440,18 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint)
|
||||
}
|
||||
|
||||
func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error {
|
||||
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
}
|
||||
|
||||
err := handler.DataStore.Endpoint().CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
90
api/http/handler/endpoints/endpoint_settings_update.go
Normal file
90
api/http/handler/endpoints/endpoint_settings_update.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
type endpointSettingsUpdatePayload struct {
|
||||
AllowBindMountsForRegularUsers *bool `json:"allowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers *bool `json:"allowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers *bool `json:"allowVolumeBrowserForRegularUsers"`
|
||||
AllowHostNamespaceForRegularUsers *bool `json:"allowHostNamespaceForRegularUsers"`
|
||||
AllowDeviceMappingForRegularUsers *bool `json:"allowDeviceMappingForRegularUsers"`
|
||||
AllowStackManagementForRegularUsers *bool `json:"allowStackManagementForRegularUsers"`
|
||||
AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers"`
|
||||
EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures"`
|
||||
}
|
||||
|
||||
func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/endpoints/:id/settings
|
||||
func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointSettingsUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securitySettings := endpoint.SecuritySettings
|
||||
|
||||
if payload.AllowBindMountsForRegularUsers != nil {
|
||||
securitySettings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
|
||||
securitySettings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowDeviceMappingForRegularUsers != nil {
|
||||
securitySettings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowHostNamespaceForRegularUsers != nil {
|
||||
securitySettings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowPrivilegedModeForRegularUsers != nil {
|
||||
securitySettings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowStackManagementForRegularUsers != nil {
|
||||
securitySettings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
||||
securitySettings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
||||
}
|
||||
|
||||
if payload.EnableHostManagementFeatures != nil {
|
||||
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||
}
|
||||
|
||||
endpoint.SecuritySettings = securitySettings
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
||||
@@ -39,6 +39,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
|
||||
h.Handle("/endpoints",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/settings",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSettingsUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/snapshot",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
|
||||
@@ -10,19 +10,11 @@ import (
|
||||
)
|
||||
|
||||
type publicSettingsResponse struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
|
||||
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
|
||||
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
|
||||
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
EnableTelemetry bool `json:"EnableTelemetry"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
EnableTelemetry bool `json:"EnableTelemetry"`
|
||||
}
|
||||
|
||||
// GET request on /api/settings/public
|
||||
@@ -33,18 +25,10 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
|
||||
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
|
||||
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
|
||||
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
|
||||
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
|
||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: settings.EnableTelemetry,
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: settings.EnableTelemetry,
|
||||
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||
settings.OAuthSettings.AuthorizationURI,
|
||||
settings.OAuthSettings.ClientID,
|
||||
|
||||
@@ -14,25 +14,17 @@ import (
|
||||
)
|
||||
|
||||
type settingsUpdatePayload struct {
|
||||
LogoURL *string
|
||||
BlackListedLabels []portainer.Pair
|
||||
AuthenticationMethod *int
|
||||
LDAPSettings *portainer.LDAPSettings
|
||||
OAuthSettings *portainer.OAuthSettings
|
||||
AllowBindMountsForRegularUsers *bool
|
||||
AllowPrivilegedModeForRegularUsers *bool
|
||||
AllowHostNamespaceForRegularUsers *bool
|
||||
AllowVolumeBrowserForRegularUsers *bool
|
||||
AllowDeviceMappingForRegularUsers *bool
|
||||
AllowStackManagementForRegularUsers *bool
|
||||
AllowContainerCapabilitiesForRegularUsers *bool
|
||||
EnableHostManagementFeatures *bool
|
||||
SnapshotInterval *string
|
||||
TemplatesURL *string
|
||||
EdgeAgentCheckinInterval *int
|
||||
EnableEdgeComputeFeatures *bool
|
||||
UserSessionTimeout *string
|
||||
EnableTelemetry *bool
|
||||
LogoURL *string
|
||||
BlackListedLabels []portainer.Pair
|
||||
AuthenticationMethod *int
|
||||
LDAPSettings *portainer.LDAPSettings
|
||||
OAuthSettings *portainer.OAuthSettings
|
||||
SnapshotInterval *string
|
||||
TemplatesURL *string
|
||||
EdgeAgentCheckinInterval *int
|
||||
EnableEdgeComputeFeatures *bool
|
||||
UserSessionTimeout *string
|
||||
EnableTelemetry *bool
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -107,38 +99,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
settings.OAuthSettings.ClientSecret = clientSecret
|
||||
}
|
||||
|
||||
if payload.AllowBindMountsForRegularUsers != nil {
|
||||
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowPrivilegedModeForRegularUsers != nil {
|
||||
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
||||
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
||||
}
|
||||
|
||||
if payload.EnableHostManagementFeatures != nil {
|
||||
settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||
}
|
||||
|
||||
if payload.EnableEdgeComputeFeatures != nil {
|
||||
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
|
||||
}
|
||||
|
||||
if payload.AllowHostNamespaceForRegularUsers != nil {
|
||||
settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowStackManagementForRegularUsers != nil {
|
||||
settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
|
||||
settings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
|
||||
}
|
||||
|
||||
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
||||
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
|
||||
if err != nil {
|
||||
@@ -158,10 +122,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
handler.JWTService.SetUserSessionDuration(userSessionDuration)
|
||||
}
|
||||
|
||||
if payload.AllowDeviceMappingForRegularUsers != nil {
|
||||
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
|
||||
}
|
||||
|
||||
if payload.EnableTelemetry != nil {
|
||||
settings.EnableTelemetry = *payload.EnableTelemetry
|
||||
}
|
||||
|
||||
@@ -339,21 +339,18 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
|
||||
// clean it. Hence the use of the mutex.
|
||||
// We should contribute to libcompose to support authentication without using the config.json file.
|
||||
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (!settings.AllowBindMountsForRegularUsers ||
|
||||
!settings.AllowPrivilegedModeForRegularUsers ||
|
||||
!settings.AllowHostNamespaceForRegularUsers ||
|
||||
!settings.AllowDeviceMappingForRegularUsers ||
|
||||
!settings.AllowContainerCapabilitiesForRegularUsers) &&
|
||||
securitySettings := &config.endpoint.SecuritySettings
|
||||
|
||||
if (!securitySettings.AllowBindMountsForRegularUsers ||
|
||||
!securitySettings.AllowPrivilegedModeForRegularUsers ||
|
||||
!securitySettings.AllowHostNamespaceForRegularUsers ||
|
||||
!securitySettings.AllowDeviceMappingForRegularUsers ||
|
||||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
|
||||
!isAdminOrEndpointAdmin {
|
||||
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
@@ -362,7 +359,7 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.isValidStackFile(stackContent, settings)
|
||||
err = handler.isValidStackFile(stackContent, securitySettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -344,16 +344,13 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
||||
}
|
||||
|
||||
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings := &config.endpoint.SecuritySettings
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
|
||||
|
||||
@@ -46,12 +46,14 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if !settings.AllowStackManagementForRegularUsers {
|
||||
if !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||
@@ -69,13 +71,6 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||
}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
@@ -129,7 +124,7 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error {
|
||||
func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
|
||||
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -154,7 +149,7 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
|
||||
|
||||
for key := range composeConfig.Services {
|
||||
service := composeConfig.Services[key]
|
||||
if !settings.AllowBindMountsForRegularUsers {
|
||||
if !securitySettings.AllowBindMountsForRegularUsers {
|
||||
for _, volume := range service.Volumes {
|
||||
if volume.Type == "bind" {
|
||||
return errors.New("bind-mount disabled for non administrator users")
|
||||
@@ -162,19 +157,19 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
|
||||
}
|
||||
}
|
||||
|
||||
if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
|
||||
if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
|
||||
return errors.New("privileged mode disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
return errors.New("pid host disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
|
||||
if !securitySettings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
|
||||
return errors.New("device mapping disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
|
||||
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
|
||||
return errors.New("container capabilities disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
@@ -181,7 +181,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
||||
}
|
||||
|
||||
if !isAdminOrEndpointAdmin {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -197,23 +197,23 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
|
||||
if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
|
||||
return forbiddenResponse, errors.New("forbidden to use privileged mode")
|
||||
}
|
||||
|
||||
if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
|
||||
if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
|
||||
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
|
||||
}
|
||||
|
||||
if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
|
||||
if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
|
||||
return forbiddenResponse, errors.New("forbidden to use device mapping")
|
||||
}
|
||||
|
||||
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
|
||||
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
|
||||
return nil, errors.New("forbidden to use container capabilities")
|
||||
}
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
|
||||
return forbiddenResponse, errors.New("forbidden to use bind mounts")
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -111,7 +111,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
|
||||
}
|
||||
|
||||
if !isAdminOrEndpointAdmin {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
|
||||
for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts {
|
||||
if mount.Type == "bind" {
|
||||
return forbiddenResponse, errors.New("forbidden to use bind mounts")
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -407,12 +407,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
if volumeBrowseRestrictionCheck {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !settings.AllowVolumeBrowserForRegularUsers {
|
||||
if !securitySettings.AllowVolumeBrowserForRegularUsers {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
@@ -682,3 +682,12 @@ func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool,
|
||||
|
||||
return tokenData.Role == portainer.AdministratorRole, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) {
|
||||
endpoint, err := transport.dataStore.Endpoint().Endpoint(portainer.EndpointID(transport.endpoint.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &endpoint.SecuritySettings, nil
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ type (
|
||||
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
|
||||
Kubernetes KubernetesData `json:"Kubernetes"`
|
||||
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"`
|
||||
SecuritySettings EndpointSecuritySettings
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
@@ -272,6 +273,18 @@ type (
|
||||
// Deprecated
|
||||
EndpointSyncJob struct{}
|
||||
|
||||
// EndpointSecuritySettings represents settings for an endpoint
|
||||
EndpointSecuritySettings struct {
|
||||
AllowBindMountsForRegularUsers bool `json:"allowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"allowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"allowVolumeBrowserForRegularUsers"`
|
||||
AllowHostNamespaceForRegularUsers bool `json:"allowHostNamespaceForRegularUsers"`
|
||||
AllowDeviceMappingForRegularUsers bool `json:"allowDeviceMappingForRegularUsers"`
|
||||
AllowStackManagementForRegularUsers bool `json:"allowStackManagementForRegularUsers"`
|
||||
AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers"`
|
||||
EnableHostManagementFeatures bool `json:"enableHostManagementFeatures"`
|
||||
}
|
||||
|
||||
// EndpointType represents the type of an endpoint
|
||||
EndpointType int
|
||||
|
||||
@@ -516,29 +529,31 @@ type (
|
||||
|
||||
// Settings represents the application settings
|
||||
Settings struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
|
||||
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
|
||||
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
|
||||
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
|
||||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
UserSessionTimeout string `json:"UserSessionTimeout"`
|
||||
EnableTelemetry bool `json:"EnableTelemetry"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
UserSessionTimeout string `json:"UserSessionTimeout"`
|
||||
EnableTelemetry bool `json:"EnableTelemetry"`
|
||||
|
||||
// Deprecated fields
|
||||
DisplayDonationHeader bool
|
||||
DisplayExternalContributors bool
|
||||
|
||||
// Deprecated fields v26
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
|
||||
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
|
||||
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
|
||||
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
|
||||
}
|
||||
|
||||
// SnapshotJob represents a scheduled job that can create endpoint snapshots
|
||||
@@ -1125,9 +1140,9 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.0.1"
|
||||
APIVersion = "2.1.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 25
|
||||
DBVersion = 26
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
|
||||
@@ -625,57 +625,6 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||
margin-left: 21px;
|
||||
}
|
||||
|
||||
/* switch box */
|
||||
:root {
|
||||
--switch-size: 24px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.switch i,
|
||||
.bootbox-form .checkbox i {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
padding-right: var(--switch-size);
|
||||
transition: all ease 0.2s;
|
||||
-webkit-transition: all ease 0.2s;
|
||||
-moz-transition: all ease 0.2s;
|
||||
-o-transition: all ease 0.2s;
|
||||
border-radius: var(--switch-size);
|
||||
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.switch i:before,
|
||||
.bootbox-form .checkbox i:before {
|
||||
display: block;
|
||||
content: '';
|
||||
width: var(--switch-size);
|
||||
height: var(--switch-size);
|
||||
border-radius: var(--switch-size);
|
||||
background: white;
|
||||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.switch :checked + i,
|
||||
.bootbox-form .checkbox :checked ~ i {
|
||||
padding-right: 0;
|
||||
padding-left: var(--switch-size);
|
||||
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||
-moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||
}
|
||||
/* !switch box */
|
||||
|
||||
/* small switch box */
|
||||
.switch.small {
|
||||
--switch-size: 12px;
|
||||
}
|
||||
|
||||
/* !small switch box */
|
||||
|
||||
.boxselector_wrapper {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
@@ -581,6 +581,16 @@ angular.module('portainer.docker', ['portainer.app']).config([
|
||||
},
|
||||
};
|
||||
|
||||
const dockerFeaturesConfiguration = {
|
||||
name: 'docker.featuresConfiguration',
|
||||
url: '/feat-config',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'dockerFeaturesConfigurationView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(configs);
|
||||
$stateRegistryProvider.register(config);
|
||||
$stateRegistryProvider.register(configCreation);
|
||||
@@ -630,5 +640,6 @@ angular.module('portainer.docker', ['portainer.app']).config([
|
||||
$stateRegistryProvider.register(volume);
|
||||
$stateRegistryProvider.register(volumeBrowse);
|
||||
$stateRegistryProvider.register(volumeCreation);
|
||||
$stateRegistryProvider.register(dockerFeaturesConfiguration);
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -37,7 +37,15 @@
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
|
||||
<a ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
|
||||
|
||||
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.swarm'].includes($ctrl.currentRouteName)">
|
||||
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
|
||||
<a ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
|
||||
|
||||
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.host'].includes($ctrl.currentRouteName)">
|
||||
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -27,9 +27,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
'ModalService',
|
||||
'RegistryService',
|
||||
'SystemService',
|
||||
'SettingsService',
|
||||
'PluginService',
|
||||
'HttpRequestHelper',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
@@ -53,14 +53,20 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
ModalService,
|
||||
RegistryService,
|
||||
SystemService,
|
||||
SettingsService,
|
||||
PluginService,
|
||||
HttpRequestHelper
|
||||
HttpRequestHelper,
|
||||
endpoint
|
||||
) {
|
||||
$scope.create = create;
|
||||
|
||||
$scope.formValues = {
|
||||
alwaysPull: true,
|
||||
GPU: {
|
||||
enabled: false,
|
||||
useSpecific: false,
|
||||
selectedGPUs: [],
|
||||
capabilities: [],
|
||||
},
|
||||
Console: 'none',
|
||||
Volumes: [],
|
||||
NetworkContainer: null,
|
||||
@@ -90,6 +96,15 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
mode: '',
|
||||
nvidiaCapabilities: [
|
||||
// Taken from https://github.com/containerd/containerd/blob/master/contrib/nvidia/nvidia.go#L40
|
||||
{ name: 'compute', description: 'required for CUDA and OpenCL applications', selected: true },
|
||||
{ name: 'compat32', description: 'required for running 32-bit applications', selected: false },
|
||||
{ name: 'graphics', description: 'required for running OpenGL and Vulkan applications', selected: false },
|
||||
{ name: 'utility', description: 'required for using nvidia-smi and NVML', selected: true },
|
||||
{ name: 'video', description: 'required for using the Video Codec SDK', selected: false },
|
||||
{ name: 'display', description: 'required for leveraging X11 display', selected: false },
|
||||
],
|
||||
};
|
||||
|
||||
$scope.refreshSlider = function () {
|
||||
@@ -124,6 +139,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
Runtime: null,
|
||||
ExtraHosts: [],
|
||||
Devices: [],
|
||||
DeviceRequests: [],
|
||||
CapAdd: [],
|
||||
CapDrop: [],
|
||||
},
|
||||
@@ -181,6 +197,20 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
$scope.config.HostConfig.Devices.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addGPU = () => $scope.formValues.GPU.selectedGPUs.push({ key: '' });
|
||||
$scope.removeGPU = (index) => $scope.formValues.GPU.selectedGPUs.splice(index, 1);
|
||||
$scope.computeDockerGPUCommand = () => {
|
||||
const useSpecific = $scope.formValues.GPU.useSpecific;
|
||||
let gpuStr = 'all';
|
||||
if (useSpecific) {
|
||||
const computeGPUs = _.flow([(arr) => _.map(arr, 'key'), (arr) => _.join(arr, ',')]);
|
||||
gpuStr = `"device=${computeGPUs($scope.formValues.GPU.selectedGPUs)}"`;
|
||||
}
|
||||
const computeCapabilities = _.flow([(arr) => _.map(arr, 'name'), (arr) => _.join(arr, ',')]);
|
||||
const capStr = `"capabilities=${computeCapabilities($scope.formValues.GPU.capabilities)}"`;
|
||||
return `--gpus '${gpuStr},${capStr}'`;
|
||||
};
|
||||
|
||||
$scope.addLogDriverOpt = function () {
|
||||
$scope.formValues.LogDriverOpts.push({ name: '', value: '' });
|
||||
};
|
||||
@@ -387,6 +417,36 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
config.HostConfig.CapDrop = notAllowed.map(getCapName);
|
||||
}
|
||||
|
||||
function prepareGPUOptions(config) {
|
||||
const gpuOptions = $scope.formValues.GPU;
|
||||
if (!gpuOptions.enabled) {
|
||||
return;
|
||||
}
|
||||
const driver = 'nvidia';
|
||||
const existingDeviceRequest = _.find($scope.config.HostConfig.DeviceRequests, { Driver: driver });
|
||||
if (existingDeviceRequest) {
|
||||
_.pullAllBy(config.HostConfig.DeviceRequests, [existingDeviceRequest], 'Driver');
|
||||
}
|
||||
const deviceRequest = existingDeviceRequest || {
|
||||
Driver: driver,
|
||||
Count: -1,
|
||||
DeviceIDs: [], // must be empty if Count != 0 https://github.com/moby/moby/blob/master/daemon/nvidia_linux.go#L50
|
||||
Capabilities: [], // array of ORed arrays of ANDed capabilites = [ [c1 AND c2] OR [c1 AND c3] ] : https://github.com/moby/moby/blob/master/api/types/container/host_config.go#L272
|
||||
// Options: { property1: "string", property2: "string" }, // seems to never be evaluated/used in docker API ?
|
||||
};
|
||||
if (gpuOptions.useSpecific) {
|
||||
const gpuIds = _.map(gpuOptions.selectedGPUs, 'key');
|
||||
deviceRequest.DeviceIDs = gpuIds;
|
||||
deviceRequest.Count = gpuIds.length;
|
||||
}
|
||||
const caps = _.map(gpuOptions.capabilities, 'name');
|
||||
// we only support a single set of capabilities for now
|
||||
// UI needs to be reworked in order to support OR combinations of AND capabilities
|
||||
deviceRequest.Capabilities = [caps];
|
||||
|
||||
config.HostConfig.DeviceRequests.push(deviceRequest);
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
prepareCmd(config);
|
||||
@@ -402,6 +462,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
prepareResources(config);
|
||||
prepareLogDriver(config);
|
||||
prepareCapabilities(config);
|
||||
prepareGPUOptions(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -547,6 +608,32 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
$scope.config.HostConfig.Devices = path;
|
||||
}
|
||||
|
||||
function loadFromContainerDeviceRequests() {
|
||||
const deviceRequest = _.find($scope.config.HostConfig.DeviceRequests, { Driver: 'nvidia' });
|
||||
if (deviceRequest) {
|
||||
$scope.formValues.GPU.enabled = true;
|
||||
$scope.formValues.GPU.useSpecific = deviceRequest.Count !== -1;
|
||||
if ($scope.formValues.GPU.useSpecific) {
|
||||
$scope.formValues.GPU.selectedGPUs = _.map(deviceRequest.DevicesIDs, (id) => {
|
||||
return { key: id };
|
||||
});
|
||||
}
|
||||
// we only support a single set of capabilities for now
|
||||
// UI needs to be reworked in order to support OR combinations of AND capabilities
|
||||
const caps = deviceRequest.Capabilities[0];
|
||||
const fvCaps = _.map(caps, (cap) => {
|
||||
return { name: cap };
|
||||
});
|
||||
$scope.formValues.GPU.capabilities = fvCaps;
|
||||
_.forEach(caps, (cap) => {
|
||||
const c = _.find($scope.state.nvidiaCapabilities, { name: cap });
|
||||
if (c) {
|
||||
c.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromContainerImageConfig() {
|
||||
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image)
|
||||
.then((model) => {
|
||||
@@ -619,6 +706,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
loadFromContainerLabels(d);
|
||||
loadFromContainerConsole(d);
|
||||
loadFromContainerDevices(d);
|
||||
loadFromContainerDeviceRequests(d);
|
||||
loadFromContainerImageConfig(d);
|
||||
loadFromContainerResources(d);
|
||||
loadFromContainerCapabilities(d);
|
||||
@@ -709,14 +797,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
Notifications.error('Failure', err, 'Unable to retrieve engine details');
|
||||
});
|
||||
|
||||
SettingsService.publicSettings()
|
||||
.then(function success(data) {
|
||||
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || data.AllowBindMountsForRegularUsers;
|
||||
$scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||
});
|
||||
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers;
|
||||
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
|
||||
|
||||
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
|
||||
$scope.availableLoggingDrivers = loggingDrivers;
|
||||
@@ -933,15 +1015,11 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
}
|
||||
|
||||
async function shouldShowDevices() {
|
||||
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return allowDeviceMappingForRegularUsers || Authentication.isAdmin();
|
||||
return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
async function checkIfContainerCapabilitiesEnabled() {
|
||||
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
|
||||
return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
@@ -53,10 +53,8 @@
|
||||
<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>
|
||||
<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>
|
||||
@@ -88,7 +86,8 @@
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="When a range of ports on the host and a single port on the container is specified, Docker will randomly choose a single available port in the defined range and forward that to the container port."
|
||||
></portainer-tooltip>
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> publish a new network port
|
||||
@@ -151,7 +150,8 @@
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="When enabled, Portainer will automatically remove the container when it exits. This is useful when you want to use the container only once."
|
||||
></portainer-tooltip>
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="config.HostConfig.AutoRemove" /><i></i> </label>
|
||||
</div>
|
||||
@@ -227,7 +227,8 @@
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="When container entrypoint is entered as part of the Command field, set Entrypoint to Override mode and leave blank, else it will revert to default."
|
||||
></portainer-tooltip>
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
@@ -322,7 +323,8 @@
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="Add button is disabled unless a driver other than none or default is selected. Options are specific to the selected driver, refer to the driver documentation."
|
||||
></portainer-tooltip>
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<span
|
||||
class="label label-default interactive"
|
||||
@@ -670,8 +672,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !runtimes -->
|
||||
</form>
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- devices -->
|
||||
<div ng-if="showDeviceMapping" class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
@@ -699,6 +699,86 @@
|
||||
<!-- !devices-input-list -->
|
||||
</div>
|
||||
<!-- !devices-->
|
||||
<!-- #region GPU -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
GPU
|
||||
</div>
|
||||
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.4">
|
||||
<label class="col-xs-3 col-sm-3 col-lg-2 control-label text-left">
|
||||
Enable GPU
|
||||
</label>
|
||||
<div class="col-xs-9 col-sm-9 col-lg-4">
|
||||
<label class="switch" style="margin-left: 5px;"> <input type="checkbox" name="use_gpu" ng-model="formValues.GPU.enabled" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="formValues.GPU.enabled">
|
||||
<!-- #region GPU DEVICES -->
|
||||
<div class="form-group">
|
||||
<label class="col-xs-3 col-sm-3 col-lg-2 control-label text-left">
|
||||
Use specific GPUs
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="By default all GPUs will be usable by the container. Enable this if you want to give access to only specific host GPUs. Accept GPU indexes or UUIDs."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-xs-9 col-lg-4">
|
||||
<label class="switch" style="margin-left: 5px;"> <input type="checkbox" name="use_gpu" ng-model="formValues.GPU.useSpecific" /><i></i> </label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-if="formValues.GPU.useSpecific" ng-click="addGPU()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add gpu
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;" ng-if="formValues.GPU.useSpecific && formValues.GPU.selectedGPUs.length">
|
||||
<div ng-repeat="gpu in formValues.GPU.selectedGPUs track by $index" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-8 input-group-sm">
|
||||
<span class="input-group-addon">index or UUID</span>
|
||||
<input type="text" class="form-control" ng-model="gpu.key" placeholder="e.g. 0 or GPU-fef8089b" />
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeGPU($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-xs-3 col-sm-3 col-lg-2 control-label text-left">
|
||||
Capabilities
|
||||
<portainer-tooltip
|
||||
position="top"
|
||||
message="'compute' and 'utility' capabilities are preselected by Portainer because they are used by default when you don't explicitly specify capabilities with docker CLI '--gpus' option."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-xs-12 col-sm-9 col-lg-4">
|
||||
<span
|
||||
isteven-multi-select
|
||||
input-model="state.nvidiaCapabilities"
|
||||
output-model="formValues.GPU.capabilities"
|
||||
output-properties="name"
|
||||
button-label="name"
|
||||
item-label="name - description"
|
||||
tick-property="selected"
|
||||
directive-id="nvidia_capabilities"
|
||||
helper-elements=""
|
||||
translation="{nothingSelected: 'No capabilities selected'}"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-xs-3 col-sm-3 col-lg-2 control-label text-left">
|
||||
Control
|
||||
<portainer-tooltip position="top" message="This is the generated equivalent of the '--gpus' docker CLI parameter based on your settings."> </portainer-tooltip>
|
||||
</label>
|
||||
<label class="col-xs-12 col-sm-9 col-lg-4">
|
||||
<code>{{ computeDockerGPUCommand() }}</code>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- #endregion GPU -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resources
|
||||
</div>
|
||||
|
||||
@@ -274,6 +274,10 @@
|
||||
</container-restart-policy>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="container.HostConfig.DeviceRequests.length">
|
||||
<td>GPUS</td>
|
||||
<td>{{ computeDockerGPUCommand() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
|
||||
@@ -22,6 +22,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
'HttpRequestHelper',
|
||||
'Authentication',
|
||||
'StateManager',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
@@ -41,7 +42,8 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
ImageService,
|
||||
HttpRequestHelper,
|
||||
Authentication,
|
||||
StateManager
|
||||
StateManager,
|
||||
endpoint
|
||||
) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
@@ -60,6 +62,21 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
|
||||
$scope.updateRestartPolicy = updateRestartPolicy;
|
||||
|
||||
$scope.computeDockerGPUCommand = () => {
|
||||
const gpuOptions = _.find($scope.container.HostConfig.DeviceRequests, { Driver: 'nvidia' });
|
||||
if (!gpuOptions) {
|
||||
return 'No GPU config found';
|
||||
}
|
||||
let gpuStr = 'all';
|
||||
if (gpuOptions.Count !== -1) {
|
||||
gpuStr = `"device=${_.join(gpuOptions.DeviceIDs, ',')}"`;
|
||||
}
|
||||
// we only support a single set of capabilities for now
|
||||
// creation UI needs to be reworked in order to support OR combinations of AND capabilities
|
||||
const capStr = `"capabilities=${_.join(gpuOptions.Capabilities[0], ',')}"`;
|
||||
return `${gpuStr},${capStr}`;
|
||||
};
|
||||
|
||||
var update = function () {
|
||||
var nodeName = $transition$.params().nodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
@@ -97,14 +114,13 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id'];
|
||||
const autoRemove = $scope.container.HostConfig.AutoRemove;
|
||||
const admin = Authentication.isAdmin();
|
||||
const appState = StateManager.getState();
|
||||
const {
|
||||
allowContainerCapabilitiesForRegularUsers,
|
||||
allowHostNamespaceForRegularUsers,
|
||||
allowDeviceMappingForRegularUsers,
|
||||
allowBindMountsForRegularUsers,
|
||||
allowPrivilegedModeForRegularUsers,
|
||||
} = appState.application;
|
||||
} = endpoint.SecuritySettings;
|
||||
|
||||
const settingRestrictsRegularUsers =
|
||||
!allowContainerCapabilitiesForRegularUsers ||
|
||||
|
||||
@@ -17,6 +17,7 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||
'EndpointProvider',
|
||||
'StateManager',
|
||||
'TagService',
|
||||
'endpoint',
|
||||
function (
|
||||
$scope,
|
||||
$q,
|
||||
@@ -32,7 +33,8 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||
Notifications,
|
||||
EndpointProvider,
|
||||
StateManager,
|
||||
TagService
|
||||
TagService,
|
||||
endpoint
|
||||
) {
|
||||
$scope.dismissInformationPanel = function (id) {
|
||||
StateManager.dismissInformationPanel(id);
|
||||
@@ -89,9 +91,8 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||
|
||||
async function shouldShowStacks() {
|
||||
const isAdmin = Authentication.isAdmin();
|
||||
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return isAdmin || allowStackManagementForRegularUsers;
|
||||
return isAdmin || endpoint.SecuritySettings.allowStackManagementForRegularUsers;
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export default class DockerFeaturesConfigurationController {
|
||||
/* @ngInject */
|
||||
constructor($async, EndpointService, Notifications, StateManager) {
|
||||
this.$async = $async;
|
||||
this.EndpointService = EndpointService;
|
||||
this.Notifications = Notifications;
|
||||
this.StateManager = StateManager;
|
||||
|
||||
this.formValues = {
|
||||
enableHostManagementFeatures: false,
|
||||
allowVolumeBrowserForRegularUsers: false,
|
||||
disableBindMountsForRegularUsers: false,
|
||||
disablePrivilegedModeForRegularUsers: false,
|
||||
disableHostNamespaceForRegularUsers: false,
|
||||
disableStackManagementForRegularUsers: false,
|
||||
disableDeviceMappingForRegularUsers: false,
|
||||
disableContainerCapabilitiesForRegularUsers: false,
|
||||
};
|
||||
|
||||
this.isAgent = false;
|
||||
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
this.save = this.save.bind(this);
|
||||
}
|
||||
|
||||
isContainerEditDisabled() {
|
||||
const {
|
||||
disableBindMountsForRegularUsers,
|
||||
disableHostNamespaceForRegularUsers,
|
||||
disablePrivilegedModeForRegularUsers,
|
||||
disableDeviceMappingForRegularUsers,
|
||||
disableContainerCapabilitiesForRegularUsers,
|
||||
} = this.formValues;
|
||||
return (
|
||||
disableBindMountsForRegularUsers ||
|
||||
disableHostNamespaceForRegularUsers ||
|
||||
disablePrivilegedModeForRegularUsers ||
|
||||
disableDeviceMappingForRegularUsers ||
|
||||
disableContainerCapabilitiesForRegularUsers
|
||||
);
|
||||
}
|
||||
|
||||
async save() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
const securitySettings = {
|
||||
enableHostManagementFeatures: this.formValues.enableHostManagementFeatures,
|
||||
allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers,
|
||||
allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers,
|
||||
allowVolumeBrowserForRegularUsers: this.formValues.allowVolumeBrowserForRegularUsers,
|
||||
allowHostNamespaceForRegularUsers: !this.formValues.disableHostNamespaceForRegularUsers,
|
||||
allowDeviceMappingForRegularUsers: !this.formValues.disableDeviceMappingForRegularUsers,
|
||||
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
|
||||
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
|
||||
};
|
||||
|
||||
await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings);
|
||||
|
||||
this.endpoint.SecuritySettings = securitySettings;
|
||||
this.Notifications.success('Saved settings successfully');
|
||||
} catch (e) {
|
||||
this.Notifications.error('Failure', e, 'Failed saving settings');
|
||||
}
|
||||
this.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
checkAgent() {
|
||||
const applicationState = this.StateManager.getState();
|
||||
return applicationState.endpoint.mode.agentProxy;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
const securitySettings = this.endpoint.SecuritySettings;
|
||||
|
||||
const isAgent = this.checkAgent();
|
||||
this.isAgent = isAgent;
|
||||
|
||||
this.formValues = {
|
||||
enableHostManagementFeatures: isAgent && securitySettings.enableHostManagementFeatures,
|
||||
allowVolumeBrowserForRegularUsers: isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
|
||||
disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers,
|
||||
disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers,
|
||||
disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers,
|
||||
disableDeviceMappingForRegularUsers: !securitySettings.allowDeviceMappingForRegularUsers,
|
||||
disableStackManagementForRegularUsers: !securitySettings.allowStackManagementForRegularUsers,
|
||||
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Docker features configuration"></rd-header-title>
|
||||
<rd-header-content>Docker configuration</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="$ctrl.form">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Host and Filesystem
|
||||
</div>
|
||||
<div ng-if="!$ctrl.isAgent" class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
These features are only available for an Agent enabled endpoints.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="$ctrl.formValues.enableHostManagementFeatures"
|
||||
name="enableHostManagementFeatures"
|
||||
label="Enable host management features"
|
||||
tooltip="Enable host management features: host system browsing and advanced host details."
|
||||
disabled="!$ctrl.isAgent"
|
||||
label-class="col-sm-7 col-lg-4"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="$ctrl.formValues.allowVolumeBrowserForRegularUsers"
|
||||
name="allowVolumeBrowserForRegularUsers"
|
||||
label="Enable volume management for non-administrators"
|
||||
tooltip="When enabled, regular users will be able to use Portainer volume management features."
|
||||
disabled="!$ctrl.isAgent"
|
||||
label-class="col-sm-7 col-lg-4"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- security -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Docker Security Settings
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="$ctrl.formValues.disableBindMountsForRegularUsers"
|
||||
name="disableBindMountsForRegularUsers"
|
||||
label="Disable bind mounts for non-administrators"
|
||||
tooltip="When enabled, regular users will not be able to use bind mounts when creating containers."
|
||||
label-class="col-sm-7 col-lg-4"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="$ctrl.formValues.disablePrivilegedModeForRegularUsers"
|
||||
name="disablePrivilegedModeForRegularUsers"
|
||||
label="Disable privileged mode for non-administrators"
|
||||
tooltip="When enabled, regular users will not be able to use privileged mode when creating containers."
|
||||
label-class="col-sm-7 col-lg-4"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="$ctrl.formValues.disableHostNamespaceForRegularUsers"
|
||||
name="disableHostNamespaceForRegularUsers"
|
||||
label="Disable the use of host PID 1 for non-administrators"
|
||||
tooltip="Prevent users from accessing the host filesystem through the host PID namespace."
|
||||
label-class="col-sm-7 col-lg-4"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="$ctrl.formValues.disableStackManagementForRegularUsers"
|
||||
name="disableStackManagementForRegularUsers"
|
||||
label="Disable the use of Stacks for non-administrators"
|
||||
label-class="col-sm-7 col-lg-4"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="$ctrl.formValues.disableDeviceMappingForRegularUsers"
|
||||
name="disableDeviceMappingForRegularUsers"
|
||||
label="Disable device mappings for non-administrators"
|
||||
label-class="col-sm-7 col-lg-4"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
ng-model="$ctrl.formValues.disableContainerCapabilitiesForRegularUsers"
|
||||
name="disableContainerCapabilitiesForRegularUsers"
|
||||
label="Disable container capabilities for non-administrators"
|
||||
label-class="col-sm-7 col-lg-4"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.isContainerEditDisabled()">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings.
|
||||
</span>
|
||||
</div>
|
||||
<!-- !security -->
|
||||
|
||||
<!-- 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-click="$ctrl.save()" ng-disabled="$ctrl.state.actionInProgress" button-spinner="$ctrl.state.actionInProgress">
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Save configuration</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
11
app/docker/views/docker-features-configuration/index.js
Normal file
11
app/docker/views/docker-features-configuration/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import controller from './docker-features-configuration.controller';
|
||||
|
||||
angular.module('portainer.docker').component('dockerFeaturesConfigurationView', {
|
||||
templateUrl: './docker-features-configuration.html',
|
||||
controller,
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
@@ -29,7 +29,7 @@ angular.module('portainer.docker').controller('HostViewController', [
|
||||
ctrl.state.isAdmin = Authentication.isAdmin();
|
||||
var agentApiVersion = applicationState.endpoint.agentApiVersion;
|
||||
ctrl.state.agentApiVersion = agentApiVersion;
|
||||
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
|
||||
ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures;
|
||||
|
||||
$q.all({
|
||||
version: SystemService.version(),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
angular.module('portainer.docker').component('hostView', {
|
||||
templateUrl: './host-view.html',
|
||||
controller: 'HostViewController',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [
|
||||
var applicationState = StateManager.getState();
|
||||
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||
ctrl.state.isAdmin = Authentication.isAdmin();
|
||||
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
|
||||
ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures;
|
||||
|
||||
var fetchJobs = ctrl.state.isAdmin && ctrl.state.isAgent;
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
angular.module('portainer.docker').component('nodeDetailsView', {
|
||||
templateUrl: './node-details-view.html',
|
||||
controller: 'NodeDetailsViewController',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,9 +30,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
'RegistryService',
|
||||
'HttpRequestHelper',
|
||||
'NodeService',
|
||||
'SettingsService',
|
||||
'WebhookService',
|
||||
'EndpointProvider',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
@@ -56,9 +56,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
RegistryService,
|
||||
HttpRequestHelper,
|
||||
NodeService,
|
||||
SettingsService,
|
||||
WebhookService,
|
||||
EndpointProvider
|
||||
EndpointProvider,
|
||||
endpoint
|
||||
) {
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
@@ -593,10 +593,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
async function checkIfAllowedBindMounts() {
|
||||
const isAdmin = Authentication.isAdmin();
|
||||
|
||||
const settings = await SettingsService.publicSettings();
|
||||
const { AllowBindMountsForRegularUsers } = settings;
|
||||
const { allowBindMountsForRegularUsers } = endpoint.SecuritySettings;
|
||||
|
||||
return isAdmin || AllowBindMountsForRegularUsers;
|
||||
return isAdmin || allowBindMountsForRegularUsers;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted"> By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers. </div>
|
||||
<div class="col-sm-12 small text-muted">
|
||||
By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers (Linux) or
|
||||
<code>C:\ProgramData\Docker\secrets\$SECRET_NAME</code> (Windows).</div
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
|
||||
@@ -24,7 +24,14 @@
|
||||
<tbody>
|
||||
<tr ng-repeat="mount in service.ServiceMounts">
|
||||
<td ng-if="isAdmin || allowBindMounts">
|
||||
<select name="mountType" class="form-control" ng-model="mount.Type" ng-disabled="isUpdating" disable-authorization="DockerServiceUpdate">
|
||||
<select
|
||||
name="mountType"
|
||||
class="form-control"
|
||||
ng-model="mount.Type"
|
||||
ng-change="updateMount(service, mount)"
|
||||
ng-disabled="isUpdating"
|
||||
disable-authorization="DockerServiceUpdate"
|
||||
>
|
||||
<option value="volume">Volume</option>
|
||||
<option value="bind">Bind</option>
|
||||
</select>
|
||||
@@ -35,6 +42,7 @@
|
||||
ng-model="mount.Source"
|
||||
ng-options="vol.Id as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
||||
ng-if="mount.Type === 'volume'"
|
||||
ng-change="updateMount(service, mount)"
|
||||
disable-authorization="DockerServiceUpdate"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
|
||||
@@ -34,7 +34,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
'SecretService',
|
||||
'ImageService',
|
||||
'SecretHelper',
|
||||
'Service',
|
||||
'ServiceHelper',
|
||||
'LabelHelper',
|
||||
'TaskService',
|
||||
@@ -45,7 +44,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
'ModalService',
|
||||
'PluginService',
|
||||
'Authentication',
|
||||
'SettingsService',
|
||||
'VolumeService',
|
||||
'ImageHelper',
|
||||
'WebhookService',
|
||||
@@ -53,6 +51,7 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
'clipboard',
|
||||
'WebhookHelper',
|
||||
'NetworkService',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
@@ -67,7 +66,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
SecretService,
|
||||
ImageService,
|
||||
SecretHelper,
|
||||
Service,
|
||||
ServiceHelper,
|
||||
LabelHelper,
|
||||
TaskService,
|
||||
@@ -78,14 +76,14 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
ModalService,
|
||||
PluginService,
|
||||
Authentication,
|
||||
SettingsService,
|
||||
VolumeService,
|
||||
ImageHelper,
|
||||
WebhookService,
|
||||
EndpointProvider,
|
||||
clipboard,
|
||||
WebhookHelper,
|
||||
NetworkService
|
||||
NetworkService,
|
||||
endpoint
|
||||
) {
|
||||
$scope.state = {
|
||||
updateInProgress: false,
|
||||
@@ -666,7 +664,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
availableImages: ImageService.images(),
|
||||
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
|
||||
availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25),
|
||||
settings: SettingsService.publicSettings(),
|
||||
webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
|
||||
});
|
||||
})
|
||||
@@ -677,7 +674,7 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
$scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
|
||||
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
|
||||
$scope.availableVolumes = data.volumes;
|
||||
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
|
||||
$scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers;
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$scope.availableNetworks = data.availableNetworks;
|
||||
$scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm');
|
||||
|
||||
@@ -10,7 +10,8 @@ angular.module('portainer.docker').controller('VolumesController', [
|
||||
'EndpointProvider',
|
||||
'Authentication',
|
||||
'ModalService',
|
||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService) {
|
||||
'endpoint',
|
||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService, endpoint) {
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
@@ -75,8 +76,7 @@ angular.module('portainer.docker').controller('VolumesController', [
|
||||
function initView() {
|
||||
getVolumes();
|
||||
|
||||
$scope.showBrowseAction =
|
||||
$scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || $scope.applicationState.application.enableVolumeBrowserForNonAdminUsers);
|
||||
$scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || endpoint.SecuritySettings.allowVolumeBrowserForRegularUsers);
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
@@ -78,6 +78,13 @@
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('ImagePullPolicy')">
|
||||
Image Pull Policy
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ImagePullPolicy' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ImagePullPolicy' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Status')">
|
||||
Status
|
||||
@@ -110,6 +117,7 @@
|
||||
<td ng-if="!$ctrl.isPod">{{ item.PodName }}</td>
|
||||
<td>{{ item.Name }}</td>
|
||||
<td>{{ item.Image }}</td>
|
||||
<td>{{ item.ImagePullPolicy }}</td>
|
||||
<td
|
||||
><span class="label label-{{ item.Status | kubernetesPodStatusColor }}">{{ item.Status }}</span></td
|
||||
>
|
||||
|
||||
@@ -59,7 +59,9 @@ class KubernetesApplicationConverter {
|
||||
res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : '';
|
||||
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name;
|
||||
res.ResourcePool = data.metadata.namespace;
|
||||
res.Image = containers[0].image;
|
||||
if (containers.length) {
|
||||
res.Image = containers[0].image;
|
||||
}
|
||||
res.CreationDate = data.metadata.creationTimestamp;
|
||||
res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
|
||||
res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data];
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesConfigMap } from 'Kubernetes/models/config-map/models';
|
||||
import { KubernetesConfigMap, KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
|
||||
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
|
||||
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||
|
||||
class KubernetesConfigMapConverter {
|
||||
static apiToPortainerAccessConfigMap(data) {
|
||||
const res = new KubernetesPortainerAccessConfigMap();
|
||||
res.Id = data.metadata.uid;
|
||||
res.Data = data.data;
|
||||
return res;
|
||||
}
|
||||
|
||||
static createAccessPayload(data) {
|
||||
const res = new KubernetesConfigMapCreatePayload();
|
||||
_.unset(res, 'binaryData');
|
||||
res.metadata.name = data.Name;
|
||||
res.metadata.namespace = data.Namespace;
|
||||
res.data = data.Data;
|
||||
return res;
|
||||
}
|
||||
|
||||
static updateAccessPayload(data) {
|
||||
const res = KubernetesConfigMapConverter.createAccessPayload(data);
|
||||
res.metadata.uid = data.Id;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* API ConfigMap to front ConfigMap
|
||||
*/
|
||||
|
||||
@@ -52,6 +52,8 @@ class KubernetesConfigurationHelper {
|
||||
}
|
||||
|
||||
static parseData(formValues) {
|
||||
if (!formValues.Data.length) return '';
|
||||
|
||||
const data = _.reduce(
|
||||
formValues.Data,
|
||||
(acc, entry) => {
|
||||
|
||||
@@ -2,6 +2,15 @@ export const KubernetesPortainerConfigMapNamespace = 'portainer';
|
||||
export const KubernetesPortainerConfigMapConfigName = 'portainer-config';
|
||||
export const KubernetesPortainerConfigMapAccessKey = 'NamespaceAccessPolicies';
|
||||
|
||||
export function KubernetesPortainerAccessConfigMap() {
|
||||
return {
|
||||
Id: 0,
|
||||
Name: KubernetesPortainerConfigMapConfigName,
|
||||
Namespace: KubernetesPortainerConfigMapNamespace,
|
||||
Data: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ConfigMap Model
|
||||
*/
|
||||
|
||||
@@ -70,6 +70,7 @@ function computeContainers(data) {
|
||||
res.PodName = data.metadata.name;
|
||||
res.Name = item.name;
|
||||
res.Image = item.image;
|
||||
res.ImagePullPolicy = item.imagePullPolicy;
|
||||
res.Node = data.spec.nodeName;
|
||||
res.CreationDate = data.status.startTime;
|
||||
res.Status = computeContainerStatus(data.status.containerStatuses, item.name);
|
||||
|
||||
@@ -47,6 +47,7 @@ const _KubernetesPodContainer = Object.freeze({
|
||||
PodName: '',
|
||||
Name: '',
|
||||
Image: '',
|
||||
ImagePullPolicy: '',
|
||||
Node: '',
|
||||
CreationDate: '',
|
||||
Status: '',
|
||||
|
||||
@@ -3,6 +3,7 @@ import _ from 'lodash-es';
|
||||
import PortainerError from 'Portainer/error';
|
||||
import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap';
|
||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||
import { KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
|
||||
|
||||
class KubernetesConfigMapService {
|
||||
/* @ngInject */
|
||||
@@ -17,6 +18,54 @@ class KubernetesConfigMapService {
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
|
||||
getAccess(namespace, name) {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = name;
|
||||
const raw = await this.KubernetesConfigMaps(namespace).get(params).$promise;
|
||||
return KubernetesConfigMapConverter.apiToPortainerAccessConfigMap(raw);
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
return new KubernetesPortainerAccessConfigMap();
|
||||
}
|
||||
throw new PortainerError('Unable to retrieve Portainer accesses', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createAccess(config) {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const payload = KubernetesConfigMapConverter.createAccessPayload(config);
|
||||
const params = {};
|
||||
const namespace = payload.metadata.namespace;
|
||||
const data = await this.KubernetesConfigMaps(namespace).create(params, payload).$promise;
|
||||
return KubernetesConfigMapConverter.apiToPortainerAccessConfigMap(data);
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to create Portainer accesses', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateAccess(config) {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
if (!config.Id) {
|
||||
return await this.createAccess(config);
|
||||
}
|
||||
const payload = KubernetesConfigMapConverter.updateAccessPayload(config);
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = payload.metadata.name;
|
||||
const namespace = payload.metadata.namespace;
|
||||
const data = await this.KubernetesConfigMaps(namespace).update(params, payload).$promise;
|
||||
return KubernetesConfigMapConverter.apiToPortainerAccessConfigMap(data);
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to update Portainer accesses', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
@@ -29,12 +78,12 @@ class KubernetesConfigMapService {
|
||||
this.KubernetesConfigMaps(namespace).getYaml(params).$promise,
|
||||
]);
|
||||
|
||||
if (_.get(rawPromise, 'reason.status') == 404 && _.get(yamlPromise, 'reason.status') == 404) {
|
||||
if (_.get(rawPromise, 'reason.status') === 404 && _.get(yamlPromise, 'reason.status') === 404) {
|
||||
return KubernetesConfigMapConverter.defaultConfigMap(namespace, name);
|
||||
}
|
||||
|
||||
// Saving binary data to 'data' field in configMap Object is not allowed by kubernetes and getYaml() may get
|
||||
// an error. We should keep binary data to 'binaryData' field instead of 'data'. Before that, we
|
||||
// Saving binary data to 'data' field in configMap Object is not allowed by kubernetes and getYaml() may get
|
||||
// an error. We should keep binary data to 'binaryData' field instead of 'data'. Before that, we
|
||||
// use response from get() and ignore 500 error as a workaround.
|
||||
if (rawPromise.value) {
|
||||
return KubernetesConfigMapConverter.apiToConfigMap(rawPromise.value, yamlPromise.value);
|
||||
|
||||
@@ -1299,7 +1299,7 @@
|
||||
ng-min="1"
|
||||
ng-max="65535"
|
||||
ng-required="!publishedPort.NeedsDeletion"
|
||||
ng-change="ctrl.onChangePortMappingLoadBalancerPort()"
|
||||
ng-change="ctrl.onChangePortMappingLoadBalancer()"
|
||||
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -409,13 +409,14 @@ class KubernetesCreateApplicationController {
|
||||
}
|
||||
|
||||
onChangePortProtocol(index) {
|
||||
this.onChangePortMappingContainerPort();
|
||||
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
||||
const newPorts = _.filter(this.formValues.PublishedPorts, { IsNew: true });
|
||||
_.forEach(newPorts, (port) => {
|
||||
port.Protocol = index ? this.formValues.PublishedPorts[index].Protocol : newPorts[0].Protocol;
|
||||
});
|
||||
this.onChangePortMappingLoadBalancer();
|
||||
}
|
||||
this.onChangePortMappingContainerPort();
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
ng-if="ctrl.availableUsersAndTeams.length > 0"
|
||||
input-model="ctrl.availableUsersAndTeams"
|
||||
output-model="ctrl.formValues.multiselectOutput"
|
||||
button-label="icon '-' Name"
|
||||
item-label="icon '-' Name"
|
||||
button-label="icon Name"
|
||||
item-label="icon Name"
|
||||
tick-property="ticked"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
|
||||
@@ -52,7 +52,7 @@ class KubernetesResourcePoolAccessController {
|
||||
let [endpoint, pool, configMap] = await Promise.all([
|
||||
this.EndpointService.endpoint(this.endpointId),
|
||||
this.KubernetesResourcePoolService.get(name),
|
||||
this.KubernetesConfigMapService.get(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
|
||||
this.KubernetesConfigMapService.getAccess(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
|
||||
]);
|
||||
const group = await this.GroupService.group(endpoint.GroupId);
|
||||
const roles = [];
|
||||
@@ -96,7 +96,7 @@ class KubernetesResourcePoolAccessController {
|
||||
this.state.actionInProgress = true;
|
||||
const newAccesses = _.concat(this.authorizedUsersAndTeams, this.formValues.multiselectOutput);
|
||||
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
|
||||
await this.KubernetesConfigMapService.update(accessConfigMap);
|
||||
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
|
||||
this.Notifications.success('Access successfully created');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
@@ -116,7 +116,7 @@ class KubernetesResourcePoolAccessController {
|
||||
this.state.actionInProgress = true;
|
||||
const newAccesses = _.without(this.authorizedUsersAndTeams, ...selectedItems);
|
||||
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
|
||||
await this.KubernetesConfigMapService.update(accessConfigMap);
|
||||
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
|
||||
this.Notifications.success('Access successfully removed');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
ng-if="ctrl.availableUsersAndTeams.length > 0"
|
||||
input-model="ctrl.availableUsersAndTeams"
|
||||
output-model="ctrl.formValues.multiselectOutput"
|
||||
button-label="icon '-' Name"
|
||||
item-label="icon '-' Name"
|
||||
button-label="icon Name"
|
||||
item-label="icon Name"
|
||||
tick-property="ticked"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/* switch box */
|
||||
|
||||
.switch {
|
||||
--switch-size: 24px;
|
||||
}
|
||||
|
||||
.switch.small {
|
||||
--switch-size: 12px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.switch i,
|
||||
.bootbox-form .checkbox i {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
padding-right: var(--switch-size);
|
||||
transition: all ease 0.2s;
|
||||
-webkit-transition: all ease 0.2s;
|
||||
-moz-transition: all ease 0.2s;
|
||||
-o-transition: all ease 0.2s;
|
||||
border-radius: var(--switch-size);
|
||||
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.switch i:before,
|
||||
.bootbox-form .checkbox i:before {
|
||||
display: block;
|
||||
content: '';
|
||||
width: var(--switch-size);
|
||||
height: var(--switch-size);
|
||||
border-radius: var(--switch-size);
|
||||
background: white;
|
||||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.switch :checked + i,
|
||||
.bootbox-form .checkbox :checked ~ i {
|
||||
padding-right: 0;
|
||||
padding-left: var(--switch-size);
|
||||
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||
-moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||
}
|
||||
|
||||
.switch :disabled + i {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<label style="display: flex; align-items: center; margin: 0;">
|
||||
<span for="toggle_{{::$ctrl.name}}" class="control-label text-left space-right" ng-class="$ctrl.labelClass" style="padding: 0;">
|
||||
{{::$ctrl.label}}
|
||||
<portainer-tooltip ng-if="$ctrl.tooltip" position="bottom" message="{{::$ctrl.tooltip}}"></portainer-tooltip>
|
||||
</span>
|
||||
<por-switch
|
||||
class-name="space-right"
|
||||
name="toggle_{{::$ctrl.name}}"
|
||||
id="toggle_{{::$ctrl.name}}"
|
||||
ng-model="$ctrl.ngModel"
|
||||
disabled="$ctrl.disabled"
|
||||
on-change="($ctrl.onChange)"
|
||||
></por-switch>
|
||||
</label>
|
||||
@@ -0,0 +1,18 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import './por-switch-field.css';
|
||||
|
||||
export const porSwitchField = {
|
||||
templateUrl: './por-switch-field.html',
|
||||
bindings: {
|
||||
tooltip: '@',
|
||||
ngModel: '=',
|
||||
label: '@',
|
||||
name: '@',
|
||||
labelClass: '@',
|
||||
disabled: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
};
|
||||
|
||||
angular.module('portainer.app').component('porSwitchField', porSwitchField);
|
||||
@@ -0,0 +1,3 @@
|
||||
<label class="switch" ng-class="$ctrl.className" style="margin-bottom: 0;">
|
||||
<input type="checkbox" name="{{::$ctrl.name}}" id="{{::$ctrl.id}}" ng-model="$ctrl.ngModel" ng-disabled="$ctrl.disabled" ng-change="$ctrl.onChange($ctrl.ngModel)" /><i></i>
|
||||
</label>
|
||||
15
app/portainer/components/forms/por-switch/por-switch.js
Normal file
15
app/portainer/components/forms/por-switch/por-switch.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import angular from 'angular';
|
||||
|
||||
const porSwitch = {
|
||||
templateUrl: './por-switch.html',
|
||||
bindings: {
|
||||
ngModel: '=',
|
||||
id: '@',
|
||||
className: '@',
|
||||
name: '@',
|
||||
disabled: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
};
|
||||
|
||||
angular.module('portainer.app').component('porSwitch', porSwitch);
|
||||
@@ -4,16 +4,8 @@ export function SettingsViewModel(data) {
|
||||
this.AuthenticationMethod = data.AuthenticationMethod;
|
||||
this.LDAPSettings = data.LDAPSettings;
|
||||
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
|
||||
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
|
||||
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
|
||||
this.AllowVolumeBrowserForRegularUsers = data.AllowVolumeBrowserForRegularUsers;
|
||||
this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers;
|
||||
this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers;
|
||||
this.AllowStackManagementForRegularUsers = data.AllowStackManagementForRegularUsers;
|
||||
this.AllowContainerCapabilitiesForRegularUsers = data.AllowContainerCapabilitiesForRegularUsers;
|
||||
this.SnapshotInterval = data.SnapshotInterval;
|
||||
this.TemplatesURL = data.TemplatesURL;
|
||||
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
||||
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
|
||||
this.UserSessionTimeout = data.UserSessionTimeout;
|
||||
@@ -21,15 +13,7 @@ export function SettingsViewModel(data) {
|
||||
}
|
||||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
this.AllowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers;
|
||||
this.AllowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers;
|
||||
this.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers;
|
||||
this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
|
||||
this.AllowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers;
|
||||
this.AllowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers;
|
||||
this.AllowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers;
|
||||
this.AuthenticationMethod = settings.AuthenticationMethod;
|
||||
this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
||||
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
this.LogoURL = settings.LogoURL;
|
||||
this.OAuthLoginURI = settings.OAuthLoginURI;
|
||||
|
||||
@@ -21,6 +21,7 @@ angular.module('portainer.app').factory('Endpoints', [
|
||||
snapshots: { method: 'POST', params: { action: 'snapshot' } },
|
||||
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } },
|
||||
status: { method: 'GET', params: { id: '@id', action: 'status' } },
|
||||
updateSecuritySettings: { method: 'PUT', params: { id: '@id', action: 'settings' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,7 +6,9 @@ angular.module('portainer.app').factory('EndpointService', [
|
||||
'FileUploadService',
|
||||
function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
var service = {
|
||||
updateSecuritySettings,
|
||||
};
|
||||
|
||||
service.endpoint = function (endpointID) {
|
||||
return Endpoints.get({ id: endpointID }).$promise;
|
||||
@@ -146,5 +148,9 @@ angular.module('portainer.app').factory('EndpointService', [
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
function updateSecuritySettings(id, securitySettings) {
|
||||
return Endpoints.updateSecuritySettings({ id }, securitySettings).$promise;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -72,51 +72,11 @@ angular.module('portainer.app').factory('StateManager', [
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateEnableHostManagementFeatures = function (enableHostManagementFeatures) {
|
||||
state.application.enableHostManagementFeatures = enableHostManagementFeatures;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateEnableVolumeBrowserForNonAdminUsers = function (enableVolumeBrowserForNonAdminUsers) {
|
||||
state.application.enableVolumeBrowserForNonAdminUsers = enableVolumeBrowserForNonAdminUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateEnableEdgeComputeFeatures = function updateEnableEdgeComputeFeatures(enableEdgeComputeFeatures) {
|
||||
state.application.enableEdgeComputeFeatures = enableEdgeComputeFeatures;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateAllowHostNamespaceForRegularUsers = function (allowHostNamespaceForRegularUsers) {
|
||||
state.application.allowHostNamespaceForRegularUsers = allowHostNamespaceForRegularUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateAllowDeviceMappingForRegularUsers = function updateAllowDeviceMappingForRegularUsers(allowDeviceMappingForRegularUsers) {
|
||||
state.application.allowDeviceMappingForRegularUsers = allowDeviceMappingForRegularUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateAllowStackManagementForRegularUsers = function updateAllowStackManagementForRegularUsers(allowStackManagementForRegularUsers) {
|
||||
state.application.allowStackManagementForRegularUsers = allowStackManagementForRegularUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateAllowContainerCapabilitiesForRegularUsers = function updateAllowContainerCapabilitiesForRegularUsers(allowContainerCapabilitiesForRegularUsers) {
|
||||
state.application.allowContainerCapabilitiesForRegularUsers = allowContainerCapabilitiesForRegularUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateAllowBindMountsForRegularUsers = function updateAllowBindMountsForRegularUsers(allowBindMountsForRegularUsers) {
|
||||
state.application.allowBindMountsForRegularUsers = allowBindMountsForRegularUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateAllowPrivilegedModeForRegularUsers = function (AllowPrivilegedModeForRegularUsers) {
|
||||
state.application.allowPrivilegedModeForRegularUsers = AllowPrivilegedModeForRegularUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateEnableTelemetry = function updateEnableTelemetry(enableTelemetry) {
|
||||
state.application.enableTelemetry = enableTelemetry;
|
||||
$analytics.setOptOut(!enableTelemetry);
|
||||
@@ -128,15 +88,7 @@ angular.module('portainer.app').factory('StateManager', [
|
||||
state.application.enableTelemetry = settings.EnableTelemetry;
|
||||
state.application.logo = settings.LogoURL;
|
||||
state.application.snapshotInterval = settings.SnapshotInterval;
|
||||
state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
||||
state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers;
|
||||
state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
|
||||
state.application.allowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers;
|
||||
state.application.allowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers;
|
||||
state.application.allowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers;
|
||||
state.application.allowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers;
|
||||
state.application.allowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers;
|
||||
state.application.validity = moment().unix();
|
||||
}
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ angular
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
--name portainer_edge_agent \\localhost
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent`;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,100 +76,7 @@
|
||||
</div>
|
||||
<!-- !templates -->
|
||||
<!-- host-filesystem -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Host and Filesystem
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_enableHostManagementFeatures" class="control-label text-left">
|
||||
Enable host management features
|
||||
<portainer-tooltip position="bottom" message="Enable host management features: host system browsing and advanced host details."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_enableHostManagementFeatures" ng-model="formValues.enableHostManagementFeatures" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_allowvolumebrowser" class="control-label text-left">
|
||||
Enable volume management for non-administrators
|
||||
<portainer-tooltip position="bottom" message="When enabled, regular users will be able to use Portainer volume management features."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowvolumebrowser" ng-model="formValues.enableVolumeBrowser" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !host-filesystem -->
|
||||
<!-- security -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Docker Endpoint Security Options
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_allowbindmounts" class="control-label text-left">
|
||||
Disable bind mounts for non-administrators
|
||||
<portainer-tooltip position="bottom" message="When enabled, regular users will not be able to use bind mounts when creating containers."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowbindmounts" ng-model="formValues.restrictBindMounts" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_allowbindmounts" class="control-label text-left">
|
||||
Disable privileged mode for non-administrators
|
||||
<portainer-tooltip position="bottom" message="When enabled, regular users will not be able to use privileged mode when creating containers."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowbindmounts" ng-model="formValues.restrictPrivilegedMode" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_allowHostNamespaceForRegularUsers" class="control-label text-left">
|
||||
Disable the use of host PID 1 for non-administrators
|
||||
<portainer-tooltip position="bottom" message="Prevent users from accessing the host filesystem through the host PID namespace."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_allowHostNamespaceForRegularUsers" ng-model="formValues.restrictHostNamespaceForRegularUsers" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_disableStackManagementForRegularUsers" class="control-label text-left">
|
||||
Disable the use of Stacks for non-administrators
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_disableStackManagementForRegularUsers" ng-model="formValues.disableStackManagementForRegularUsers" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_disableDeviceMappingForRegularUsers" class="control-label text-left">
|
||||
Disable device mappings for non-administrators
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_disableDeviceMappingForRegularUsers" ng-model="formValues.disableDeviceMappingForRegularUsers" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_disableContainerCapabilitiesForRegularUsers" class="control-label text-left">
|
||||
Disable container capabilities for non-administrators
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_disableContainerCapabilitiesForRegularUsers" ng-model="formValues.disableContainerCapabilitiesForRegularUsers" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="isContainerEditDisabled()">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings.
|
||||
</span>
|
||||
</div>
|
||||
<!-- !security -->
|
||||
<!-- edge -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Edge Compute
|
||||
|
||||
@@ -25,33 +25,12 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
|
||||
$scope.formValues = {
|
||||
customLogo: false,
|
||||
restrictBindMounts: false,
|
||||
restrictPrivilegedMode: false,
|
||||
labelName: '',
|
||||
labelValue: '',
|
||||
enableHostManagementFeatures: false,
|
||||
enableVolumeBrowser: false,
|
||||
enableEdgeComputeFeatures: false,
|
||||
restrictHostNamespaceForRegularUsers: false,
|
||||
allowDeviceMappingForRegularUsers: false,
|
||||
allowStackManagementForRegularUsers: false,
|
||||
disableContainerCapabilitiesForRegularUsers: false,
|
||||
enableTelemetry: false,
|
||||
};
|
||||
|
||||
$scope.isContainerEditDisabled = function isContainerEditDisabled() {
|
||||
const {
|
||||
restrictBindMounts,
|
||||
restrictHostNamespaceForRegularUsers,
|
||||
restrictPrivilegedMode,
|
||||
disableDeviceMappingForRegularUsers,
|
||||
disableContainerCapabilitiesForRegularUsers,
|
||||
} = this.formValues;
|
||||
return (
|
||||
restrictBindMounts || restrictHostNamespaceForRegularUsers || restrictPrivilegedMode || disableDeviceMappingForRegularUsers || disableContainerCapabilitiesForRegularUsers
|
||||
);
|
||||
};
|
||||
|
||||
$scope.removeFilteredContainerLabel = function (index) {
|
||||
var settings = $scope.settings;
|
||||
settings.BlackListedLabels.splice(index, 1);
|
||||
@@ -77,15 +56,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
settings.LogoURL = '';
|
||||
}
|
||||
|
||||
settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
|
||||
settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
|
||||
settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser;
|
||||
settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures;
|
||||
settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures;
|
||||
settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers;
|
||||
settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers;
|
||||
settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers;
|
||||
settings.AllowContainerCapabilitiesForRegularUsers = !$scope.formValues.disableContainerCapabilitiesForRegularUsers;
|
||||
settings.EnableTelemetry = $scope.formValues.enableTelemetry;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
@@ -98,15 +69,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
Notifications.success('Settings updated');
|
||||
StateManager.updateLogo(settings.LogoURL);
|
||||
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
|
||||
StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures);
|
||||
StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers);
|
||||
StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers);
|
||||
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
|
||||
StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers);
|
||||
StateManager.updateAllowStackManagementForRegularUsers(settings.AllowStackManagementForRegularUsers);
|
||||
StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers);
|
||||
StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers);
|
||||
StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers);
|
||||
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
|
||||
$state.reload();
|
||||
})
|
||||
@@ -127,15 +90,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
if (settings.LogoURL !== '') {
|
||||
$scope.formValues.customLogo = true;
|
||||
}
|
||||
$scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers;
|
||||
$scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers;
|
||||
$scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers;
|
||||
$scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
||||
$scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
$scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers;
|
||||
$scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers;
|
||||
$scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers;
|
||||
$scope.formValues.disableContainerCapabilitiesForRegularUsers = !settings.AllowContainerCapabilitiesForRegularUsers;
|
||||
$scope.formValues.enableTelemetry = settings.EnableTelemetry;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
||||
@@ -46,9 +46,17 @@ angular.module('portainer.app').controller('SidebarController', [
|
||||
|
||||
async function shouldShowStacks() {
|
||||
const isAdmin = Authentication.isAdmin();
|
||||
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return isAdmin || allowStackManagementForRegularUsers;
|
||||
if (isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const endpoint = EndpointProvider.currentEndpoint();
|
||||
if (!endpoint || !endpoint.SecuritySettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return endpoint.SecuritySettings.allowStackManagementForRegularUsers;
|
||||
}
|
||||
|
||||
$transitions.onEnter({}, async () => {
|
||||
|
||||
@@ -167,7 +167,6 @@ angular
|
||||
|
||||
async function initView() {
|
||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||
const endpointId = +$state.params.endpointId;
|
||||
$scope.state.StackType = 2;
|
||||
if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER') {
|
||||
$scope.state.StackType = 1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
angular.module('portainer.app').controller('StacksController', StacksController);
|
||||
|
||||
/* @ngInject */
|
||||
function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, StateManager) {
|
||||
function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, endpoint) {
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
ModalService.confirmDeletion('Do you want to remove the selected stack(s)? Associated services will be removed as well.', function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
@@ -55,8 +55,7 @@ function StacksController($scope, $state, Notifications, StackService, ModalServ
|
||||
}
|
||||
|
||||
async function loadCreateEnabled() {
|
||||
const appState = StateManager.getState().application;
|
||||
return appState.allowStackManagementForRegularUsers || Authentication.isAdmin();
|
||||
return endpoint.SecuritySettings.allowStackManagementForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
async function initView() {
|
||||
|
||||
@@ -16,8 +16,8 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
'ResourceControlService',
|
||||
'Authentication',
|
||||
'FormValidator',
|
||||
'SettingsService',
|
||||
'StackService',
|
||||
'endpoint',
|
||||
function (
|
||||
$scope,
|
||||
$q,
|
||||
@@ -33,8 +33,8 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
ResourceControlService,
|
||||
Authentication,
|
||||
FormValidator,
|
||||
SettingsService,
|
||||
StackService
|
||||
StackService,
|
||||
endpoint
|
||||
) {
|
||||
$scope.state = {
|
||||
selectedTemplate: null,
|
||||
@@ -263,7 +263,6 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
false,
|
||||
endpointMode.provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
|
||||
),
|
||||
settings: SettingsService.publicSettings(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
var templates = data.templates;
|
||||
@@ -271,8 +270,7 @@ angular.module('portainer.app').controller('TemplatesController', [
|
||||
$scope.availableVolumes = _.orderBy(data.volumes.Volumes, [(volume) => volume.Name.toLowerCase()], ['asc']);
|
||||
var networks = data.networks;
|
||||
$scope.availableNetworks = networks;
|
||||
var settings = data.settings;
|
||||
$scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
|
||||
$scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.templates = [];
|
||||
|
||||
@@ -29,7 +29,7 @@ angular.module('portainer.app').controller('UsersController', [
|
||||
$scope.checkUsernameValidity = function () {
|
||||
var valid = true;
|
||||
for (var i = 0; i < $scope.users.length; i++) {
|
||||
if ($scope.formValues.Username === $scope.users[i].Username) {
|
||||
if ($scope.formValues.Username.toLocaleLowerCase() === $scope.users[i].Username.toLocaleLowerCase()) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
param (
|
||||
[string]$platform,
|
||||
[string]$arch
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop";
|
||||
|
||||
$binary = "portainer.exe"
|
||||
$go_path = "$($(Get-ITEM -Path env:AGENT_TEMPDIRECTORY).Value)\go"
|
||||
|
||||
Set-Item env:GOPATH "$go_path"
|
||||
|
||||
New-Item -Name dist -Path "." -ItemType Directory -Force | Out-Null
|
||||
New-Item -Name portainer -Path "$go_path\src\github.com\portainer" -ItemType Directory -Force | Out-Null
|
||||
|
||||
Copy-Item -Path "api" -Destination "$go_path\src\github.com\portainer\portainer\api" -Recurse -Force
|
||||
|
||||
Set-Location -Path "api\cmd\portainer"
|
||||
|
||||
go get -t -d -v ./...
|
||||
## go build -v
|
||||
& cmd /c 'go build -v 2>&1'
|
||||
|
||||
Copy-Item -Path "portainer.exe" -Destination "$($env:BUILD_SOURCESDIRECTORY)\dist\portainer.exe" -Force
|
||||
@@ -1,3 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PLATFORM=$1
|
||||
ARCH=$2
|
||||
|
||||
export GOPATH="/tmp/go"
|
||||
|
||||
binary="portainer"
|
||||
@@ -10,6 +15,10 @@ cp -R api ${GOPATH}/src/github.com/portainer/portainer/api
|
||||
cd 'api/cmd/portainer'
|
||||
|
||||
go get -t -d -v ./...
|
||||
GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s'
|
||||
GOOS=${PLATFORM} GOARCH=${ARCH} CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s'
|
||||
|
||||
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer"
|
||||
if [ "${PLATFORM}" == 'windows' ]; then
|
||||
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/${binary}.exe" "$BUILD_SOURCESDIRECTORY/dist/portainer.exe"
|
||||
else
|
||||
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer"
|
||||
fi
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
param (
|
||||
[string]$docker_version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop";
|
||||
$ProgressPreference = "SilentlyContinue";
|
||||
|
||||
New-Item -Path "docker-binary" -ItemType Directory | Out-Null
|
||||
|
||||
$download_folder = "docker-binary"
|
||||
|
||||
Invoke-WebRequest -O "$($download_folder)/docker-binaries.zip" "https://dockermsft.azureedge.net/dockercontainer/docker-$($docker_version).zip"
|
||||
Expand-Archive -Path "$($download_folder)/docker-binaries.zip" -DestinationPath "$($download_folder)"
|
||||
Move-Item -Path "$($download_folder)/docker/docker.exe" -Destination "dist"
|
||||
Move-Item -Path "$($download_folder)/docker/*.dll" -Destination "dist"
|
||||
@@ -10,9 +10,10 @@ rm -rf "${DOWNLOAD_FOLDER}"
|
||||
mkdir -pv "${DOWNLOAD_FOLDER}"
|
||||
|
||||
if [ "${PLATFORM}" == 'win' ]; then
|
||||
wget -O "${DOWNLOAD_FOLDER}/docker-binaries.zip" "https://download.docker.com/${PLATFORM}/static/stable/${ARCH}/docker-${DOCKER_VERSION}.zip"
|
||||
wget -O "${DOWNLOAD_FOLDER}/docker-binaries.zip" "https://dockermsft.azureedge.net/dockercontainer/docker-${DOCKER_VERSION}.zip"
|
||||
unzip "${DOWNLOAD_FOLDER}/docker-binaries.zip" -d "${DOWNLOAD_FOLDER}"
|
||||
mv "${DOWNLOAD_FOLDER}/docker/docker.exe" dist/
|
||||
mv ${DOWNLOAD_FOLDER}/docker/*.dll dist/
|
||||
else
|
||||
wget -O "${DOWNLOAD_FOLDER}/docker-binaries.tgz" "https://download.docker.com/${PLATFORM}/static/stable/${ARCH}/docker-${DOCKER_VERSION}.tgz"
|
||||
tar -xf "${DOWNLOAD_FOLDER}/docker-binaries.tgz" -C "${DOWNLOAD_FOLDER}"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
param (
|
||||
[string]$docker_compose_version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop";
|
||||
$ProgressPreference = "SilentlyContinue";
|
||||
|
||||
Invoke-WebRequest -O "dist/docker-compose.exe" "https://github.com/docker/compose/releases/download/$($docker_compose_version)/docker-compose-Windows-x86_64.exe"
|
||||
@@ -10,6 +10,9 @@ if [ "${PLATFORM}" == 'linux' ] && [ "${ARCH}" == 'amd64' ]; then
|
||||
elif [ "${PLATFORM}" == 'mac' ]; then
|
||||
wget -O "dist/docker-compose" "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-Darwin-x86_64"
|
||||
chmod +x "dist/docker-compose"
|
||||
elif [ "${PLATFORM}" == 'win' ]; then
|
||||
wget -O "dist/docker-compose.exe" "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-Windows-x86_64.exe"
|
||||
chmod +x "dist/docker-compose.exe"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
param (
|
||||
[string]$kompose_version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop";
|
||||
$ProgressPreference = "SilentlyContinue";
|
||||
|
||||
Invoke-WebRequest -O "dist/kompose.exe" "https://github.com/kubernetes/kompose/releases/download/$($kompose_version)/kompose-windows-amd64.exe"
|
||||
@@ -4,7 +4,12 @@ PLATFORM=$1
|
||||
ARCH=$2
|
||||
KOMPOSE_VERSION=$3
|
||||
|
||||
wget -O "dist/kompose" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-${PLATFORM}-${ARCH}"
|
||||
chmod +x "dist/kompose"
|
||||
if [ "${PLATFORM}" == 'linux' ]; then
|
||||
wget -O "dist/kompose" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-${PLATFORM}-${ARCH}"
|
||||
chmod +x "dist/kompose"
|
||||
elif [ "${PLATFORM}" == 'windows' ]; then
|
||||
wget -O "dist/kompose.exe" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-windows-amd64.exe"
|
||||
chmod +x "dist/kompose.exe"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
param (
|
||||
[string]$kubectl_version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop";
|
||||
$ProgressPreference = "SilentlyContinue";
|
||||
|
||||
Invoke-WebRequest -O "dist/kubectl.exe" "https://storage.googleapis.com/kubernetes-release/release/$($kubectl_version)/bin/windows/amd64/kubectl.exe"
|
||||
@@ -4,7 +4,12 @@ PLATFORM=$1
|
||||
ARCH=$2
|
||||
KUBECTL_VERSION=$3
|
||||
|
||||
wget -O "dist/kubectl" "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/${PLATFORM}/${ARCH}/kubectl"
|
||||
chmod +x "dist/kubectl"
|
||||
if [ "${PLATFORM}" == 'linux' ]; then
|
||||
wget -O "dist/kubectl" "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/${PLATFORM}/${ARCH}/kubectl"
|
||||
chmod +x "dist/kubectl"
|
||||
elif [ "${PLATFORM}" == 'windows' ]; then
|
||||
wget -O "dist/kubectl.exe" "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/windows/amd64/kubectl.exe"
|
||||
chmod +x "dist/kubectl.exe"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
24
build/windows/Dockerfile
Normal file
24
build/windows/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
ARG OSVERSION
|
||||
FROM --platform=linux/amd64 gcr.io/k8s-staging-e2e-test-images/windows-servercore-cache:1.0-linux-amd64-${OSVERSION} as core
|
||||
FROM --platform=linux/amd64 alpine:3.13.0 as downloader
|
||||
ENV GIT_VERSION 2.30.0
|
||||
ENV GIT_PATCH_VERSION 2
|
||||
|
||||
RUN mkdir mingit/ \
|
||||
&& wget https://github.com/git-for-windows/git/releases/download/v$GIT_VERSION.windows.$GIT_PATCH_VERSION/MinGit-$GIT_VERSION.$GIT_PATCH_VERSION-busybox-64-bit.zip \
|
||||
&& unzip MinGit-$GIT_VERSION.$GIT_PATCH_VERSION-busybox-64-bit.zip -d mingit/
|
||||
|
||||
FROM mcr.microsoft.com/windows/nanoserver:${OSVERSION}
|
||||
ENV PATH "C:\mingit\cmd;C:\Windows\system32;C:\Windows;"
|
||||
|
||||
COPY --from=downloader /mingit mingit/
|
||||
COPY --from=core /Windows/System32/netapi32.dll /Windows/System32/netapi32.dll
|
||||
|
||||
USER ContainerAdministrator
|
||||
|
||||
COPY dist /
|
||||
|
||||
EXPOSE 9000
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["/portainer.exe"]
|
||||
@@ -1,27 +0,0 @@
|
||||
FROM mcr.microsoft.com/windows/servercore:ltsc2019 as core
|
||||
|
||||
ENV GIT_VERSION 2.30.0
|
||||
ENV GIT_PATCH_VERSION 2
|
||||
|
||||
RUN powershell -Command $ErrorActionPreference = 'Stop' ; \
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; \
|
||||
Invoke-WebRequest $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}.{1}-busybox-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION) -OutFile 'mingit.zip' -UseBasicParsing ; \
|
||||
Expand-Archive mingit.zip -DestinationPath c:\mingit
|
||||
|
||||
FROM mcr.microsoft.com/windows/nanoserver:1809-amd64
|
||||
|
||||
USER ContainerAdministrator
|
||||
|
||||
COPY --from=core /windows/system32/netapi32.dll /windows/system32/netapi32.dll
|
||||
COPY --from=core /mingit /mingit
|
||||
|
||||
COPY dist /
|
||||
|
||||
RUN setx /M path "C:\mingit\cmd;%path%"
|
||||
|
||||
WORKDIR /
|
||||
|
||||
EXPOSE 9000
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["/portainer.exe"]
|
||||
103
gruntfile.js
103
gruntfile.js
@@ -165,11 +165,7 @@ function shell_build_binary(p, a) {
|
||||
}
|
||||
|
||||
function shell_build_binary_azuredevops(p, a) {
|
||||
if (p === 'linux') {
|
||||
return 'build/build_binary_azuredevops.sh ' + p + ' ' + a + ';';
|
||||
} else {
|
||||
return 'powershell -Command ".\\build\\build_binary_azuredevops.ps1 -platform ' + p + ' -arch ' + a + '"';
|
||||
}
|
||||
return 'build/build_binary_azuredevops.sh ' + p + ' ' + a + ';';
|
||||
}
|
||||
|
||||
function shell_run_container() {
|
||||
@@ -195,83 +191,52 @@ function shell_download_docker_binary(p, a) {
|
||||
var ip = ps[p] === undefined ? p : ps[p];
|
||||
var ia = as[a] === undefined ? a : as[a];
|
||||
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsVersion %>' : '<%= binaries.dockerLinuxVersion %>';
|
||||
|
||||
if (p === 'linux' || p === 'mac') {
|
||||
return ['if [ -f dist/docker ]; then', 'echo "docker binary exists";', 'else', 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'fi'].join(' ');
|
||||
} else {
|
||||
return [
|
||||
'powershell -Command "& {if (Test-Path -Path "dist/docker.exe") {',
|
||||
'Write-Host "Skipping download, Docker binary exists"',
|
||||
'return',
|
||||
'} else {',
|
||||
'& ".\\build\\download_docker_binary.ps1" -docker_version ' + binaryVersion + '',
|
||||
'}}"',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
return [
|
||||
'if [ -f dist/docker ] || [ -f dist/docker.exe ]; then',
|
||||
'echo "docker binary exists";',
|
||||
'else',
|
||||
'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';',
|
||||
'fi',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function shell_download_docker_compose_binary(p, a) {
|
||||
console.log('request docker compose for ' + p + ':' + a);
|
||||
var ps = { windows: 'win', darwin: 'mac' };
|
||||
var as = { arm: 'armhf', arm64: 'aarch64' };
|
||||
var ip = ps[p] || p;
|
||||
var ia = as[a] || a;
|
||||
console.log('download docker compose for ' + ip + ':' + ia);
|
||||
var linuxBinaryVersion = '<%= binaries.dockerLinuxComposeVersion %>';
|
||||
var windowsBinaryVersion = '<%= binaries.dockerWindowsComposeVersion %>';
|
||||
console.log('download docker compose versions; Linux: ' + linuxBinaryVersion + ' Windows: ' + windowsBinaryVersion);
|
||||
|
||||
if (ip === 'linux' || ip === 'mac') {
|
||||
return [
|
||||
'if [ -f dist/docker-compose ]; then',
|
||||
'echo "Docker Compose binary exists";',
|
||||
'else',
|
||||
'build/download_docker_compose_binary.sh ' + ip + ' ' + ia + ' ' + linuxBinaryVersion + ';',
|
||||
'fi',
|
||||
].join(' ');
|
||||
} else if (ip === 'win') {
|
||||
return [
|
||||
'powershell -Command "& {if (Test-Path -Path "dist/docker-compose.exe") {',
|
||||
'Write-Host "Skipping download, Docker Compose binary exists"',
|
||||
'return',
|
||||
'} else {',
|
||||
'& ".\\build\\download_docker_compose_binary.ps1" -docker_compose_version ' + windowsBinaryVersion + '',
|
||||
'}}"',
|
||||
].join(' ');
|
||||
}
|
||||
console.log('docker compose is downloaded');
|
||||
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsComposeVersion %>' : '<%= binaries.dockerLinuxComposeVersion %>';
|
||||
|
||||
return [
|
||||
'if [ -f dist/docker-compose ] || [ -f dist/docker-compose.exe ]; then',
|
||||
'echo "Docker Compose binary exists";',
|
||||
'else',
|
||||
'build/download_docker_compose_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';',
|
||||
'fi',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function shell_download_kompose_binary(p, a) {
|
||||
var binaryVersion = '<%= binaries.komposeVersion %>';
|
||||
|
||||
if (p === 'linux' || p === 'darwin') {
|
||||
return ['if [ -f dist/kompose ]; then', 'echo "kompose binary exists";', 'else', 'build/download_kompose_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' ');
|
||||
} else {
|
||||
return [
|
||||
'powershell -Command "& {if (Test-Path -Path "dist/kompose.exe") {',
|
||||
'Write-Host "Skipping download, Kompose binary exists"',
|
||||
'return',
|
||||
'} else {',
|
||||
'& ".\\build\\download_kompose_binary.ps1" -kompose_version ' + binaryVersion + '',
|
||||
'}}"',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
return [
|
||||
'if [ -f dist/kompose ] || [ -f dist/kompose.exe ]; then',
|
||||
'echo "kompose binary exists";',
|
||||
'else',
|
||||
'build/download_kompose_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';',
|
||||
'fi',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function shell_download_kubectl_binary(p, a) {
|
||||
var binaryVersion = '<%= binaries.kubectlVersion %>';
|
||||
|
||||
if (p === 'linux' || p === 'darwin') {
|
||||
return ['if [ -f dist/kubectl ]; then', 'echo "kubectl binary exists";', 'else', 'build/download_kubectl_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' ');
|
||||
} else {
|
||||
return [
|
||||
'powershell -Command "& {if (Test-Path -Path "dist/kubectl.exe") {',
|
||||
'Write-Host "Skipping download, Kubectl binary exists"',
|
||||
'return',
|
||||
'} else {',
|
||||
'& ".\\build\\download_kubectl_binary.ps1" -kubectl_version ' + binaryVersion + '',
|
||||
'}}"',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
return [
|
||||
'if [ -f dist/kubectl ] || [ -f dist/kubectl.exe ]; then',
|
||||
'echo "kubectl binary exists";',
|
||||
'else',
|
||||
'build/download_kubectl_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';',
|
||||
'fi',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
@@ -62,7 +62,7 @@
|
||||
"angular-messages": "1.8.0",
|
||||
"angular-mocks": "1.8.0",
|
||||
"angular-moment-picker": "^0.10.2",
|
||||
"angular-multiselect": "github:portainer/angular-multi-select#semver:~v4.0.1",
|
||||
"angular-multiselect": "github:portainer/angular-multi-select#semver:v4.0.x",
|
||||
"angular-resource": "1.8.0",
|
||||
"angular-sanitize": "1.8.0",
|
||||
"angular-ui-bootstrap": "~2.5.0",
|
||||
@@ -77,10 +77,8 @@
|
||||
"chart.js": "~2.6.0",
|
||||
"codemirror": "~5.30.0",
|
||||
"fast-json-patch": "^3.0.0-1",
|
||||
"file-system": "^2.2.2",
|
||||
"filesize": "~3.3.0",
|
||||
"filesize-parser": "^1.5.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"jquery": "^3.5.1",
|
||||
"js-base64": "^3.6.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
@@ -107,7 +105,7 @@
|
||||
"clean-terminal-webpack-plugin": "^1.1.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"css-loader": "^1.0.0",
|
||||
"cssnano": "^3.10.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"cypress": "^5.2.0",
|
||||
"cypress-wait-until": "^1.7.1",
|
||||
"eslint": "5.16.0",
|
||||
|
||||
Reference in New Issue
Block a user