diff --git a/api/bolt/migrator/migrate_dbversion14.go b/api/bolt/migrator/migrate_dbversion14.go new file mode 100644 index 000000000..f74e4ae05 --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion14.go @@ -0,0 +1,11 @@ +package migrator + +func (m *Migrator) updateSettingsToDBVersion15() error { + legacySettings, err := m.settingsService.Settings() + if err != nil { + return err + } + + legacySettings.EnableHostManagementFeatures = false + return m.settingsService.UpdateSettings(legacySettings) +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 4d05820aa..1fc16c930 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -186,5 +186,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 1.20-dev + if m.currentDBVersion < 15 { + err := m.updateSettingsToDBVersion15() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 5cf99b3d7..7e5b4f581 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -256,6 +256,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL }, AllowBindMountsForRegularUsers: true, AllowPrivilegedModeForRegularUsers: true, + EnableHostManagementFeatures: false, SnapshotInterval: *flags.SnapshotInterval, } diff --git a/api/errors.go b/api/errors.go index da6c4edfe..6aeec542e 100644 --- a/api/errors.go +++ b/api/errors.go @@ -93,6 +93,11 @@ const ( ErrUnableToPingEndpoint = Error("Unable to communicate with the endpoint") ) +// Schedule errors. +const ( + ErrHostManagementFeaturesDisabled = Error("Host management features are disabled") +) + // Error represents an application error. type Error string diff --git a/api/http/handler/schedules/handler.go b/api/http/handler/schedules/handler.go index 4c5b64bbd..c2c091209 100644 --- a/api/http/handler/schedules/handler.go +++ b/api/http/handler/schedules/handler.go @@ -14,6 +14,7 @@ type Handler struct { *mux.Router ScheduleService portainer.ScheduleService EndpointService portainer.EndpointService + SettingsService portainer.SettingsService FileService portainer.FileService JobService portainer.JobService JobScheduler portainer.JobScheduler diff --git a/api/http/handler/schedules/schedule_create.go b/api/http/handler/schedules/schedule_create.go index 4dca4c3f3..893ec49f3 100644 --- a/api/http/handler/schedules/schedule_create.go +++ b/api/http/handler/schedules/schedule_create.go @@ -113,6 +113,14 @@ func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) e // POST /api/schedules?method=file/string func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} + } + if !settings.EnableHostManagementFeatures { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled} + } + method, err := request.RetrieveQueryParameter(r, "method", false) if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method. Valid values are: file or string", err} diff --git a/api/http/handler/schedules/schedule_delete.go b/api/http/handler/schedules/schedule_delete.go index 7597fb6c0..67a02010a 100644 --- a/api/http/handler/schedules/schedule_delete.go +++ b/api/http/handler/schedules/schedule_delete.go @@ -12,6 +12,14 @@ import ( ) func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} + } + if !settings.EnableHostManagementFeatures { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled} + } + scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} diff --git a/api/http/handler/schedules/schedule_file.go b/api/http/handler/schedules/schedule_file.go index 790f4d2e4..f24e10867 100644 --- a/api/http/handler/schedules/schedule_file.go +++ b/api/http/handler/schedules/schedule_file.go @@ -16,6 +16,14 @@ type scheduleFileResponse struct { // GET request on /api/schedules/:id/file func (handler *Handler) scheduleFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} + } + if !settings.EnableHostManagementFeatures { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled} + } + scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} diff --git a/api/http/handler/schedules/schedule_inspect.go b/api/http/handler/schedules/schedule_inspect.go index 9b721801e..bcd74b4b9 100644 --- a/api/http/handler/schedules/schedule_inspect.go +++ b/api/http/handler/schedules/schedule_inspect.go @@ -11,6 +11,14 @@ import ( ) func (handler *Handler) scheduleInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} + } + if !settings.EnableHostManagementFeatures { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled} + } + scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} diff --git a/api/http/handler/schedules/schedule_list.go b/api/http/handler/schedules/schedule_list.go index 4bc658b91..f67eee452 100644 --- a/api/http/handler/schedules/schedule_list.go +++ b/api/http/handler/schedules/schedule_list.go @@ -5,10 +5,19 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" + "github.com/portainer/portainer" ) // GET request on /api/schedules func (handler *Handler) scheduleList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} + } + if !settings.EnableHostManagementFeatures { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled} + } + schedules, err := handler.ScheduleService.Schedules() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve schedules from the database", err} diff --git a/api/http/handler/schedules/schedule_tasks.go b/api/http/handler/schedules/schedule_tasks.go index da88fdac7..0cbf24372 100644 --- a/api/http/handler/schedules/schedule_tasks.go +++ b/api/http/handler/schedules/schedule_tasks.go @@ -22,6 +22,14 @@ type taskContainer struct { // GET request on /api/schedules/:id/tasks func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} + } + if !settings.EnableHostManagementFeatures { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled} + } + scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} diff --git a/api/http/handler/schedules/schedule_update.go b/api/http/handler/schedules/schedule_update.go index ed680dfa1..7e741631d 100644 --- a/api/http/handler/schedules/schedule_update.go +++ b/api/http/handler/schedules/schedule_update.go @@ -31,6 +31,14 @@ func (payload *scheduleUpdatePayload) Validate(r *http.Request) error { } func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} + } + if !settings.EnableHostManagementFeatures { + return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled} + } + scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 549cf999e..9744b319a 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -13,6 +13,7 @@ type publicSettingsResponse struct { AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` ExternalTemplates bool `json:"ExternalTemplates"` } @@ -28,6 +29,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AuthenticationMethod: settings.AuthenticationMethod, AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + EnableHostManagementFeatures: settings.EnableHostManagementFeatures, ExternalTemplates: false, } diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index bef9cee9e..5b4d33ae3 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -18,6 +18,7 @@ type settingsUpdatePayload struct { LDAPSettings *portainer.LDAPSettings AllowBindMountsForRegularUsers *bool AllowPrivilegedModeForRegularUsers *bool + EnableHostManagementFeatures *bool SnapshotInterval *string TemplatesURL *string } @@ -76,6 +77,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers } + if payload.EnableHostManagementFeatures != nil { + settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures + } + if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { diff --git a/api/http/server.go b/api/http/server.go index 8bcfb6921..461a1f4ee 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -140,6 +140,7 @@ func (server *Server) Start() error { schedulesHandler.FileService = server.FileService schedulesHandler.JobService = server.JobService schedulesHandler.JobScheduler = server.JobScheduler + schedulesHandler.SettingsService = server.SettingsService var settingsHandler = settings.NewHandler(requestBouncer) settingsHandler.SettingsService = server.SettingsService diff --git a/api/portainer.go b/api/portainer.go index 634d8635c..1c6d103c6 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -89,6 +89,7 @@ type ( AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` SnapshotInterval string `json:"SnapshotInterval"` TemplatesURL string `json:"TemplatesURL"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` // Deprecated fields DisplayDonationHeader bool @@ -712,7 +713,7 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "1.20-dev" // DBVersion is the version number of the Portainer database - DBVersion = 14 + DBVersion = 15 // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved MessageOfTheDayURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/motd.html" // PortainerAgentHeader represents the name of the header available in any agent response diff --git a/app/docker/components/host-overview/host-overview.html b/app/docker/components/host-overview/host-overview.html index c54e0b052..968c09708 100644 --- a/app/docker/components/host-overview/host-overview.html +++ b/app/docker/components/host-overview/host-overview.html @@ -12,23 +12,23 @@ - - + + diff --git a/app/docker/components/host-overview/host-overview.js b/app/docker/components/host-overview/host-overview.js index b3e7bdf7c..4eae6a787 100644 --- a/app/docker/components/host-overview/host-overview.js +++ b/app/docker/components/host-overview/host-overview.js @@ -12,6 +12,7 @@ angular.module('portainer.docker').component('hostOverview', { browseUrl: '@', jobUrl: '@', isJobEnabled: '<', + hostFeaturesEnabled: '<', jobs: '<' }, transclude: true diff --git a/app/docker/views/host/host-view-controller.js b/app/docker/views/host/host-view-controller.js index 0875140d2..c68dfed3d 100644 --- a/app/docker/views/host/host-view-controller.js +++ b/app/docker/views/host/host-view-controller.js @@ -22,6 +22,7 @@ angular.module('portainer.docker').controller('HostViewController', [ ctrl.state.isAdmin = Authentication.getUserDetails().role === 1; var agentApiVersion = applicationState.endpoint.agentApiVersion; ctrl.state.agentApiVersion = agentApiVersion; + ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures; $q.all({ version: SystemService.version(), diff --git a/app/docker/views/host/host-view.html b/app/docker/views/host/host-view.html index 61597ecca..8a62e64bc 100644 --- a/app/docker/views/host/host-view.html +++ b/app/docker/views/host/host-view.html @@ -9,6 +9,7 @@ browse-url="docker.host.browser" offline-mode="$ctrl.state.offlineMode" is-job-enabled="$ctrl.state.isAdmin && !$ctrl.state.offlineMode" + host-features-enabled="$ctrl.state.enableHostManagementFeatures" job-url="docker.host.job" jobs="$ctrl.jobs" > diff --git a/app/docker/views/nodes/node-details/node-details-view-controller.js b/app/docker/views/nodes/node-details/node-details-view-controller.js index 25abc6285..7267da7f5 100644 --- a/app/docker/views/nodes/node-details/node-details-view-controller.js +++ b/app/docker/views/nodes/node-details/node-details-view-controller.js @@ -14,6 +14,7 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ var applicationState = StateManager.getState(); ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy; ctrl.state.isAdmin = Authentication.getUserDetails().role === 1; + ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures; var fetchJobs = ctrl.state.isAdmin && ctrl.state.isAgent; diff --git a/app/docker/views/nodes/node-details/node-details-view.html b/app/docker/views/nodes/node-details/node-details-view.html index 6544e1aac..c2dd7c7e2 100644 --- a/app/docker/views/nodes/node-details/node-details-view.html +++ b/app/docker/views/nodes/node-details/node-details-view.html @@ -8,6 +8,7 @@ refresh-url="docker.nodes.node" browse-url="docker.nodes.node.browse" is-job-enabled="$ctrl.state.isAdmin && $ctrl.state.isAgent" + host-features-enabled="$ctrl.state.enableHostManagementFeatures" job-url="docker.nodes.node.job" jobs="$ctrl.jobs" > diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 03af7a686..a4f2e8bf6 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -8,6 +8,7 @@ function SettingsViewModel(data) { this.SnapshotInterval = data.SnapshotInterval; this.TemplatesURL = data.TemplatesURL; this.ExternalTemplates = data.ExternalTemplates; + this.EnableHostManagementFeatures = data.EnableHostManagementFeatures; } function LDAPSettingsViewModel(data) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 987022476..5a229c339 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -43,6 +43,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin LocalStorage.storeApplicationState(state.application); }; + manager.updateEnableHostManagementFeatures = function(enableHostManagementFeatures) { + state.application.enableHostManagementFeatures = enableHostManagementFeatures; + LocalStorage.storeApplicationState(state.application); + }; + function assignStateFromStatusAndSettings(status, settings) { state.application.authentication = status.Authentication; state.application.analytics = status.Analytics; @@ -51,6 +56,7 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin state.application.version = status.Version; state.application.logo = settings.LogoURL; state.application.snapshotInterval = settings.SnapshotInterval; + state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures; state.application.validity = moment().unix(); } diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index e1939ee9e..cedb0aeb1 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -101,6 +101,17 @@ +
+
+ + +
+
diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index e1b7156e2..81ed2588e 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -12,7 +12,8 @@ function ($scope, $state, Notifications, SettingsService, StateManager) { restrictBindMounts: false, restrictPrivilegedMode: false, labelName: '', - labelValue: '' + labelValue: '', + enableHostManagementFeatures: false }; $scope.removeFilteredContainerLabel = function(index) { @@ -46,6 +47,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager) { settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts; settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode; + settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures; $scope.state.actionInProgress = true; updateSettings(settings); @@ -57,6 +59,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager) { Notifications.success('Settings updated'); StateManager.updateLogo(settings.LogoURL); StateManager.updateSnapshotInterval(settings.SnapshotInterval); + StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures); $state.reload(); }) .catch(function error(err) { @@ -80,6 +83,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager) { } $scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers; $scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers; + $scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings'); diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 031cfe91e..a78c4f986 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -36,10 +36,10 @@ Profiles
- -