From 67de71a18fa8f42cf58128a3da13e96db0795d0e Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 11 Jun 2019 10:34:54 +1200 Subject: [PATCH 01/37] docs(api): update swagger documentation --- api/swagger.yaml | 352 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) diff --git a/api/swagger.yaml b/api/swagger.yaml index 45df32348..4cb697566 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -69,10 +69,14 @@ tags: description: "Manage Docker environments" - name: "endpoint_groups" description: "Manage endpoint groups" +- name: "extensions" + description: "Manage extensions" - name: "registries" description: "Manage Docker registries" - name: "resource_controls" description: "Manage access control on Docker resources" +- name: "roles" + description: "Manage roles" - name: "settings" description: "Manage Portainer settings" - name: "status" @@ -741,6 +745,196 @@ paths: examples: application/json: err: "EndpointGroup management is disabled" + /extensions: + get: + tags: + - "extensions" + summary: "List extensions" + description: | + List all extensions registered inside Portainer. If the store parameter is set to true, + will retrieve extensions details from the online repository. + **Access policy**: administrator + operationId: "ExtensionList" + produces: + - "application/json" + security: + - jwt: [] + parameters: + - name: "store" + in: "query" + description: "Retrieve online information about extensions. Possible values: true or false." + required: false + type: "boolean" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/ExtensionListResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + post: + tags: + - "extensions" + summary: "Enable an extension" + description: | + Enable an extension. + **Access policy**: administrator + operationId: "ExtensionCreate" + consumes: + - "application/json" + produces: + - "application/json" + security: + - jwt: [] + parameters: + - in: "body" + name: "body" + description: "Extension details" + required: true + schema: + $ref: "#/definitions/ExtensionCreateRequest" + responses: + 204: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /extensions/{id}: + get: + tags: + - "extensions" + summary: "Inspect an extension" + description: | + Retrieve details abount an extension. + **Access policy**: administrator + operationId: "ExtensionInspect" + produces: + - "application/json" + security: + - jwt: [] + parameters: + - name: "id" + in: "path" + description: "extension identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/Extension" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "Extension not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Extension not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + put: + tags: + - "extensions" + summary: "Update an extension" + description: | + Update an extension to a specific version of the extension. + **Access policy**: administrator + operationId: "ExtensionUpdate" + consumes: + - "application/json" + produces: + - "application/json" + security: + - jwt: [] + parameters: + - name: "id" + in: "path" + description: "Extension identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "Extension details" + required: true + schema: + $ref: "#/definitions/ExtensionUpdateRequest" + responses: + 204: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 404: + description: "Extension not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Extension not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + delete: + tags: + - "extensions" + summary: "Disable an extension" + description: | + Disable an extension. + **Access policy**: administrator + operationId: "ExtensionDelete" + security: + - jwt: [] + parameters: + - name: "id" + in: "path" + description: "Extension identifier" + required: true + type: "integer" + responses: + 204: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "Extension not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Extension not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" /registries: get: tags: @@ -1098,6 +1292,29 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" + /roles: + get: + tags: + - "roles" + summary: "List roles" + description: | + List all roles available for use with the RBAC extension. + **Access policy**: administrator + operationId: "RoleList" + produces: + - "application/json" + security: + - jwt: [] + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/RoleListResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" /settings: get: tags: @@ -4335,3 +4552,138 @@ definitions: type: "string" example: "version: 3\n services:\n web:\n image:nginx" description: "Content of the Stack file." + LicenseInformation: + type: "object" + properties: + LicenseKey: + type: "string" + description: "License key" + example: "1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ" + Company: + type: "string" + description: "Company associated to the license" + example: "Portainer.io" + Expiration: + type: "string" + description: "License expiry date" + example: "2077-07-07" + Valid: + type: "boolean" + description: "Is the license valid" + example: "true" + Extension: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Extension identifier" + Name: + type: "string" + example: "Registry Manager" + description: "Extension name" + Enabled: + type: "boolean" + example: "true" + description: "Is the extension enabled" + ShortDescription: + type: "string" + description: "Short description about the extension" + example: "Enable in-app registry management" + DescriptionURL: + type: "string" + description: "URL to the file containing the extension description" + example: https://portainer-io-assets.sfo2.digitaloceanspaces.com/description_registry_manager.html" + Available: + type: "boolean" + description: "Is the extension available for download and activation" + example: "true" + Images: + type: "array" + description: "List of screenshot URLs" + items: + type: "string" + example: "https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/rm01.png" + description: "Screenshot URL" + Logo: + type: "string" + description: "Icon associated to the extension" + example: "fa-database" + Price: + type: "string" + description: "Extension price" + example: "US$9.95" + PriceDescription: + type: "string" + description: "Details about extension pricing" + example: "Price per instance per year" + ShopURL: + type: "string" + description: "URL used to buy the extension" + example: "https://portainer.io/checkout/?add-to-cart=1164" + UpdateAvailable: + type: "boolean" + description: "Is an update available for this extension" + example: "true" + Version: + type: "string" + description: "Extension version" + example: "1.0.0" + License: + $ref: "#/definitions/LicenseInformation" + ExtensionListResponse: + type: "array" + items: + $ref: "#/definitions/Extension" + ExtensionCreateRequest: + type: "object" + required: + - "License" + properties: + License: + type: "string" + example: "1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ" + description: "License key" + ExtensionUpdateRequest: + type: "object" + required: + - "Version" + properties: + Version: + type: "string" + example: "1.1.0" + description: "New version of the extension" + RoleListResponse: + type: "array" + items: + $ref: "#/definitions/Role" + Role: + type: "object" + properties: + Id: + type: "integer" + description: "Role identifier" + example: 2 + Name: + type: "string" + description: "Role name" + example: "HelpDesk" + Description: + type: "string" + description: "Role description" + example: "Read-only access of all resources in an endpoint" + Authorizations: + $ref: "#/definitions/Authorizations" + Authorizations: + type: "object" + description: "Authorizations associated to a role" + additionalProperties: + type: "object" + properties: + authorization: + type: "string" + value: + type: "boolean" + example: + "DockerContainerList": true + "DockerVolumeList": true From 144e0ae07efab7ac33e64aefaf335e3f0e48ed53 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Tue, 11 Jun 2019 23:13:18 +0200 Subject: [PATCH 02/37] refactor(app): move storidge to new 'integrations' module (#2905) * refactor(app): move storidge to new 'integrations' module * style(storidge): revert TODO note removal --- app/__module.js | 3 +-- app/integrations/_module.js | 3 +++ .../storidge/__module.js => integrations/storidge/_module.js} | 3 ++- .../storidgeClusterEventsDatatable.html | 0 .../cluster-events-datatable/storidgeClusterEventsDatatable.js | 2 +- .../components/drives-datatable/storidgeDrivesDatatable.html | 0 .../components/drives-datatable/storidgeDrivesDatatable.js | 2 +- .../drives-datatable/storidgeDrivesDatatableController.js | 0 .../components/nodes-datatable/storidgeNodesDatatable.html | 0 .../components/nodes-datatable/storidgeNodesDatatable.js | 2 +- .../nodes-datatable/storidgeNodesDatatableController.js | 2 +- .../components/profileSelector/storidgeProfileSelector.html | 0 .../components/profileSelector/storidgeProfileSelector.js | 2 +- .../profileSelector/storidgeProfileSelectorController.js | 2 +- .../profiles-datatable/storidgeProfilesDatatable.html | 0 .../components/profiles-datatable/storidgeProfilesDatatable.js | 2 +- .../components/snapshot-creation/storidgeSnapshotCreation.html | 0 .../components/snapshot-creation/storidgeSnapshotCreation.js | 0 .../snapshot-creation/storidgeSnapshotCreationController.js | 0 .../snapshots-datatable/storidgeSnapshotsDatatable.html | 0 .../snapshots-datatable/storidgeSnapshotsDatatable.js | 2 +- .../storidgeSnapshotsDatatableController.js | 0 .../components/volume-storidge-info/volumeStoridgeInfo.html | 0 .../components/volume-storidge-info/volumeStoridgeInfo.js | 0 .../volume-storidge-info/volumeStoridgeInfoController.js | 0 app/{extensions => integrations}/storidge/filters/filters.js | 2 +- app/{extensions => integrations}/storidge/models/drive.js | 0 app/{extensions => integrations}/storidge/models/events.js | 0 app/{extensions => integrations}/storidge/models/info.js | 0 app/{extensions => integrations}/storidge/models/node.js | 0 app/{extensions => integrations}/storidge/models/profile.js | 0 app/{extensions => integrations}/storidge/models/snapshot.js | 0 app/{extensions => integrations}/storidge/models/volume.js | 0 app/{extensions => integrations}/storidge/rest/storidge.js | 2 +- .../storidge/services/chartService.js | 2 +- .../storidge/services/clusterService.js | 2 +- .../storidge/services/driveService.js | 2 +- .../storidge/services/nodeService.js | 2 +- .../storidge/services/profileService.js | 2 +- .../storidge/services/snapshotService.js | 2 +- .../storidge/services/volumeService.js | 2 +- .../storidge/views/cluster/cluster.html | 0 .../storidge/views/cluster/clusterController.js | 2 +- .../storidge/views/drives/drives.html | 0 .../storidge/views/drives/drivesController.js | 2 +- .../storidge/views/drives/inspect/drive.html | 0 .../storidge/views/drives/inspect/driveController.js | 2 +- .../storidge/views/monitor/monitor.html | 0 .../storidge/views/monitor/monitorController.js | 2 +- .../storidge/views/nodes/inspect/node.html | 0 .../storidge/views/nodes/inspect/nodeController.js | 2 +- .../storidge/views/profiles/create/createProfileController.js | 2 +- .../storidge/views/profiles/create/createprofile.html | 0 .../storidge/views/profiles/edit/profile.html | 0 .../storidge/views/profiles/edit/profileController.js | 2 +- .../storidge/views/profiles/profiles.html | 0 .../storidge/views/profiles/profilesController.js | 2 +- .../storidge/views/snapshots/inspect/snapshot.html | 0 .../storidge/views/snapshots/inspect/snapshotController.js | 2 +- 59 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 app/integrations/_module.js rename app/{extensions/storidge/__module.js => integrations/storidge/_module.js} (98%) rename app/{extensions => integrations}/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html (100%) rename app/{extensions => integrations}/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js (71%) rename app/{extensions => integrations}/storidge/components/drives-datatable/storidgeDrivesDatatable.html (100%) rename app/{extensions => integrations}/storidge/components/drives-datatable/storidgeDrivesDatatable.js (80%) rename app/{extensions => integrations}/storidge/components/drives-datatable/storidgeDrivesDatatableController.js (100%) rename app/{extensions => integrations}/storidge/components/nodes-datatable/storidgeNodesDatatable.html (100%) rename app/{extensions => integrations}/storidge/components/nodes-datatable/storidgeNodesDatatable.js (72%) rename app/{extensions => integrations}/storidge/components/nodes-datatable/storidgeNodesDatatableController.js (93%) rename app/{extensions => integrations}/storidge/components/profileSelector/storidgeProfileSelector.html (100%) rename app/{extensions => integrations}/storidge/components/profileSelector/storidgeProfileSelector.js (62%) rename app/{extensions => integrations}/storidge/components/profileSelector/storidgeProfileSelectorController.js (90%) rename app/{extensions => integrations}/storidge/components/profiles-datatable/storidgeProfilesDatatable.html (100%) rename app/{extensions => integrations}/storidge/components/profiles-datatable/storidgeProfilesDatatable.js (73%) rename app/{extensions => integrations}/storidge/components/snapshot-creation/storidgeSnapshotCreation.html (100%) rename app/{extensions => integrations}/storidge/components/snapshot-creation/storidgeSnapshotCreation.js (100%) rename app/{extensions => integrations}/storidge/components/snapshot-creation/storidgeSnapshotCreationController.js (100%) rename app/{extensions => integrations}/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html (100%) rename app/{extensions => integrations}/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js (74%) rename app/{extensions => integrations}/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js (100%) rename app/{extensions => integrations}/storidge/components/volume-storidge-info/volumeStoridgeInfo.html (100%) rename app/{extensions => integrations}/storidge/components/volume-storidge-info/volumeStoridgeInfo.js (100%) rename app/{extensions => integrations}/storidge/components/volume-storidge-info/volumeStoridgeInfoController.js (100%) rename app/{extensions => integrations}/storidge/filters/filters.js (96%) rename app/{extensions => integrations}/storidge/models/drive.js (100%) rename app/{extensions => integrations}/storidge/models/events.js (100%) rename app/{extensions => integrations}/storidge/models/info.js (100%) rename app/{extensions => integrations}/storidge/models/node.js (100%) rename app/{extensions => integrations}/storidge/models/profile.js (100%) rename app/{extensions => integrations}/storidge/models/snapshot.js (100%) rename app/{extensions => integrations}/storidge/models/volume.js (100%) rename app/{extensions => integrations}/storidge/rest/storidge.js (98%) rename app/{extensions => integrations}/storidge/services/chartService.js (98%) rename app/{extensions => integrations}/storidge/services/clusterService.js (97%) rename app/{extensions => integrations}/storidge/services/driveService.js (97%) rename app/{extensions => integrations}/storidge/services/nodeService.js (96%) rename app/{extensions => integrations}/storidge/services/profileService.js (96%) rename app/{extensions => integrations}/storidge/services/snapshotService.js (97%) rename app/{extensions => integrations}/storidge/services/volumeService.js (95%) rename app/{extensions => integrations}/storidge/views/cluster/cluster.html (100%) rename app/{extensions => integrations}/storidge/views/cluster/clusterController.js (97%) rename app/{extensions => integrations}/storidge/views/drives/drives.html (100%) rename app/{extensions => integrations}/storidge/views/drives/drivesController.js (96%) rename app/{extensions => integrations}/storidge/views/drives/inspect/drive.html (100%) rename app/{extensions => integrations}/storidge/views/drives/inspect/driveController.js (96%) rename app/{extensions => integrations}/storidge/views/monitor/monitor.html (100%) rename app/{extensions => integrations}/storidge/views/monitor/monitorController.js (98%) rename app/{extensions => integrations}/storidge/views/nodes/inspect/node.html (100%) rename app/{extensions => integrations}/storidge/views/nodes/inspect/nodeController.js (98%) rename app/{extensions => integrations}/storidge/views/profiles/create/createProfileController.js (98%) rename app/{extensions => integrations}/storidge/views/profiles/create/createprofile.html (100%) rename app/{extensions => integrations}/storidge/views/profiles/edit/profile.html (100%) rename app/{extensions => integrations}/storidge/views/profiles/edit/profileController.js (98%) rename app/{extensions => integrations}/storidge/views/profiles/profiles.html (100%) rename app/{extensions => integrations}/storidge/views/profiles/profilesController.js (97%) rename app/{extensions => integrations}/storidge/views/snapshots/inspect/snapshot.html (100%) rename app/{extensions => integrations}/storidge/views/snapshots/inspect/snapshotController.js (96%) diff --git a/app/__module.js b/app/__module.js index 64252819d..881453c9a 100644 --- a/app/__module.js +++ b/app/__module.js @@ -4,7 +4,6 @@ import angular from 'angular'; import './agent/_module'; import './azure/_module'; import './docker/__module'; -import './extensions/storidge/__module'; import './portainer/__module'; angular.module('portainer', [ @@ -31,7 +30,7 @@ angular.module('portainer', [ 'portainer.azure', 'portainer.docker', 'portainer.extensions', - 'extension.storidge', + 'portainer.integrations', 'rzModule', 'moment-picker' ]); diff --git a/app/integrations/_module.js b/app/integrations/_module.js new file mode 100644 index 000000000..1a3b470c6 --- /dev/null +++ b/app/integrations/_module.js @@ -0,0 +1,3 @@ +angular.module('portainer.integrations', [ + 'portainer.integrations.storidge' +]); diff --git a/app/extensions/storidge/__module.js b/app/integrations/storidge/_module.js similarity index 98% rename from app/extensions/storidge/__module.js rename to app/integrations/storidge/_module.js index f22b28f94..9f618e230 100644 --- a/app/extensions/storidge/__module.js +++ b/app/integrations/storidge/_module.js @@ -1,5 +1,6 @@ // TODO: legacy extension management -angular.module('extension.storidge', []) + +angular.module('portainer.integrations.storidge', []) .config(['$stateRegistryProvider', function ($stateRegistryProvider) { 'use strict'; diff --git a/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html b/app/integrations/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html similarity index 100% rename from app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html rename to app/integrations/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html diff --git a/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js b/app/integrations/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js similarity index 71% rename from app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js rename to app/integrations/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js index 34d897b48..72f4b58e5 100644 --- a/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js +++ b/app/integrations/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge').component('storidgeClusterEventsDatatable', { +angular.module('portainer.integrations.storidge').component('storidgeClusterEventsDatatable', { templateUrl: './storidgeClusterEventsDatatable.html', controller: 'GenericDatatableController', bindings: { diff --git a/app/extensions/storidge/components/drives-datatable/storidgeDrivesDatatable.html b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatable.html similarity index 100% rename from app/extensions/storidge/components/drives-datatable/storidgeDrivesDatatable.html rename to app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatable.html diff --git a/app/extensions/storidge/components/drives-datatable/storidgeDrivesDatatable.js b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatable.js similarity index 80% rename from app/extensions/storidge/components/drives-datatable/storidgeDrivesDatatable.js rename to app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatable.js index 9e236ce7c..cf6e06f9d 100644 --- a/app/extensions/storidge/components/drives-datatable/storidgeDrivesDatatable.js +++ b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatable.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge').component('storidgeDrivesDatatable', { +angular.module('portainer.integrations.storidge').component('storidgeDrivesDatatable', { templateUrl: './storidgeDrivesDatatable.html', controller: 'StoridgeDrivesDatatableController', bindings: { diff --git a/app/extensions/storidge/components/drives-datatable/storidgeDrivesDatatableController.js b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js similarity index 100% rename from app/extensions/storidge/components/drives-datatable/storidgeDrivesDatatableController.js rename to app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js diff --git a/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.html b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatable.html similarity index 100% rename from app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.html rename to app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatable.html diff --git a/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.js b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatable.js similarity index 72% rename from app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.js rename to app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatable.js index 4f22e67e7..7b92faf41 100644 --- a/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.js +++ b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatable.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge').component('storidgeNodesDatatable', { +angular.module('portainer.integrations.storidge').component('storidgeNodesDatatable', { templateUrl: './storidgeNodesDatatable.html', controller: 'StoridgeNodesDatatableController', bindings: { diff --git a/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatableController.js b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js similarity index 93% rename from app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatableController.js rename to app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js index 6de915392..517b6566c 100644 --- a/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatableController.js +++ b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeNodesDatatableController', ['$scope', '$controller', 'clipboard', 'Notifications', 'StoridgeNodeService', function($scope, $controller, clipboard, Notifications, StoridgeNodeService) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); diff --git a/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html b/app/integrations/storidge/components/profileSelector/storidgeProfileSelector.html similarity index 100% rename from app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html rename to app/integrations/storidge/components/profileSelector/storidgeProfileSelector.html diff --git a/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.js b/app/integrations/storidge/components/profileSelector/storidgeProfileSelector.js similarity index 62% rename from app/extensions/storidge/components/profileSelector/storidgeProfileSelector.js rename to app/integrations/storidge/components/profileSelector/storidgeProfileSelector.js index ea5c93dee..5a6924dca 100644 --- a/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.js +++ b/app/integrations/storidge/components/profileSelector/storidgeProfileSelector.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge').component('storidgeProfileSelector', { +angular.module('portainer.integrations.storidge').component('storidgeProfileSelector', { templateUrl: './storidgeProfileSelector.html', controller: 'StoridgeProfileSelectorController', bindings: { diff --git a/app/extensions/storidge/components/profileSelector/storidgeProfileSelectorController.js b/app/integrations/storidge/components/profileSelector/storidgeProfileSelectorController.js similarity index 90% rename from app/extensions/storidge/components/profileSelector/storidgeProfileSelectorController.js rename to app/integrations/storidge/components/profileSelector/storidgeProfileSelectorController.js index b75b7824a..30605def3 100644 --- a/app/extensions/storidge/components/profileSelector/storidgeProfileSelectorController.js +++ b/app/integrations/storidge/components/profileSelector/storidgeProfileSelectorController.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeProfileSelectorController', ['StoridgeProfileService', 'Notifications', function (StoridgeProfileService, Notifications) { var ctrl = this; diff --git a/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.html b/app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.html similarity index 100% rename from app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.html rename to app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.html diff --git a/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.js b/app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.js similarity index 73% rename from app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.js rename to app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.js index 1fb5f51d5..2a6fc545c 100644 --- a/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.js +++ b/app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge').component('storidgeProfilesDatatable', { +angular.module('portainer.integrations.storidge').component('storidgeProfilesDatatable', { templateUrl: './storidgeProfilesDatatable.html', controller: 'GenericDatatableController', bindings: { diff --git a/app/extensions/storidge/components/snapshot-creation/storidgeSnapshotCreation.html b/app/integrations/storidge/components/snapshot-creation/storidgeSnapshotCreation.html similarity index 100% rename from app/extensions/storidge/components/snapshot-creation/storidgeSnapshotCreation.html rename to app/integrations/storidge/components/snapshot-creation/storidgeSnapshotCreation.html diff --git a/app/extensions/storidge/components/snapshot-creation/storidgeSnapshotCreation.js b/app/integrations/storidge/components/snapshot-creation/storidgeSnapshotCreation.js similarity index 100% rename from app/extensions/storidge/components/snapshot-creation/storidgeSnapshotCreation.js rename to app/integrations/storidge/components/snapshot-creation/storidgeSnapshotCreation.js diff --git a/app/extensions/storidge/components/snapshot-creation/storidgeSnapshotCreationController.js b/app/integrations/storidge/components/snapshot-creation/storidgeSnapshotCreationController.js similarity index 100% rename from app/extensions/storidge/components/snapshot-creation/storidgeSnapshotCreationController.js rename to app/integrations/storidge/components/snapshot-creation/storidgeSnapshotCreationController.js diff --git a/app/extensions/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html similarity index 100% rename from app/extensions/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html rename to app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html diff --git a/app/extensions/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js similarity index 74% rename from app/extensions/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js rename to app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js index 5f3c145e9..ee0ef830d 100644 --- a/app/extensions/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js +++ b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge').component('storidgeSnapshotsDatatable', { +angular.module('portainer.integrations.storidge').component('storidgeSnapshotsDatatable', { templateUrl: './storidgeSnapshotsDatatable.html', controller: 'StoridgeSnapshotsDatatableController', bindings: { diff --git a/app/extensions/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js similarity index 100% rename from app/extensions/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js rename to app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js diff --git a/app/extensions/storidge/components/volume-storidge-info/volumeStoridgeInfo.html b/app/integrations/storidge/components/volume-storidge-info/volumeStoridgeInfo.html similarity index 100% rename from app/extensions/storidge/components/volume-storidge-info/volumeStoridgeInfo.html rename to app/integrations/storidge/components/volume-storidge-info/volumeStoridgeInfo.html diff --git a/app/extensions/storidge/components/volume-storidge-info/volumeStoridgeInfo.js b/app/integrations/storidge/components/volume-storidge-info/volumeStoridgeInfo.js similarity index 100% rename from app/extensions/storidge/components/volume-storidge-info/volumeStoridgeInfo.js rename to app/integrations/storidge/components/volume-storidge-info/volumeStoridgeInfo.js diff --git a/app/extensions/storidge/components/volume-storidge-info/volumeStoridgeInfoController.js b/app/integrations/storidge/components/volume-storidge-info/volumeStoridgeInfoController.js similarity index 100% rename from app/extensions/storidge/components/volume-storidge-info/volumeStoridgeInfoController.js rename to app/integrations/storidge/components/volume-storidge-info/volumeStoridgeInfoController.js diff --git a/app/extensions/storidge/filters/filters.js b/app/integrations/storidge/filters/filters.js similarity index 96% rename from app/extensions/storidge/filters/filters.js rename to app/integrations/storidge/filters/filters.js index 494135785..3749134f4 100644 --- a/app/extensions/storidge/filters/filters.js +++ b/app/integrations/storidge/filters/filters.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .filter('drivestatusbadge', function () { 'use strict'; return function (text) { diff --git a/app/extensions/storidge/models/drive.js b/app/integrations/storidge/models/drive.js similarity index 100% rename from app/extensions/storidge/models/drive.js rename to app/integrations/storidge/models/drive.js diff --git a/app/extensions/storidge/models/events.js b/app/integrations/storidge/models/events.js similarity index 100% rename from app/extensions/storidge/models/events.js rename to app/integrations/storidge/models/events.js diff --git a/app/extensions/storidge/models/info.js b/app/integrations/storidge/models/info.js similarity index 100% rename from app/extensions/storidge/models/info.js rename to app/integrations/storidge/models/info.js diff --git a/app/extensions/storidge/models/node.js b/app/integrations/storidge/models/node.js similarity index 100% rename from app/extensions/storidge/models/node.js rename to app/integrations/storidge/models/node.js diff --git a/app/extensions/storidge/models/profile.js b/app/integrations/storidge/models/profile.js similarity index 100% rename from app/extensions/storidge/models/profile.js rename to app/integrations/storidge/models/profile.js diff --git a/app/extensions/storidge/models/snapshot.js b/app/integrations/storidge/models/snapshot.js similarity index 100% rename from app/extensions/storidge/models/snapshot.js rename to app/integrations/storidge/models/snapshot.js diff --git a/app/extensions/storidge/models/volume.js b/app/integrations/storidge/models/volume.js similarity index 100% rename from app/extensions/storidge/models/volume.js rename to app/integrations/storidge/models/volume.js diff --git a/app/extensions/storidge/rest/storidge.js b/app/integrations/storidge/rest/storidge.js similarity index 98% rename from app/extensions/storidge/rest/storidge.js rename to app/integrations/storidge/rest/storidge.js index 97655f875..7fd896c14 100644 --- a/app/extensions/storidge/rest/storidge.js +++ b/app/integrations/storidge/rest/storidge.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .factory('Storidge', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function StoridgeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/storidge/:resource/:id/:action', { diff --git a/app/extensions/storidge/services/chartService.js b/app/integrations/storidge/services/chartService.js similarity index 98% rename from app/extensions/storidge/services/chartService.js rename to app/integrations/storidge/services/chartService.js index e320ba8f0..55ec60f14 100644 --- a/app/extensions/storidge/services/chartService.js +++ b/app/integrations/storidge/services/chartService.js @@ -1,7 +1,7 @@ import Chart from 'chart.js'; import filesize from 'filesize'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .factory('StoridgeChartService', [function StoridgeChartService() { 'use strict'; diff --git a/app/extensions/storidge/services/clusterService.js b/app/integrations/storidge/services/clusterService.js similarity index 97% rename from app/extensions/storidge/services/clusterService.js rename to app/integrations/storidge/services/clusterService.js index 61f78a1ae..daf0d81cd 100644 --- a/app/extensions/storidge/services/clusterService.js +++ b/app/integrations/storidge/services/clusterService.js @@ -1,7 +1,7 @@ import { StoridgeInfoModel } from '../models/info'; import { StoridgeEventModel } from '../models/events'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .factory('StoridgeClusterService', ['$q', 'Storidge', function StoridgeClusterServiceFactory($q, Storidge) { 'use strict'; var service = {}; diff --git a/app/extensions/storidge/services/driveService.js b/app/integrations/storidge/services/driveService.js similarity index 97% rename from app/extensions/storidge/services/driveService.js rename to app/integrations/storidge/services/driveService.js index 8daa9d358..03cbda3e9 100644 --- a/app/extensions/storidge/services/driveService.js +++ b/app/integrations/storidge/services/driveService.js @@ -1,6 +1,6 @@ import { StoridgeDriveModel } from '../models/drive'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .factory('StoridgeDriveService', ['$q', 'Storidge', function StoridgeDriveServiceFactory($q, Storidge) { 'use strict'; var service = {}; diff --git a/app/extensions/storidge/services/nodeService.js b/app/integrations/storidge/services/nodeService.js similarity index 96% rename from app/extensions/storidge/services/nodeService.js rename to app/integrations/storidge/services/nodeService.js index de960224f..6f3fa72d1 100644 --- a/app/extensions/storidge/services/nodeService.js +++ b/app/integrations/storidge/services/nodeService.js @@ -1,6 +1,6 @@ import { StoridgeNodeModel, StoridgeNodeDetailedModel } from '../models/node'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .factory('StoridgeNodeService', ['$q', 'Storidge', function StoridgeNodeServiceFactory($q, Storidge) { 'use strict'; var service = {}; diff --git a/app/extensions/storidge/services/profileService.js b/app/integrations/storidge/services/profileService.js similarity index 96% rename from app/extensions/storidge/services/profileService.js rename to app/integrations/storidge/services/profileService.js index 0e4d683a2..506e4f2bc 100644 --- a/app/extensions/storidge/services/profileService.js +++ b/app/integrations/storidge/services/profileService.js @@ -4,7 +4,7 @@ import { StoridgeProfileModel } from '../models/profile'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .factory('StoridgeProfileService', ['$q', 'Storidge', function StoridgeProfileServiceFactory($q, Storidge) { 'use strict'; var service = {}; diff --git a/app/extensions/storidge/services/snapshotService.js b/app/integrations/storidge/services/snapshotService.js similarity index 97% rename from app/extensions/storidge/services/snapshotService.js rename to app/integrations/storidge/services/snapshotService.js index eac194073..551783a81 100644 --- a/app/extensions/storidge/services/snapshotService.js +++ b/app/integrations/storidge/services/snapshotService.js @@ -1,6 +1,6 @@ import { StoridgeSnapshotModel } from '../models/snapshot' -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .factory('StoridgeSnapshotService', ['$q', 'Storidge', function StoridgeSnapshotServiceFactory($q, Storidge) { 'use strict'; var service = {}; diff --git a/app/extensions/storidge/services/volumeService.js b/app/integrations/storidge/services/volumeService.js similarity index 95% rename from app/extensions/storidge/services/volumeService.js rename to app/integrations/storidge/services/volumeService.js index a0da586c3..7aaed2b28 100644 --- a/app/extensions/storidge/services/volumeService.js +++ b/app/integrations/storidge/services/volumeService.js @@ -1,6 +1,6 @@ import { StoridgeVolumeModel, StoridgeVolumeUpdateModel } from '../models/volume'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .factory('StoridgeVolumeService', ['$q', 'Storidge', function StoridgeVolumeServiceFactory($q, Storidge) { 'use strict'; var service = {}; diff --git a/app/extensions/storidge/views/cluster/cluster.html b/app/integrations/storidge/views/cluster/cluster.html similarity index 100% rename from app/extensions/storidge/views/cluster/cluster.html rename to app/integrations/storidge/views/cluster/cluster.html diff --git a/app/extensions/storidge/views/cluster/clusterController.js b/app/integrations/storidge/views/cluster/clusterController.js similarity index 97% rename from app/extensions/storidge/views/cluster/clusterController.js rename to app/integrations/storidge/views/cluster/clusterController.js index 0bf2a1568..2ee60ab30 100644 --- a/app/extensions/storidge/views/cluster/clusterController.js +++ b/app/integrations/storidge/views/cluster/clusterController.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeClusterController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeClusterService', 'StoridgeNodeService', 'ModalService', function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNodeService, ModalService) { diff --git a/app/extensions/storidge/views/drives/drives.html b/app/integrations/storidge/views/drives/drives.html similarity index 100% rename from app/extensions/storidge/views/drives/drives.html rename to app/integrations/storidge/views/drives/drives.html diff --git a/app/extensions/storidge/views/drives/drivesController.js b/app/integrations/storidge/views/drives/drivesController.js similarity index 96% rename from app/extensions/storidge/views/drives/drivesController.js rename to app/integrations/storidge/views/drives/drivesController.js index 837433240..b10630992 100644 --- a/app/extensions/storidge/views/drives/drivesController.js +++ b/app/integrations/storidge/views/drives/drivesController.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeDrivesController', ['$scope', '$state', 'Notifications', 'StoridgeDriveService', function ($scope, $state, Notifications, StoridgeDriveService) { diff --git a/app/extensions/storidge/views/drives/inspect/drive.html b/app/integrations/storidge/views/drives/inspect/drive.html similarity index 100% rename from app/extensions/storidge/views/drives/inspect/drive.html rename to app/integrations/storidge/views/drives/inspect/drive.html diff --git a/app/extensions/storidge/views/drives/inspect/driveController.js b/app/integrations/storidge/views/drives/inspect/driveController.js similarity index 96% rename from app/extensions/storidge/views/drives/inspect/driveController.js rename to app/integrations/storidge/views/drives/inspect/driveController.js index 7d4c0ccef..552716e5f 100644 --- a/app/extensions/storidge/views/drives/inspect/driveController.js +++ b/app/integrations/storidge/views/drives/inspect/driveController.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeDriveController', ['$scope', '$state', '$transition$', 'Notifications', 'ModalService', 'StoridgeDriveService', function ($scope, $state, $transition$, Notifications, ModalService, StoridgeDriveService) { diff --git a/app/extensions/storidge/views/monitor/monitor.html b/app/integrations/storidge/views/monitor/monitor.html similarity index 100% rename from app/extensions/storidge/views/monitor/monitor.html rename to app/integrations/storidge/views/monitor/monitor.html diff --git a/app/extensions/storidge/views/monitor/monitorController.js b/app/integrations/storidge/views/monitor/monitorController.js similarity index 98% rename from app/extensions/storidge/views/monitor/monitorController.js rename to app/integrations/storidge/views/monitor/monitorController.js index 4bf94a613..9cb23dbc9 100644 --- a/app/extensions/storidge/views/monitor/monitorController.js +++ b/app/integrations/storidge/views/monitor/monitorController.js @@ -1,6 +1,6 @@ import moment from 'moment'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeMonitorController', ['$q', '$scope', '$interval', '$document', 'Notifications', 'StoridgeClusterService', 'StoridgeChartService', function ($q, $scope, $interval, $document, Notifications, StoridgeClusterService, StoridgeChartService) { diff --git a/app/extensions/storidge/views/nodes/inspect/node.html b/app/integrations/storidge/views/nodes/inspect/node.html similarity index 100% rename from app/extensions/storidge/views/nodes/inspect/node.html rename to app/integrations/storidge/views/nodes/inspect/node.html diff --git a/app/extensions/storidge/views/nodes/inspect/nodeController.js b/app/integrations/storidge/views/nodes/inspect/nodeController.js similarity index 98% rename from app/extensions/storidge/views/nodes/inspect/nodeController.js rename to app/integrations/storidge/views/nodes/inspect/nodeController.js index 3d9993cc4..3af1432be 100644 --- a/app/extensions/storidge/views/nodes/inspect/nodeController.js +++ b/app/integrations/storidge/views/nodes/inspect/nodeController.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeNodeController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeNodeService', 'ModalService', function ($scope, $state, $transition$, Notifications, StoridgeNodeService, ModalService) { diff --git a/app/extensions/storidge/views/profiles/create/createProfileController.js b/app/integrations/storidge/views/profiles/create/createProfileController.js similarity index 98% rename from app/extensions/storidge/views/profiles/create/createProfileController.js rename to app/integrations/storidge/views/profiles/create/createProfileController.js index 89c0658af..55ecbb61f 100644 --- a/app/extensions/storidge/views/profiles/create/createProfileController.js +++ b/app/integrations/storidge/views/profiles/create/createProfileController.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; import { StoridgeProfileDefaultModel } from '../../../models/profile'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeCreateProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', function ($scope, $state, $transition$, Notifications, StoridgeProfileService) { diff --git a/app/extensions/storidge/views/profiles/create/createprofile.html b/app/integrations/storidge/views/profiles/create/createprofile.html similarity index 100% rename from app/extensions/storidge/views/profiles/create/createprofile.html rename to app/integrations/storidge/views/profiles/create/createprofile.html diff --git a/app/extensions/storidge/views/profiles/edit/profile.html b/app/integrations/storidge/views/profiles/edit/profile.html similarity index 100% rename from app/extensions/storidge/views/profiles/edit/profile.html rename to app/integrations/storidge/views/profiles/edit/profile.html diff --git a/app/extensions/storidge/views/profiles/edit/profileController.js b/app/integrations/storidge/views/profiles/edit/profileController.js similarity index 98% rename from app/extensions/storidge/views/profiles/edit/profileController.js rename to app/integrations/storidge/views/profiles/edit/profileController.js index ecafae46f..ab43ede87 100644 --- a/app/extensions/storidge/views/profiles/edit/profileController.js +++ b/app/integrations/storidge/views/profiles/edit/profileController.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'ModalService', function ($scope, $state, $transition$, Notifications, StoridgeProfileService, ModalService) { diff --git a/app/extensions/storidge/views/profiles/profiles.html b/app/integrations/storidge/views/profiles/profiles.html similarity index 100% rename from app/extensions/storidge/views/profiles/profiles.html rename to app/integrations/storidge/views/profiles/profiles.html diff --git a/app/extensions/storidge/views/profiles/profilesController.js b/app/integrations/storidge/views/profiles/profilesController.js similarity index 97% rename from app/extensions/storidge/views/profiles/profilesController.js rename to app/integrations/storidge/views/profiles/profilesController.js index f60d5c6d5..0340f0b30 100644 --- a/app/extensions/storidge/views/profiles/profilesController.js +++ b/app/integrations/storidge/views/profiles/profilesController.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; import { StoridgeProfileDefaultModel } from '../../models/profile'; -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeProfilesController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeProfileService', function ($q, $scope, $state, Notifications, StoridgeProfileService) { diff --git a/app/extensions/storidge/views/snapshots/inspect/snapshot.html b/app/integrations/storidge/views/snapshots/inspect/snapshot.html similarity index 100% rename from app/extensions/storidge/views/snapshots/inspect/snapshot.html rename to app/integrations/storidge/views/snapshots/inspect/snapshot.html diff --git a/app/extensions/storidge/views/snapshots/inspect/snapshotController.js b/app/integrations/storidge/views/snapshots/inspect/snapshotController.js similarity index 96% rename from app/extensions/storidge/views/snapshots/inspect/snapshotController.js rename to app/integrations/storidge/views/snapshots/inspect/snapshotController.js index 1534087cd..814efeee1 100644 --- a/app/extensions/storidge/views/snapshots/inspect/snapshotController.js +++ b/app/integrations/storidge/views/snapshots/inspect/snapshotController.js @@ -1,4 +1,4 @@ -angular.module('extension.storidge') +angular.module('portainer.integrations.storidge') .controller('StoridgeSnapshotController', ['$scope', '$state', '$transition$', 'Notifications', 'ModalService', 'StoridgeSnapshotService', function ($scope, $state, $transition$, Notifications, ModalService, StoridgeSnapshotService) { From ead160f792e004ff720c125e2b816e5ef63c5a1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Tue, 11 Jun 2019 23:41:32 +0200 Subject: [PATCH 03/37] chore(deps): bump js-yaml from 3.10.0 to 3.13.1 (#2930) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.10.0 to 3.13.1. - [Release notes](https://github.com/nodeca/js-yaml/releases) - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.10.0...3.13.1) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 0b0dacc15..511910f5e 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "filesize": "~3.3.0", "isteven-angular-multiselect": "~4.0.0", "jquery": "3.4.0", - "js-yaml": "~3.10.0", + "js-yaml": "~3.13.1", "lodash-es": "^4.17.11", "moment": "^2.21.0", "ng-file-upload": "~12.2.13", diff --git a/yarn.lock b/yarn.lock index da8210262..78747fc7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6528,18 +6528,10 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.12.0, js-yaml@^3.9.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" - integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^3.3.0, js-yaml@^3.5.1, js-yaml@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" - integrity sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA== +js-yaml@^3.12.0, js-yaml@^3.3.0, js-yaml@^3.5.1, js-yaml@^3.9.0, js-yaml@~3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== dependencies: argparse "^1.0.7" esprima "^4.0.0" From 09cf55a7dc90ca7303ac1c49d203738a35c1e6a1 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Fri, 14 Jun 2019 14:22:49 +0200 Subject: [PATCH 04/37] fix(build): bump auto-ngtemplate-loader to 2.0.1 to fix windows builds (#2935) --- package.json | 2 +- yarn.lock | 43 +++++++++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 511910f5e..faeef63ba 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "devDependencies": { "@babel/core": "^7.1.2", "@babel/preset-env": "^7.1.0", - "auto-ngtemplate-loader": "^1.3.0", + "auto-ngtemplate-loader": "^2.0.1", "autoprefixer": "^7.1.1", "babel-loader": "^8.0.4", "babel-plugin-lodash": "^3.3.4", diff --git a/yarn.lock b/yarn.lock index 78747fc7e..69d09c2a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1282,13 +1282,15 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -auto-ngtemplate-loader@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/auto-ngtemplate-loader/-/auto-ngtemplate-loader-1.3.0.tgz#da52abe96824ed4f56a0e6a447ecf79d82f23cc2" - integrity sha512-C6WN964U96E+Q6JJhfvZRXHG00LL6Rw6HVG0TgLCyIyOUNLxFAz0qGbR+Vb5nHFEjPhPItHlwgyuPBGLiSgKvQ== +auto-ngtemplate-loader@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/auto-ngtemplate-loader/-/auto-ngtemplate-loader-2.0.1.tgz#73799ad7b3f64a2de1b2a72e7082b0854c4b076e" + integrity sha512-V/dI3MlHl9ZyV8gk1/lh4xaf/3eFqBxZVftd+h8EZxTfAYhB0dv9d5z164J8Yj+0X00Una5tUUeP6Hl1OMP4sg== dependencies: - loader-utils "~1.1.0" - lodash "~4.17.4" + html-loader "~0.5.5" + loader-utils "~1.2.3" + lodash "~4.17.11" + ngtemplate-loader "~2.0.1" var-validator "0.0.3" autoprefixer@^6.3.1: @@ -1432,6 +1434,11 @@ big.js@^3.1.3: resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + bin-build@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/bin-build/-/bin-build-2.2.0.tgz#11f8dd61f70ffcfa2bdcaa5b46f5e8fedd4221cc" @@ -5579,7 +5586,7 @@ html-entities@^1.2.0: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= -html-loader@^0.5.5: +html-loader@^0.5.5, html-loader@~0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.5.5.tgz#6356dbeb0c49756d8ebd5ca327f16ff06ab5faea" integrity sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog== @@ -6613,6 +6620,13 @@ json5@^0.5.0: resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -6792,7 +6806,7 @@ loader-utils@^0.2.16: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@~1.1.0: +loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= @@ -6801,6 +6815,15 @@ loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@~1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" +loader-utils@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -7029,7 +7052,7 @@ lodash@^4.17.10: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== -lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.4: +lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.11: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -7691,7 +7714,7 @@ ng-file-upload@~12.2.13: resolved "https://registry.yarnpkg.com/ng-file-upload/-/ng-file-upload-12.2.13.tgz#01800f3872e526f95310f8477e99e4f12d0d8d14" integrity sha1-AYAPOHLlJvlTEPhHfpnk8S0NjRQ= -ngtemplate-loader@^2.0.1: +ngtemplate-loader@^2.0.1, ngtemplate-loader@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ngtemplate-loader/-/ngtemplate-loader-2.0.1.tgz#9d7ed2e8a2363523ad7b64d74aac402d8daff3f3" integrity sha1-nX7S6KI2NSOte2TXSqxALY2v8/M= From 71b1da8d326cc36e154f755cd78718609aedfae1 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Mon, 17 Jun 2019 16:51:39 +0200 Subject: [PATCH 05/37] fix(app): wrapper for UI refresh trigger with async/await (#2945) * fix(app): wrapper for UI refresh trigger with async/await * fix(async): $async wrapper now accepts functions with params * fix(async): $async should return a promise to be chained with ES5 .then() style * fix(async): $async with multiple params was not working * refactor(app): wrap all async functions with $async * docs(async): add link to async wrapper documentation --- app/docker/views/configs/configsController.js | 10 +++- .../configs/create/createConfigController.js | 56 ++++++++++--------- .../rbac/directives/authorization.js | 52 +++++++++-------- .../rbac/directives/disable-authorization.js | 41 ++++++++------ .../services/api/extensionService.js | 46 ++++++++++----- app/portainer/services/async.js | 23 ++++++++ app/portainer/views/auth/authController.js | 10 +++- .../access/endpointAccessController.js | 11 +++- .../views/init/admin/initAdminController.js | 10 +++- 9 files changed, 167 insertions(+), 92 deletions(-) create mode 100644 app/portainer/services/async.js diff --git a/app/docker/views/configs/configsController.js b/app/docker/views/configs/configsController.js index 08c16d2bd..fc047501f 100644 --- a/app/docker/views/configs/configsController.js +++ b/app/docker/views/configs/configsController.js @@ -3,12 +3,14 @@ import angular from 'angular'; class ConfigsController { /* @ngInject */ - constructor($state, ConfigService, Notifications) { + constructor($state, ConfigService, Notifications, $async) { this.$state = $state; this.ConfigService = ConfigService; this.Notifications = Notifications; + this.$async = $async; this.removeAction = this.removeAction.bind(this); + this.removeActionAsync = this.removeActionAsync.bind(this); } async $onInit() { @@ -20,7 +22,11 @@ class ConfigsController { } } - async removeAction(selectedItems) { + removeAction(selectedItems) { + return this.$async(this.removeActionAsync, selectedItems); + } + + async removeActionAsync(selectedItems) { let actionCount = selectedItems.length; for (const config of selectedItems) { try { diff --git a/app/docker/views/configs/create/createConfigController.js b/app/docker/views/configs/create/createConfigController.js index f77b8fa17..8baebf698 100644 --- a/app/docker/views/configs/create/createConfigController.js +++ b/app/docker/views/configs/create/createConfigController.js @@ -5,7 +5,7 @@ import angular from "angular"; class CreateConfigController { /* @ngInject */ - constructor($state, $transition$, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) { + constructor($async, $state, $transition$, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) { this.$state = $state; this.$transition$ = $transition$; this.Notifications = Notifications; @@ -13,6 +13,7 @@ class CreateConfigController { this.Authentication = Authentication; this.FormValidator = FormValidator; this.ResourceControlService = ResourceControlService; + this.$async = $async; this.formValues = { Name: "", @@ -26,6 +27,30 @@ class CreateConfigController { }; this.editorUpdate = this.editorUpdate.bind(this); + this.createAsync = this.createAsync.bind(this); + } + + async $onInit() { + if (!this.$transition$.params().id) { + this.formValues.displayCodeEditor = true; + return; + } + + try { + let data = await this.ConfigService.config(this.$transition$.params().id); + this.formValues.Name = data.Name + "_copy"; + this.formValues.Data = data.Data; + let labels = _.keys(data.Labels); + for (let i = 0; i < labels.length; i++) { + let labelName = labels[i]; + let labelValue = data.Labels[labelName]; + this.formValues.Labels.push({ name: labelName, value: labelValue }); + } + this.formValues.displayCodeEditor = true; + } catch (err) { + this.formValues.displayCodeEditor = true; + this.Notifications.error("Failure", err, "Unable to clone config"); + } } addLabel() { @@ -74,7 +99,11 @@ class CreateConfigController { return true; } - async create() { + create() { + return this.$async(this.createAsync); + } + + async createAsync() { let accessControlData = this.formValues.AccessControlData; let userDetails = this.Authentication.getUserDetails(); let isAdmin = this.Authentication.isAdmin(); @@ -111,29 +140,6 @@ class CreateConfigController { editorUpdate(cm) { this.formValues.ConfigContent = cm.getValue(); } - - async $onInit() { - if (!this.$transition$.params().id) { - this.formValues.displayCodeEditor = true; - return; - } - - try { - let data = await this.ConfigService.config(this.$transition$.params().id); - this.formValues.Name = data.Name + "_copy"; - this.formValues.Data = data.Data; - let labels = _.keys(data.Labels); - for (let i = 0; i < labels.length; i++) { - let labelName = labels[i]; - let labelValue = data.Labels[labelName]; - this.formValues.Labels.push({ name: labelName, value: labelValue }); - } - this.formValues.displayCodeEditor = true; - } catch (err) { - this.formValues.displayCodeEditor = true; - this.Notifications.error("Failure", err, "Unable to clone config"); - } - } } export default CreateConfigController; diff --git a/app/extensions/rbac/directives/authorization.js b/app/extensions/rbac/directives/authorization.js index 668314800..966266331 100644 --- a/app/extensions/rbac/directives/authorization.js +++ b/app/extensions/rbac/directives/authorization.js @@ -1,33 +1,37 @@ -angular.module('portainer.extensions.rbac').directive('authorization', ['Authentication', 'ExtensionService', - function(Authentication, ExtensionService) { - return { - restrict: 'A', - link: async function(scope, elem, attrs) { - elem.hide(); - try { - const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); - if (!rbacEnabled) { - elem.show(); - return; - } - } catch (err) { +angular.module('portainer.extensions.rbac').directive('authorization', ['Authentication', 'ExtensionService', '$async', + function(Authentication, ExtensionService, $async) { + + async function linkAsync(scope, elem, attrs) { + elem.hide(); + try { + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (!rbacEnabled) { elem.show(); return; } + } catch (err) { + elem.show(); + return; + } + var authorizations = attrs.authorization.split(","); + for (var i = 0; i < authorizations.length; i++) { + authorizations[i] = authorizations[i].trim(); + } - var authorizations = attrs.authorization.split(","); - for (var i = 0; i < authorizations.length; i++) { - authorizations[i] = authorizations[i].trim(); - } + var hasAuthorizations = Authentication.hasAuthorizations(authorizations); - var hasAuthorizations = Authentication.hasAuthorizations(authorizations); + if (hasAuthorizations) { + elem.show(); + } else if (!hasAuthorizations && elem[0].tagName === 'A') { + elem.show(); + elem.addClass('portainer-disabled-link'); + } + } - if (hasAuthorizations) { - elem.show(); - } else if (!hasAuthorizations && elem[0].tagName === 'A') { - elem.show(); - elem.addClass('portainer-disabled-link'); - } + return { + restrict: 'A', + link: function(scope, elem, attrs) { + return $async(linkAsync, scope, elem, attrs); } } }]); diff --git a/app/extensions/rbac/directives/disable-authorization.js b/app/extensions/rbac/directives/disable-authorization.js index 094aeb855..5dad08074 100644 --- a/app/extensions/rbac/directives/disable-authorization.js +++ b/app/extensions/rbac/directives/disable-authorization.js @@ -1,27 +1,32 @@ angular.module('portainer.extensions.rbac') - .directive('disableAuthorization', ['Authentication', 'ExtensionService', function(Authentication, ExtensionService) { - return { - restrict: 'A', - link: async function (scope, elem, attrs) { - try { - const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); - if (!rbacEnabled) { - elem.show(); - return; - } - } catch (err) { + .directive('disableAuthorization', ['Authentication', 'ExtensionService', '$async', function(Authentication, ExtensionService, $async) { + + async function linkAsync(scope, elem, attrs) { + try { + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (!rbacEnabled) { elem.show(); return; } + } catch (err) { + elem.show(); + return; + } - var authorizations = attrs.disableAuthorization.split(","); - for (var i = 0; i < authorizations.length; i++) { - authorizations[i] = authorizations[i].trim(); - } + var authorizations = attrs.disableAuthorization.split(","); + for (var i = 0; i < authorizations.length; i++) { + authorizations[i] = authorizations[i].trim(); + } - if (!Authentication.hasAuthorizations(authorizations)) { - elem.attr('disabled', true); - } + if (!Authentication.hasAuthorizations(authorizations)) { + elem.attr('disabled', true); + } + } + + return { + restrict: 'A', + link: function (scope, elem, attrs) { + return $async(linkAsync, scope, elem, attrs); } } }]); diff --git a/app/portainer/services/api/extensionService.js b/app/portainer/services/api/extensionService.js index b53a1375b..159674745 100644 --- a/app/portainer/services/api/extensionService.js +++ b/app/portainer/services/api/extensionService.js @@ -2,7 +2,7 @@ import _ from 'lodash-es'; import { ExtensionViewModel } from '../../models/extension'; angular.module('portainer.app') -.factory('ExtensionService', ['$q', 'Extension', 'StateManager', function ExtensionServiceFactory($q, Extension, StateManager) { +.factory('ExtensionService', ['$q', 'Extension', 'StateManager', '$async', function ExtensionServiceFactory($q, Extension, StateManager, $async) { 'use strict'; var service = {}; @@ -12,19 +12,27 @@ angular.module('portainer.app') RBAC: 3 }); - service.enable = function(license) { + service.enable = enable; + service.update = update; + service.delete = _delete; + service.extensions = extensions; + service.extension = extension; + service.extensionEnabled = extensionEnabled; + service.retrieveAndSaveEnabledExtensions = retrieveAndSaveEnabledExtensions; + + function enable(license) { return Extension.create({ license: license }).$promise; - }; + } - service.update = function(id, version) { + function update(id, version) { return Extension.update({ id: id, version: version }).$promise; - }; + } - service.delete = function(id) { + function _delete(id) { return Extension.delete({ id: id }).$promise; - }; + } - service.extensions = function(store) { + function extensions(store) { var deferred = $q.defer(); Extension.query({ store: store }).$promise @@ -39,9 +47,9 @@ angular.module('portainer.app') }); return deferred.promise; - }; + } - service.extension = function(id) { + function extension(id) { var deferred = $q.defer(); Extension.get({ id: id }).$promise @@ -54,9 +62,13 @@ angular.module('portainer.app') }); return deferred.promise; - }; + } - service.extensionEnabled = async function(extensionId) { + function extensionEnabled(extensionId) { + return $async(extensionsEnabledAsync, extensionId) + } + + async function extensionsEnabledAsync(extensionId) { if (extensionId === service.EXTENSIONS.RBAC) { return StateManager.getExtension(extensionId) ? true : false; } else { @@ -64,13 +76,17 @@ angular.module('portainer.app') const extension = _.find(extensions, (ext) => ext.Id === extensionId); return extension ? extension.Enabled : false; } - }; + } - service.retrieveAndSaveEnabledExtensions = async function() { + function retrieveAndSaveEnabledExtensions() { + return $async(retrieveAndSaveEnabledExtensionsAsync) + } + + async function retrieveAndSaveEnabledExtensionsAsync() { const extensions = await service.extensions(false); _.forEach(extensions, (ext) => delete ext.License); StateManager.saveExtensions(extensions); - }; + } return service; }]); diff --git a/app/portainer/services/async.js b/app/portainer/services/async.js new file mode 100644 index 000000000..ab4369c60 --- /dev/null +++ b/app/portainer/services/async.js @@ -0,0 +1,23 @@ +/** + * Look a the following PR for how to use the wrapper + * and documentation about it + * https://github.com/portainer/portainer/pull/2945 + */ + +angular.module('portainer').factory('$async', ['$q', + function($q) { + return function(asyncFunc, ...args) { + const def = $q.defer(); + const wrapper = function(params) { + const deferred = $q.defer(); + asyncFunc(...params) + .then(deferred.resolve) + .catch(deferred.reject); + return deferred.promise; + }; + + wrapper(args).then(def.resolve).catch(def.reject) + return def.promise; + }; + } +]); diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 517cc03fd..840e9a7d8 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('AuthenticationController', ['$q', '$scope', '$state', '$stateParams', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'ExtensionService', 'StateManager', 'Notifications', 'SettingsService', 'URLHelper', -function($q, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, ExtensionService, StateManager, Notifications, SettingsService, URLHelper) { +.controller('AuthenticationController', ['$async', '$q', '$scope', '$state', '$stateParams', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'ExtensionService', 'StateManager', 'Notifications', 'SettingsService', 'URLHelper', +function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, ExtensionService, StateManager, Notifications, SettingsService, URLHelper) { $scope.logo = StateManager.getState().application.logo; $scope.formValues = { @@ -14,7 +14,11 @@ function($q, $scope, $state, $stateParams, $sanitize, Authentication, UserServic OAuthProvider: '' }; - async function retrieveAndSaveEnabledExtensions() { + function retrieveAndSaveEnabledExtensions() { + return $async(retrieveAndSaveEnabledExtensionsAsync); + } + + async function retrieveAndSaveEnabledExtensionsAsync() { try { await ExtensionService.retrieveAndSaveEnabledExtensions(); } catch (err) { diff --git a/app/portainer/views/endpoints/access/endpointAccessController.js b/app/portainer/views/endpoints/access/endpointAccessController.js index 708343a6e..3738cc9cc 100644 --- a/app/portainer/views/endpoints/access/endpointAccessController.js +++ b/app/portainer/views/endpoints/access/endpointAccessController.js @@ -2,13 +2,16 @@ import angular from "angular"; class EndpointAccessController { /* @ngInject */ - constructor($state, $transition$, Notifications, EndpointService, GroupService) { + constructor($state, $transition$, Notifications, EndpointService, GroupService, $async) { this.$state = $state; this.$transition$ = $transition$; this.Notifications = Notifications; this.EndpointService = EndpointService; this.GroupService = GroupService; + this.$async = $async; + this.updateAccess = this.updateAccess.bind(this); + this.updateAccessAsync = this.updateAccessAsync.bind(this); } async $onInit() { @@ -23,7 +26,11 @@ class EndpointAccessController { } } - async updateAccess() { + updateAccess() { + return this.$async(this.updateAccessAsync); + } + + async updateAccessAsync() { try { this.state.actionInProgress = true; await this.EndpointService.updateEndpoint(this.$transition$.params().id, this.endpoint); diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js index 8384b3cd6..bc7f66c0e 100644 --- a/app/portainer/views/init/admin/initAdminController.js +++ b/app/portainer/views/init/admin/initAdminController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('InitAdminController', ['$scope', '$state', 'Notifications', 'Authentication', 'StateManager', 'UserService', 'EndpointService', 'ExtensionService', -function ($scope, $state, Notifications, Authentication, StateManager, UserService, EndpointService, ExtensionService) { +.controller('InitAdminController', ['$async', '$scope', '$state', 'Notifications', 'Authentication', 'StateManager', 'UserService', 'EndpointService', 'ExtensionService', +function ($async, $scope, $state, Notifications, Authentication, StateManager, UserService, EndpointService, ExtensionService) { $scope.logo = StateManager.getState().application.logo; @@ -14,7 +14,11 @@ function ($scope, $state, Notifications, Authentication, StateManager, UserServi actionInProgress: false }; - async function retrieveAndSaveEnabledExtensions() { + function retrieveAndSaveEnabledExtensions() { + return $async(retrieveAndSaveEnabledExtensionsAsync) + } + + async function retrieveAndSaveEnabledExtensionsAsync() { try { await ExtensionService.retrieveAndSaveEnabledExtensions(); } catch (err) { From f3b9668629b5dbfeeff08c3bcf9a181c14778ef8 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 19 Jun 2019 00:45:49 +1200 Subject: [PATCH 06/37] fix(endpoints): always reset agent headers when switching state (#2939) --- app/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app.js b/app/app.js index 38f0151e7..293a5fd3e 100644 --- a/app/app.js +++ b/app/app.js @@ -31,7 +31,7 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin } }; - $transitions.onBefore({ to: 'docker.**' }, function() { + $transitions.onBefore({}, function() { HttpRequestHelper.resetAgentHeaders(); }); }]); From 96f266adf63559dc058351d77359503621557df6 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Date: Fri, 21 Jun 2019 10:44:29 -0300 Subject: [PATCH 07/37] fix(volume-browser): download capability for all types of files (#2954) --- app/agent/components/volume-browser/volumeBrowserController.js | 2 +- app/agent/rest/browse.js | 3 ++- app/agent/rest/v1/browse.js | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/agent/components/volume-browser/volumeBrowserController.js b/app/agent/components/volume-browser/volumeBrowserController.js index 20bd82891..0f8e99d86 100644 --- a/app/agent/components/volume-browser/volumeBrowserController.js +++ b/app/agent/components/volume-browser/volumeBrowserController.js @@ -42,7 +42,7 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; VolumeBrowserService.get(this.volumeId, filePath) .then(function success(data) { - var downloadData = new Blob([data.file], { type: 'text/plain;charset=utf-8' }); + var downloadData = new Blob([data.file]); FileSaver.saveAs(downloadData, file); }) .catch(function error(err) { diff --git a/app/agent/rest/browse.js b/app/agent/rest/browse.js index c17028ecf..82e5d4b21 100644 --- a/app/agent/rest/browse.js +++ b/app/agent/rest/browse.js @@ -14,7 +14,8 @@ angular.module('portainer.agent') }, get: { method: 'GET', params: { action: 'get' }, - transformResponse: browseGetResponse + transformResponse: browseGetResponse, + responseType: 'arraybuffer' }, delete: { method: 'DELETE', params: { action: 'delete' } diff --git a/app/agent/rest/v1/browse.js b/app/agent/rest/v1/browse.js index c175f2369..62a198743 100644 --- a/app/agent/rest/v1/browse.js +++ b/app/agent/rest/v1/browse.js @@ -12,7 +12,8 @@ angular.module('portainer.agent') }, get: { method: 'GET', params: { action: 'get' }, - transformResponse: browseGetResponse + transformResponse: browseGetResponse, + responseType: 'arraybuffer' }, delete: { method: 'DELETE', params: { action: 'delete' } From fb69ffa76472cb63eee3cbba4114e8897c6be6dc Mon Sep 17 00:00:00 2001 From: Michael Oborne Date: Fri, 21 Jun 2019 21:48:52 +0800 Subject: [PATCH 08/37] fix(create-container): init ExposedPorts on container creation process (#2933) --- .../views/containers/create/createContainerController.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 60fde3de1..c91e54523 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -139,6 +139,9 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai function preparePortBindings(config) { var bindings = {}; + if (config.ExposedPorts === undefined) { + config.ExposedPorts = {}; + } config.HostConfig.PortBindings.forEach(function (portBinding) { if (portBinding.containerPort) { var key = portBinding.containerPort + '/' + portBinding.protocol; From b3c7c76be29fb165ec796045f2e86b0020feb101 Mon Sep 17 00:00:00 2001 From: Anthony Brame Date: Fri, 21 Jun 2019 15:50:58 +0200 Subject: [PATCH 09/37] fix(swarmvisualizer): nodes are now sorted by roles then by hostname (#2885) --- app/docker/views/swarm/visualizer/swarmvisualizer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/docker/views/swarm/visualizer/swarmvisualizer.html b/app/docker/views/swarm/visualizer/swarmvisualizer.html index 612354e25..8851ac41c 100644 --- a/app/docker/views/swarm/visualizer/swarmvisualizer.html +++ b/app/docker/views/swarm/visualizer/swarmvisualizer.html @@ -80,7 +80,7 @@
-
+
From 7a8a54c96aa704279fbbd3a87fb997bdd4a4c7fc Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 23 Jun 2019 11:36:45 +1200 Subject: [PATCH 10/37] refactor(api): introduce ExtensionServer constant --- api/http/proxy/manager.go | 2 +- api/portainer.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 8d1efb404..67da091a6 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -92,7 +92,7 @@ func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) htt // CreateExtensionProxy creates a new HTTP reverse proxy for an extension and // registers it in the extension map associated to the specified extension identifier func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) { - address := "http://localhost:" + extensionPorts[extensionID] + address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID] extensionURL, err := url.Parse(address) if err != nil { diff --git a/api/portainer.go b/api/portainer.go index 3d6e73739..1cb48f826 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -876,6 +876,8 @@ const ( PortainerAgentSignatureMessage = "Portainer-App" // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer SupportedDockerAPIVersion = "1.24" + // ExtensionServer represents the server used by Portainer to communicate with extensions + ExtensionServer = "localhost" ) const ( From 6591498ab94b602e1384aa7503e9fec73002cf48 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Tue, 25 Jun 2019 01:22:45 +0200 Subject: [PATCH 11/37] feat(node-details): move engine labels from node-details panel to engine-details panel (#2966) --- .../engine-details-panel/engine-details-panel.html | 4 ++++ .../swarm-node-details-panel/swarm-node-details-panel.html | 4 ---- .../views/nodes/node-details/node-details-view-controller.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html b/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html index 4eebf6bcd..036e4d7b7 100644 --- a/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html +++ b/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html @@ -29,6 +29,10 @@ Network Plugins {{ $ctrl.engine.networkPlugins | arraytostr: ', ' }} + + Engine Labels + {{ $ctrl.engine.engineLabels | labelsToStr:', ' }} + diff --git a/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html b/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html index e99c5039b..d290cb2aa 100644 --- a/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html +++ b/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html @@ -26,10 +26,6 @@ {{ $ctrl.details.status }} - - Engine Labels - {{ $ctrl.details.engineLabels | labelsToStr:', ' }} -
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 b4066a8f8..3cc0a6e90 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 @@ -62,7 +62,8 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ return { releaseVersion: node.EngineVersion, volumePlugins: transformPlugins(node.Plugins, 'Volume'), - networkPlugins: transformPlugins(node.Plugins, 'Network') + networkPlugins: transformPlugins(node.Plugins, 'Network'), + engineLabels: node.EngineLabels, }; } @@ -73,7 +74,6 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ managerAddress: node.ManagerAddr, availability: node.Availability, status: node.Status, - engineLabels: node.EngineLabels, nodeLabels: node.Labels }; } From 1138fd5ab148288ef011a45cc353e6d95cee1337 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Tue, 2 Jul 2019 17:51:17 +0200 Subject: [PATCH 12/37] fix(datatables): allow selecting range using shift (#344) (#2962) * fix(datatables): allow selecting range using shift (#344) * feat(datatables): more intuitive batch select behaviour * feat(datatables): add overridable function called on selection change * refactor(datatables): remove custom selectAll on Generic-extending Controllers * fix(datatables): stored state data retrieval on Generic-extanding datatables controllers * refactor(datatables): remove code duplication between GenericController and extending controllers --- .../containerGroupsDatatable.html | 2 +- .../configs-datatable/configsDatatable.html | 2 +- .../containersDatatable.html | 2 +- .../containersDatatableController.js | 83 +++------- .../jobsDatatableController.js | 52 ++---- .../images-datatable/imagesDatatable.html | 14 +- .../imagesDatatableController.js | 82 +++------- .../macvlanNodesDatatable.html | 2 +- .../networks-datatable/networksDatatable.html | 2 +- .../networksDatatableController.js | 41 +++-- .../secrets-datatable/secretsDatatable.html | 2 +- .../serviceTasksDatatableController.js | 149 +++++++++--------- .../services-datatable/servicesDatatable.html | 2 +- .../servicesDatatableController.js | 77 +++------ .../tasksDatatableController.js | 70 +++----- .../volumes-datatable/volumesDatatable.html | 14 +- .../volumesDatatableController.js | 82 +++------- .../registryRepositoriesDatatable.html | 2 +- .../registriesRepositoryTagsDatatable.html | 2 +- .../storidgeDrivesDatatableController.js | 37 +++-- .../storidgeNodesDatatableController.js | 29 +++- .../storidgeProfilesDatatable.html | 2 +- .../storidgeSnapshotsDatatable.html | 2 +- .../storidgeSnapshotsDatatable.js | 2 +- .../storidgeSnapshotsDatatableController.js | 7 - .../access-datatable/accessDatatable.html | 2 +- .../accessDatatableController.js | 37 +++-- .../components/datatables/datatable.css | 1 + .../endpointsDatatable.html | 2 +- .../datatables/genericDatatableController.js | 95 +++++++++-- .../groups-datatable/groupsDatatable.html | 2 +- .../registriesDatatable.html | 2 +- .../schedulesDatatable.html | 2 +- .../schedulesDatatableController.js | 79 ++++------ .../stacks-datatable/stacksDatatable.html | 2 +- .../stacksDatatableController.js | 68 +++----- .../tags-datatable/tagsDatatable.html | 2 +- .../teams-datatable/teamsDatatable.html | 2 +- .../users-datatable/usersDatatable.html | 2 +- 39 files changed, 477 insertions(+), 582 deletions(-) delete mode 100644 app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html index 9f85333b7..ae7a459ce 100644 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html @@ -50,7 +50,7 @@ - + {{ item.Name | truncate:50 }} diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.html b/app/docker/components/datatables/configs-datatable/configsDatatable.html index 623359a4b..4e127d605 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.html +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.html @@ -54,7 +54,7 @@ - + {{ item.Name }} diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 86c26ae37..1ecd6d223 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -217,7 +217,7 @@ - + {{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }} diff --git a/app/docker/components/datatables/containers-datatable/containersDatatableController.js b/app/docker/components/datatables/containers-datatable/containersDatatableController.js index 337f794bc..b09b0bc7b 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatableController.js +++ b/app/docker/components/datatables/containers-datatable/containersDatatableController.js @@ -1,22 +1,19 @@ import _ from 'lodash-es'; angular.module('portainer.docker') -.controller('ContainersDatatableController', ['PaginationService', 'DatatableService', 'EndpointProvider', -function (PaginationService, DatatableService, EndpointProvider) { +.controller('ContainersDatatableController', ['$scope', '$controller', 'DatatableService', 'EndpointProvider', +function ($scope, $controller, DatatableService, EndpointProvider) { + + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); + var ctrl = this; - this.state = { - selectAll: false, - orderBy: this.orderBy, - paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), - displayTextFilter: false, - selectedItemCount: 0, - selectedItems: [], + this.state = Object.assign(this.state, { noStoppedItemsSelected: true, noRunningItemsSelected: true, noPausedItemsSelected: true, publicURL: EndpointProvider.endpointPublicURL() - }; + }); this.settings = { open: false, @@ -81,45 +78,13 @@ function (PaginationService, DatatableService, EndpointProvider) { } }; - this.onTextFilterChange = function() { - DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); - }; - this.onColumnVisibilityChange = function() { DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility); }; - this.changeOrderBy = function(orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.toggleItemSelection = function(item) { - if (item.Checked) { - this.state.selectedItemCount++; - this.state.selectedItems.push(item); - } else { - this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); - this.state.selectedItemCount--; - } - }; - - this.selectItem = function(item) { - this.toggleItemSelection(item); + this.onSelectionChanged = function() { this.updateSelectionState(); - }; - - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.toggleItemSelection(item); - } - } - this.updateSelectionState(); - }; + } this.updateSelectionState = function() { this.state.noStoppedItemsSelected = true; @@ -144,10 +109,6 @@ function (PaginationService, DatatableService, EndpointProvider) { } }; - this.changePaginationLimit = function() { - PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); - }; - this.applyFilters = function(value) { var container = value; var filters = ctrl.filters; @@ -209,40 +170,38 @@ function (PaginationService, DatatableService, EndpointProvider) { }; this.$onInit = function() { - setDefaults(this); + this.setDefaults(); this.prepareTableFromDataset(); + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); if (storedOrder !== null) { this.state.reverseOrder = storedOrder.reverse; this.state.orderBy = storedOrder.orderBy; } + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); if (storedFilters !== null) { + this.filters = storedFilters; + this.filters.state.open = false; this.updateStoredFilters(storedFilters.state.values); } - this.filters.state.open = false; var storedSettings = DatatableService.getDataTableSettings(this.tableKey); if (storedSettings !== null) { this.settings = storedSettings; + this.settings.open = false; } - this.settings.open = false; var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey); if (storedColumnVisibility !== null) { this.columnVisibility = storedColumnVisibility; - } - this.columnVisibility.state.open = false; - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; + this.columnVisibility.state.open = false; } }; - - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - } }]); diff --git a/app/docker/components/datatables/host-jobs-datatable/jobsDatatableController.js b/app/docker/components/datatables/host-jobs-datatable/jobsDatatableController.js index d156c0d88..71d1c6a14 100644 --- a/app/docker/components/datatables/host-jobs-datatable/jobsDatatableController.js +++ b/app/docker/components/datatables/host-jobs-datatable/jobsDatatableController.js @@ -1,15 +1,12 @@ import _ from 'lodash-es'; angular.module('portainer.docker') - .controller('JobsDatatableController', ['$q', '$state', 'PaginationService', 'DatatableService', 'ContainerService', 'ModalService', 'Notifications', - function ($q, $state, PaginationService, DatatableService, ContainerService, ModalService, Notifications) { - var ctrl = this; + .controller('JobsDatatableController', ['$scope', '$controller', '$q', '$state', 'PaginationService', 'DatatableService', 'ContainerService', 'ModalService', 'Notifications', + function ($scope, $controller, $q, $state, PaginationService, DatatableService, ContainerService, ModalService, Notifications) { - this.state = { - orderBy: this.orderBy, - paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), - displayTextFilter: false - }; + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); + + var ctrl = this; this.filters = { state: { @@ -19,20 +16,6 @@ angular.module('portainer.docker') } }; - this.onTextFilterChange = function() { - DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); - }; - - this.changeOrderBy = function (orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.changePaginationLimit = function () { - PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); - }; - this.applyFilters = function (value) { var container = value; var filters = ctrl.filters; @@ -121,8 +104,8 @@ angular.module('portainer.docker') }); }; - this.$onInit = function () { - setDefaults(this); + this.$onInit = function() { + this.setDefaults(); this.prepareTableFromDataset(); var storedOrder = DatatableService.getDataTableOrder(this.tableKey); @@ -131,21 +114,20 @@ angular.module('portainer.docker') this.state.orderBy = storedOrder.orderBy; } - var storedFilters = DatatableService.getDataTableFilters(this.tableKey); - if (storedFilters !== null) { - this.updateStoredFilters(storedFilters.state.values); - } - this.filters.state.open = false; - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); if (textFilter !== null) { this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + this.updateStoredFilters(storedFilters.state.values); + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; } }; - - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - } } ]); diff --git a/app/docker/components/datatables/images-datatable/imagesDatatable.html b/app/docker/components/datatables/images-datatable/imagesDatatable.html index de6c720c5..b2e1506ee 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatable.html +++ b/app/docker/components/datatables/images-datatable/imagesDatatable.html @@ -43,7 +43,7 @@ -
+ @@ -54,8 +54,8 @@
- Filter - Filter + Filter + Filter
@@ -112,7 +112,7 @@
- + {{ item.Id | truncate:40 }} diff --git a/app/docker/components/datatables/images-datatable/imagesDatatableController.js b/app/docker/components/datatables/images-datatable/imagesDatatableController.js index c197ccc64..afd0a0930 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatableController.js +++ b/app/docker/components/datatables/images-datatable/imagesDatatableController.js @@ -1,20 +1,13 @@ angular.module('portainer.docker') -.controller('ImagesDatatableController', ['PaginationService', 'DatatableService', -function (PaginationService, DatatableService) { +.controller('ImagesDatatableController', ['$scope', '$controller', 'DatatableService', +function ($scope, $controller, DatatableService) { + + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); var ctrl = this; - this.state = { - selectAll: false, - orderBy: this.orderBy, - paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), - displayTextFilter: false, - selectedItemCount: 0, - selectedItems: [] - }; - this.filters = { - usage: { + state: { open: false, enabled: false, showUsedImages: true, @@ -22,62 +15,29 @@ function (PaginationService, DatatableService) { } }; - this.onTextFilterChange = function() { - DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); - }; - - this.changeOrderBy = function(orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.selectItem = function(item) { - if (item.Checked) { - this.state.selectedItemCount++; - this.state.selectedItems.push(item); - } else { - this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); - this.state.selectedItemCount--; - } - }; - - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); - } - } - }; - - this.changePaginationLimit = function() { - PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); - }; - this.applyFilters = function(value) { var image = value; var filters = ctrl.filters; - if ((image.ContainerCount === 0 && filters.usage.showUnusedImages) - || (image.ContainerCount !== 0 && filters.usage.showUsedImages)) { + if ((image.ContainerCount === 0 && filters.state.showUnusedImages) + || (image.ContainerCount !== 0 && filters.state.showUsedImages)) { return true; } return false; }; - this.onUsageFilterChange = function() { - var filters = this.filters.usage; + this.onstateFilterChange = function() { + var filters = this.filters.state; var filtered = false; if (!filters.showUsedImages || !filters.showUnusedImages) { filtered = true; } - this.filters.usage.enabled = filtered; + this.filters.state.enabled = filtered; DatatableService.setDataTableFilters(this.tableKey, this.filters); }; this.$onInit = function() { - setDefaults(this); + this.setDefaults(); + this.prepareTableFromDataset(); var storedOrder = DatatableService.getDataTableOrder(this.tableKey); if (storedOrder !== null) { @@ -85,20 +45,18 @@ function (PaginationService, DatatableService) { this.state.orderBy = storedOrder.orderBy; } + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); if (storedFilters !== null) { this.filters = storedFilters; } - this.filters.usage.open = false; - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; + if (this.filters && this.filters.state) { + this.filters.state.open = false; } }; - - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - } }]); diff --git a/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html b/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html index f4a65f6c2..574aa2fe6 100644 --- a/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html +++ b/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html @@ -60,7 +60,7 @@ ng-class="{active: item.Checked}"> - + {{ item.Hostname }} diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.html b/app/docker/components/datatables/networks-datatable/networksDatatable.html index df718e7a4..1a7deb6de 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.html +++ b/app/docker/components/datatables/networks-datatable/networksDatatable.html @@ -110,7 +110,7 @@
- + {{ item.Name | truncate:40 }} diff --git a/app/docker/components/datatables/networks-datatable/networksDatatableController.js b/app/docker/components/datatables/networks-datatable/networksDatatableController.js index 8c9afd635..6ebdb1884 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatableController.js +++ b/app/docker/components/datatables/networks-datatable/networksDatatableController.js @@ -1,19 +1,42 @@ angular.module('portainer.docker') - .controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', - function ($scope, $controller, PREDEFINED_NETWORKS) { + .controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService', + function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); this.disableRemove = function(item) { return PREDEFINED_NETWORKS.includes(item.Name); }; - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (!this.disableRemove(item) && item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); - } + /** + * Do not allow PREDEFINED_NETWORKS to be selected + */ + this.allowSelection = function(item) { + return !this.disableRemove(item); + } + + this.$onInit = function() { + this.setDefaults(); + this.prepareTableFromDataset(); + + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; } }; } diff --git a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html index 8db6b2e5c..24f4d45bb 100644 --- a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html +++ b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html @@ -54,7 +54,7 @@
- + {{ item.Name }} diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js index ddd9a0b34..6ecd9ab24 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js @@ -1,77 +1,84 @@ import _ from 'lodash-es'; angular.module('portainer.docker') -.controller('ServiceTasksDatatableController', ['DatatableService', -function (DatatableService) { - var ctrl = this; + .controller('ServiceTasksDatatableController', ['$scope', '$controller', 'DatatableService', + function ($scope, $controller, DatatableService) { - this.state = { - orderBy: this.orderBy, - showQuickActionStats: true, - showQuickActionLogs: true, - showQuickActionExec: true, - showQuickActionInspect: true, - showQuickActionAttach: false - }; + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); - this.filters = { - state: { - open: false, - enabled: false, - values: [] + var ctrl = this; + + this.state = Object.assign(this.state, { + showQuickActionStats: true, + showQuickActionLogs: true, + showQuickActionConsole: true, + showQuickActionInspect: true, + showQuickActionAttach: false + }); + + this.filters = { + state: { + open: false, + enabled: false, + values: [] + } + }; + + this.applyFilters = function(item) { + var filters = ctrl.filters; + for (var i = 0; i < filters.state.values.length; i++) { + var filter = filters.state.values[i]; + if (item.Status.State === filter.label && filter.display) { + return true; + } + } + return false; + }; + + this.onStateFilterChange = function() { + var filters = this.filters.state.values; + var filtered = false; + for (var i = 0; i < filters.length; i++) { + var filter = filters[i]; + if (!filter.display) { + filtered = true; + } + } + this.filters.state.enabled = filtered; + }; + + this.prepareTableFromDataset = function() { + var availableStateFilters = []; + for (var i = 0; i < this.dataset.length; i++) { + var item = this.dataset[i]; + availableStateFilters.push({ label: item.Status.State, display: true }); + } + this.filters.state.values = _.uniqBy(availableStateFilters, 'label'); + }; + + this.$onInit = function() { + this.setDefaults(); + this.prepareTableFromDataset(); + + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + }; } - }; - - this.applyFilters = function(item) { - var filters = ctrl.filters; - for (var i = 0; i < filters.state.values.length; i++) { - var filter = filters.state.values[i]; - if (item.Status.State === filter.label && filter.display) { - return true; - } - } - return false; - }; - - this.onStateFilterChange = function() { - var filters = this.filters.state.values; - var filtered = false; - for (var i = 0; i < filters.length; i++) { - var filter = filters[i]; - if (!filter.display) { - filtered = true; - } - } - this.filters.state.enabled = filtered; - }; - - this.changeOrderBy = function(orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.prepareTableFromDataset = function() { - var availableStateFilters = []; - for (var i = 0; i < this.dataset.length; i++) { - var item = this.dataset[i]; - availableStateFilters.push({ label: item.Status.State, display: true }); - } - this.filters.state.values = _.uniqBy(availableStateFilters, 'label'); - }; - - this.$onInit = function() { - setDefaults(this); - this.prepareTableFromDataset(); - - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; - } - }; - - function setDefaults(ctrl) { - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - } -}]); +]); diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html index e941aee41..214b13edf 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.html +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html @@ -84,7 +84,7 @@
- + diff --git a/app/docker/components/datatables/services-datatable/servicesDatatableController.js b/app/docker/components/datatables/services-datatable/servicesDatatableController.js index adee53500..7e32ad996 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatableController.js +++ b/app/docker/components/datatables/services-datatable/servicesDatatableController.js @@ -1,52 +1,18 @@ import _ from 'lodash-es'; angular.module('portainer.docker') -.controller('ServicesDatatableController', ['PaginationService', 'DatatableService', 'EndpointProvider', -function (PaginationService, DatatableService, EndpointProvider) { +.controller('ServicesDatatableController', ['$scope', '$controller', 'DatatableService', 'EndpointProvider', +function ($scope, $controller, DatatableService, EndpointProvider) { + + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); var ctrl = this; - this.state = { - selectAll: false, + this.state = Object.assign(this.state,{ expandAll: false, - orderBy: this.orderBy, - paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), - displayTextFilter: false, - selectedItemCount: 0, - selectedItems: [], expandedItems: [], publicURL: EndpointProvider.endpointPublicURL() - }; - - this.onTextFilterChange = function() { - DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); - }; - - this.changeOrderBy = function(orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.selectItem = function(item) { - if (item.Checked) { - this.state.selectedItemCount++; - this.state.selectedItems.push(item); - } else { - this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); - this.state.selectedItemCount--; - } - }; - - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); - } - } - }; + }); this.expandAll = function() { this.state.expandAll = !this.state.expandAll; @@ -56,10 +22,6 @@ function (PaginationService, DatatableService, EndpointProvider) { } }; - this.changePaginationLimit = function() { - PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); - }; - this.expandItem = function(item, expanded) { item.Expanded = expanded; if (item.Expanded) { @@ -103,7 +65,8 @@ function (PaginationService, DatatableService, EndpointProvider) { }; this.$onInit = function() { - setDefaults(this); + this.setDefaults(); + this.prepareTableFromDataset(); var storedOrder = DatatableService.getDataTableOrder(this.tableKey); if (storedOrder !== null) { @@ -111,19 +74,23 @@ function (PaginationService, DatatableService, EndpointProvider) { this.state.orderBy = storedOrder.orderBy; } + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + var storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey); if (storedExpandedItems !== null) { this.expandItems(storedExpandedItems); } - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - } }; - - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - } }]); diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js b/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js index eabf6202b..75e6d99c1 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js @@ -1,56 +1,20 @@ angular.module('portainer.docker') -.controller('TasksDatatableController', ['PaginationService', 'DatatableService', -function (PaginationService, DatatableService) { - this.state = { +.controller('TasksDatatableController', ['$scope', '$controller', 'DatatableService', +function ($scope, $controller, DatatableService) { + + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); + + this.state = Object.assign(this.state, { showQuickActionStats: true, showQuickActionLogs: true, showQuickActionExec: true, showQuickActionInspect: true, - showQuickActionAttach: false, - selectAll: false, - orderBy: this.orderBy, - paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), - displayTextFilter: false, - selectedItemCount: 0, - selectedItems: [] - }; - - this.onTextFilterChange = function() { - DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); - }; - - this.changeOrderBy = function(orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.selectItem = function(item) { - if (item.Checked) { - this.state.selectedItemCount++; - this.state.selectedItems.push(item); - } else { - this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); - this.state.selectedItemCount--; - } - }; - - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); - } - } - }; - - this.changePaginationLimit = function() { - PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); - }; + showQuickActionAttach: false + }); this.$onInit = function() { - setDefaults(this); + this.setDefaults(); + this.prepareTableFromDataset(); var storedOrder = DatatableService.getDataTableOrder(this.tableKey); if (storedOrder !== null) { @@ -61,11 +25,15 @@ function (PaginationService, DatatableService) { var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); if (textFilter !== null) { this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; } }; - - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - } }]); diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html index 211759c46..c42c7e2a5 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html @@ -23,7 +23,7 @@ - diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js index f0247eb40..a235dfcad 100644 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js @@ -10,6 +10,7 @@ angular.module('portainer.app').component('registriesDatatable', { reverseOrder: '<', accessManagement: '<', removeAction: '<', - registryManagement: '<' + registryManagement: '<', + canBrowse: '<' } }); diff --git a/app/portainer/views/registries/registries.html b/app/portainer/views/registries/registries.html index 670baba7e..3bf7d8eb7 100644 --- a/app/portainer/views/registries/registries.html +++ b/app/portainer/views/registries/registries.html @@ -77,6 +77,7 @@ access-management="applicationState.application.authentication && isAdmin" remove-action="removeAction" registry-management="registryManagementAvailable" + can-browse="canBrowse" > diff --git a/app/portainer/views/registries/registriesController.js b/app/portainer/views/registries/registriesController.js index c504cf5e6..bdb0a062d 100644 --- a/app/portainer/views/registries/registriesController.js +++ b/app/portainer/views/registries/registriesController.js @@ -1,3 +1,5 @@ +import _ from 'lodash-es'; + angular.module('portainer.app') .controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'ExtensionService', 'Authentication', function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService, Authentication) { @@ -10,6 +12,12 @@ function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, N dockerHubPassword: '' }; + const nonBrowsableUrls = ['quay.io']; + + $scope.canBrowse = function(item) { + return ! _.includes(nonBrowsableUrls, item.URL); + } + $scope.updateDockerHub = function() { var dockerhub = $scope.dockerhub; dockerhub.Password = $scope.formValues.dockerHubPassword; From f20526d6622511a3d94a23d40dd076ba828ed79e Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Thu, 4 Jul 2019 15:36:29 +0200 Subject: [PATCH 14/37] fix(app): disable-authorization directive was hiding/showing elements instead of disabling them --- app/extensions/rbac/directives/disable-authorization.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/extensions/rbac/directives/disable-authorization.js b/app/extensions/rbac/directives/disable-authorization.js index 5dad08074..7efb113ee 100644 --- a/app/extensions/rbac/directives/disable-authorization.js +++ b/app/extensions/rbac/directives/disable-authorization.js @@ -5,11 +5,9 @@ angular.module('portainer.extensions.rbac') try { const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); if (!rbacEnabled) { - elem.show(); return; } } catch (err) { - elem.show(); return; } From 4084e7c8ece779fce323b05358b1b8c2d7f569b9 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Thu, 4 Jul 2019 23:46:59 +0200 Subject: [PATCH 15/37] feat(home): pagination on endpoints list (#2981) --- .../endpoint-list/endpoint-list-controller.js | 12 ++++++++--- .../endpoint-list/endpointList.html | 21 ++++++++++++++++++- app/portainer/views/home/home.html | 2 +- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/portainer/components/endpoint-list/endpoint-list-controller.js b/app/portainer/components/endpoint-list/endpoint-list-controller.js index 53c9308a9..b9851a804 100644 --- a/app/portainer/components/endpoint-list/endpoint-list-controller.js +++ b/app/portainer/components/endpoint-list/endpoint-list-controller.js @@ -1,11 +1,12 @@ import _ from 'lodash-es'; -angular.module('portainer.app').controller('EndpointListController', ['DatatableService', - function EndpointListController(DatatableService) { +angular.module('portainer.app').controller('EndpointListController', ['DatatableService', 'PaginationService', + function EndpointListController(DatatableService, PaginationService) { var ctrl = this; ctrl.state = { textFilter: '', - filteredEndpoints: [] + filteredEndpoints: [], + paginatedItemLimit: '10' }; ctrl.$onChanges = $onChanges; @@ -58,12 +59,17 @@ angular.module('portainer.app').controller('EndpointListController', ['Datatable }); } + this.changePaginationLimit = function() { + PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); + }; + function convertStatusToString(status) { return status === 1 ? 'up' : 'down'; } function $onInit() { var textFilter = DatatableService.getDataTableTextFilters(ctrl.tableKey); + this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey); if (textFilter !== null) { ctrl.state.textFilter = textFilter; onTextFilterChange(); diff --git a/app/portainer/components/endpoint-list/endpointList.html b/app/portainer/components/endpoint-list/endpointList.html index f429be03f..9909a6ed7 100644 --- a/app/portainer/components/endpoint-list/endpointList.html +++ b/app/portainer/components/endpoint-list/endpointList.html @@ -26,7 +26,7 @@
+ diff --git a/app/portainer/views/home/home.html b/app/portainer/views/home/home.html index 2fb8a4afc..ac008b153 100644 --- a/app/portainer/views/home/home.html +++ b/app/portainer/views/home/home.html @@ -28,7 +28,7 @@
Date: Wed, 10 Jul 2019 17:16:43 +1200 Subject: [PATCH 16/37] style(container-creation): clarify ports mapping --- app/docker/views/containers/create/createcontainer.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index c76e77225..a2d688d84 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -50,13 +50,13 @@
- Ports configuration + Network ports configuration
- +
From a85605333886143776ead33fb9843147714b8def Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Mon, 15 Jul 2019 13:28:30 +0200 Subject: [PATCH 19/37] fix(containers): multiple clics on image commit button were creating a lot of images --- app/docker/views/containers/edit/containerController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index 7a9aff9c9..5e7b5f127 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -151,12 +151,12 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co $scope.commit = function () { var image = $scope.config.Image; + $scope.config.Image = ''; var registry = $scope.config.Registry; var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL); Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function () { update(); Notifications.success('Container commited', $transition$.params().id); - $scope.config.Image = ''; }, function (e) { update(); Notifications.error('Failure', e, 'Unable to commit container'); From c432ead45fc1525f0f33fbcbdf01c1fa2951cad0 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Wed, 17 Jul 2019 06:34:31 +0200 Subject: [PATCH 20/37] fix(api): AddCron fix after library update (#3014) --- api/cron/scheduler.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/cron/scheduler.go b/api/cron/scheduler.go index 9a2de7450..85b27398b 100644 --- a/api/cron/scheduler.go +++ b/api/cron/scheduler.go @@ -19,7 +19,8 @@ func NewJobScheduler() *JobScheduler { // ScheduleJob schedules the execution of a job via a runner func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error { - return scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner) + _, err := scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner) + return err } // UpdateSystemJobSchedule updates the first occurence of the specified @@ -35,7 +36,7 @@ func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType for _, entry := range cronEntries { if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType { - err := newCron.AddJob(newCronExpression, entry.Job) + _, err := newCron.AddJob(newCronExpression, entry.Job) if err != nil { return err } @@ -69,7 +70,7 @@ func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) err jobRunner = entry.Job } - err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner) + _, err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner) if err != nil { return err } From 92a615d7b683cb3078b090136056cdea127eba2d Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 16 Jul 2019 22:22:57 -0700 Subject: [PATCH 21/37] Revert "fix(api): AddCron fix after library update (#3014)" (#3016) This reverts commit c432ead45fc1525f0f33fbcbdf01c1fa2951cad0. --- api/cron/scheduler.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api/cron/scheduler.go b/api/cron/scheduler.go index 85b27398b..9a2de7450 100644 --- a/api/cron/scheduler.go +++ b/api/cron/scheduler.go @@ -19,8 +19,7 @@ func NewJobScheduler() *JobScheduler { // ScheduleJob schedules the execution of a job via a runner func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error { - _, err := scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner) - return err + return scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner) } // UpdateSystemJobSchedule updates the first occurence of the specified @@ -36,7 +35,7 @@ func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType for _, entry := range cronEntries { if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType { - _, err := newCron.AddJob(newCronExpression, entry.Job) + err := newCron.AddJob(newCronExpression, entry.Job) if err != nil { return err } @@ -70,7 +69,7 @@ func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) err jobRunner = entry.Job } - _, err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner) + err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner) if err != nil { return err } From f71a565acc177f893ba241df77099ef00d3f90a3 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Thu, 18 Jul 2019 17:19:00 +0200 Subject: [PATCH 22/37] refactor(container): reword notification messages --- app/docker/views/containers/edit/containerController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index 5e7b5f127..12f7ce7ca 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -156,10 +156,10 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL); Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function () { update(); - Notifications.success('Container commited', $transition$.params().id); + Notifications.success('Image created', $transition$.params().id); }, function (e) { update(); - Notifications.error('Failure', e, 'Unable to commit container'); + Notifications.error('Failure', e, 'Unable to create image'); }); }; From 0b7500827be84722b6fef1bafedf0050523f7f4d Mon Sep 17 00:00:00 2001 From: William Date: Fri, 19 Jul 2019 11:28:08 +1200 Subject: [PATCH 23/37] chore(project): stalebot integration (#3019) * chore(project): stalebot integration * chore(project): put file in correct directory --- .github/stale.yml | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..b2b38e3fc --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,53 @@ +# Config for Stalebot, limited to only `issues` +only: issues + +# Issues config +issues: + daysUntilStale: 60 + daysUntilClose: 7 + + # Limit the number of actions per hour, from 1-30. Default is 30 + limitPerRun: 30 + + # Issues with these labels will never be considered stale + exemptLabels: + - kind/enhancement + - kind/feature + - kind/question + - bug/need-confirmation + - bug/confirmed + - status/discuss + + # Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled) + onlyLabels: [] + + # Set to true to ignore issues in a project (defaults to false) + exemptProjects: true + # Set to true to ignore issues in a milestone (defaults to false) + exemptMilestones: true + # Set to true to ignore issues with an assignee (defaults to false) + exemptAssignees: true + + # Label to use when marking an issue as stale + staleLabel: status/stale + + # Comment to post when marking an issue as stale. Set to `false` to disable + markComment: > + This issue has been marked as stale as it has not had recent activity, + it will be closed if no further activity occurs in the next 7 days. + If you believe that it has been incorrectly labelled as stale, + leave a comment and the label will be removed. + + # Comment to post when removing the stale label. + # unmarkComment: > + # Your comment here. + + # Comment to post when closing a stale issue. Set to `false` to disable + closeComment: > + Since no further activity has appeared on this issue it will be closed. + If you believe that it has been incorrectly closed, leave a comment + and mention @itsconquest. One of our staff will then review the issue. + + Note - If it is an old bug report, make sure that it is reproduceable in the + latest version of Portainer as it may have already been fixed. + \ No newline at end of file From d52a1a870c6b3f9a9af0958cbb28e5c75af243ef Mon Sep 17 00:00:00 2001 From: William Date: Fri, 19 Jul 2019 12:46:25 +1200 Subject: [PATCH 24/37] chore(project): clarify bug template (#3021) --- .github/ISSUE_TEMPLATE/Bug_report.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 4da716c9a..aee590d7e 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -21,24 +21,18 @@ Also, be sure to check our FAQ and documentation first: https://portainer.readth --> **Bug description** - A clear and concise description of what the bug is. **Expected behavior** A clear and concise description of what you expected to happen. -Briefly describe what you were expecting. - **Steps to reproduce the issue:** - -Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Technical details:** - * Portainer version: * Docker version (managed by Portainer): * Platform (windows/linux): From 90d3f3a358e7e778ff8ca64f7e9a933d67516be8 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sat, 20 Jul 2019 16:28:11 -0700 Subject: [PATCH 25/37] Enable endpoint backend pagination (#2989) * feat(api): remove SnapshotRaw from EndpointList response * feat(api): add pagination for EndpointList operation * feat(api): rename last_id query parameter to start * feat(api): implement filter for EndpointList operation * feat(home): front - endpoint backend pagination (#2990) * feat(home): endpoint pagination with backend * feat(api): remove default limit value * fix(endpoints): fix a minor issue with column span * fix(endpointgroup-create): fix an issue with endpoint group creation * feat(app): minor loading optimizations * refactor(api): small refactor of EndpointList operation * fix(home): fix minor loading text display issue * refactor(api): document bolt services functions * feat(home): minor optimization * fix(api): replace seek with index scanning for EndpointPaginated * fix(api): fix invalid starting index issue * fix(api): first implementation of working filter * fix(home): endpoints list keeps backend pagination when it needs to * fix(api): endpoint pagination doesn't drop the first item on pages >=2 anymore * fix(home): UI flickering on page/filter load/change * feat(api): support searching in associated endpoint group data * feat(api): declare EndpointList params as optional * feat(endpoints): backend pagination for endpoints view (#3004) * feat(endpoint-group): enable backend pagination (#3017) * feat(api): support groupID filter on endpoints route * feat(api): add new API operations endpointGroupAddEndpoint and endpointGroupDeleteEndpoint * feat(endpoint-groups): backend pagination support for create and edit * feat(endpoint-groups): debounce on filter for create/edit views * feat(endpoint-groups): filter assigned on create view * (endpoint-groups): unassigned endpoints edit view * refactor(endpoint-groups): code clean * feat(endpoint-groups): remove message for Unassigned group * refactor(api): endpoint group endpoint association refactor * refactor(api): rename files and remove comments * refactor(api): remove usage of utils * refactor(api): optional parameters * feat(api): update endpointListOperation behavior and parameters * refactor(api): remove unused methods associated to EndpointService * refactor(api): remove unused methods associated to EndpointService * refactor(api): minor refactor --- api/bolt/endpoint/endpoint.go | 3 +- .../endpointgroups/endpointgroup_create.go | 16 +- .../endpointgroup_endpoint_add.go | 46 ++++++ .../endpointgroup_endpoint_delete.go | 46 ++++++ .../endpointgroups/endpointgroup_update.go | 25 +--- api/http/handler/endpointgroups/handler.go | 36 +---- api/http/handler/endpoints/endpoint_list.go | 137 +++++++++++++++++- api/http/handler/endpoints/handler.go | 3 + api/portainer.go | 2 +- .../access-viewer/accessViewerController.js | 2 +- .../endpointsDatatable.html | 23 ++- .../endpoints-datatable/endpointsDatatable.js | 6 +- .../endpointsDatatableController.js | 80 ++++++++++ .../endpoint-list/endpoint-list-controller.js | 81 +++++++---- .../components/endpoint-list/endpoint-list.js | 4 +- .../endpoint-list/endpointList.html | 19 ++- .../components/forms/group-form/group-form.js | 109 +++++++++++--- .../forms/group-form/groupForm.html | 30 ++-- .../group-association-table.js | 35 ++++- .../groupAssociationTable.html | 27 +++- app/portainer/rest/endpoint.js | 8 +- app/portainer/rest/group.js | 2 + .../rest/transform/getEndpointsTotalCount.js | 6 + app/portainer/services/api/endpointService.js | 21 +-- app/portainer/services/api/groupService.js | 8 + app/portainer/views/auth/authController.js | 8 +- app/portainer/views/endpoints/endpoints.html | 3 +- .../views/endpoints/endpointsController.js | 18 +-- .../groups/create/createGroupController.js | 20 +-- .../views/groups/create/creategroup.html | 2 + app/portainer/views/groups/edit/group.html | 2 + .../views/groups/edit/groupController.js | 30 +--- app/portainer/views/home/home.html | 4 +- app/portainer/views/home/homeController.js | 50 ++++--- .../views/init/admin/initAdminController.js | 4 +- .../create/createScheduleController.js | 2 +- .../schedules/edit/scheduleController.js | 4 +- .../views/stacks/edit/stackController.js | 2 +- 38 files changed, 681 insertions(+), 243 deletions(-) create mode 100644 api/http/handler/endpointgroups/endpointgroup_endpoint_add.go create mode 100644 api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go create mode 100644 app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js create mode 100644 app/portainer/rest/transform/getEndpointsTotalCount.js diff --git a/api/bolt/endpoint/endpoint.go b/api/bolt/endpoint/endpoint.go index 723c2046b..53156d2a2 100644 --- a/api/bolt/endpoint/endpoint.go +++ b/api/bolt/endpoint/endpoint.go @@ -1,10 +1,9 @@ package endpoint import ( + "github.com/boltdb/bolt" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/internal" - - "github.com/boltdb/bolt" ) const ( diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index 6cfb72468..32a617c92 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -53,11 +53,17 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} } - for _, endpoint := range endpoints { - if endpoint.GroupID == portainer.EndpointGroupID(1) { - err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, payload.AssociatedEndpoints) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + for _, id := range payload.AssociatedEndpoints { + for _, endpoint := range endpoints { + if endpoint.ID == id { + endpoint.GroupID = endpointGroup.ID + + err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + } + + break } } } diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go new file mode 100644 index 000000000..c67f730e0 --- /dev/null +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go @@ -0,0 +1,46 @@ +package endpointgroups + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" +) + +// PUT request on /api/endpoint_groups/:id/endpoints/:endpointId +func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} + } + + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + } + + endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} + } + + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + endpoint.GroupID = endpointGroup.ID + + err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + } + + return response.Empty(w) +} diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go new file mode 100644 index 000000000..2054b428f --- /dev/null +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go @@ -0,0 +1,46 @@ +package endpointgroups + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" +) + +// DELETE request on /api/endpoint_groups/:id/endpoints/:endpointId +func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} + } + + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + } + + _, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} + } + + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + endpoint.GroupID = portainer.EndpointGroupID(1) + + err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + } + + return response.Empty(w) +} diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 1aa523403..92dbc9037 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -10,12 +10,11 @@ import ( ) type endpointGroupUpdatePayload struct { - Name string - Description string - AssociatedEndpoints []portainer.EndpointID - Tags []string - UserAccessPolicies portainer.UserAccessPolicies - TeamAccessPolicies portainer.TeamAccessPolicies + Name string + Description string + Tags []string + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies } func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error { @@ -67,19 +66,5 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err} } - if payload.AssociatedEndpoints != nil { - endpoints, err := handler.EndpointService.Endpoints() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} - } - - for _, endpoint := range endpoints { - err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} - } - } - } - return response.JSON(w, endpointGroup) } diff --git a/api/http/handler/endpointgroups/handler.go b/api/http/handler/endpointgroups/handler.go index c210373e9..fa10d92c9 100644 --- a/api/http/handler/endpointgroups/handler.go +++ b/api/http/handler/endpointgroups/handler.go @@ -31,37 +31,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut) h.Handle("/endpoint_groups/{id}", bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete) - + h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}", + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupAddEndpoint))).Methods(http.MethodPut) + h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}", + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete) return h } - -func (handler *Handler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error { - for _, id := range associatedEndpoints { - if id == endpoint.ID { - return nil - } - } - - endpoint.GroupID = portainer.EndpointGroupID(1) - return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) -} - -func (handler *Handler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error { - for _, id := range associatedEndpoints { - - if id == endpoint.ID { - endpoint.GroupID = groupID - return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) - } - } - return nil -} - -func (handler *Handler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error { - if endpoint.GroupID == groupID { - return handler.checkForGroupUnassignment(endpoint, associatedEndpoints) - } else if endpoint.GroupID == portainer.EndpointGroupID(1) { - return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints) - } - return nil -} diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index d7e1ba173..b89899d16 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -2,24 +2,43 @@ package endpoints import ( "net/http" + "strconv" + "strings" + + portainer "github.com/portainer/portainer/api" + + "github.com/portainer/libhttp/request" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api/http/security" ) -// GET request on /api/endpoints +// GET request on /api/endpoints?(start=)&(limit=)&(search=)&(groupId= endpointCount { + start = endpointCount + } + + end := start + limit + if end > endpointCount { + end = endpointCount + } + + return endpoints[start:end] +} + +func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID portainer.EndpointGroupID) []portainer.Endpoint { + filteredEndpoints := make([]portainer.Endpoint, 0) + + for _, endpoint := range endpoints { + if endpoint.GroupID == endpointGroupID { + filteredEndpoints = append(filteredEndpoints, endpoint) + } + } + + return filteredEndpoints +} + +func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) []portainer.Endpoint { + filteredEndpoints := make([]portainer.Endpoint, 0) + + for _, endpoint := range endpoints { + + if endpointMatchSearchCriteria(&endpoint, searchCriteria) { + filteredEndpoints = append(filteredEndpoints, endpoint) + continue + } + + if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, searchCriteria) { + filteredEndpoints = append(filteredEndpoints, endpoint) + } + } + + return filteredEndpoints +} + +func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria string) bool { + if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) { + return true + } + + if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) { + return true + } + + if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" { + return true + } else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" { + return true + } + + for _, tag := range endpoint.Tags { + if strings.Contains(strings.ToLower(tag), searchCriteria) { + return true + } + } + + return false +} + +func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) bool { + for _, group := range endpointGroups { + if group.ID == endpoint.GroupID { + if strings.Contains(strings.ToLower(group.Name), searchCriteria) { + return true + } + + for _, tag := range group.Tags { + if strings.Contains(strings.ToLower(tag), searchCriteria) { + return true + } + } + } + } + + return false } diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 03192f47f..9896bd92a 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -19,6 +19,9 @@ const ( func hideFields(endpoint *portainer.Endpoint) { endpoint.AzureCredentials = portainer.AzureCredentials{} + if len(endpoint.Snapshots) > 0 { + endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{} + } } // Handler is the HTTP handler used to handle endpoint operations. diff --git a/api/portainer.go b/api/portainer.go index 1cb48f826..852552ba6 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -876,7 +876,7 @@ const ( PortainerAgentSignatureMessage = "Portainer-App" // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer SupportedDockerAPIVersion = "1.24" - // ExtensionServer represents the server used by Portainer to communicate with extensions + // ExtensionServer represents the server used by Portainer to communicate with extensions ExtensionServer = "localhost" ) diff --git a/app/extensions/rbac/components/access-viewer/accessViewerController.js b/app/extensions/rbac/components/access-viewer/accessViewerController.js index 40c06754b..7b4fc5d4d 100644 --- a/app/extensions/rbac/components/access-viewer/accessViewerController.js +++ b/app/extensions/rbac/components/access-viewer/accessViewerController.js @@ -103,7 +103,7 @@ class AccessViewerController { this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC); if (this.rbacEnabled) { this.users = await this.UserService.users(); - this.endpoints = _.keyBy(await this.EndpointService.endpoints(), 'Id'); + this.endpoints = _.keyBy((await this.EndpointService.endpoints()).value, 'Id'); const groups = await this.GroupService.groups(); this.groupUserAccessPolicies = {}; this.groupTeamAccessPolicies = {}; diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html index e61721001..a9f360e8e 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html @@ -17,7 +17,12 @@
+ @@ -34,8 +34,8 @@
- Filter - Filter + Filter + Filter
@@ -106,7 +106,7 @@
- + {{ item.Id | truncate:40 }} diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js b/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js index e7a88dd96..81bb158fb 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js @@ -1,20 +1,13 @@ angular.module('portainer.docker') -.controller('VolumesDatatableController', ['PaginationService', 'DatatableService', -function (PaginationService, DatatableService) { +.controller('VolumesDatatableController', ['$scope', '$controller', 'DatatableService', +function ($scope, $controller, DatatableService) { + + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); var ctrl = this; - this.state = { - selectAll: false, - orderBy: this.orderBy, - paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), - displayTextFilter: false, - selectedItemCount: 0, - selectedItems: [] - }; - this.filters = { - usage: { + state: { open: false, enabled: false, showUsedVolumes: true, @@ -22,62 +15,29 @@ function (PaginationService, DatatableService) { } }; - this.onTextFilterChange = function() { - DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); - }; - - this.changeOrderBy = function(orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.selectItem = function(item) { - if (item.Checked) { - this.state.selectedItemCount++; - this.state.selectedItems.push(item); - } else { - this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); - this.state.selectedItemCount--; - } - }; - - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); - } - } - }; - - this.changePaginationLimit = function() { - PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); - }; - this.applyFilters = function(value) { var volume = value; var filters = ctrl.filters; - if ((volume.dangling && filters.usage.showUnusedVolumes) - || (!volume.dangling && filters.usage.showUsedVolumes)) { + if ((volume.dangling && filters.state.showUnusedVolumes) + || (!volume.dangling && filters.state.showUsedVolumes)) { return true; } return false; }; - this.onUsageFilterChange = function() { - var filters = this.filters.usage; + this.onstateFilterChange = function() { + var filters = this.filters.state; var filtered = false; if (!filters.showUsedVolumes || !filters.showUnusedVolumes) { filtered = true; } - this.filters.usage.enabled = filtered; + this.filters.state.enabled = filtered; DatatableService.setDataTableFilters(this.tableKey, this.filters); }; this.$onInit = function() { - setDefaults(this); + this.setDefaults(); + this.prepareTableFromDataset(); var storedOrder = DatatableService.getDataTableOrder(this.tableKey); if (storedOrder !== null) { @@ -85,20 +45,18 @@ function (PaginationService, DatatableService) { this.state.orderBy = storedOrder.orderBy; } + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); if (storedFilters !== null) { this.filters = storedFilters; } - this.filters.usage.open = false; - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; + if (this.filters && this.filters.state) { + this.filters.state.open = false; } }; - - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - } }]); diff --git a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html index c416605a3..7ea3164b1 100644 --- a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html +++ b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html @@ -39,7 +39,7 @@ ng-class="{active: item.Checked}"> - + - + {{ item.Name }} diff --git a/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js index 65f44972b..b70bae033 100644 --- a/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js +++ b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js @@ -1,15 +1,34 @@ angular.module('portainer.docker') - .controller('StoridgeDrivesDatatableController', ['$scope', '$controller', - function ($scope, $controller) { + .controller('StoridgeDrivesDatatableController', ['$scope', '$controller', 'DatatableService', + function ($scope, $controller, DatatableService) { angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (item.Status !== 'normal' && item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); - } + this.allowSelection = function (item) { + return item.Status !== 'normal'; + }; + + this.$onInit = function() { + this.setDefaults(); + this.prepareTableFromDataset(); + + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; } }; } diff --git a/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js index 517b6566c..c3b2e635d 100644 --- a/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js +++ b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js @@ -1,6 +1,6 @@ angular.module('portainer.integrations.storidge') -.controller('StoridgeNodesDatatableController', ['$scope', '$controller', 'clipboard', 'Notifications', 'StoridgeNodeService', -function($scope, $controller, clipboard, Notifications, StoridgeNodeService) { +.controller('StoridgeNodesDatatableController', ['$scope', '$controller', 'clipboard', 'Notifications', 'StoridgeNodeService', 'DatatableService', +function($scope, $controller, clipboard, Notifications, StoridgeNodeService, DatatableService) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); var ctrl = this; @@ -20,4 +20,29 @@ function($scope, $controller, clipboard, Notifications, StoridgeNodeService) { $('#copyNotification').show(); $('#copyNotification').fadeOut(2000); }; + + this.$onInit = function() { + this.setDefaults(); + this.prepareTableFromDataset(); + + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + }; }]); diff --git a/app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.html b/app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.html index 594e1d079..36147404f 100644 --- a/app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.html +++ b/app/integrations/storidge/components/profiles-datatable/storidgeProfilesDatatable.html @@ -37,7 +37,7 @@
- + {{ item.Name }} diff --git a/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html index bd652567a..915e07280 100644 --- a/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html +++ b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.html @@ -51,7 +51,7 @@
- + {{ item.Id }} diff --git a/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js index ee0ef830d..e479f7c29 100644 --- a/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js +++ b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatable.js @@ -1,6 +1,6 @@ angular.module('portainer.integrations.storidge').component('storidgeSnapshotsDatatable', { templateUrl: './storidgeSnapshotsDatatable.html', - controller: 'StoridgeSnapshotsDatatableController', + controller: 'GenericDatatableController', bindings: { titleText: '@', titleIcon: '@', diff --git a/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js b/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js deleted file mode 100644 index 501a74343..000000000 --- a/app/integrations/storidge/components/snapshots-datatable/storidgeSnapshotsDatatableController.js +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('portainer.docker') - .controller('StoridgeSnapshotsDatatableController', ['$scope', '$controller', - function ($scope, $controller) { - angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); - - } -]); \ No newline at end of file diff --git a/app/portainer/components/access-datatable/accessDatatable.html b/app/portainer/components/access-datatable/accessDatatable.html index 7defc92a7..2d33f779e 100644 --- a/app/portainer/components/access-datatable/accessDatatable.html +++ b/app/portainer/components/access-datatable/accessDatatable.html @@ -70,7 +70,7 @@ + ng-click="$ctrl.selectItem(item, $event)" /> {{ item.Name }} diff --git a/app/portainer/components/access-datatable/accessDatatableController.js b/app/portainer/components/access-datatable/accessDatatableController.js index 7004fb81c..de143c72d 100644 --- a/app/portainer/components/access-datatable/accessDatatableController.js +++ b/app/portainer/components/access-datatable/accessDatatableController.js @@ -1,19 +1,38 @@ angular.module('portainer.app') - .controller('AccessDatatableController', ['$scope', '$controller', - function ($scope, $controller) { + .controller('AccessDatatableController', ['$scope', '$controller', 'DatatableService', + function ($scope, $controller, DatatableService) { angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); this.disableRemove = function(item) { return item.Inherited; }; - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (!this.disableRemove(item) && item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); - } + this.allowSelection = function(item) { + return !this.disableRemove(item); + }; + + this.$onInit = function() { + this.setDefaults(); + this.prepareTableFromDataset(); + + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; } }; } diff --git a/app/portainer/components/datatables/datatable.css b/app/portainer/components/datatables/datatable.css index 8f95085d5..10259d119 100644 --- a/app/portainer/components/datatables/datatable.css +++ b/app/portainer/components/datatables/datatable.css @@ -145,6 +145,7 @@ left: 0; position: absolute; top: 0; + pointer-events: none; } .md-checkbox label:before { diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html index d83793598..e61721001 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html @@ -62,7 +62,7 @@
- + {{ item.Name }} diff --git a/app/portainer/components/datatables/genericDatatableController.js b/app/portainer/components/datatables/genericDatatableController.js index 3fb1db1c8..21af10060 100644 --- a/app/portainer/components/datatables/genericDatatableController.js +++ b/app/portainer/components/datatables/genericDatatableController.js @@ -1,5 +1,10 @@ +import _ from 'lodash-es'; import './datatable.css'; +function isBetween(value, a, b) { + return (value >= a && value <= b) || (value >= b && value <= a) ; +} + angular.module('portainer.app') .controller('GenericDatatableController', ['PaginationService', 'DatatableService', 'PAGINATION_MAX_ITEMS', function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { @@ -9,7 +14,9 @@ function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { orderBy: this.orderBy, paginatedItemLimit: PAGINATION_MAX_ITEMS, displayTextFilter: false, - selectedItemCount: 0, + get selectedItemCount() { + return this.selectedItems.length || 0; + }, selectedItems: [] }; @@ -24,32 +31,86 @@ function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); }; - this.selectItem = function(item) { - if (item.Checked) { - this.state.selectedItemCount++; - this.state.selectedItems.push(item); - } else { - this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); - this.state.selectedItemCount--; + this.selectItem = function(item, event) { + // Handle range select using shift + if (event && event.originalEvent.shiftKey && this.state.firstClickedItem) { + const firstItemIndex = this.state.filteredDataSet.indexOf(this.state.firstClickedItem); + const lastItemIndex = this.state.filteredDataSet.indexOf(item); + const itemsInRange = _.filter(this.state.filteredDataSet, (item, index) => { + return isBetween(index, firstItemIndex, lastItemIndex); + }); + const value = item.Checked; + + _.forEach(itemsInRange, (i) => { + if (!this.allowSelection(i)) { + return; + } + i.Checked = value; + }); + this.state.firstClickedItem = item; + } else if (event) { + this.state.firstClickedItem = item; } + this.state.selectedItems = this.state.filteredDataSet.filter(i => i.Checked); + if (event && this.state.selectAll && this.state.selectedItems.length !== this.state.filteredDataSet.length) { + this.state.selectAll = false; + } + this.onSelectionChanged(); }; this.selectAll = function() { + this.state.firstClickedItem = null; for (var i = 0; i < this.state.filteredDataSet.length; i++) { var item = this.state.filteredDataSet[i]; - if (item.Checked !== this.state.selectAll) { + if (this.allowSelection(item) && item.Checked !== this.state.selectAll) { item.Checked = this.state.selectAll; this.selectItem(item); } } + this.onSelectionChanged(); }; + /** + * Override this method to allow/deny selection + */ + this.allowSelection = function(/*item*/) { + return true; + } + + /** + * Override this method to prepare data table + */ + this.prepareTableFromDataset = function() { + return; + } + + /** + * Override this method to execute code after selection changed on datatable + */ + this.onSelectionChanged = function () { + return; + } + this.changePaginationLimit = function() { PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); }; + this.setDefaults = function() { + this.showTextFilter = this.showTextFilter ? this.showTextFilter : false; + this.state.reverseOrder = this.reverseOrder ? this.reverseOrder : false; + this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey); + } + + /** + * Duplicate this function when extending GenericDatatableController + * Extending-controller's bindings are not accessible there + * For more details see the following comments + * https://github.com/portainer/portainer/pull/2877#issuecomment-503333425 + * https://github.com/portainer/portainer/pull/2877#issuecomment-503537249 + */ this.$onInit = function() { - setDefaults(this); + this.setDefaults(); + this.prepareTableFromDataset(); var storedOrder = DatatableService.getDataTableOrder(this.tableKey); if (storedOrder !== null) { @@ -62,11 +123,13 @@ function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { this.state.textFilter = textFilter; this.onTextFilterChange(); } - }; - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - ctrl.state.paginatedItemLimit = PaginationService.getPaginationLimit(ctrl.tableKey); - } + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + }; }]); diff --git a/app/portainer/components/datatables/groups-datatable/groupsDatatable.html b/app/portainer/components/datatables/groups-datatable/groupsDatatable.html index 003469270..64a63c413 100644 --- a/app/portainer/components/datatables/groups-datatable/groupsDatatable.html +++ b/app/portainer/components/datatables/groups-datatable/groupsDatatable.html @@ -41,7 +41,7 @@
- + {{ item.Name }} diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html index 58377e2c3..f26e77443 100644 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html @@ -48,7 +48,7 @@
- + {{ item.Name }} diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html b/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html index adccfff95..86134c34e 100644 --- a/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html +++ b/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html @@ -54,7 +54,7 @@
- + {{ item.Name }} diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js index bac777070..498e72ff3 100644 --- a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js +++ b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js @@ -1,58 +1,39 @@ angular.module('portainer.app') -.controller('SchedulesDatatableController', ['PaginationService', 'DatatableService', -function (PaginationService, DatatableService) { + .controller('SchedulesDatatableController', ['$scope', '$controller', 'DatatableService', + function ($scope, $controller, DatatableService) { - this.state = { - selectAll: false, - orderBy: this.orderBy, - paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), - displayTextFilter: false, - selectedItemCount: 0, - selectedItems: [] - }; + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); - this.changeOrderBy = function(orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.selectItem = function(item) { - if (item.Checked) { - this.state.selectedItemCount++; - this.state.selectedItems.push(item); - } else { - this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); - this.state.selectedItemCount--; - } - }; - - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (item.JobType ===1 && item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); + /** + * Do not allow items + */ + this.allowSelection = function(item) { + return item.JobType === 1 } - } - }; - this.changePaginationLimit = function() { - PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); - }; + this.$onInit = function() { + this.setDefaults(); + this.prepareTableFromDataset(); - this.$onInit = function() { - setDefaults(this); + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; - } - }; + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + }; } -}]); +]); diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html index c149d58fc..6bd6852f3 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html @@ -55,7 +55,7 @@
- + {{ item.Name }} diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js b/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js index a1d735964..6c8457cd6 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js @@ -1,52 +1,19 @@ angular.module('portainer.app') -.controller('StacksDatatableController', ['PaginationService', 'DatatableService', -function (PaginationService, DatatableService) { +.controller('StacksDatatableController', ['$scope', '$controller', 'DatatableService', +function ($scope, $controller, DatatableService) { - this.state = { - selectAll: false, - orderBy: this.orderBy, - paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), - displayTextFilter: false, - selectedItemCount: 0, - selectedItems: [] - }; + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); - this.onTextFilterChange = function() { - DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); - }; - - this.changeOrderBy = function(orderField) { - this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; - this.state.orderBy = orderField; - DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - }; - - this.selectItem = function(item) { - if (item.Checked) { - this.state.selectedItemCount++; - this.state.selectedItems.push(item); - } else { - this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); - this.state.selectedItemCount--; - } - }; - - this.selectAll = function() { - for (var i = 0; i < this.state.filteredDataSet.length; i++) { - var item = this.state.filteredDataSet[i]; - if (!(item.External && item.Type === 2) && item.Checked !== this.state.selectAll) { - item.Checked = this.state.selectAll; - this.selectItem(item); - } - } - }; - - this.changePaginationLimit = function() { - PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); - }; + /** + * Do not allow external items + */ + this.allowSelection = function(item) { + return !(item.External && item.Type === 2); + } this.$onInit = function() { - setDefaults(this); + this.setDefaults(); + this.prepareTableFromDataset(); var storedOrder = DatatableService.getDataTableOrder(this.tableKey); if (storedOrder !== null) { @@ -57,11 +24,16 @@ function (PaginationService, DatatableService) { var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); if (textFilter !== null) { this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; } }; - function setDefaults(ctrl) { - ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; - ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; - } }]); diff --git a/app/portainer/components/datatables/tags-datatable/tagsDatatable.html b/app/portainer/components/datatables/tags-datatable/tagsDatatable.html index e4eade921..a34d8b8a2 100644 --- a/app/portainer/components/datatables/tags-datatable/tagsDatatable.html +++ b/app/portainer/components/datatables/tags-datatable/tagsDatatable.html @@ -37,7 +37,7 @@
- + {{ item.Name }} diff --git a/app/portainer/components/datatables/teams-datatable/teamsDatatable.html b/app/portainer/components/datatables/teams-datatable/teamsDatatable.html index 0d056e512..268526737 100644 --- a/app/portainer/components/datatables/teams-datatable/teamsDatatable.html +++ b/app/portainer/components/datatables/teams-datatable/teamsDatatable.html @@ -37,7 +37,7 @@
- + {{ item.Name }} diff --git a/app/portainer/components/datatables/users-datatable/usersDatatable.html b/app/portainer/components/datatables/users-datatable/usersDatatable.html index f6848c7ba..74ce4b5b0 100644 --- a/app/portainer/components/datatables/users-datatable/usersDatatable.html +++ b/app/portainer/components/datatables/users-datatable/usersDatatable.html @@ -51,7 +51,7 @@
- + {{ item.Username }} From 3d4af7c54fcb2aba9e9e62e9e821b28cc137b4f0 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Wed, 3 Jul 2019 00:33:46 +0200 Subject: [PATCH 13/37] feat(registry): disable browse for quay.io registry (#2971) * feat(registry): disable browse for quay.io registry * refactor(registry): browsable urls check done with function --- .../registries-datatable/registriesDatatable.html | 4 ++-- .../registries-datatable/registriesDatatable.js | 3 ++- app/portainer/views/registries/registries.html | 1 + app/portainer/views/registries/registriesController.js | 8 ++++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html index f26e77443..45ebc13a8 100644 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html @@ -62,10 +62,10 @@ Manage access - + Browse - + Browse (extension)
@@ -59,7 +64,9 @@ - + - - + + - - + +
@@ -82,16 +89,16 @@
Loading...
Loading...
No endpoint available.
No endpoint available.
- diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js index f355e559f..a452cf396 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js @@ -1,15 +1,15 @@ angular.module('portainer.app').component('endpointsDatatable', { templateUrl: './endpointsDatatable.html', - controller: 'GenericDatatableController', + controller: 'EndpointsDatatableController', bindings: { titleText: '@', titleIcon: '@', - dataset: '<', tableKey: '@', orderBy: '@', reverseOrder: '<', endpointManagement: '<', accessManagement: '<', - removeAction: '<' + removeAction: '<', + retrievePage: '<' } }); diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js new file mode 100644 index 000000000..0cdc1573d --- /dev/null +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js @@ -0,0 +1,80 @@ +angular.module('portainer.app') + .controller('EndpointsDatatableController', ['$scope', '$controller', 'DatatableService', 'PaginationService', + function ($scope, $controller, DatatableService, PaginationService) { + + angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); + + this.state = Object.assign(this.state, { + orderBy: this.orderBy, + loading: true, + filteredDataSet: [], + totalFilteredDataset: 0, + pageNumber: 1 + }); + + this.paginationChanged = function() { + this.state.loading = true; + this.state.filteredDataSet = []; + const start = (this.state.pageNumber - 1) * this.state.paginatedItemLimit + 1; + this.retrievePage(start, this.state.paginatedItemLimit, this.state.textFilter) + .then((data) => { + this.state.filteredDataSet = data.endpoints; + this.state.totalFilteredDataSet = data.totalCount; + }).finally(() => { + this.state.loading = false; + }); + } + + this.onPageChange = function(newPageNumber) { + this.state.pageNumber = newPageNumber; + this.paginationChanged(); + } + + /** + * Overridden + */ + this.onTextFilterChange = function() { + var filterValue = this.state.textFilter; + DatatableService.setDataTableTextFilters(this.tableKey, filterValue); + this.paginationChanged(); + } + + /** + * Overridden + */ + this.changePaginationLimit = function() { + PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); + this.paginationChanged(); + }; + + /** + * Overridden + */ + this.$onInit = function() { + this.setDefaults(); + this.prepareTableFromDataset(); + + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + this.paginationChanged(); + }; + } +]); diff --git a/app/portainer/components/endpoint-list/endpoint-list-controller.js b/app/portainer/components/endpoint-list/endpoint-list-controller.js index b9851a804..bff29eb47 100644 --- a/app/portainer/components/endpoint-list/endpoint-list-controller.js +++ b/app/portainer/components/endpoint-list/endpoint-list-controller.js @@ -2,42 +2,39 @@ import _ from 'lodash-es'; angular.module('portainer.app').controller('EndpointListController', ['DatatableService', 'PaginationService', function EndpointListController(DatatableService, PaginationService) { - var ctrl = this; - ctrl.state = { + this.state = { + totalFilteredEndpoints: this.totalCount, textFilter: '', filteredEndpoints: [], - paginatedItemLimit: '10' + paginatedItemLimit: '10', + pageNumber: 1, + loading: true }; - ctrl.$onChanges = $onChanges; - ctrl.onTextFilterChange = onTextFilterChange; - ctrl.$onInit = $onInit - - function $onChanges(changesObj) { - handleEndpointsChange(changesObj.endpoints); + this.$onChanges = function(changesObj) { + this.handleEndpointsChange(changesObj.endpoints); } - function handleEndpointsChange(endpoints) { - if (!endpoints) { + this.handleEndpointsChange = function(endpoints) { + if (!endpoints || !endpoints.currentValue) { return; } - if (!endpoints.currentValue) { - return; + this.onTextFilterChange(); + } + + this.onTextFilterChange = function() { + this.state.loading = true; + var filterValue = this.state.textFilter; + DatatableService.setDataTableTextFilters(this.tableKey, filterValue); + if (this.hasBackendPagination()) { + this.paginationChangedAction(); + } else { + this.state.filteredEndpoints = frontEndpointFilter(this.endpoints, filterValue); + this.state.loading = false; } - - onTextFilterChange(); } - function onTextFilterChange() { - var filterValue = ctrl.state.textFilter; - ctrl.state.filteredEndpoints = filterEndpoints( - ctrl.endpoints, - filterValue - ); - DatatableService.setDataTableTextFilters(ctrl.tableKey, filterValue); - } - - function filterEndpoints(endpoints, filterValue) { + function frontEndpointFilter(endpoints, filterValue) { if (!endpoints || !endpoints.length || !filterValue) { return endpoints; } @@ -59,20 +56,46 @@ angular.module('portainer.app').controller('EndpointListController', ['Datatable }); } + this.hasBackendPagination = function() { + return this.totalCount && this.totalCount > 100; + } + + this.paginationChangedAction = function() { + if (this.hasBackendPagination()) { + this.state.loading = true; + this.state.filteredEndpoints = []; + const start = (this.state.pageNumber - 1) * this.state.paginatedItemLimit + 1; + this.retrievePage(start, this.state.paginatedItemLimit, this.state.textFilter) + .then((data) => { + this.state.filteredEndpoints = data.endpoints; + this.state.totalFilteredEndpoints = data.totalCount; + this.state.loading = false; + }); + } + } + + this.pageChangeHandler = function(newPageNumber) { + this.state.pageNumber = newPageNumber; + this.paginationChangedAction(); + } + this.changePaginationLimit = function() { PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); + this.paginationChangedAction(); }; function convertStatusToString(status) { return status === 1 ? 'up' : 'down'; } - function $onInit() { - var textFilter = DatatableService.getDataTableTextFilters(ctrl.tableKey); + this.$onInit = function() { + this.state.loading = true; + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey); if (textFilter !== null) { - ctrl.state.textFilter = textFilter; - onTextFilterChange(); + this.state.textFilter = textFilter; + } else { + this.paginationChangedAction(); } } } diff --git a/app/portainer/components/endpoint-list/endpoint-list.js b/app/portainer/components/endpoint-list/endpoint-list.js index e4ce9cff5..06835c9e0 100644 --- a/app/portainer/components/endpoint-list/endpoint-list.js +++ b/app/portainer/components/endpoint-list/endpoint-list.js @@ -10,6 +10,8 @@ angular.module('portainer.app').component('endpointList', { snapshotAction: '<', showSnapshotAction: '<', editAction: '<', - isAdmin:'<' + isAdmin:'<', + totalCount: '<', + retrievePage: '<' } }); diff --git a/app/portainer/components/endpoint-list/endpointList.html b/app/portainer/components/endpoint-list/endpointList.html index 9909a6ed7..028ccc122 100644 --- a/app/portainer/components/endpoint-list/endpointList.html +++ b/app/portainer/components/endpoint-list/endpointList.html @@ -21,21 +21,30 @@ class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" + ng-model-options="{ debounce: 300 }" placeholder="Search by name, group, tag, status, URL..." auto-focus>
- + -
+
Loading...
-
+
No endpoint available.
@@ -48,14 +57,14 @@ Items per page - +
diff --git a/app/portainer/components/forms/group-form/group-form.js b/app/portainer/components/forms/group-form/group-form.js index ff5b47b33..10e12f62c 100644 --- a/app/portainer/components/forms/group-form/group-form.js +++ b/app/portainer/components/forms/group-form/group-form.js @@ -1,26 +1,99 @@ import _ from 'lodash-es'; +import angular from 'angular'; + +class GroupFormController { + /* @ngInject */ + constructor($q, EndpointService, GroupService, Notifications) { + this.$q = $q; + this.EndpointService = EndpointService; + this.GroupService = GroupService; + this.Notifications = Notifications; + + this.associateEndpoint = this.associateEndpoint.bind(this); + this.dissociateEndpoint = this.dissociateEndpoint.bind(this); + this.getPaginatedEndpointsByGroup = this.getPaginatedEndpointsByGroup.bind(this); + } + + $onInit() { + this.state = { + available: { + limit: '10', + filter: '', + pageNumber: 1, + totalCount: 0 + }, + associated: { + limit: '10', + filter: '', + pageNumber: 1, + totalCount: 0 + } + }; + } + associateEndpoint(endpoint) { + if (this.pageType === 'create' && !_.includes(this.associatedEndpoints, endpoint)) { + this.associatedEndpoints.push(endpoint); + } else if (this.pageType === 'edit') { + this.GroupService.addEndpoint(this.model.Id, endpoint) + .then(() => { + this.Notifications.success('Success', 'Endpoint successfully added to group'); + this.reloadTablesContent(); + }) + .catch((err) => this.Notifications.error('Error', err, 'Unable to add endpoint to group')); + } + } + + dissociateEndpoint(endpoint) { + if (this.pageType === 'create') { + _.remove(this.associatedEndpoints, (item) => item.Id === endpoint.Id); + } else if (this.pageType === 'edit') { + this.GroupService.removeEndpoint(this.model.Id, endpoint.Id) + .then(() => { + this.Notifications.success('Success', 'Endpoint successfully removed from group'); + this.reloadTablesContent(); + }) + .catch((err) => this.Notifications.error('Error', err, 'Unable to remove endpoint from group')); + } + } + + reloadTablesContent() { + this.getPaginatedEndpointsByGroup(this.pageType, 'available'); + this.getPaginatedEndpointsByGroup(this.pageType, 'associated'); + this.GroupService.group(this.model.Id) + .then((data) => { + this.model = data; + }) + } + + getPaginatedEndpointsByGroup(pageType, tableType) { + if (tableType === 'available') { + const context = this.state.available; + const start = (context.pageNumber - 1) * context.limit + 1; + this.EndpointService.endpointsByGroup(start, context.limit, context.filter, 1) + .then((data) => { + this.availableEndpoints = data.value; + this.state.available.totalCount = data.totalCount; + }); + } else if (tableType === 'associated' && pageType === 'edit') { + const groupId = this.model.Id ? this.model.Id : 1; + const context = this.state.associated; + const start = (context.pageNumber - 1) * context.limit + 1; + this.EndpointService.endpointsByGroup(start, context.limit, context.filter, groupId) + .then((data) => { + this.associatedEndpoints = data.value; + this.state.associated.totalCount = data.totalCount; + }); + } + // ignore (associated + create) group as there is no backend pagination for this table + } +} angular.module('portainer.app').component('groupForm', { templateUrl: './groupForm.html', - controller: function() { - var ctrl = this; - - this.associateEndpoint = function(endpoint) { - ctrl.associatedEndpoints.push(endpoint); - _.remove(ctrl.availableEndpoints, function(n) { - return n.Id === endpoint.Id; - }); - }; - - this.dissociateEndpoint = function(endpoint) { - ctrl.availableEndpoints.push(endpoint); - _.remove(ctrl.associatedEndpoints, function(n) { - return n.Id === endpoint.Id; - }); - }; - - }, + controller: GroupFormController, bindings: { + loaded: '<', + pageType: '@', model: '=', availableEndpoints: '=', availableTags: '<', diff --git a/app/portainer/components/forms/group-form/groupForm.html b/app/portainer/components/forms/group-form/groupForm.html index 9d3b9a8a3..f16d3bd2c 100644 --- a/app/portainer/components/forms/group-form/groupForm.html +++ b/app/portainer/components/forms/group-form/groupForm.html @@ -49,8 +49,13 @@
Available endpoints
@@ -61,8 +66,13 @@
Associated endpoints
@@ -75,16 +85,16 @@
Unassociated endpoints
-
-
- -
-
-
- All the endpoints are assigned to a group. +
+
diff --git a/app/portainer/components/group-association-table/group-association-table.js b/app/portainer/components/group-association-table/group-association-table.js index d61bbddc5..0fd36822f 100644 --- a/app/portainer/components/group-association-table/group-association-table.js +++ b/app/portainer/components/group-association-table/group-association-table.js @@ -5,15 +5,48 @@ angular.module('portainer.app').component('groupAssociationTable', { orderBy: 'Name', reverseOrder: false, paginatedItemLimit: '10', - textFilter: '' + textFilter: '', + loading:true, + pageNumber: 1 }; this.changeOrderBy = function(orderField) { this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; this.state.orderBy = orderField; }; + + this.hasBackendPagination = function() { + return !(this.pageType === 'create' && this.tableType === 'associated'); + } + this.onTextFilterChange = function() { + this.paginationChangedAction(); + } + + this.onPageChanged = function(newPageNumber) { + this.paginationState.pageNumber = newPageNumber; + this.paginationChangedAction(); + } + + this.onPaginationLimitChanged = function() { + this.paginationChangedAction(); + }; + + this.paginationChangedAction = function() { + this.retrievePage(this.pageType, this.tableType); + }; + + this.$onChanges = function(changes) { + if (changes.loaded && changes.loaded.currentValue) { + this.paginationChangedAction(); + } + }; }, bindings: { + paginationState: '=', + loaded: '<', + pageType: '<', + tableType: '@', + retrievePage: '<', dataset: '<', entryClick: '<', emptyDatasetMessage: '@' diff --git a/app/portainer/components/group-association-table/groupAssociationTable.html b/app/portainer/components/group-association-table/groupAssociationTable.html index c457bd295..348420919 100644 --- a/app/portainer/components/group-association-table/groupAssociationTable.html +++ b/app/portainer/components/group-association-table/groupAssociationTable.html @@ -2,7 +2,11 @@
- +
@@ -16,13 +20,25 @@ - + + + + - + @@ -34,15 +50,14 @@ Items per page - - + diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index 068a8ef66..85f671b60 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -1,8 +1,14 @@ +import getEndpointsTotalCount from './transform/getEndpointsTotalCount'; + angular.module('portainer.app') .factory('Endpoints', ['$resource', 'API_ENDPOINT_ENDPOINTS', function EndpointsFactory($resource, API_ENDPOINT_ENDPOINTS) { 'use strict'; return $resource(API_ENDPOINT_ENDPOINTS + '/:id/:action', {}, { - query: { method: 'GET', isArray: true }, + query: { + method: 'GET', + params: {start: '@start', limit: '@limit', search: '@search', groupId: '@groupId'}, + transformResponse: getEndpointsTotalCount + }, get: { method: 'GET', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } }, updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } }, diff --git a/app/portainer/rest/group.js b/app/portainer/rest/group.js index be4507756..ed8f6180f 100644 --- a/app/portainer/rest/group.js +++ b/app/portainer/rest/group.js @@ -7,6 +7,8 @@ angular.module('portainer.app') get: { method: 'GET', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } }, updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } }, + addEndpoint: {method: 'PUT', params: {id: '@id', action: '@action'}}, + removeEndpoint: {method: 'DELETE', params:{id:'@id', action: '@action'}}, remove: { method: 'DELETE', params: { id: '@id'} } }); }]); diff --git a/app/portainer/rest/transform/getEndpointsTotalCount.js b/app/portainer/rest/transform/getEndpointsTotalCount.js new file mode 100644 index 000000000..e3205eac1 --- /dev/null +++ b/app/portainer/rest/transform/getEndpointsTotalCount.js @@ -0,0 +1,6 @@ +export default function getEndpointsTotalCount(data, headers) { + const response = {}; + response.value = angular.fromJson(data); + response.totalCount = headers('X-Total-Count'); + return response; +} \ No newline at end of file diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index e61ebbfc4..9ee3db98d 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -8,8 +8,8 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { return Endpoints.get({id: endpointID}).$promise; }; - service.endpoints = function() { - return Endpoints.query({}).$promise; + service.endpoints = function(start, limit, search) { + return Endpoints.query({start, limit, search}).$promise; }; service.snapshotEndpoints = function() { @@ -20,21 +20,8 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { return Endpoints.snapshot({ id: endpointID }, {}).$promise; }; - service.endpointsByGroup = function(groupId) { - var deferred = $q.defer(); - - Endpoints.query({}).$promise - .then(function success(data) { - var endpoints = data.filter(function (endpoint) { - return endpoint.GroupId === groupId; - }); - deferred.resolve(endpoints); - }) - .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve endpoints', err: err}); - }); - - return deferred.promise; + service.endpointsByGroup = function(start, limit, search, groupId) { + return Endpoints.query({ start, limit, search, groupId }).$promise; }; service.updateAccess = function(id, userAccessPolicies, teamAccessPolicies) { diff --git a/app/portainer/services/api/groupService.js b/app/portainer/services/api/groupService.js index b81342b7e..073b4d122 100644 --- a/app/portainer/services/api/groupService.js +++ b/app/portainer/services/api/groupService.js @@ -43,6 +43,14 @@ function GroupService($q, EndpointGroups) { return EndpointGroups.updateAccess({ id: groupId }, {UserAccessPolicies: userAccessPolicies, TeamAccessPolicies: teamAccessPolicies}).$promise; }; + service.addEndpoint = function(groupId, endpoint) { + return EndpointGroups.addEndpoint({id: groupId, action: 'endpoints/' + endpoint.Id}, endpoint).$promise; + } + + service.removeEndpoint = function(groupId, endpointId) { + return EndpointGroups.removeEndpoint({id: groupId, action: 'endpoints/' + endpointId}).$promise + } + service.deleteGroup = function(groupId) { return EndpointGroups.remove({ id: groupId }).$promise; }; diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 9e239636f..120de69af 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -61,9 +61,9 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us }; function unauthenticatedFlow() { - EndpointService.endpoints() + EndpointService.endpoints(0, 100) .then(function success(endpoints) { - if (endpoints.length === 0) { + if (endpoints.value.length === 0) { $state.go('portainer.init.endpoint'); } else { $state.go('portainer.home'); @@ -87,9 +87,9 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us } function checkForEndpoints() { - EndpointService.endpoints() + EndpointService.endpoints(0, 100) .then(function success(data) { - var endpoints = data; + var endpoints = data.value; if (endpoints.length === 0 && Authentication.isAdmin()) { $state.go('portainer.init.endpoint'); diff --git a/app/portainer/views/endpoints/endpoints.html b/app/portainer/views/endpoints/endpoints.html index 31591fcdc..3f9564b27 100644 --- a/app/portainer/views/endpoints/endpoints.html +++ b/app/portainer/views/endpoints/endpoints.html @@ -26,11 +26,12 @@
diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js index b47d9afba..3b2313700 100644 --- a/app/portainer/views/endpoints/endpointsController.js +++ b/app/portainer/views/endpoints/endpointsController.js @@ -8,8 +8,6 @@ function ($q, $scope, $state, EndpointService, GroupService, EndpointHelper, Not EndpointService.deleteEndpoint(endpoint.Id) .then(function success() { Notifications.success('Endpoint successfully removed', endpoint.Name); - var index = $scope.endpoints.indexOf(endpoint); - $scope.endpoints.splice(index, 1); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to remove endpoint'); @@ -23,22 +21,22 @@ function ($q, $scope, $state, EndpointService, GroupService, EndpointHelper, Not }); }; - function initView() { + $scope.getPaginatedEndpoints = getPaginatedEndpoints; + function getPaginatedEndpoints(lastId, limit, filter) { + const deferred = $q.defer(); $q.all({ - endpoints: EndpointService.endpoints(), + endpoints: EndpointService.endpoints(lastId, limit, filter), groups: GroupService.groups() }) .then(function success(data) { - var endpoints = data.endpoints; + var endpoints = data.endpoints.value; var groups = data.groups; EndpointHelper.mapGroupNameToEndpoint(endpoints, groups); - $scope.groups = groups; - $scope.endpoints = endpoints; + deferred.resolve({endpoints: endpoints, totalCount: data.endpoints.totalCount}); }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to load view'); + Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); }); + return deferred.promise; } - - initView(); }]); diff --git a/app/portainer/views/groups/create/createGroupController.js b/app/portainer/views/groups/create/createGroupController.js index b825abfe4..0f1e86ad3 100644 --- a/app/portainer/views/groups/create/createGroupController.js +++ b/app/portainer/views/groups/create/createGroupController.js @@ -1,4 +1,4 @@ -import { EndpointGroupDefaultModel } from '../../../models/group'; +import {EndpointGroupDefaultModel} from '../../../models/group'; angular.module('portainer.app') .controller('CreateGroupController', ['$q', '$scope', '$state', 'GroupService', 'EndpointService', 'TagService', 'Notifications', @@ -32,19 +32,15 @@ function ($q, $scope, $state, GroupService, EndpointService, TagService, Notific }; function initView() { - $scope.model = new EndpointGroupDefaultModel(); - - $q.all({ - endpoints: EndpointService.endpointsByGroup(1), - tags: TagService.tagNames() - }) - .then(function success(data) { - $scope.availableEndpoints = data.endpoints; + TagService.tagNames() + .then((tags) => { + $scope.availableTags = tags; $scope.associatedEndpoints = []; - $scope.availableTags = data.tags; + $scope.model = new EndpointGroupDefaultModel(); + $scope.loaded = true; }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + .catch((err) => { + Notifications.error('Failure', err, 'Unable to retrieve tags'); }); } diff --git a/app/portainer/views/groups/create/creategroup.html b/app/portainer/views/groups/create/creategroup.html index 36334979c..480d04f81 100644 --- a/app/portainer/views/groups/create/creategroup.html +++ b/app/portainer/views/groups/create/creategroup.html @@ -10,6 +10,8 @@ -
+
diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index ee890a937..f43a9ffc5 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -111,28 +111,44 @@ angular.module('portainer.app') }); } + $scope.getPaginatedEndpoints = getPaginatedEndpoints; + function getPaginatedEndpoints(lastId, limit, filter) { + const deferred = $q.defer(); + $q.all({ + endpoints: EndpointService.endpoints(lastId, limit, filter), + groups: GroupService.groups() + }) + .then(function success(data) { + var endpoints = data.endpoints.value; + var groups = data.groups; + EndpointHelper.mapGroupNameToEndpoint(endpoints, groups); + EndpointProvider.setEndpoints(endpoints); + deferred.resolve({endpoints: endpoints, totalCount: data.endpoints.totalCount}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); + }); + return deferred.promise; + } + function initView() { $scope.isAdmin = Authentication.isAdmin(); MotdService.motd() - .then(function success(data) { - $scope.motd = data; - }); + .then(function success(data) { + $scope.motd = data; + }); - $q.all({ - endpoints: EndpointService.endpoints(), - groups: GroupService.groups() - }) - .then(function success(data) { - var endpoints = data.endpoints; - var groups = data.groups; - EndpointHelper.mapGroupNameToEndpoint(endpoints, groups); - $scope.endpoints = endpoints; - EndpointProvider.setEndpoints(endpoints); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); - }); + getPaginatedEndpoints(0, 100) + .then((data) => { + const totalCount = data.totalCount; + $scope.totalCount = totalCount; + if (totalCount > 100) { + $scope.endpoints = []; + } else { + $scope.endpoints = data.endpoints; + } + }); } initView(); diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js index bc7f66c0e..6cbf202f7 100644 --- a/app/portainer/views/init/admin/initAdminController.js +++ b/app/portainer/views/init/admin/initAdminController.js @@ -39,10 +39,10 @@ function ($async, $scope, $state, Notifications, Authentication, StateManager, U return retrieveAndSaveEnabledExtensions(); }) .then(function () { - return EndpointService.endpoints(); + return EndpointService.endpoints(0, 100); }) .then(function success(data) { - if (data.length === 0) { + if (data.value.length === 0) { $state.go('portainer.init.endpoint'); } else { $state.go('portainer.home'); diff --git a/app/portainer/views/schedules/create/createScheduleController.js b/app/portainer/views/schedules/create/createScheduleController.js index 770981511..6b6b34734 100644 --- a/app/portainer/views/schedules/create/createScheduleController.js +++ b/app/portainer/views/schedules/create/createScheduleController.js @@ -42,7 +42,7 @@ function ($q, $scope, $state, Notifications, EndpointService, GroupService, Sche groups: GroupService.groups() }) .then(function success(data) { - $scope.endpoints = data.endpoints; + $scope.endpoints = data.endpoints.value; $scope.groups = data.groups; }) .catch(function error(err) { diff --git a/app/portainer/views/schedules/edit/scheduleController.js b/app/portainer/views/schedules/edit/scheduleController.js index 06313f3ab..bfb87deeb 100644 --- a/app/portainer/views/schedules/edit/scheduleController.js +++ b/app/portainer/views/schedules/edit/scheduleController.js @@ -60,13 +60,13 @@ function ($q, $scope, $transition$, $state, Notifications, EndpointService, Grou var schedule = data.schedule; schedule.Job.FileContent = data.file.ScheduleFileContent; - var endpoints = data.endpoints; + var endpoints = data.endpoints.value; var tasks = data.tasks; associateEndpointsToTasks(tasks, endpoints); $scope.schedule = schedule; $scope.tasks = data.tasks; - $scope.endpoints = data.endpoints; + $scope.endpoints = data.endpoints.value; $scope.groups = data.groups; }) .catch(function error(err) { diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js index 3185fcdda..fa6f1218f 100644 --- a/app/portainer/views/stacks/edit/stackController.js +++ b/app/portainer/views/stacks/edit/stackController.js @@ -165,7 +165,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe }) .then(function success(data) { var stack = data.stack; - $scope.endpoints = data.endpoints; + $scope.endpoints = data.endpoints.value; $scope.groups = data.groups; $scope.stack = stack; From cc487ae68a065027fb6f2f4b2f3344f5676347ed Mon Sep 17 00:00:00 2001 From: linquize Date: Sun, 21 Jul 2019 07:48:59 +0800 Subject: [PATCH 26/37] fix(registries): can edit registries when --no-auth is set (#2763) --- app/portainer/views/registries/registries.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/portainer/views/registries/registries.html b/app/portainer/views/registries/registries.html index 3bf7d8eb7..e90481fd4 100644 --- a/app/portainer/views/registries/registries.html +++ b/app/portainer/views/registries/registries.html @@ -7,7 +7,7 @@ Registry management -
+
@@ -74,7 +74,7 @@ title-text="Registries" title-icon="fa-database" dataset="registries" table-key="registries" order-by="Name" - access-management="applicationState.application.authentication && isAdmin" + access-management="!applicationState.application.authentication || isAdmin" remove-action="removeAction" registry-management="registryManagementAvailable" can-browse="canBrowse" From 03c82cac69a678076f8473a41f6c6b7164b163aa Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Mon, 22 Jul 2019 12:54:59 +0200 Subject: [PATCH 27/37] feat(datatables): auto refresh on datatables (#2974) * feat(datatables): auto refresh on datatables * feat(datatables): auto refresh implementation on docker related resources --- .../configs-datatable/configsDatatable.html | 38 +++++++++++++ .../configs-datatable/configsDatatable.js | 3 +- .../containersDatatable.html | 21 +++++++ .../containersDatatable.js | 3 +- .../containersDatatableController.js | 6 +- .../jobsDatatableController.js | 7 +++ .../images-datatable/imagesDatatable.html | 38 +++++++++++++ .../images-datatable/imagesDatatable.js | 3 +- .../imagesDatatableController.js | 8 +++ .../networks-datatable/networksDatatable.html | 38 +++++++++++++ .../networks-datatable/networksDatatable.js | 3 +- .../networksDatatableController.js | 7 +++ .../nodes-datatable/nodesDatatable.html | 38 +++++++++++++ .../nodes-datatable/nodesDatatable.js | 3 +- .../secrets-datatable/secretsDatatable.html | 38 +++++++++++++ .../secrets-datatable/secretsDatatable.js | 3 +- .../serviceTasksDatatableController.js | 7 +++ .../services-datatable/servicesDatatable.html | 38 +++++++++++++ .../services-datatable/servicesDatatable.js | 3 +- .../servicesDatatableController.js | 7 +++ .../tasksDatatableController.js | 7 +++ .../volumes-datatable/volumesDatatable.html | 38 +++++++++++++ .../volumes-datatable/volumesDatatable.js | 3 +- .../volumesDatatableController.js | 7 +++ app/docker/views/configs/configs.html | 1 + app/docker/views/configs/configsController.js | 14 ++++- app/docker/views/containers/containers.html | 1 + .../views/containers/containersController.js | 8 ++- app/docker/views/images/images.html | 1 + app/docker/views/images/imagesController.js | 7 ++- app/docker/views/networks/networks.html | 1 + .../views/networks/networksController.js | 8 ++- app/docker/views/secrets/secrets.html | 1 + app/docker/views/secrets/secretsController.js | 8 ++- app/docker/views/services/services.html | 1 + .../views/services/servicesController.js | 7 ++- app/docker/views/swarm/swarm.html | 1 + app/docker/views/swarm/swarmController.js | 22 ++++++-- app/docker/views/volumes/volumes.html | 1 + app/docker/views/volumes/volumesController.js | 7 ++- .../storidgeDrivesDatatableController.js | 7 +++ .../storidgeNodesDatatableController.js | 7 +++ .../accessDatatableController.js | 7 +++ .../datatables/genericDatatableController.js | 55 ++++++++++++++++++- .../schedulesDatatableController.js | 7 +++ .../stacks-datatable/stacksDatatable.html | 38 +++++++++++++ .../stacks-datatable/stacksDatatable.js | 3 +- .../stacksDatatableController.js | 7 +++ app/portainer/views/stacks/stacks.html | 1 + .../views/stacks/stacksController.js | 7 ++- assets/css/app.css | 11 ++++ 51 files changed, 577 insertions(+), 29 deletions(-) diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.html b/app/docker/components/datatables/configs-datatable/configsDatatable.html index 4e127d605..846fdbee4 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.html +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.html @@ -5,6 +5,44 @@
{{ $ctrl.titleText }}
+
+ + Settings + + +
+
+
+ + +
+
+ + + + + +
+
diff --git a/app/docker/components/datatables/images-datatable/imagesDatatable.js b/app/docker/components/datatables/images-datatable/imagesDatatable.js index fdc8ff994..c440cb737 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatable.js +++ b/app/docker/components/datatables/images-datatable/imagesDatatable.js @@ -13,6 +13,7 @@ angular.module('portainer.docker').component('imagesDatatable', { downloadAction: '<', forceRemoveAction: '<', exportInProgress: '<', - offlineMode: '<' + offlineMode: '<', + refreshCallback: '<' } }); diff --git a/app/docker/components/datatables/images-datatable/imagesDatatableController.js b/app/docker/components/datatables/images-datatable/imagesDatatableController.js index afd0a0930..b487efd5b 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatableController.js +++ b/app/docker/components/datatables/images-datatable/imagesDatatableController.js @@ -58,5 +58,13 @@ function ($scope, $controller, DatatableService) { if (this.filters && this.filters.state) { this.filters.state.open = false; } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + + this.onSettingsRepeaterChange(); }; }]); diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.html b/app/docker/components/datatables/networks-datatable/networksDatatable.html index 1a7deb6de..c2347d9cd 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.html +++ b/app/docker/components/datatables/networks-datatable/networksDatatable.html @@ -5,6 +5,44 @@
{{ $ctrl.titleText }}
+
+ + Settings + + +
+
+ + Settings + + +
+
+ + Settings + + +
{{ $ctrl.titleText }}
+
+ + Settings + + +
diff --git a/app/docker/views/configs/configsController.js b/app/docker/views/configs/configsController.js index fc047501f..41cd2e313 100644 --- a/app/docker/views/configs/configsController.js +++ b/app/docker/views/configs/configsController.js @@ -11,10 +11,15 @@ class ConfigsController { this.removeAction = this.removeAction.bind(this); this.removeActionAsync = this.removeActionAsync.bind(this); + this.getConfigs = this.getConfigs.bind(this); + this.getConfigsAsync = this.getConfigsAsync.bind(this); } - async $onInit() { - this.configs = []; + getConfigs() { + return this.$async(this.getConfigsAsync); + } + + async getConfigsAsync() { try { this.configs = await this.ConfigService.configs(); } catch (err) { @@ -22,6 +27,11 @@ class ConfigsController { } } + async $onInit() { + this.configs = []; + this.getConfigs(); + } + removeAction(selectedItems) { return this.$async(this.removeActionAsync, selectedItems); } diff --git a/app/docker/views/containers/containers.html b/app/docker/views/containers/containers.html index 88c6d0b6a..91695618b 100644 --- a/app/docker/views/containers/containers.html +++ b/app/docker/views/containers/containers.html @@ -17,6 +17,7 @@ show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" show-add-action="true" offline-mode="offlineMode" + refresh-callback="getContainers" > diff --git a/app/docker/views/containers/containersController.js b/app/docker/views/containers/containersController.js index f054f9025..06e1022c3 100644 --- a/app/docker/views/containers/containersController.js +++ b/app/docker/views/containers/containersController.js @@ -4,7 +4,9 @@ function ($scope, ContainerService, Notifications, EndpointProvider) { $scope.offlineMode = false; - function initView() { + $scope.getContainers = getContainers; + + function getContainers() { ContainerService.containers(1) .then(function success(data) { $scope.containers = data; @@ -16,5 +18,9 @@ function ($scope, ContainerService, Notifications, EndpointProvider) { }); } + function initView() { + getContainers(); + } + initView(); }]); diff --git a/app/docker/views/images/images.html b/app/docker/views/images/images.html index db1b0f7ff..d1cfba39a 100644 --- a/app/docker/views/images/images.html +++ b/app/docker/views/images/images.html @@ -64,6 +64,7 @@ force-remove-action="confirmRemovalAction" export-in-progress="state.exportInProgress" offline-mode="offlineMode" + refresh-callback="getImages" > diff --git a/app/docker/views/images/imagesController.js b/app/docker/views/images/imagesController.js index 382747c49..8369d0e67 100644 --- a/app/docker/views/images/imagesController.js +++ b/app/docker/views/images/imagesController.js @@ -117,7 +117,8 @@ function ($scope, $state, ImageService, Notifications, ModalService, HttpRequest $scope.offlineMode = false; - function initView() { + $scope.getImages = getImages; + function getImages() { ImageService.images(true) .then(function success(data) { $scope.images = data; @@ -129,5 +130,9 @@ function ($scope, $state, ImageService, Notifications, ModalService, HttpRequest }); } + function initView() { + getImages(); + } + initView(); }]); diff --git a/app/docker/views/networks/networks.html b/app/docker/views/networks/networks.html index c547d54ba..497d95cde 100644 --- a/app/docker/views/networks/networks.html +++ b/app/docker/views/networks/networks.html @@ -17,6 +17,7 @@ show-ownership-column="applicationState.application.authentication" show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" offline-mode="offlineMode" + refresh-callback="getNetworks" > diff --git a/app/docker/views/networks/networksController.js b/app/docker/views/networks/networksController.js index 77c7647d9..f2f79b395 100644 --- a/app/docker/views/networks/networksController.js +++ b/app/docker/views/networks/networksController.js @@ -26,7 +26,9 @@ function ($scope, $state, NetworkService, Notifications, HttpRequestHelper, Endp $scope.offlineMode = false; - function initView() { + $scope.getNetworks = getNetworks; + + function getNetworks() { NetworkService.networks(true, true, true) .then(function success(data) { $scope.networks = data; @@ -38,5 +40,9 @@ function ($scope, $state, NetworkService, Notifications, HttpRequestHelper, Endp }); } + function initView() { + getNetworks(); + } + initView(); }]); diff --git a/app/docker/views/secrets/secrets.html b/app/docker/views/secrets/secrets.html index 850e44fd1..76c8205d7 100644 --- a/app/docker/views/secrets/secrets.html +++ b/app/docker/views/secrets/secrets.html @@ -15,6 +15,7 @@ order-by="Name" show-ownership-column="applicationState.application.authentication" remove-action="removeAction" + refresh-callback="getSecrets" > diff --git a/app/docker/views/secrets/secretsController.js b/app/docker/views/secrets/secretsController.js index e69111375..98f44e5e3 100644 --- a/app/docker/views/secrets/secretsController.js +++ b/app/docker/views/secrets/secretsController.js @@ -23,7 +23,9 @@ function ($scope, $state, SecretService, Notifications) { }); }; - function initView() { + $scope.getSecrets = getSecrets; + + function getSecrets() { SecretService.secrets() .then(function success(data) { $scope.secrets = data; @@ -34,5 +36,9 @@ function ($scope, $state, SecretService, Notifications) { }); } + function initView() { + getSecrets(); + } + initView(); }]); diff --git a/app/docker/views/services/services.html b/app/docker/views/services/services.html index 4e666ae73..d56bd2622 100644 --- a/app/docker/views/services/services.html +++ b/app/docker/views/services/services.html @@ -20,6 +20,7 @@ show-task-logs-button="applicationState.endpoint.apiVersion >= 1.30" show-add-action="true" show-stack-column="true" + refresh-callback="getServices" > diff --git a/app/docker/views/services/servicesController.js b/app/docker/views/services/servicesController.js index fa0cafe86..1c4f4a126 100644 --- a/app/docker/views/services/servicesController.js +++ b/app/docker/views/services/servicesController.js @@ -2,7 +2,8 @@ angular.module('portainer.docker') .controller('ServicesController', ['$q', '$scope', 'ServiceService', 'ServiceHelper', 'Notifications', 'TaskService', 'TaskHelper', 'NodeService', 'ContainerService', function ($q, $scope, ServiceService, ServiceHelper, Notifications, TaskService, TaskHelper, NodeService, ContainerService) { - function initView() { + $scope.getServices = getServices; + function getServices() { var agentProxy = $scope.applicationState.endpoint.mode.agentProxy; $q.all({ @@ -38,5 +39,9 @@ function ($q, $scope, ServiceService, ServiceHelper, Notifications, TaskService, }); } + function initView() { + getServices(); + } + initView(); }]); diff --git a/app/docker/views/swarm/swarm.html b/app/docker/views/swarm/swarm.html index aa2f9db20..271b3ef92 100644 --- a/app/docker/views/swarm/swarm.html +++ b/app/docker/views/swarm/swarm.html @@ -52,6 +52,7 @@ order-by="Hostname" show-ip-address-column="applicationState.endpoint.apiVersion >= 1.25" access-to-node-details="!applicationState.application.authentication || isAdmin" + refresh-callback="getNodes" > diff --git a/app/docker/views/swarm/swarmController.js b/app/docker/views/swarm/swarmController.js index 62cf3a347..ec7a18c37 100644 --- a/app/docker/views/swarm/swarmController.js +++ b/app/docker/views/swarm/swarmController.js @@ -58,6 +58,21 @@ function ($q, $scope, SystemService, NodeService, Notifications, StateManager, A $scope.totalMemory = memory; } + $scope.getNodes = getNodes; + function getNodes() { + var provider = $scope.applicationState.endpoint.mode.provider; + if (provider === 'DOCKER_SWARM_MODE') { + NodeService.nodes().then(function(data) { + var nodes = data; + processTotalCPUAndMemory(nodes); + $scope.nodes = nodes; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve cluster details'); + }); + } + } + function initView() { if (StateManager.getState().application.authentication) { $scope.isAdmin = Authentication.isAdmin(); @@ -66,16 +81,13 @@ function ($q, $scope, SystemService, NodeService, Notifications, StateManager, A var provider = $scope.applicationState.endpoint.mode.provider; $q.all({ version: SystemService.version(), - info: SystemService.info(), - nodes: provider !== 'DOCKER_SWARM_MODE' || NodeService.nodes() + info: SystemService.info() }) .then(function success(data) { $scope.docker = data.version; $scope.info = data.info; if (provider === 'DOCKER_SWARM_MODE') { - var nodes = data.nodes; - processTotalCPUAndMemory(nodes); - $scope.nodes = nodes; + getNodes(); } else { extractSwarmInfo(data.info); } diff --git a/app/docker/views/volumes/volumes.html b/app/docker/views/volumes/volumes.html index 958246e49..c31b7ac3b 100644 --- a/app/docker/views/volumes/volumes.html +++ b/app/docker/views/volumes/volumes.html @@ -18,6 +18,7 @@ show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" show-browse-action="applicationState.endpoint.mode.agentProxy" offline-mode="offlineMode" + refresh-callback="getVolumes" > diff --git a/app/docker/views/volumes/volumesController.js b/app/docker/views/volumes/volumesController.js index 3ab620266..0cad1b914 100644 --- a/app/docker/views/volumes/volumesController.js +++ b/app/docker/views/volumes/volumesController.js @@ -26,7 +26,8 @@ function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notif $scope.offlineMode = false; - function initView() { + $scope.getVolumes = getVolumes; + function getVolumes() { var endpointProvider = $scope.applicationState.endpoint.mode.provider; var endpointRole = $scope.applicationState.endpoint.mode.role; @@ -53,5 +54,9 @@ function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notif }); } + function initView() { + getVolumes(); + } + initView(); }]); diff --git a/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js index b70bae033..132b3a810 100644 --- a/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js +++ b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js @@ -30,6 +30,13 @@ angular.module('portainer.docker') if (this.filters && this.filters.state) { this.filters.state.open = false; } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); }; } ]); \ No newline at end of file diff --git a/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js index c3b2e635d..d21b0d6d2 100644 --- a/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js +++ b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js @@ -44,5 +44,12 @@ function($scope, $controller, clipboard, Notifications, StoridgeNodeService, Dat if (this.filters && this.filters.state) { this.filters.state.open = false; } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); }; }]); diff --git a/app/portainer/components/access-datatable/accessDatatableController.js b/app/portainer/components/access-datatable/accessDatatableController.js index de143c72d..317b6c5a4 100644 --- a/app/portainer/components/access-datatable/accessDatatableController.js +++ b/app/portainer/components/access-datatable/accessDatatableController.js @@ -34,6 +34,13 @@ angular.module('portainer.app') if (this.filters && this.filters.state) { this.filters.state.open = false; } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); }; } ]); \ No newline at end of file diff --git a/app/portainer/components/datatables/genericDatatableController.js b/app/portainer/components/datatables/genericDatatableController.js index 21af10060..7cb5d0143 100644 --- a/app/portainer/components/datatables/genericDatatableController.js +++ b/app/portainer/components/datatables/genericDatatableController.js @@ -6,8 +6,8 @@ function isBetween(value, a, b) { } angular.module('portainer.app') -.controller('GenericDatatableController', ['PaginationService', 'DatatableService', 'PAGINATION_MAX_ITEMS', -function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { +.controller('GenericDatatableController', ['$interval', 'PaginationService', 'DatatableService', 'PAGINATION_MAX_ITEMS', +function ($interval, PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { this.state = { selectAll: false, @@ -20,6 +20,13 @@ function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { selectedItems: [] }; + this.settings = { + open: false, + repeater: { + autoRefresh: false, + refreshRate: '30' + } + } this.onTextFilterChange = function() { DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); @@ -87,7 +94,7 @@ function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { /** * Override this method to execute code after selection changed on datatable */ - this.onSelectionChanged = function () { + this.onSelectionChanged = function() { return; } @@ -131,5 +138,47 @@ function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) { if (this.filters && this.filters.state) { this.filters.state.open = false; } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); }; + + /** + * REPEATER SECTION + */ + this.repeater = undefined; + + this.$onDestroy = function() { + this.stopRepeater(); + }; + + this.stopRepeater = function() { + if (angular.isDefined(this.repeater)) { + $interval.cancel(this.repeater); + this.repeater = undefined; + } + } + + this.startRepeater = function() { + this.repeater = $interval(() => { + this.refreshCallback(); + }, this.settings.repeater.refreshRate * 1000); + } + + this.onSettingsRepeaterChange = function() { + this.stopRepeater(); + if (angular.isDefined(this.refreshCallback) && this.settings.repeater.autoRefresh) { + this.startRepeater(); + $('#refreshRateChange').show(); + $('#refreshRateChange').fadeOut(1500); + } + DatatableService.setDataTableSettings(this.tableKey, this.settings); + } + /** + * !REPEATER SECTION + */ }]); diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js index 498e72ff3..edf2665d3 100644 --- a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js +++ b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js @@ -34,6 +34,13 @@ angular.module('portainer.app') if (this.filters && this.filters.state) { this.filters.state.open = false; } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); }; } ]); diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html index 6bd6852f3..480888746 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html @@ -5,6 +5,44 @@
{{ $ctrl.titleText }}
+
+ + Settings + + +
diff --git a/app/portainer/views/stacks/stacksController.js b/app/portainer/views/stacks/stacksController.js index 3c331a8f6..868720b5d 100644 --- a/app/portainer/views/stacks/stacksController.js +++ b/app/portainer/views/stacks/stacksController.js @@ -35,7 +35,8 @@ function ($scope, $state, Notifications, StackService, ModalService, EndpointPro $scope.offlineMode = false; - function initView() { + $scope.getStacks = getStacks; + function getStacks() { var endpointMode = $scope.applicationState.endpoint.mode; var endpointId = EndpointProvider.endpointID(); @@ -55,5 +56,9 @@ function ($scope, $state, Notifications, StackService, ModalService, EndpointPro }); } + function initView() { + getStacks(); + } + initView(); }]); diff --git a/assets/css/app.css b/assets/css/app.css index 48a153f38..f397a0a2f 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -560,6 +560,17 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { display: none; } +.small-select { + display: inline-block; + padding: 0px 6px; + margin-left: 10px; + color: #555555; + background-color: #fff; + background-image: none; + border-radius: 4px; + font-size: 14px; +} + .bootbox-form .checkbox i { margin-left: 21px; } From 16234aa0c1b0960b834aa3f324900613582cca9d Mon Sep 17 00:00:00 2001 From: William Date: Mon, 22 Jul 2019 22:55:40 +1200 Subject: [PATCH 28/37] style(users): fix typo/grammar (#3010) --- app/portainer/views/users/users.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/portainer/views/users/users.html b/app/portainer/views/users/users.html index 57e79d6cb..f950b825f 100644 --- a/app/portainer/views/users/users.html +++ b/app/portainer/views/users/users.html @@ -71,7 +71,7 @@ Add to team(s) - You have not yet created any team. Head over the teams view to manage user teams. + You don't seem to have any teams to add user's into. Head over to the Teams view to create some.
- Note: non-administrator users with no team do not have access to any endpoints by default. Head over the endpoints view to manage their accesses. + Note: non-administrator users who aren't in a team don't have access to any endpoints by default. Head over to the Endpoints view to manage their accesses.
From fbcffb79695a2bce6c9d6b39b1bafd043e63cacb Mon Sep 17 00:00:00 2001 From: William Date: Tue, 23 Jul 2019 11:28:39 +1200 Subject: [PATCH 29/37] chore(project): adjust stalebot config (#3029) --- .github/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/stale.yml b/.github/stale.yml index b2b38e3fc..9efe94673 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -14,6 +14,7 @@ issues: - kind/enhancement - kind/feature - kind/question + - kind/style - bug/need-confirmation - bug/confirmed - status/discuss From 1089846fd692fa294ea49e9633450f15f1dcac25 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Tue, 23 Jul 2019 05:09:43 +0200 Subject: [PATCH 30/37] fix(datatables): default orderby now applied correctly (#3022) --- .../containers-datatable/containersDatatableController.js | 1 + .../datatables/host-jobs-datatable/jobsDatatableController.js | 1 + .../datatables/images-datatable/imagesDatatableController.js | 1 + .../datatables/networks-datatable/networksDatatableController.js | 1 + .../service-tasks-datatable/serviceTasksDatatableController.js | 1 + .../datatables/services-datatable/servicesDatatableController.js | 1 + .../datatables/tasks-datatable/tasksDatatableController.js | 1 + .../datatables/volumes-datatable/volumesDatatableController.js | 1 + .../drives-datatable/storidgeDrivesDatatableController.js | 1 + .../nodes-datatable/storidgeNodesDatatableController.js | 1 + .../components/access-datatable/accessDatatableController.js | 1 + .../components/datatables/genericDatatableController.js | 1 + .../schedules-datatable/schedulesDatatableController.js | 1 + .../datatables/stacks-datatable/stacksDatatableController.js | 1 + 14 files changed, 14 insertions(+) diff --git a/app/docker/components/datatables/containers-datatable/containersDatatableController.js b/app/docker/components/datatables/containers-datatable/containersDatatableController.js index b677f38a3..929686a60 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatableController.js +++ b/app/docker/components/datatables/containers-datatable/containersDatatableController.js @@ -203,5 +203,6 @@ function ($scope, $controller, DatatableService, EndpointProvider) { this.columnVisibility = storedColumnVisibility; this.columnVisibility.state.open = false; } + this.state.orderBy = this.orderBy; }; }]); diff --git a/app/docker/components/datatables/host-jobs-datatable/jobsDatatableController.js b/app/docker/components/datatables/host-jobs-datatable/jobsDatatableController.js index 8ec21ecd2..4acc5472a 100644 --- a/app/docker/components/datatables/host-jobs-datatable/jobsDatatableController.js +++ b/app/docker/components/datatables/host-jobs-datatable/jobsDatatableController.js @@ -135,6 +135,7 @@ angular.module('portainer.docker') this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; } ]); diff --git a/app/docker/components/datatables/images-datatable/imagesDatatableController.js b/app/docker/components/datatables/images-datatable/imagesDatatableController.js index b487efd5b..67bb932e2 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatableController.js +++ b/app/docker/components/datatables/images-datatable/imagesDatatableController.js @@ -66,5 +66,6 @@ function ($scope, $controller, DatatableService) { } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; }]); diff --git a/app/docker/components/datatables/networks-datatable/networksDatatableController.js b/app/docker/components/datatables/networks-datatable/networksDatatableController.js index 627c25dbb..1fcaec634 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatableController.js +++ b/app/docker/components/datatables/networks-datatable/networksDatatableController.js @@ -45,6 +45,7 @@ angular.module('portainer.docker') this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; } ]); \ No newline at end of file diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js index ed375972f..a31d5b4ca 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js @@ -86,6 +86,7 @@ angular.module('portainer.docker') this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; } ]); diff --git a/app/docker/components/datatables/services-datatable/servicesDatatableController.js b/app/docker/components/datatables/services-datatable/servicesDatatableController.js index c72b39603..13a4314e1 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatableController.js +++ b/app/docker/components/datatables/services-datatable/servicesDatatableController.js @@ -99,5 +99,6 @@ function ($scope, $controller, DatatableService, EndpointProvider) { this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; }]); diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js b/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js index 3fcaa1d58..0d8c4d1f2 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js @@ -42,5 +42,6 @@ function ($scope, $controller, DatatableService) { this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; }]); diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js b/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js index 801ec5d8a..68cc70055 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js @@ -65,5 +65,6 @@ function ($scope, $controller, DatatableService) { this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; }]); diff --git a/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js index 132b3a810..68348427b 100644 --- a/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js +++ b/app/integrations/storidge/components/drives-datatable/storidgeDrivesDatatableController.js @@ -37,6 +37,7 @@ angular.module('portainer.docker') this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; } ]); \ No newline at end of file diff --git a/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js index d21b0d6d2..61e8fec46 100644 --- a/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js +++ b/app/integrations/storidge/components/nodes-datatable/storidgeNodesDatatableController.js @@ -51,5 +51,6 @@ function($scope, $controller, clipboard, Notifications, StoridgeNodeService, Dat this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; }]); diff --git a/app/portainer/components/access-datatable/accessDatatableController.js b/app/portainer/components/access-datatable/accessDatatableController.js index 317b6c5a4..2ed056421 100644 --- a/app/portainer/components/access-datatable/accessDatatableController.js +++ b/app/portainer/components/access-datatable/accessDatatableController.js @@ -41,6 +41,7 @@ angular.module('portainer.app') this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; } ]); \ No newline at end of file diff --git a/app/portainer/components/datatables/genericDatatableController.js b/app/portainer/components/datatables/genericDatatableController.js index 7cb5d0143..991869481 100644 --- a/app/portainer/components/datatables/genericDatatableController.js +++ b/app/portainer/components/datatables/genericDatatableController.js @@ -145,6 +145,7 @@ function ($interval, PaginationService, DatatableService, PAGINATION_MAX_ITEMS) this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; /** diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js index edf2665d3..9a61f74d9 100644 --- a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js +++ b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js @@ -41,6 +41,7 @@ angular.module('portainer.app') this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; } ]); diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js b/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js index 04ed5a37c..15b2a11b7 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js @@ -41,6 +41,7 @@ function ($scope, $controller, DatatableService) { this.settings.open = false; } this.onSettingsRepeaterChange(); + this.state.orderBy = this.orderBy; }; }]); From 66b6a6cbbd640551ebd5c0c82e8f82020b241914 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Tue, 23 Jul 2019 05:10:49 +0200 Subject: [PATCH 31/37] fix(app): UI settings persistency (#3025) --- app/portainer/services/localStorage.js | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index 31754881d..134eaff60 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -60,40 +60,40 @@ angular.module('portainer.app') localStorageService.remove('JWT'); }, storePaginationLimit: function(key, count) { - localStorageService.set('datatable_pagination_' + key, count); + localStorageService.cookie.set('datatable_pagination_' + key, count); }, getPaginationLimit: function(key) { - return localStorageService.get('datatable_pagination_' + key); + return localStorageService.cookie.get('datatable_pagination_' + key); }, getDataTableOrder: function(key) { - return localStorageService.get('datatable_order_' + key); + return localStorageService.cookie.get('datatable_order_' + key); }, storeDataTableOrder: function(key, data) { - localStorageService.set('datatable_order_' + key, data); + localStorageService.cookie.set('datatable_order_' + key, data); }, getDataTableTextFilters: function(key) { - return localStorageService.get('datatable_text_filter_' + key); + return localStorageService.cookie.get('datatable_text_filter_' + key); }, storeDataTableTextFilters: function(key, data) { - localStorageService.set('datatable_text_filter_' + key, data); + localStorageService.cookie.set('datatable_text_filter_' + key, data); }, getDataTableFilters: function(key) { - return localStorageService.get('datatable_filters_' + key); + return localStorageService.cookie.get('datatable_filters_' + key); }, storeDataTableFilters: function(key, data) { - localStorageService.set('datatable_filters_' + key, data); + localStorageService.cookie.set('datatable_filters_' + key, data); }, getDataTableSettings: function(key) { - return localStorageService.get('datatable_settings_' + key); + return localStorageService.cookie.get('datatable_settings_' + key); }, storeDataTableSettings: function(key, data) { - localStorageService.set('datatable_settings_' + key, data); + localStorageService.cookie.set('datatable_settings_' + key, data); }, getDataTableExpandedItems: function(key) { - return localStorageService.get('datatable_expandeditems_' + key); + return localStorageService.cookie.get('datatable_expandeditems_' + key); }, storeDataTableExpandedItems: function(key, data) { - localStorageService.set('datatable_expandeditems_' + key, data); + localStorageService.cookie.set('datatable_expandeditems_' + key, data); }, getDataTableSelectedItems: function(key) { return localStorageService.get('datatable_selecteditems_' + key); @@ -102,16 +102,16 @@ angular.module('portainer.app') localStorageService.set('datatable_selecteditems_' + key, data); }, storeSwarmVisualizerSettings: function(key, data) { - localStorageService.set('swarmvisualizer_' + key, data); + localStorageService.cookie.set('swarmvisualizer_' + key, data); }, getSwarmVisualizerSettings: function(key) { - return localStorageService.get('swarmvisualizer_' + key); + return localStorageService.cookie.get('swarmvisualizer_' + key); }, storeColumnVisibilitySettings: function(key, data) { - localStorageService.set('col_visibility_' + key, data); + localStorageService.cookie.set('col_visibility_' + key, data); }, getColumnVisibilitySettings: function(key) { - return localStorageService.get('col_visibility_' + key); + return localStorageService.cookie.get('col_visibility_' + key); }, storeJobImage: function(data) { localStorageService.set('job_image', data); From 5b91b1a6c9499af2f1dddae0f5b2ee6604288872 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 24 Jul 2019 11:56:31 +1200 Subject: [PATCH 32/37] feat(api): bump default Docker library timeout to 60s (#3038) --- api/docker/client.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/docker/client.go b/api/docker/client.go index 9dcdf33dc..af68781b2 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -11,7 +11,8 @@ import ( ) const ( - unsupportedEnvironmentType = portainer.Error("Environment not supported") + unsupportedEnvironmentType = portainer.Error("Environment not supported") + defaultDockerRequestTimeout = 60 ) // ClientFactory is used to create Docker clients @@ -103,6 +104,6 @@ func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) { return &http.Client{ Transport: transport, - Timeout: 30 * time.Second, + Timeout: defaultDockerRequestTimeout * time.Second, }, nil } From 7338e5fabd48fa3b21c6143d8359e1bb57e3ea13 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Fri, 26 Jul 2019 00:14:18 +0200 Subject: [PATCH 33/37] fix(security): bump lodash to 4.17.15 (#3043) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index faeef63ba..47e68b13b 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "isteven-angular-multiselect": "~4.0.0", "jquery": "3.4.0", "js-yaml": "~3.13.1", - "lodash-es": "^4.17.11", + "lodash-es": "^4.17.15", "moment": "^2.21.0", "ng-file-upload": "~12.2.13", "rdash-ui": "1.0.*", From 2252ab9da7117b8fce16c7cea74314bec0380a99 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 26 Jul 2019 10:20:38 +1200 Subject: [PATCH 34/37] style(app): update app loading text (#3046) --- app/index.html | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/index.html b/app/index.html index 2fd12be28..86a09d276 100644 --- a/app/index.html +++ b/app/index.html @@ -45,7 +45,7 @@
- Connecting to the Docker endpoint... + Loading Portainer...
diff --git a/yarn.lock b/yarn.lock index 69d09c2a3..681a1389e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6840,10 +6840,10 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash-es@^4.17.11: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0" - integrity sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q== +lodash-es@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" + integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== lodash-webpack-plugin@^0.11.5: version "0.11.5" From 12a512f01f4e73220056ae609116930bf25ed16a Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 26 Jul 2019 10:38:07 +1200 Subject: [PATCH 35/37] feat(edge): introduce support for Edge agent (#3031) * feat(edge): fix webconsole and agent deployment command * feat(edge): display agent features when connected to IoT endpoint * feat(edge): add -e CAP_HOST_MANAGEMENT=1 to agent command * feat(edge): add -v /:/host and --name portainer_agent_iot to agent command * style(endpoint-creation): refactor IoT agent to Edge agent * refactor(api): rename AgentIoTEnvironment to AgentEdgeEnvironment * refactor(api): rename AgentIoTEnvironment to AgentEdgeEnvironment * feat(endpoint-creation): update Edge agent deployment instructions * feat(edge): wip edge * feat(edge): refactor key creation * feat(edge): update deployment instructions * feat(home): update Edge agent endpoint item * feat(edge): support dynamic ports * feat(edge): support sleep/wake and snapshots * feat(edge): support offline mode * feat(edge): host job support for Edge endpoints * feat(edge): introduce STANDBY state * feat(edge): update Edge agent deployment command * feat(edge): introduce EDGE_ID support * feat(edge): update default inactivity interval to 5min * feat(edge): reload Edge schedules after restart * fix(edge): fix execution of endpoint job against an Edge endpoint * fix(edge): fix minor issues with scheduling UI/UX * feat(edge): introduce EdgeSchedule version management * feat(edge): switch back to REQUIRED state from ACTIVE on error * refactor(edge): remove comment * feat(edge): updated tunnel status management * feat(edge): fix flickering UI when accessing Edge endpoint from home view * feat(edge): remove STANDBY status * fix(edge): fix an issue with console and Swarm endpoint * fix(edge): fix an issue with stack deployment * fix(edge): reset timer when applying active status * feat(edge): add background ping for Edge endpoints * fix(edge): fix infinite loading loop after Edge endpoint connection failure * fix(home): fix an issue with merge * feat(api): remove SnapshotRaw from EndpointList response * feat(api): add pagination for EndpointList operation * feat(api): rename last_id query parameter to start * feat(api): implement filter for EndpointList operation * fix(edge): prevent a pointer issue after removing an active Edge endpoint * feat(home): front - endpoint backend pagination (#2990) * feat(home): endpoint pagination with backend * feat(api): remove default limit value * fix(endpoints): fix a minor issue with column span * fix(endpointgroup-create): fix an issue with endpoint group creation * feat(app): minor loading optimizations * refactor(api): small refactor of EndpointList operation * fix(home): fix minor loading text display issue * refactor(api): document bolt services functions * feat(home): minor optimization * fix(api): replace seek with index scanning for EndpointPaginated * fix(api): fix invalid starting index issue * fix(api): first implementation of working filter * fix(home): endpoints list keeps backend pagination when it needs to * fix(api): endpoint pagination doesn't drop the first item on pages >=2 anymore * fix(home): UI flickering on page/filter load/change * feat(auth): login spinner * feat(api): support searching in associated endpoint group data * refactor(api): remove unused API endpoint * refactor(api): remove comment * refactor(api): refactor proxy manager * feat(api): declare EndpointList params as optional * feat(api): support groupID filter on endpoints route * feat(api): add new API operations endpointGroupAddEndpoint and endpointGroupDeleteEndpoint * feat(edge): new icon for Edge agent endpoint * fix(edge): fix missing exec quick action * fix(edge): add loading indicator when connecting to Edge endpoint * feat(edge): disable service webhooks for Edge endpoints * feat(endpoints): backend pagination for endpoints view (#3004) * feat(edge): dynamic loading for stack migration feature * feat(edge): wordwrap edge key * feat(endpoint-groups): backend pagination support for create and edit * feat(endpoint-groups): debounce on filter for create/edit views * feat(endpoint-groups): filter assigned on create view * (endpoint-groups): unassigned endpoints edit view * refactor(endpoint-groups): code clean * feat(endpoint-groups): remove message for Unassigned group * refactor(websocket): minor refactor associated to Edge agent * feat(endpoint-group): enable backend pagination (#3017) * feat(api): support groupID filter on endpoints route * feat(api): add new API operations endpointGroupAddEndpoint and endpointGroupDeleteEndpoint * feat(endpoint-groups): backend pagination support for create and edit * feat(endpoint-groups): debounce on filter for create/edit views * feat(endpoint-groups): filter assigned on create view * (endpoint-groups): unassigned endpoints edit view * refactor(endpoint-groups): code clean * feat(endpoint-groups): remove message for Unassigned group * refactor(api): endpoint group endpoint association refactor * refactor(api): rename files and remove comments * refactor(api): remove usage of utils * refactor(api): optional parameters * Merge branch 'feat-endpoint-backend-pagination' into edge # Conflicts: # api/bolt/endpoint/endpoint.go # api/http/handler/endpointgroups/endpointgroup_update.go # api/http/handler/endpointgroups/handler.go # api/http/handler/endpoints/endpoint_list.go # app/portainer/services/api/endpointService.js * fix(api): fix default tunnel server credentials * feat(api): update endpointListOperation behavior and parameters * fix(api): fix interface declaration * feat(edge): support configurable Edge agent checkin interval * feat(edge): support dynamic tunnel credentials * feat(edge): update Edge agent deployment commands * style(edge): update Edge agent settings text * refactor(edge): remove unused credentials management methods * feat(edge): associate a remote addr to tunnel credentials * style(edge): update Edge endpoint icon * feat(edge): support encrypted tunnel credentials * fix(edge): fix invalid pointer cast * feat(bolt): decode endpoints with jsoniter * feat(edge): persist reverse tunnel keyseed * refactor(edge): minor refactor * feat(edge): update chisel library usage * refactor(endpoint): use controller function * feat(api): database migration to DBVersion 19 * refactor(api): refactor AddSchedule function * refactor(schedules): remove comment * refactor(api): remove comment * refactor(api): remove comment * feat(api): tunnel manager now only manage Edge endpoints * refactor(api): clean-up and clarification of the Edge service * refactor(api): clean-up and clarification of the Edge service * fix(api): fix an issue with Edge agent snapshots * refactor(api): add missing comments * refactor(api): update constant description * style(home): remove loading text on error * feat(endpoint): remove 15s timeout for ping request * style(home): display information about associated Edge endpoints * feat(home): redirect to endpoint details on click on unassociated Edge endpoint * feat(settings): remove 60s Edge poll frequency option --- api/bolt/datastore.go | 9 + api/bolt/endpoint/endpoint.go | 2 +- api/bolt/internal/json.go | 10 + api/bolt/migrator/migrate_dbversion18.go | 16 ++ api/bolt/migrator/migrator.go | 8 + api/bolt/tunnelserver/tunnelserver.go | 48 +++++ api/chisel/key.go | 24 +++ api/chisel/schedules.go | 47 +++++ api/chisel/service.go | 191 ++++++++++++++++++ api/chisel/tunnel.go | 144 +++++++++++++ api/cli/cli.go | 2 + api/cli/defaults.go | 36 ++-- api/cli/defaults_windows.go | 36 ++-- api/cmd/portainer/main.go | 43 ++-- api/cron/job_snapshot.go | 2 +- api/crypto/ecdsa.go | 4 +- api/crypto/{crypto.go => hash.go} | 0 api/crypto/md5.go | 10 - api/docker/client.go | 33 ++- api/exec/swarm_stack.go | 40 ++-- api/http/handler/endpointproxy/handler.go | 8 +- api/http/handler/endpointproxy/proxy_azure.go | 2 +- .../handler/endpointproxy/proxy_docker.go | 29 ++- api/http/handler/endpoints/endpoint_create.go | 53 ++++- api/http/handler/endpoints/endpoint_delete.go | 2 +- .../endpoints/endpoint_status_inspect.go | 77 +++++++ api/http/handler/endpoints/handler.go | 5 + api/http/handler/motd/motd.go | 4 +- api/http/handler/schedules/handler.go | 13 +- api/http/handler/schedules/schedule_create.go | 44 +++- api/http/handler/schedules/schedule_delete.go | 2 + api/http/handler/schedules/schedule_tasks.go | 19 ++ api/http/handler/schedules/schedule_update.go | 51 ++++- api/http/handler/settings/settings_update.go | 5 + api/http/handler/websocket/attach.go | 6 +- api/http/handler/websocket/exec.go | 6 +- api/http/handler/websocket/handler.go | 9 +- api/http/handler/websocket/proxy.go | 29 ++- api/http/proxy/docker_transport.go | 16 +- api/http/proxy/factory.go | 25 ++- api/http/proxy/factory_local.go | 6 +- api/http/proxy/factory_local_windows.go | 9 +- api/http/proxy/manager.go | 55 +++-- api/http/server.go | 8 + api/libcompose/compose_stack.go | 24 ++- api/portainer.go | 67 +++++- app/app.js | 16 +- .../serviceTasksDatatableController.js | 1 + app/docker/helpers/infoHelper.js | 2 +- app/docker/rest/container.js | 4 +- app/docker/rest/image.js | 6 +- app/docker/rest/network.js | 4 +- app/docker/rest/system.js | 4 +- app/docker/rest/systemEndpoint.js | 2 +- app/docker/rest/volume.js | 4 +- app/docker/services/systemService.js | 2 +- app/docker/views/services/edit/service.html | 2 +- .../access-viewer/accessViewerController.js | 6 +- .../scheduleTasksDatatable.html | 10 +- .../scheduleTasksDatatable.js | 3 +- .../endpoint-item/endpointItem.html | 18 +- .../endpoint-list/endpoint-list-controller.js | 1 - .../forms/schedule-form/schedule-form.js | 4 +- .../forms/schedule-form/scheduleForm.html | 34 +++- app/portainer/filters/filters.js | 4 + app/portainer/models/schedule.js | 16 +- app/portainer/models/settings.js | 1 + app/portainer/rest/endpoint.js | 7 +- app/portainer/services/api/endpointService.js | 7 +- app/portainer/services/api/groupService.js | 6 +- app/portainer/services/stateManager.js | 1 + .../create/createEndpointController.js | 27 ++- .../endpoints/create/createendpoint.html | 48 ++++- .../views/endpoints/edit/endpoint.html | 104 +++++++++- .../endpoints/edit/endpointController.js | 43 +++- app/portainer/views/home/home.html | 7 +- app/portainer/views/home/homeController.js | 41 +++- .../create/createScheduleController.js | 2 +- .../views/schedules/edit/schedule.html | 1 + .../schedules/edit/scheduleController.js | 25 ++- app/portainer/views/settings/settings.html | 18 +- .../views/settings/settingsController.js | 16 +- .../views/stacks/edit/stackController.js | 10 +- assets/images/edge_endpoint.png | Bin 0 -> 1080 bytes gruntfile.js | 6 +- package.json | 1 + 86 files changed, 1568 insertions(+), 225 deletions(-) create mode 100644 api/bolt/migrator/migrate_dbversion18.go create mode 100644 api/bolt/tunnelserver/tunnelserver.go create mode 100644 api/chisel/key.go create mode 100644 api/chisel/schedules.go create mode 100644 api/chisel/service.go create mode 100644 api/chisel/tunnel.go rename api/crypto/{crypto.go => hash.go} (100%) delete mode 100644 api/crypto/md5.go create mode 100644 api/http/handler/endpoints/endpoint_status_inspect.go create mode 100644 assets/images/edge_endpoint.png diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index adc3df1e7..42100f514 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -5,6 +5,8 @@ import ( "path" "time" + "github.com/portainer/portainer/api/bolt/tunnelserver" + "github.com/boltdb/bolt" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/dockerhub" @@ -51,6 +53,7 @@ type Store struct { TeamMembershipService *teammembership.Service TeamService *team.Service TemplateService *template.Service + TunnelServerService *tunnelserver.Service UserService *user.Service VersionService *version.Service WebhookService *webhook.Service @@ -220,6 +223,12 @@ func (store *Store) initServices() error { } store.TemplateService = templateService + tunnelServerService, err := tunnelserver.NewService(store.db) + if err != nil { + return err + } + store.TunnelServerService = tunnelServerService + userService, err := user.NewService(store.db) if err != nil { return err diff --git a/api/bolt/endpoint/endpoint.go b/api/bolt/endpoint/endpoint.go index 53156d2a2..69d9dc4ac 100644 --- a/api/bolt/endpoint/endpoint.go +++ b/api/bolt/endpoint/endpoint.go @@ -63,7 +63,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) { cursor := bucket.Cursor() for k, v := cursor.First(); k != nil; k, v = cursor.Next() { var endpoint portainer.Endpoint - err := internal.UnmarshalObject(v, &endpoint) + err := internal.UnmarshalObjectWithJsoniter(v, &endpoint) if err != nil { return err } diff --git a/api/bolt/internal/json.go b/api/bolt/internal/json.go index 9f69f06ee..f0636a599 100644 --- a/api/bolt/internal/json.go +++ b/api/bolt/internal/json.go @@ -2,6 +2,8 @@ package internal import ( "encoding/json" + + jsoniter "github.com/json-iterator/go" ) // MarshalObject encodes an object to binary format @@ -13,3 +15,11 @@ func MarshalObject(object interface{}) ([]byte, error) { func UnmarshalObject(data []byte, object interface{}) error { return json.Unmarshal(data, object) } + +// UnmarshalObjectWithJsoniter decodes an object from binary data +// using the jsoniter library. It is mainly used to accelerate endpoint +// decoding at the moment. +func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error { + var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary + return jsoni.Unmarshal(data, &object) +} diff --git a/api/bolt/migrator/migrate_dbversion18.go b/api/bolt/migrator/migrate_dbversion18.go new file mode 100644 index 000000000..2de9dc2fd --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion18.go @@ -0,0 +1,16 @@ +package migrator + +import portainer "github.com/portainer/portainer/api" + +func (m *Migrator) updateSettingsToDBVersion19() error { + legacySettings, err := m.settingsService.Settings() + if err != nil { + return err + } + + if legacySettings.EdgeAgentCheckinInterval == 0 { + legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds + } + + return m.settingsService.UpdateSettings(legacySettings) +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 570e8be5e..5210fe0b0 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -249,5 +249,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 1.21.1 + if m.currentDBVersion < 19 { + err := m.updateSettingsToDBVersion19() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/bolt/tunnelserver/tunnelserver.go b/api/bolt/tunnelserver/tunnelserver.go new file mode 100644 index 000000000..52ba4c101 --- /dev/null +++ b/api/bolt/tunnelserver/tunnelserver.go @@ -0,0 +1,48 @@ +package tunnelserver + +import ( + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" + + "github.com/boltdb/bolt" +) + +const ( + // BucketName represents the name of the bucket where this service stores data. + BucketName = "tunnel_server" + infoKey = "INFO" +) + +// Service represents a service for managing endpoint data. +type Service struct { + db *bolt.DB +} + +// NewService creates a new instance of a service. +func NewService(db *bolt.DB) (*Service, error) { + err := internal.CreateBucket(db, BucketName) + if err != nil { + return nil, err + } + + return &Service{ + db: db, + }, nil +} + +// Info retrieve the TunnelServerInfo object. +func (service *Service) Info() (*portainer.TunnelServerInfo, error) { + var info portainer.TunnelServerInfo + + err := internal.GetObject(service.db, BucketName, []byte(infoKey), &info) + if err != nil { + return nil, err + } + + return &info, nil +} + +// UpdateInfo persists a TunnelServerInfo object. +func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error { + return internal.UpdateObject(service.db, BucketName, []byte(infoKey), settings) +} diff --git a/api/chisel/key.go b/api/chisel/key.go new file mode 100644 index 000000000..7629f208c --- /dev/null +++ b/api/chisel/key.go @@ -0,0 +1,24 @@ +package chisel + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" +) + +// GenerateEdgeKey will generate a key that can be used by an Edge agent to register with a Portainer instance. +// The key represents the following data in this particular format: +// portainer_instance_url|tunnel_server_addr|tunnel_server_fingerprint|endpoint_ID +// The key returned by this function is a base64 encoded version of the data. +func (service *Service) GenerateEdgeKey(url, host string, endpointIdentifier int) string { + keyInformation := []string{ + url, + fmt.Sprintf("%s:%s", host, service.serverPort), + service.serverFingerprint, + strconv.Itoa(endpointIdentifier), + } + + key := strings.Join(keyInformation, "|") + return base64.RawStdEncoding.EncodeToString([]byte(key)) +} diff --git a/api/chisel/schedules.go b/api/chisel/schedules.go new file mode 100644 index 000000000..39ba9a340 --- /dev/null +++ b/api/chisel/schedules.go @@ -0,0 +1,47 @@ +package chisel + +import ( + "strconv" + + portainer "github.com/portainer/portainer/api" +) + +// AddSchedule register a schedule inside the tunnel details associated to an endpoint. +func (service *Service) AddSchedule(endpointID portainer.EndpointID, schedule *portainer.EdgeSchedule) { + tunnel := service.GetTunnelDetails(endpointID) + + existingScheduleIndex := -1 + for idx, existingSchedule := range tunnel.Schedules { + if existingSchedule.ID == schedule.ID { + existingScheduleIndex = idx + break + } + } + + if existingScheduleIndex == -1 { + tunnel.Schedules = append(tunnel.Schedules, *schedule) + } else { + tunnel.Schedules[existingScheduleIndex] = *schedule + } + + key := strconv.Itoa(int(endpointID)) + service.tunnelDetailsMap.Set(key, tunnel) +} + +// RemoveSchedule will remove the specified schedule from each tunnel it was registered with. +func (service *Service) RemoveSchedule(scheduleID portainer.ScheduleID) { + for item := range service.tunnelDetailsMap.IterBuffered() { + tunnelDetails := item.Val.(*portainer.TunnelDetails) + + updatedSchedules := make([]portainer.EdgeSchedule, 0) + for _, schedule := range tunnelDetails.Schedules { + if schedule.ID == scheduleID { + continue + } + updatedSchedules = append(updatedSchedules, schedule) + } + + tunnelDetails.Schedules = updatedSchedules + service.tunnelDetailsMap.Set(item.Key, tunnelDetails) + } +} diff --git a/api/chisel/service.go b/api/chisel/service.go new file mode 100644 index 000000000..bb2d3357d --- /dev/null +++ b/api/chisel/service.go @@ -0,0 +1,191 @@ +package chisel + +import ( + "fmt" + "log" + "strconv" + "time" + + "github.com/dchest/uniuri" + + cmap "github.com/orcaman/concurrent-map" + + chserver "github.com/jpillora/chisel/server" + portainer "github.com/portainer/portainer/api" +) + +const ( + tunnelCleanupInterval = 10 * time.Second + requiredTimeout = 15 * time.Second + activeTimeout = 4*time.Minute + 30*time.Second +) + +// Service represents a service to manage the state of multiple reverse tunnels. +// It is used to start a reverse tunnel server and to manage the connection status of each tunnel +// connected to the tunnel server. +type Service struct { + serverFingerprint string + serverPort string + tunnelDetailsMap cmap.ConcurrentMap + endpointService portainer.EndpointService + tunnelServerService portainer.TunnelServerService + snapshotter portainer.Snapshotter + chiselServer *chserver.Server +} + +// NewService returns a pointer to a new instance of Service +func NewService(endpointService portainer.EndpointService, tunnelServerService portainer.TunnelServerService) *Service { + return &Service{ + tunnelDetailsMap: cmap.New(), + endpointService: endpointService, + tunnelServerService: tunnelServerService, + } +} + +// StartTunnelServer starts a tunnel server on the specified addr and port. +// It uses a seed to generate a new private/public key pair. If the seed cannot +// be found inside the database, it will generate a new one randomly and persist it. +// It starts the tunnel status verification process in the background. +// The snapshotter is used in the tunnel status verification process. +func (service *Service) StartTunnelServer(addr, port string, snapshotter portainer.Snapshotter) error { + keySeed, err := service.retrievePrivateKeySeed() + if err != nil { + return err + } + + config := &chserver.Config{ + Reverse: true, + KeySeed: keySeed, + } + + chiselServer, err := chserver.NewServer(config) + if err != nil { + return err + } + + service.serverFingerprint = chiselServer.GetFingerprint() + service.serverPort = port + + err = chiselServer.Start(addr, port) + if err != nil { + return err + } + service.chiselServer = chiselServer + + // TODO: work-around Chisel default behavior. + // By default, Chisel will allow anyone to connect if no user exists. + username, password := generateRandomCredentials() + err = service.chiselServer.AddUser(username, password, "127.0.0.1") + if err != nil { + return err + } + + service.snapshotter = snapshotter + go service.startTunnelVerificationLoop() + + return nil +} + +func (service *Service) retrievePrivateKeySeed() (string, error) { + var serverInfo *portainer.TunnelServerInfo + + serverInfo, err := service.tunnelServerService.Info() + if err == portainer.ErrObjectNotFound { + keySeed := uniuri.NewLen(16) + + serverInfo = &portainer.TunnelServerInfo{ + PrivateKeySeed: keySeed, + } + + err := service.tunnelServerService.UpdateInfo(serverInfo) + if err != nil { + return "", err + } + } else if err != nil { + return "", err + } + + return serverInfo.PrivateKeySeed, nil +} + +func (service *Service) startTunnelVerificationLoop() { + log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds()) + ticker := time.NewTicker(tunnelCleanupInterval) + stopSignal := make(chan struct{}) + + for { + select { + case <-ticker.C: + service.checkTunnels() + case <-stopSignal: + ticker.Stop() + return + } + } +} + +func (service *Service) checkTunnels() { + for item := range service.tunnelDetailsMap.IterBuffered() { + tunnel := item.Val.(*portainer.TunnelDetails) + + if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle { + continue + } + + elapsed := time.Since(tunnel.LastActivity) + log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: endpoint tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds()) + + if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() { + continue + } else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() { + log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds()) + } + + if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() { + continue + } else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() { + log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds()) + + endpointID, err := strconv.Atoi(item.Key) + if err != nil { + log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err) + } + + err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port) + if err != nil { + log.Printf("[ERROR] [snapshot] Unable to snapshot Edge endpoint (id: %s): %s", item.Key, err) + } + } + + if len(tunnel.Schedules) > 0 { + endpointID, err := strconv.Atoi(item.Key) + if err != nil { + log.Printf("[ERROR] [chisel,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err) + continue + } + + service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID)) + } else { + service.tunnelDetailsMap.Remove(item.Key) + } + + } +} + +func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error { + endpoint, err := service.endpointService.Endpoint(portainer.EndpointID(endpointID)) + if err != nil { + return err + } + + endpointURL := endpoint.URL + endpoint.URL = fmt.Sprintf("tcp://localhost:%d", tunnelPort) + snapshot, err := service.snapshotter.CreateSnapshot(endpoint) + if err != nil { + return err + } + + endpoint.Snapshots = []portainer.Snapshot{*snapshot} + endpoint.URL = endpointURL + return service.endpointService.UpdateEndpoint(endpoint.ID, endpoint) +} diff --git a/api/chisel/tunnel.go b/api/chisel/tunnel.go new file mode 100644 index 000000000..e0cba1caf --- /dev/null +++ b/api/chisel/tunnel.go @@ -0,0 +1,144 @@ +package chisel + +import ( + "encoding/base64" + "fmt" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/portainer/libcrypto" + + "github.com/dchest/uniuri" + portainer "github.com/portainer/portainer/api" +) + +const ( + minAvailablePort = 49152 + maxAvailablePort = 65535 +) + +// getUnusedPort is used to generate an unused random port in the dynamic port range. +// Dynamic ports (also called private ports) are 49152 to 65535. +func (service *Service) getUnusedPort() int { + port := randomInt(minAvailablePort, maxAvailablePort) + + for item := range service.tunnelDetailsMap.IterBuffered() { + tunnel := item.Val.(*portainer.TunnelDetails) + if tunnel.Port == port { + return service.getUnusedPort() + } + } + + return port +} + +func randomInt(min, max int) int { + return min + rand.Intn(max-min) +} + +// GetTunnelDetails returns information about the tunnel associated to an endpoint. +func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails { + key := strconv.Itoa(int(endpointID)) + + if item, ok := service.tunnelDetailsMap.Get(key); ok { + tunnelDetails := item.(*portainer.TunnelDetails) + return tunnelDetails + } + + schedules := make([]portainer.EdgeSchedule, 0) + return &portainer.TunnelDetails{ + Status: portainer.EdgeAgentIdle, + Port: 0, + Schedules: schedules, + Credentials: "", + } +} + +// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint. +// It sets the status to ACTIVE. +func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) { + tunnel := service.GetTunnelDetails(endpointID) + tunnel.Status = portainer.EdgeAgentActive + tunnel.Credentials = "" + tunnel.LastActivity = time.Now() + + key := strconv.Itoa(int(endpointID)) + service.tunnelDetailsMap.Set(key, tunnel) +} + +// SetTunnelStatusToIdle update the status of the tunnel associated to the specified endpoint. +// It sets the status to IDLE. +// It removes any existing credentials associated to the tunnel. +func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) { + tunnel := service.GetTunnelDetails(endpointID) + + tunnel.Status = portainer.EdgeAgentIdle + tunnel.Port = 0 + tunnel.LastActivity = time.Now() + + credentials := tunnel.Credentials + if credentials != "" { + tunnel.Credentials = "" + service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0]) + } + + key := strconv.Itoa(int(endpointID)) + service.tunnelDetailsMap.Set(key, tunnel) +} + +// SetTunnelStatusToRequired update the status of the tunnel associated to the specified endpoint. +// It sets the status to REQUIRED. +// If no port is currently associated to the tunnel, it will associate a random unused port to the tunnel +// and generate temporary credentials that can be used to establish a reverse tunnel on that port. +// Credentials are encrypted using the Edge ID associated to the endpoint. +func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error { + tunnel := service.GetTunnelDetails(endpointID) + + if tunnel.Port == 0 { + endpoint, err := service.endpointService.Endpoint(endpointID) + if err != nil { + return err + } + + tunnel.Status = portainer.EdgeAgentManagementRequired + tunnel.Port = service.getUnusedPort() + tunnel.LastActivity = time.Now() + + username, password := generateRandomCredentials() + authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port) + err = service.chiselServer.AddUser(username, password, authorizedRemote) + if err != nil { + return err + } + + credentials, err := encryptCredentials(username, password, endpoint.EdgeID) + if err != nil { + return err + } + tunnel.Credentials = credentials + + key := strconv.Itoa(int(endpointID)) + service.tunnelDetailsMap.Set(key, tunnel) + } + + return nil +} + +func generateRandomCredentials() (string, string) { + username := uniuri.NewLen(8) + password := uniuri.NewLen(8) + return username, password +} + +func encryptCredentials(username, password, key string) (string, error) { + credentials := fmt.Sprintf("%s:%s", username, password) + + encryptedCredentials, err := libcrypto.Encrypt([]byte(credentials), []byte(key)) + if err != nil { + return "", err + } + + return base64.RawStdEncoding.EncodeToString(encryptedCredentials), nil +} diff --git a/api/cli/cli.go b/api/cli/cli.go index 6eef1a432..ba1d4d829 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -33,6 +33,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { flags := &portainer.CLIFlags{ Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(), + TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(), + TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(), Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), diff --git a/api/cli/defaults.go b/api/cli/defaults.go index 1913e4915..504742771 100644 --- a/api/cli/defaults.go +++ b/api/cli/defaults.go @@ -3,21 +3,23 @@ package cli const ( - defaultBindAddress = ":9000" - defaultDataDirectory = "/data" - defaultAssetsDirectory = "./" - defaultNoAuth = "false" - defaultNoAnalytics = "false" - defaultTLS = "false" - defaultTLSSkipVerify = "false" - defaultTLSCACertPath = "/certs/ca.pem" - defaultTLSCertPath = "/certs/cert.pem" - defaultTLSKeyPath = "/certs/key.pem" - defaultSSL = "false" - defaultSSLCertPath = "/certs/portainer.crt" - defaultSSLKeyPath = "/certs/portainer.key" - defaultSyncInterval = "60s" - defaultSnapshot = "true" - defaultSnapshotInterval = "5m" - defaultTemplateFile = "/templates.json" + defaultBindAddress = ":9000" + defaultTunnelServerAddress = "0.0.0.0" + defaultTunnelServerPort = "8000" + defaultDataDirectory = "/data" + defaultAssetsDirectory = "./" + defaultNoAuth = "false" + defaultNoAnalytics = "false" + defaultTLS = "false" + defaultTLSSkipVerify = "false" + defaultTLSCACertPath = "/certs/ca.pem" + defaultTLSCertPath = "/certs/cert.pem" + defaultTLSKeyPath = "/certs/key.pem" + defaultSSL = "false" + defaultSSLCertPath = "/certs/portainer.crt" + defaultSSLKeyPath = "/certs/portainer.key" + defaultSyncInterval = "60s" + defaultSnapshot = "true" + defaultSnapshotInterval = "5m" + defaultTemplateFile = "/templates.json" ) diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go index e2ee01795..4e7ce7c3e 100644 --- a/api/cli/defaults_windows.go +++ b/api/cli/defaults_windows.go @@ -1,21 +1,23 @@ package cli const ( - defaultBindAddress = ":9000" - defaultDataDirectory = "C:\\data" - defaultAssetsDirectory = "./" - defaultNoAuth = "false" - defaultNoAnalytics = "false" - defaultTLS = "false" - defaultTLSSkipVerify = "false" - defaultTLSCACertPath = "C:\\certs\\ca.pem" - defaultTLSCertPath = "C:\\certs\\cert.pem" - defaultTLSKeyPath = "C:\\certs\\key.pem" - defaultSSL = "false" - defaultSSLCertPath = "C:\\certs\\portainer.crt" - defaultSSLKeyPath = "C:\\certs\\portainer.key" - defaultSyncInterval = "60s" - defaultSnapshot = "true" - defaultSnapshotInterval = "5m" - defaultTemplateFile = "/templates.json" + defaultBindAddress = ":9000" + defaultTunnelServerAddress = "0.0.0.0" + defaultTunnelServerPort = "8000" + defaultDataDirectory = "C:\\data" + defaultAssetsDirectory = "./" + defaultNoAuth = "false" + defaultNoAnalytics = "false" + defaultTLS = "false" + defaultTLSSkipVerify = "false" + defaultTLSCACertPath = "C:\\certs\\ca.pem" + defaultTLSCertPath = "C:\\certs\\cert.pem" + defaultTLSKeyPath = "C:\\certs\\key.pem" + defaultSSL = "false" + defaultSSLCertPath = "C:\\certs\\portainer.crt" + defaultSSLKeyPath = "C:\\certs\\portainer.key" + defaultSyncInterval = "60s" + defaultSnapshot = "true" + defaultSnapshotInterval = "5m" + defaultTemplateFile = "/templates.json" ) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 8b34fc070..961039738 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -2,10 +2,13 @@ package main import ( "encoding/json" + "log" "os" "strings" "time" + "github.com/portainer/portainer/api/chisel" + "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt" "github.com/portainer/portainer/api/cli" @@ -20,8 +23,6 @@ import ( "github.com/portainer/portainer/api/jwt" "github.com/portainer/portainer/api/ldap" "github.com/portainer/portainer/api/libcompose" - - "log" ) func initCLI() *portainer.CLIFlags { @@ -69,12 +70,12 @@ func initStore(dataStorePath string, fileService portainer.FileService) *bolt.St return store } -func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager { - return libcompose.NewComposeStackManager(dataStorePath) +func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager { + return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService) } -func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.SwarmStackManager, error) { - return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService) +func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) { + return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService) } func initJWTService(authenticationEnabled bool) portainer.JWTService { @@ -104,8 +105,8 @@ func initGitService() portainer.GitService { return &git.Service{} } -func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory { - return docker.NewClientFactory(signatureService) +func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory { + return docker.NewClientFactory(signatureService, reverseTunnelService) } func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter { @@ -196,7 +197,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul return scheduleService.CreateSchedule(endpointSyncSchedule) } -func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error { +func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error { schedules, err := scheduleService.Schedules() if err != nil { return err @@ -213,6 +214,13 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p return err } } + + if schedule.EdgeSchedule != nil { + for _, endpointID := range schedule.EdgeSchedule.Endpoints { + reverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule) + } + } + } return nil @@ -265,6 +273,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL AllowPrivilegedModeForRegularUsers: true, EnableHostManagementFeatures: false, SnapshotInterval: *flags.SnapshotInterval, + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, } if *flags.Templates != "" { @@ -540,7 +549,9 @@ func main() { log.Fatal(err) } - clientFactory := initClientFactory(digitalSignatureService) + reverseTunnelService := chisel.NewService(store.EndpointService, store.TunnelServerService) + + clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService) jobService := initJobService(clientFactory) @@ -551,12 +562,12 @@ func main() { endpointManagement = false } - swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService) + swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService) if err != nil { log.Fatal(err) } - composeStackManager := initComposeStackManager(*flags.Data) + composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService) err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile) if err != nil { @@ -570,7 +581,7 @@ func main() { jobScheduler := initJobScheduler() - err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService) + err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService, reverseTunnelService) if err != nil { log.Fatal(err) } @@ -658,7 +669,13 @@ func main() { go terminateIfNoAdminCreated(store.UserService) } + err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter) + if err != nil { + log.Fatal(err) + } + var server portainer.Server = &http.Server{ + ReverseTunnelService: reverseTunnelService, Status: applicationStatus, BindAddress: *flags.Addr, AssetsPath: *flags.Assets, diff --git a/api/cron/job_snapshot.go b/api/cron/job_snapshot.go index c7828b164..458d026c0 100644 --- a/api/cron/job_snapshot.go +++ b/api/cron/job_snapshot.go @@ -53,7 +53,7 @@ func (runner *SnapshotJobRunner) Run() { } for _, endpoint := range endpoints { - if endpoint.Type == portainer.AzureEnvironment { + if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment { continue } diff --git a/api/crypto/ecdsa.go b/api/crypto/ecdsa.go index 35cc0b283..9148eb68d 100644 --- a/api/crypto/ecdsa.go +++ b/api/crypto/ecdsa.go @@ -8,6 +8,8 @@ import ( "encoding/base64" "encoding/hex" "math/big" + + "github.com/portainer/libcrypto" ) const ( @@ -111,7 +113,7 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) { message = service.secret } - hash := HashFromBytes([]byte(message)) + hash := libcrypto.HashFromBytes([]byte(message)) r := big.NewInt(0) s := big.NewInt(0) diff --git a/api/crypto/crypto.go b/api/crypto/hash.go similarity index 100% rename from api/crypto/crypto.go rename to api/crypto/hash.go diff --git a/api/crypto/md5.go b/api/crypto/md5.go deleted file mode 100644 index 42ca24602..000000000 --- a/api/crypto/md5.go +++ /dev/null @@ -1,10 +0,0 @@ -package crypto - -import "crypto/md5" - -// HashFromBytes returns the hash of the specified data -func HashFromBytes(data []byte) []byte { - digest := md5.New() - digest.Write(data) - return digest.Sum(nil) -} diff --git a/api/docker/client.go b/api/docker/client.go index af68781b2..b093f23cf 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -1,6 +1,7 @@ package docker import ( + "fmt" "net/http" "strings" "time" @@ -17,13 +18,15 @@ const ( // ClientFactory is used to create Docker clients type ClientFactory struct { - signatureService portainer.DigitalSignatureService + signatureService portainer.DigitalSignatureService + reverseTunnelService portainer.ReverseTunnelService } // NewClientFactory returns a new instance of a ClientFactory -func NewClientFactory(signatureService portainer.DigitalSignatureService) *ClientFactory { +func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *ClientFactory { return &ClientFactory{ - signatureService: signatureService, + signatureService: signatureService, + reverseTunnelService: reverseTunnelService, } } @@ -35,6 +38,8 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam return nil, unsupportedEnvironmentType } else if endpoint.Type == portainer.AgentOnDockerEnvironment { return createAgentClient(endpoint, factory.signatureService, nodeName) + } else if endpoint.Type == portainer.EdgeAgentEnvironment { + return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName) } if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") { @@ -63,6 +68,28 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) { ) } +func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portainer.ReverseTunnelService, nodeName string) (*client.Client, error) { + httpCli, err := httpClient(endpoint) + if err != nil { + return nil, err + } + + headers := map[string]string{} + if nodeName != "" { + headers[portainer.PortainerAgentTargetHeader] = nodeName + } + + tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpointURL := fmt.Sprintf("http://localhost:%d", tunnel.Port) + + return client.NewClientWithOpts( + client.WithHost(endpointURL), + client.WithVersion(portainer.SupportedDockerAPIVersion), + client.WithHTTPClient(httpCli), + client.WithHTTPHeaders(headers), + ) +} + func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) { httpCli, err := httpClient(endpoint) if err != nil { diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index 3f01bbb26..e50eacb63 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -3,6 +3,7 @@ package exec import ( "bytes" "encoding/json" + "fmt" "os" "os/exec" "path" @@ -13,20 +14,22 @@ import ( // SwarmStackManager represents a service for managing stacks. type SwarmStackManager struct { - binaryPath string - dataPath string - signatureService portainer.DigitalSignatureService - fileService portainer.FileService + binaryPath string + dataPath string + signatureService portainer.DigitalSignatureService + fileService portainer.FileService + reverseTunnelService portainer.ReverseTunnelService } // NewSwarmStackManager initializes a new SwarmStackManager service. // It also updates the configuration of the Docker CLI binary. -func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*SwarmStackManager, error) { +func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) { manager := &SwarmStackManager{ - binaryPath: binaryPath, - dataPath: dataPath, - signatureService: signatureService, - fileService: fileService, + binaryPath: binaryPath, + dataPath: dataPath, + signatureService: signatureService, + fileService: fileService, + reverseTunnelService: reverseTunnelService, } err := manager.updateDockerCLIConfiguration(dataPath) @@ -39,7 +42,7 @@ func NewSwarmStackManager(binaryPath, dataPath string, signatureService portaine // Login executes the docker login command against a list of registries (including DockerHub). func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) { - command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint) + command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint) for _, registry := range registries { if registry.Authentication { registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL) @@ -55,7 +58,7 @@ func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registri // Logout executes the docker logout command. func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error { - command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint) + command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint) args = append(args, "logout") return runCommandAndCaptureStdErr(command, args, nil, "") } @@ -63,7 +66,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error { // Deploy executes the docker stack deploy command. func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error { stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint) - command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint) + command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint) if prune { args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name) @@ -82,7 +85,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end // Remove executes the docker stack rm command. func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error { - command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint) + command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint) args = append(args, "stack", "rm", stack.Name) return runCommandAndCaptureStdErr(command, args, nil, "") } @@ -106,7 +109,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor return nil } -func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) { +func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) { // Assume Linux as a default command := path.Join(binaryPath, "docker") @@ -116,7 +119,14 @@ func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portaine args := make([]string, 0) args = append(args, "--config", dataPath) - args = append(args, "-H", endpoint.URL) + + endpointURL := endpoint.URL + if endpoint.Type == portainer.EdgeAgentEnvironment { + tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port) + } + + args = append(args, "-H", endpointURL) if endpoint.TLSConfig.TLS { args = append(args, "--tls") diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index 15a1101ba..69db8f54d 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -11,9 +11,11 @@ import ( // Handler is the HTTP handler used to proxy requests to external APIs. type Handler struct { *mux.Router - requestBouncer *security.RequestBouncer - EndpointService portainer.EndpointService - ProxyManager *proxy.Manager + requestBouncer *security.RequestBouncer + EndpointService portainer.EndpointService + SettingsService portainer.SettingsService + ProxyManager *proxy.Manager + ReverseTunnelService portainer.ReverseTunnelService } // NewHandler creates a handler to proxy requests to external APIs. diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go index a756a4fb7..782f81a25 100644 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -29,7 +29,7 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R } var proxy http.Handler - proxy = handler.ProxyManager.GetProxy(string(endpointID)) + proxy = handler.ProxyManager.GetProxy(endpoint) if proxy == nil { proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint) if err != nil { diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index b1b911452..bdd5c8322 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -3,6 +3,7 @@ package endpointproxy import ( "errors" "strconv" + "time" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -24,7 +25,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - if endpoint.Status == portainer.EndpointStatusDown { + if endpoint.Type != portainer.EdgeAgentEnvironment && endpoint.Status == portainer.EndpointStatusDown { return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")} } @@ -33,8 +34,32 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } + if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.EdgeID == "" { + return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")} + } + + tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID) + if tunnel.Status == portainer.EdgeAgentIdle { + handler.ProxyManager.DeleteProxy(endpoint) + + err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err} + } + + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second + time.Sleep(waitForAgentToConnect * 2) + } + } + var proxy http.Handler - proxy = handler.ProxyManager.GetProxy(string(endpointID)) + proxy = handler.ProxyManager.GetProxy(endpoint) if proxy == nil { proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint) if err != nil { diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 40348899f..6f4163025 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -1,8 +1,11 @@ package endpoints import ( + "errors" "log" + "net" "net/http" + "net/url" "runtime" "strconv" @@ -41,7 +44,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false) if err != nil || endpointType == 0 { - return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 3 (Azure environment)") + return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)") } payload.EndpointType = endpointType @@ -149,6 +152,8 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment { return handler.createAzureEndpoint(payload) + } else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment { + return handler.createEdgeAgentEndpoint(payload) } if payload.TLS { @@ -195,6 +200,52 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po return endpoint, nil } +func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { + endpointType := portainer.EdgeAgentEnvironment + endpointID := handler.EndpointService.GetNextIdentifier() + + portainerURL, err := url.Parse(payload.URL) + if err != nil { + return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", err} + } + + portainerHost, _, err := net.SplitHostPort(portainerURL.Host) + if err != nil { + portainerHost = portainerURL.Host + } + + if portainerHost == "localhost" { + return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", errors.New("cannot use localhost as endpoint URL")} + } + + edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID) + + endpoint := &portainer.Endpoint{ + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: portainerHost, + Type: endpointType, + GroupID: portainer.EndpointGroupID(payload.GroupID), + TLSConfig: portainer.TLSConfiguration{ + TLS: false, + }, + AuthorizedUsers: []portainer.UserID{}, + AuthorizedTeams: []portainer.TeamID{}, + Extensions: []portainer.EndpointExtension{}, + Tags: payload.Tags, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, + EdgeKey: edgeKey, + } + + err = handler.EndpointService.CreateEndpoint(endpoint) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err} + } + + return endpoint, nil +} + func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { endpointType := portainer.DockerEnvironment diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 0dd25963c..ca7af3aa9 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -41,7 +41,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err} } - handler.ProxyManager.DeleteProxy(string(endpointID)) + handler.ProxyManager.DeleteProxy(endpoint) return response.Empty(w) } diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go new file mode 100644 index 000000000..da34a3bfe --- /dev/null +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -0,0 +1,77 @@ +package endpoints + +import ( + "errors" + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" +) + +type endpointStatusInspectResponse struct { + Status string `json:"status"` + Port int `json:"port"` + Schedules []portainer.EdgeSchedule `json:"schedules"` + CheckinInterval int `json:"checkin"` + Credentials string `json:"credentials"` +} + +// GET request on /api/endpoints/:id/status +func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + } + + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + if endpoint.Type != portainer.EdgeAgentEnvironment { + return &httperror.HandlerError{http.StatusInternalServerError, "Status unavailable for non Edge agent endpoints", errors.New("Status unavailable")} + } + + edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader) + if edgeIdentifier == "" { + return &httperror.HandlerError{http.StatusForbidden, "Missing Edge identifier", errors.New("missing Edge identifier")} + } + + if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier { + return &httperror.HandlerError{http.StatusForbidden, "Invalid Edge identifier", errors.New("invalid Edge identifier")} + } + + if endpoint.EdgeID == "" { + endpoint.EdgeID = edgeIdentifier + + err := handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err} + } + } + + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID) + + statusResponse := endpointStatusInspectResponse{ + Status: tunnel.Status, + Port: tunnel.Port, + Schedules: tunnel.Schedules, + CheckinInterval: settings.EdgeAgentCheckinInterval, + Credentials: tunnel.Credentials, + } + + if tunnel.Status == portainer.EdgeAgentManagementRequired { + handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID) + } + + return response.JSON(w, statusResponse) +} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 9896bd92a..17d0012bb 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -35,6 +35,8 @@ type Handler struct { ProxyManager *proxy.Manager Snapshotter portainer.Snapshotter JobService portainer.JobService + ReverseTunnelService portainer.ReverseTunnelService + SettingsService portainer.SettingsService } // NewHandler creates a handler to manage endpoint operations. @@ -65,5 +67,8 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost) h.Handle("/endpoints/{id}/snapshot", bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost) + h.Handle("/endpoints/{id}/status", + bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet) + return h } diff --git a/api/http/handler/motd/motd.go b/api/http/handler/motd/motd.go index ba768becb..08ee020e8 100644 --- a/api/http/handler/motd/motd.go +++ b/api/http/handler/motd/motd.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" + "github.com/portainer/libcrypto" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/client" ) @@ -42,7 +42,7 @@ func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) { message := strings.Join(data.Message, "\n") - hash := crypto.HashFromBytes([]byte(message)) + hash := libcrypto.HashFromBytes([]byte(message)) resp := motdResponse{ Title: data.Title, Message: message, diff --git a/api/http/handler/schedules/handler.go b/api/http/handler/schedules/handler.go index 5a160cd3b..303178c25 100644 --- a/api/http/handler/schedules/handler.go +++ b/api/http/handler/schedules/handler.go @@ -12,12 +12,13 @@ import ( // Handler is the HTTP handler used to handle schedule operations. type Handler struct { *mux.Router - ScheduleService portainer.ScheduleService - EndpointService portainer.EndpointService - SettingsService portainer.SettingsService - FileService portainer.FileService - JobService portainer.JobService - JobScheduler portainer.JobScheduler + ScheduleService portainer.ScheduleService + EndpointService portainer.EndpointService + SettingsService portainer.SettingsService + FileService portainer.FileService + JobService portainer.JobService + JobScheduler portainer.JobScheduler + ReverseTunnelService portainer.ReverseTunnelService } // NewHandler creates a handler to manage schedule operations. diff --git a/api/http/handler/schedules/schedule_create.go b/api/http/handler/schedules/schedule_create.go index f4302bd4b..196913a33 100644 --- a/api/http/handler/schedules/schedule_create.go +++ b/api/http/handler/schedules/schedule_create.go @@ -1,9 +1,11 @@ package schedules import ( + "encoding/base64" "errors" "net/http" "strconv" + "strings" "time" "github.com/asaskevich/govalidator" @@ -113,7 +115,7 @@ func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) e return nil } -// POST /api/schedules?method=file/string +// 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 { @@ -219,6 +221,46 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche } func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error { + nonEdgeEndpointIDs := make([]portainer.EndpointID, 0) + edgeEndpointIDs := make([]portainer.EndpointID, 0) + + edgeCronExpression := strings.Split(schedule.CronExpression, " ") + if len(edgeCronExpression) == 6 { + edgeCronExpression = edgeCronExpression[1:] + } + + for _, ID := range schedule.ScriptExecutionJob.Endpoints { + + endpoint, err := handler.EndpointService.Endpoint(ID) + if err != nil { + return err + } + + if endpoint.Type != portainer.EdgeAgentEnvironment { + nonEdgeEndpointIDs = append(nonEdgeEndpointIDs, endpoint.ID) + } else { + edgeEndpointIDs = append(edgeEndpointIDs, endpoint.ID) + } + } + + if len(edgeEndpointIDs) > 0 { + edgeSchedule := &portainer.EdgeSchedule{ + ID: schedule.ID, + CronExpression: strings.Join(edgeCronExpression, " "), + Script: base64.RawStdEncoding.EncodeToString(file), + Endpoints: edgeEndpointIDs, + Version: 1, + } + + for _, endpointID := range edgeEndpointIDs { + handler.ReverseTunnelService.AddSchedule(endpointID, edgeSchedule) + } + + schedule.EdgeSchedule = edgeSchedule + } + + schedule.ScriptExecutionJob.Endpoints = nonEdgeEndpointIDs + scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file) if err != nil { return err diff --git a/api/http/handler/schedules/schedule_delete.go b/api/http/handler/schedules/schedule_delete.go index a2ef81393..c30b01696 100644 --- a/api/http/handler/schedules/schedule_delete.go +++ b/api/http/handler/schedules/schedule_delete.go @@ -42,6 +42,8 @@ func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err} } + handler.ReverseTunnelService.RemoveSchedule(schedule.ID) + handler.JobScheduler.UnscheduleJob(schedule.ID) err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID)) diff --git a/api/http/handler/schedules/schedule_tasks.go b/api/http/handler/schedules/schedule_tasks.go index 1c2810e45..a4993e6cd 100644 --- a/api/http/handler/schedules/schedule_tasks.go +++ b/api/http/handler/schedules/schedule_tasks.go @@ -3,6 +3,7 @@ package schedules import ( "encoding/json" "errors" + "fmt" "net/http" "strconv" @@ -18,6 +19,7 @@ type taskContainer struct { Status string `json:"Status"` Created float64 `json:"Created"` Labels map[string]string `json:"Labels"` + Edge bool `json:"Edge"` } // GET request on /api/schedules/:id/tasks @@ -64,6 +66,22 @@ func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *h tasks = append(tasks, endpointTasks...) } + if schedule.EdgeSchedule != nil { + for _, endpointID := range schedule.EdgeSchedule.Endpoints { + + cronTask := taskContainer{ + ID: fmt.Sprintf("schedule_%d", schedule.EdgeSchedule.ID), + EndpointID: endpointID, + Edge: true, + Status: "", + Created: 0, + Labels: map[string]string{}, + } + + tasks = append(tasks, cronTask) + } + } + return response.JSON(w, tasks) } @@ -87,6 +105,7 @@ func extractTasksFromContainerSnasphot(endpoint *portainer.Endpoint, scheduleID for _, container := range containers { if container.Labels["io.portainer.schedule.id"] == strconv.Itoa(int(scheduleID)) { container.EndpointID = endpoint.ID + container.Edge = false endpointTasks = append(endpointTasks, container) } } diff --git a/api/http/handler/schedules/schedule_update.go b/api/http/handler/schedules/schedule_update.go index 29a6dccbe..f68e77126 100644 --- a/api/http/handler/schedules/schedule_update.go +++ b/api/http/handler/schedules/schedule_update.go @@ -1,6 +1,7 @@ package schedules import ( + "encoding/base64" "errors" "net/http" "strconv" @@ -58,7 +59,15 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err} } - updateJobSchedule := updateSchedule(schedule, &payload) + updateJobSchedule := false + if schedule.EdgeSchedule != nil { + err := handler.updateEdgeSchedule(schedule, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update Edge schedule", err} + } + } else { + updateJobSchedule = updateSchedule(schedule, &payload) + } if payload.FileContent != nil { _, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(scheduleID), []byte(*payload.FileContent)) @@ -85,6 +94,46 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) * return response.JSON(w, schedule) } +func (handler *Handler) updateEdgeSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) error { + if payload.Name != nil { + schedule.Name = *payload.Name + } + + if payload.Endpoints != nil { + + edgeEndpointIDs := make([]portainer.EndpointID, 0) + + for _, ID := range payload.Endpoints { + endpoint, err := handler.EndpointService.Endpoint(ID) + if err != nil { + return err + } + + if endpoint.Type == portainer.EdgeAgentEnvironment { + edgeEndpointIDs = append(edgeEndpointIDs, endpoint.ID) + } + } + + schedule.EdgeSchedule.Endpoints = edgeEndpointIDs + } + + if payload.CronExpression != nil { + schedule.EdgeSchedule.CronExpression = *payload.CronExpression + schedule.EdgeSchedule.Version++ + } + + if payload.FileContent != nil { + schedule.EdgeSchedule.Script = base64.RawStdEncoding.EncodeToString([]byte(*payload.FileContent)) + schedule.EdgeSchedule.Version++ + } + + for _, endpointID := range schedule.EdgeSchedule.Endpoints { + handler.ReverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule) + } + + return nil +} + func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) bool { updateJobSchedule := false diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index cc9c901dc..6de707c38 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -22,6 +22,7 @@ type settingsUpdatePayload struct { EnableHostManagementFeatures *bool SnapshotInterval *string TemplatesURL *string + EdgeAgentCheckinInterval *int } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -103,6 +104,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * } } + if payload.EdgeAgentCheckinInterval != nil { + settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval + } + tlsError := handler.updateTLS(settings) if tlsError != nil { return tlsError diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index 95702179c..5de214eca 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -62,8 +62,10 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque r.Header.Del("Origin") - if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment { - return handler.proxyWebsocketRequest(w, r, params) + if params.endpoint.Type == portainer.AgentOnDockerEnvironment { + return handler.proxyAgentWebsocketRequest(w, r, params) + } else if params.endpoint.Type == portainer.EdgeAgentEnvironment { + return handler.proxyEdgeAgentWebsocketRequest(w, r, params) } websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil) diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index 3b3fad969..afe670a56 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -68,8 +68,10 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { r.Header.Del("Origin") - if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment { - return handler.proxyWebsocketRequest(w, r, params) + if params.endpoint.Type == portainer.AgentOnDockerEnvironment { + return handler.proxyAgentWebsocketRequest(w, r, params) + } else if params.endpoint.Type == portainer.EdgeAgentEnvironment { + return handler.proxyEdgeAgentWebsocketRequest(w, r, params) } websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil) diff --git a/api/http/handler/websocket/handler.go b/api/http/handler/websocket/handler.go index 853dea038..79dc0502a 100644 --- a/api/http/handler/websocket/handler.go +++ b/api/http/handler/websocket/handler.go @@ -11,10 +11,11 @@ import ( // Handler is the HTTP handler used to handle websocket operations. type Handler struct { *mux.Router - EndpointService portainer.EndpointService - SignatureService portainer.DigitalSignatureService - requestBouncer *security.RequestBouncer - connectionUpgrader websocket.Upgrader + EndpointService portainer.EndpointService + SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService + requestBouncer *security.RequestBouncer + connectionUpgrader websocket.Upgrader } // NewHandler creates a handler to manage websocket operations. diff --git a/api/http/handler/websocket/proxy.go b/api/http/handler/websocket/proxy.go index 7eba78b3a..4acb7a620 100644 --- a/api/http/handler/websocket/proxy.go +++ b/api/http/handler/websocket/proxy.go @@ -2,14 +2,37 @@ package websocket import ( "crypto/tls" + "fmt" + "net/http" + "net/url" + "github.com/gorilla/websocket" "github.com/koding/websocketproxy" "github.com/portainer/portainer/api" - "net/http" - "net/url" ) -func (handler *Handler) proxyWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { +func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { + tunnel := handler.ReverseTunnelService.GetTunnelDetails(params.endpoint.ID) + + endpointURL, err := url.Parse(fmt.Sprintf("http://localhost:%d", tunnel.Port)) + if err != nil { + return err + } + + endpointURL.Scheme = "ws" + proxy := websocketproxy.NewProxy(endpointURL) + + proxy.Director = func(incoming *http.Request, out http.Header) { + out.Set(portainer.PortainerAgentTargetHeader, params.nodeName) + } + + handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID) + proxy.ServeHTTP(w, r) + + return nil +} + +func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { agentURL, err := url.Parse(params.endpoint.URL) if err != nil { return err diff --git a/api/http/proxy/docker_transport.go b/api/http/proxy/docker_transport.go index 873b099cf..a3e3d99f8 100644 --- a/api/http/proxy/docker_transport.go +++ b/api/http/proxy/docker_transport.go @@ -24,7 +24,9 @@ type ( DockerHubService portainer.DockerHubService SettingsService portainer.SettingsService SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService endpointIdentifier portainer.EndpointID + endpointType portainer.EndpointType } restrictedDockerOperationContext struct { isAdmin bool @@ -58,7 +60,19 @@ func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error } func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) { - return p.dockerTransport.RoundTrip(request) + response, err := p.dockerTransport.RoundTrip(request) + + if p.endpointType != portainer.EdgeAgentEnvironment { + return response, err + } + + if err == nil { + p.ReverseTunnelService.SetTunnelStatusToActive(p.endpointIdentifier) + } else { + p.ReverseTunnelService.SetTunnelStatusToIdle(p.endpointIdentifier) + } + + return response, err } func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) { diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index 7308b7f34..0774e7f14 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -21,6 +21,7 @@ type proxyFactory struct { RegistryService portainer.RegistryService DockerHubService portainer.DockerHubService SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService } func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { @@ -29,21 +30,21 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { } func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) { - url, err := url.Parse(AzureAPIBaseURL) + remoteURL, err := url.Parse(AzureAPIBaseURL) if err != nil { return nil, err } - proxy := newSingleHostReverseProxyWithHostHeader(url) + proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) proxy.Transport = NewAzureTransport(credentials) return proxy, nil } -func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool, endpointID portainer.EndpointID) (http.Handler, error) { +func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, endpoint *portainer.Endpoint) (http.Handler, error) { u.Scheme = "https" - proxy := factory.createDockerReverseProxy(u, enableSignature, endpointID) + proxy := factory.createDockerReverseProxy(u, endpoint) config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify) if err != nil { return nil, err @@ -53,13 +54,19 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine return proxy, nil } -func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) http.Handler { +func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, endpoint *portainer.Endpoint) http.Handler { u.Scheme = "http" - return factory.createDockerReverseProxy(u, enableSignature, endpointID) + return factory.createDockerReverseProxy(u, endpoint) } -func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) *httputil.ReverseProxy { +func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, endpoint *portainer.Endpoint) *httputil.ReverseProxy { proxy := newSingleHostReverseProxyWithHostHeader(u) + + enableSignature := false + if endpoint.Type == portainer.AgentOnDockerEnvironment { + enableSignature = true + } + transport := &proxyTransport{ enableSignature: enableSignature, ResourceControlService: factory.ResourceControlService, @@ -67,8 +74,10 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignatur SettingsService: factory.SettingsService, RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, + ReverseTunnelService: factory.ReverseTunnelService, dockerTransport: &http.Transport{}, - endpointIdentifier: endpointID, + endpointIdentifier: endpoint.ID, + endpointType: endpoint.Type, } if enableSignature { diff --git a/api/http/proxy/factory_local.go b/api/http/proxy/factory_local.go index 431fd5604..1e461ab1a 100644 --- a/api/http/proxy/factory_local.go +++ b/api/http/proxy/factory_local.go @@ -8,7 +8,7 @@ import ( portainer "github.com/portainer/portainer/api" ) -func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler { +func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler { proxy := &localProxy{} transport := &proxyTransport{ enableSignature: false, @@ -18,7 +18,9 @@ func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.End RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, dockerTransport: newSocketTransport(path), - endpointIdentifier: endpointID, + ReverseTunnelService: factory.ReverseTunnelService, + endpointIdentifier: endpoint.ID, + endpointType: endpoint.Type, } proxy.Transport = transport return proxy diff --git a/api/http/proxy/factory_local_windows.go b/api/http/proxy/factory_local_windows.go index a2105f886..01b020cf8 100644 --- a/api/http/proxy/factory_local_windows.go +++ b/api/http/proxy/factory_local_windows.go @@ -5,12 +5,11 @@ package proxy import ( "net" "net/http" - "github.com/Microsoft/go-winio" - + portainer "github.com/portainer/portainer/api" ) -func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler { +func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler { proxy := &localProxy{} transport := &proxyTransport{ enableSignature: false, @@ -19,8 +18,10 @@ func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.End SettingsService: factory.SettingsService, RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, + ReverseTunnelService: factory.ReverseTunnelService, dockerTransport: newNamedPipeTransport(path), - endpointIdentifier: endpointID, + endpointIdentifier: endpoint.ID, + endpointType: endpoint.Type, } proxy.Transport = transport return proxy diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 67da091a6..6d0311cfa 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -1,6 +1,7 @@ package proxy import ( + "fmt" "net/http" "net/url" "strconv" @@ -21,6 +22,7 @@ type ( // Manager represents a service used to manage Docker proxies. Manager struct { proxyFactory *proxyFactory + reverseTunnelService portainer.ReverseTunnelService proxies cmap.ConcurrentMap extensionProxies cmap.ConcurrentMap legacyExtensionProxies cmap.ConcurrentMap @@ -34,6 +36,7 @@ type ( RegistryService portainer.RegistryService DockerHubService portainer.DockerHubService SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService } ) @@ -50,13 +53,15 @@ func NewManager(parameters *ManagerParams) *Manager { RegistryService: parameters.RegistryService, DockerHubService: parameters.DockerHubService, SignatureService: parameters.SignatureService, + ReverseTunnelService: parameters.ReverseTunnelService, }, + reverseTunnelService: parameters.ReverseTunnelService, } } // GetProxy returns the proxy associated to a key -func (manager *Manager) GetProxy(key string) http.Handler { - proxy, ok := manager.proxies.Get(key) +func (manager *Manager) GetProxy(endpoint *portainer.Endpoint) http.Handler { + proxy, ok := manager.proxies.Get(string(endpoint.ID)) if !ok { return nil } @@ -76,8 +81,8 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht } // DeleteProxy deletes the proxy associated to a key -func (manager *Manager) DeleteProxy(key string) { - manager.proxies.Remove(key) +func (manager *Manager) DeleteProxy(endpoint *portainer.Endpoint) { + manager.proxies.Remove(string(endpoint.ID)) } // GetExtensionProxy returns an extension proxy associated to an extension identifier @@ -136,28 +141,40 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) return proxy, nil } -func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration, endpointID portainer.EndpointID) (http.Handler, error) { - if endpointURL.Scheme == "tcp" { - if tlsConfig.TLS || tlsConfig.TLSSkipVerify { - return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false, endpointID) - } - return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false, endpointID), nil +func (manager *Manager) createDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + baseURL := endpoint.URL + if endpoint.Type == portainer.EdgeAgentEnvironment { + tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) + baseURL = fmt.Sprintf("http://localhost:%d", tunnel.Port) } - return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpointID), nil -} -func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) { - endpointURL, err := url.Parse(endpoint.URL) + endpointURL, err := url.Parse(baseURL) if err != nil { return nil, err } switch endpoint.Type { case portainer.AgentOnDockerEnvironment: - return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true, endpoint.ID) - case portainer.AzureEnvironment: - return newAzureProxy(&endpoint.AzureCredentials) - default: - return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig, endpoint.ID) + return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint) + case portainer.EdgeAgentEnvironment: + return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil } + + if endpointURL.Scheme == "tcp" { + if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { + return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint) + } + + return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil + } + + return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpoint), nil +} + +func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + if endpoint.Type == portainer.AzureEnvironment { + return newAzureProxy(&endpoint.AzureCredentials) + } + + return manager.createDockerProxy(endpoint) } diff --git a/api/http/server.go b/api/http/server.go index b232adcdc..f058d2e00 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -44,6 +44,7 @@ type Server struct { AuthDisabled bool EndpointManagement bool Status *portainer.Status + ReverseTunnelService portainer.ReverseTunnelService ExtensionManager portainer.ExtensionManager ComposeStackManager portainer.ComposeStackManager CryptoService portainer.CryptoService @@ -88,6 +89,7 @@ func (server *Server) Start() error { RegistryService: server.RegistryService, DockerHubService: server.DockerHubService, SignatureService: server.SignatureService, + ReverseTunnelService: server.ReverseTunnelService, } proxyManager := proxy.NewManager(proxyManagerParameters) @@ -132,6 +134,8 @@ func (server *Server) Start() error { endpointHandler.ProxyManager = proxyManager endpointHandler.Snapshotter = server.Snapshotter endpointHandler.JobService = server.JobService + endpointHandler.ReverseTunnelService = server.ReverseTunnelService + endpointHandler.SettingsService = server.SettingsService var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer) endpointGroupHandler.EndpointGroupService = server.EndpointGroupService @@ -140,6 +144,8 @@ func (server *Server) Start() error { var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer) endpointProxyHandler.EndpointService = server.EndpointService endpointProxyHandler.ProxyManager = proxyManager + endpointProxyHandler.SettingsService = server.SettingsService + endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) @@ -168,6 +174,7 @@ func (server *Server) Start() error { schedulesHandler.JobService = server.JobService schedulesHandler.JobScheduler = server.JobScheduler schedulesHandler.SettingsService = server.SettingsService + schedulesHandler.ReverseTunnelService = server.ReverseTunnelService var settingsHandler = settings.NewHandler(requestBouncer) settingsHandler.SettingsService = server.SettingsService @@ -216,6 +223,7 @@ func (server *Server) Start() error { var websocketHandler = websocket.NewHandler(requestBouncer) websocketHandler.EndpointService = server.EndpointService websocketHandler.SignatureService = server.SignatureService + websocketHandler.ReverseTunnelService = server.ReverseTunnelService var webhookHandler = webhooks.NewHandler(requestBouncer) webhookHandler.WebhookService = server.WebhookService diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go index bcd211a6b..01de1cd20 100644 --- a/api/libcompose/compose_stack.go +++ b/api/libcompose/compose_stack.go @@ -2,6 +2,7 @@ package libcompose import ( "context" + "fmt" "path" "path/filepath" @@ -17,19 +18,28 @@ import ( // ComposeStackManager represents a service for managing compose stacks. type ComposeStackManager struct { - dataPath string + dataPath string + reverseTunnelService portainer.ReverseTunnelService } // NewComposeStackManager initializes a new ComposeStackManager service. -func NewComposeStackManager(dataPath string) *ComposeStackManager { +func NewComposeStackManager(dataPath string, reverseTunnelService portainer.ReverseTunnelService) *ComposeStackManager { return &ComposeStackManager{ - dataPath: dataPath, + dataPath: dataPath, + reverseTunnelService: reverseTunnelService, } } -func createClient(endpoint *portainer.Endpoint) (client.Factory, error) { +func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (client.Factory, error) { + + endpointURL := endpoint.URL + if endpoint.Type == portainer.EdgeAgentEnvironment { + tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port) + } + clientOpts := client.Options{ - Host: endpoint.URL, + Host: endpointURL, APIVersion: portainer.SupportedDockerAPIVersion, } @@ -47,7 +57,7 @@ func createClient(endpoint *portainer.Endpoint) (client.Factory, error) { // Up will deploy a compose stack (equivalent of docker-compose up) func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error { - clientFactory, err := createClient(endpoint) + clientFactory, err := manager.createClient(endpoint) if err != nil { return err } @@ -85,7 +95,7 @@ func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portain // Down will shutdown a compose stack (equivalent of docker-compose down) func (manager *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error { - clientFactory, err := createClient(endpoint) + clientFactory, err := manager.createClient(endpoint) if err != nil { return err } diff --git a/api/portainer.go b/api/portainer.go index 852552ba6..d8152f082 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1,5 +1,7 @@ package portainer +import "time" + type ( // Pair defines a key/value string pair Pair struct { @@ -10,6 +12,8 @@ type ( // CLIFlags represents the available flags on the CLI CLIFlags struct { Addr *string + TunnelAddr *string + TunnelPort *string AdminPassword *string AdminPasswordFile *string Assets *string @@ -105,6 +109,7 @@ type ( SnapshotInterval string `json:"SnapshotInterval"` TemplatesURL string `json:"TemplatesURL"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` // Deprecated fields DisplayDonationHeader bool @@ -250,7 +255,8 @@ type ( Snapshots []Snapshot `json:"Snapshots"` UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` - + EdgeID string `json:"EdgeID,omitempty"` + EdgeKey string `json:"EdgeKey"` // Deprecated fields // Deprecated in DBVersion == 4 TLS bool `json:"TLS,omitempty"` @@ -333,11 +339,21 @@ type ( Recurring bool Created int64 JobType JobType + EdgeSchedule *EdgeSchedule ScriptExecutionJob *ScriptExecutionJob SnapshotJob *SnapshotJob EndpointSyncJob *EndpointSyncJob } + // EdgeSchedule represents a scheduled job that can run on Edge environments. + EdgeSchedule struct { + ID ScheduleID `json:"Id"` + CronExpression string `json:"CronExpression"` + Script string `json:"Script"` + Version int `json:"Version"` + Endpoints []EndpointID `json:"Endpoints"` + } + // WebhookID represents a webhook identifier. WebhookID int @@ -575,6 +591,20 @@ type ( Valid bool `json:"Valid,omitempty"` } + // TunnelDetails represents information associated to a tunnel + TunnelDetails struct { + Status string + LastActivity time.Time + Port int + Schedules []EdgeSchedule + Credentials string + } + + // TunnelServerInfo represents information associated to the tunnel server + TunnelServerInfo struct { + PrivateKeySeed string `json:"PrivateKeySeed"` + } + // CLIService represents a service for managing CLI CLIService interface { ParseFlags(version string) (*CLIFlags, error) @@ -692,6 +722,12 @@ type ( StoreDBVersion(version int) error } + // TunnelServerService represents a service for managing data associated to the tunnel server + TunnelServerService interface { + Info() (*TunnelServerInfo, error) + UpdateInfo(info *TunnelServerInfo) error + } + // WebhookService represents a service for managing webhook data. WebhookService interface { Webhooks() ([]Webhook, error) @@ -850,13 +886,25 @@ type ( DisableExtension(extension *Extension) error UpdateExtension(extension *Extension, version string) error } + + // ReverseTunnelService represensts a service used to manage reverse tunnel connections. + ReverseTunnelService interface { + StartTunnelServer(addr, port string, snapshotter Snapshotter) error + GenerateEdgeKey(url, host string, endpointIdentifier int) string + SetTunnelStatusToActive(endpointID EndpointID) + SetTunnelStatusToRequired(endpointID EndpointID) error + SetTunnelStatusToIdle(endpointID EndpointID) + GetTunnelDetails(endpointID EndpointID) *TunnelDetails + AddSchedule(endpointID EndpointID, schedule *EdgeSchedule) + RemoveSchedule(scheduleID ScheduleID) + } ) const ( // APIVersion is the version number of the Portainer API APIVersion = "1.21.0" // DBVersion is the version number of the Portainer database - DBVersion = 18 + DBVersion = 19 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved @@ -865,6 +913,8 @@ const ( ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.21.0.json" // PortainerAgentHeader represents the name of the header available in any agent response PortainerAgentHeader = "Portainer-Agent" + // PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster + PortainerAgentEdgeIDHeader = "X-PortainerAgent-EdgeID" // PortainerAgentTargetHeader represent the name of the header containing the target node name PortainerAgentTargetHeader = "X-PortainerAgent-Target" // PortainerAgentSignatureHeader represent the name of the header containing the digital signature @@ -878,6 +928,8 @@ const ( SupportedDockerAPIVersion = "1.24" // ExtensionServer represents the server used by Portainer to communicate with extensions ExtensionServer = "localhost" + // DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance + DefaultEdgeAgentCheckinIntervalInSeconds = 5 ) const ( @@ -953,6 +1005,8 @@ const ( AgentOnDockerEnvironment // AzureEnvironment represents an endpoint connected to an Azure environment AzureEnvironment + // EdgeAgentEnvironment represents an endpoint connected to an Edge agent + EdgeAgentEnvironment ) const ( @@ -1019,6 +1073,15 @@ const ( CustomRegistry ) +const ( + // EdgeAgentIdle represents an idle state for a tunnel connected to an Edge endpoint. + EdgeAgentIdle string = "IDLE" + // EdgeAgentManagementRequired represents a required state for a tunnel connected to an Edge endpoint + EdgeAgentManagementRequired string = "REQUIRED" + // EdgeAgentActive represents an active state for a tunnel connected to an Edge endpoint + EdgeAgentActive string = "ACTIVE" +) + const ( OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo" OperationDockerContainerList Authorization = "DockerContainerList" diff --git a/app/app.js b/app/app.js index 293a5fd3e..acdee2add 100644 --- a/app/app.js +++ b/app/app.js @@ -1,8 +1,8 @@ import _ from 'lodash-es'; angular.module('portainer') -.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper', -function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, cfpLoadingBar, $transitions, HttpRequestHelper) { +.run(['$rootScope', '$state', '$interval', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper', +function ($rootScope, $state, $interval, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) { 'use strict'; EndpointProvider.initialize(); @@ -34,8 +34,20 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin $transitions.onBefore({}, function() { HttpRequestHelper.resetAgentHeaders(); }); + + // Keep-alive Edge endpoints by sending a ping request every minute + $interval(function() { + ping(EndpointProvider, SystemService); + }, 60 * 1000) + }]); +function ping(EndpointProvider, SystemService) { + let endpoint = EndpointProvider.currentEndpoint(); + if (endpoint !== undefined && endpoint.Type === 4) { + SystemService.ping(endpoint.Id); + } +} function initAuthentication(authManager, Authentication, $rootScope, $state) { authManager.checkAuthOnRefresh(); diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js index a31d5b4ca..23f5c8e72 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js @@ -13,6 +13,7 @@ angular.module('portainer.docker') showQuickActionLogs: true, showQuickActionConsole: true, showQuickActionInspect: true, + showQuickActionExec: true, showQuickActionAttach: false }); diff --git a/app/docker/helpers/infoHelper.js b/app/docker/helpers/infoHelper.js index c16221375..05f00012e 100644 --- a/app/docker/helpers/infoHelper.js +++ b/app/docker/helpers/infoHelper.js @@ -13,7 +13,7 @@ angular.module('portainer.docker') agentProxy: false }; - if (type === 2) { + if (type === 2 || type === 4) { mode.agentProxy = true; } diff --git a/app/docker/rest/container.js b/app/docker/rest/container.js index 94c3befbc..1aeb38e26 100644 --- a/app/docker/rest/container.js +++ b/app/docker/rest/container.js @@ -1,4 +1,4 @@ -import { logsHandler, genericHandler } from "./response/handlers"; +import {genericHandler, logsHandler} from './response/handlers'; angular.module('portainer.docker') .factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'ContainersInterceptor', @@ -11,7 +11,7 @@ function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, C { query: { method: 'GET', params: { all: 0, action: 'json', filters: '@filters' }, - isArray: true, interceptor: ContainersInterceptor, timeout: 10000 + isArray: true, interceptor: ContainersInterceptor, timeout: 15000 }, get: { method: 'GET', params: { action: 'json' } diff --git a/app/docker/rest/image.js b/app/docker/rest/image.js index fff8921d7..928466802 100644 --- a/app/docker/rest/image.js +++ b/app/docker/rest/image.js @@ -1,5 +1,5 @@ -import { jsonObjectsToArrayHandler, deleteImageHandler } from './response/handlers'; -import { imageGetResponse } from './response/image'; +import {deleteImageHandler, jsonObjectsToArrayHandler} from './response/handlers'; +import {imageGetResponse} from './response/image'; angular.module('portainer.docker') .factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', 'ImagesInterceptor', @@ -10,7 +10,7 @@ function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpR endpointId: EndpointProvider.endpointID }, { - query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true, interceptor: ImagesInterceptor, timeout: 10000}, + query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true, interceptor: ImagesInterceptor, timeout: 15000}, get: {method: 'GET', params: {action: 'json'}}, search: {method: 'GET', params: {action: 'search'}}, history: {method: 'GET', params: {action: 'history'}, isArray: true}, diff --git a/app/docker/rest/network.js b/app/docker/rest/network.js index 192a442fe..5830483ef 100644 --- a/app/docker/rest/network.js +++ b/app/docker/rest/network.js @@ -1,4 +1,4 @@ -import { genericHandler } from './response/handlers'; +import {genericHandler} from './response/handlers'; angular.module('portainer.docker') .factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'NetworksInterceptor', @@ -10,7 +10,7 @@ function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, Net }, { query: { - method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 10000 + method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 15000 }, get: { method: 'GET' diff --git a/app/docker/rest/system.js b/app/docker/rest/system.js index 4286bd74f..844ae046f 100644 --- a/app/docker/rest/system.js +++ b/app/docker/rest/system.js @@ -1,4 +1,4 @@ -import { jsonObjectsToArrayHandler } from './response/handlers'; +import {jsonObjectsToArrayHandler} from './response/handlers'; angular.module('portainer.docker') .factory('System', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'InfoInterceptor', 'VersionInterceptor', @@ -10,7 +10,7 @@ angular.module('portainer.docker') }, { info: { - method: 'GET', params: { action: 'info' }, timeout: 10000, interceptor: InfoInterceptor + method: 'GET', params: { action: 'info' }, timeout: 15000, interceptor: InfoInterceptor }, version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor }, events: { diff --git a/app/docker/rest/systemEndpoint.js b/app/docker/rest/systemEndpoint.js index 84ecf6a72..c54b8bda1 100644 --- a/app/docker/rest/systemEndpoint.js +++ b/app/docker/rest/systemEndpoint.js @@ -7,7 +7,7 @@ angular.module('portainer.docker') }, { ping: { - method: 'GET', params: { action: '_ping', endpointId: '@endpointId' }, timeout: 10000 + method: 'GET', params: { action: '_ping', endpointId: '@endpointId' } } }); }]); diff --git a/app/docker/rest/volume.js b/app/docker/rest/volume.js index 6ad6e6bdd..1a85ceef6 100644 --- a/app/docker/rest/volume.js +++ b/app/docker/rest/volume.js @@ -1,4 +1,4 @@ -import { genericHandler } from './response/handlers'; +import {genericHandler} from './response/handlers'; angular.module('portainer.docker') .factory('Volume', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'VolumesInterceptor', @@ -9,7 +9,7 @@ angular.module('portainer.docker') endpointId: EndpointProvider.endpointID }, { - query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 10000}, + query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000}, get: { method: 'GET', params: {id: '@id'} }, create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true}, remove: { diff --git a/app/docker/services/systemService.js b/app/docker/services/systemService.js index 5fa9e8327..9e89300ce 100644 --- a/app/docker/services/systemService.js +++ b/app/docker/services/systemService.js @@ -1,4 +1,4 @@ -import { EventViewModel } from '../models/event'; +import {EventViewModel} from '../models/event'; angular.module('portainer.docker') .factory('SystemService', ['$q', 'System', 'SystemEndpoint', function SystemServiceFactory($q, System, SystemEndpoint) { diff --git a/app/docker/views/services/edit/service.html b/app/docker/views/services/edit/service.html index 6e3765f9b..e4f7ef22a 100644 --- a/app/docker/views/services/edit/service.html +++ b/app/docker/views/services/edit/service.html @@ -71,7 +71,7 @@ ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" disable-authorization="DockerServiceUpdate"> -
+ diff --git a/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.js b/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.js index 422b3ff05..238ec3926 100644 --- a/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.js +++ b/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.js @@ -8,6 +8,7 @@ angular.module('portainer.docker').component('scheduleTasksDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - goToContainerLogs: '<' + goToContainerLogs: '<', + getEdgeTaskLogs: '<' } }); diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html index a44f84163..3e15875ba 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html @@ -1,7 +1,8 @@
- + + @@ -12,13 +13,16 @@ {{ $ctrl.model.Name }} - + + associated + associated + + {{ $ctrl.model.Status === 1 ? 'up' : 'down' }} {{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }} - @@ -64,6 +68,12 @@
+
+ + No snapshot available + +
+
@@ -82,7 +92,7 @@ - + {{ $ctrl.model.URL | stripprotocol }}
diff --git a/app/portainer/components/endpoint-list/endpoint-list-controller.js b/app/portainer/components/endpoint-list/endpoint-list-controller.js index bff29eb47..0abf59e8e 100644 --- a/app/portainer/components/endpoint-list/endpoint-list-controller.js +++ b/app/portainer/components/endpoint-list/endpoint-list-controller.js @@ -89,7 +89,6 @@ angular.module('portainer.app').controller('EndpointListController', ['Datatable } this.$onInit = function() { - this.state.loading = true; var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey); if (textFilter !== null) { diff --git a/app/portainer/components/forms/schedule-form/schedule-form.js b/app/portainer/components/forms/schedule-form/schedule-form.js index e33d90684..7d3685eff 100644 --- a/app/portainer/components/forms/schedule-form/schedule-form.js +++ b/app/portainer/components/forms/schedule-form/schedule-form.js @@ -25,7 +25,7 @@ angular.module('portainer.app').component('scheduleForm', { ctrl.formValues = { datetime: ctrl.model.CronExpression ? cronToDatetime(ctrl.model.CronExpression) : moment(), scheduleValue: ctrl.scheduleValues[0], - cronMethod: 'basic' + cronMethod: ctrl.model.Recurring ? 'advanced' : 'basic' }; function cronToDatetime(cron) { @@ -38,7 +38,7 @@ angular.module('portainer.app').component('scheduleForm', { function datetimeToCron(datetime) { var date = moment(datetime); - return '0 '.concat(date.minutes(), ' ', date.hours(), ' ', date.date(), ' ', (date.month() + 1)); + return '0 '.concat(date.minutes(), ' ', date.hours(), ' ', date.date(), ' ', (date.month() + 1), ' *'); } this.action = function() { diff --git a/app/portainer/components/forms/schedule-form/scheduleForm.html b/app/portainer/components/forms/schedule-form/scheduleForm.html index 0307b5dd5..83772022d 100644 --- a/app/portainer/components/forms/schedule-form/scheduleForm.html +++ b/app/portainer/components/forms/schedule-form/scheduleForm.html @@ -1,4 +1,15 @@
+
+ Information +
+
+ +

+ Due to how schedules behave differently on Edge endpoints and other endpoints it is recommended to create specific schedules that will only target one + type of endpoint. +

+
+
Schedule configuration
@@ -114,7 +125,12 @@
- You can refer to the following documentation to get more information about the supported cron expression format. +

+ You can refer to the following documentation to get more information about the supported cron expression format. +

+

+ Edge endpoint schedules are managed by cron on the underlying host. You need to use a valid cron expression that is different from the documentation above. +

@@ -123,6 +139,13 @@
Job configuration
+
+ +

+ This configuration will be ignored for Edge endpoint schedules. +

+
+
@@ -195,8 +218,13 @@
- This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the - /host folder. +

+ This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the + /host folder. +

+

+ Edge endpoint schedules are managed by cron on the underlying host. You have full access to the filesystem without having to use the /host folder. +

diff --git a/app/portainer/filters/filters.js b/app/portainer/filters/filters.js index 30ef37384..00ec47107 100644 --- a/app/portainer/filters/filters.js +++ b/app/portainer/filters/filters.js @@ -124,6 +124,8 @@ angular.module('portainer.app') return 'Agent'; } else if (type === 3) { return 'Azure ACI'; + } else if (type === 4) { + return 'Edge Agent'; } return ''; }; @@ -133,6 +135,8 @@ angular.module('portainer.app') return function (type) { if (type === 3) { return 'fab fa-microsoft'; + } else if (type === 4) { + return 'fa fa-cloud'; } return 'fab fa-docker'; }; diff --git a/app/portainer/models/schedule.js b/app/portainer/models/schedule.js index 1ccc23fb7..93ea1880c 100644 --- a/app/portainer/models/schedule.js +++ b/app/portainer/models/schedule.js @@ -1,4 +1,5 @@ -import { createStatus } from '../../docker/models/container'; +import _ from 'lodash-es'; +import {createStatus} from '../../docker/models/container'; export function ScheduleDefaultModel() { this.Name = ''; @@ -9,7 +10,7 @@ export function ScheduleDefaultModel() { } function ScriptExecutionDefaultJobModel() { - this.Image = ''; + this.Image = 'ubuntu:latest'; this.Endpoints = []; this.FileContent = ''; this.File = null; @@ -23,14 +24,20 @@ export function ScheduleModel(data) { this.JobType = data.JobType; this.CronExpression = data.CronExpression; this.Created = data.Created; + this.EdgeSchedule = data.EdgeSchedule; if (this.JobType === 1) { - this.Job = new ScriptExecutionJobModel(data.ScriptExecutionJob); + this.Job = new ScriptExecutionJobModel(data.ScriptExecutionJob, data.EdgeSchedule); } } -function ScriptExecutionJobModel(data) { +function ScriptExecutionJobModel(data, edgeSchedule) { this.Image = data.Image; this.Endpoints = data.Endpoints; + + if (edgeSchedule !== null) { + this.Endpoints = _.concat(data.Endpoints, edgeSchedule.Endpoints); + } + this.FileContent = ''; this.Method = 'editor'; this.RetryCount = data.RetryCount; @@ -42,6 +49,7 @@ export function ScriptExecutionTaskModel(data) { this.EndpointId = data.EndpointId; this.Status = createStatus(data.Status); this.Created = data.Created; + this.Edge = data.Edge; } export function ScheduleCreateRequest(model) { diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index d20d971df..4ac71e8a3 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -10,6 +10,7 @@ export function SettingsViewModel(data) { this.TemplatesURL = data.TemplatesURL; this.ExternalTemplates = data.ExternalTemplates; this.EnableHostManagementFeatures = data.EnableHostManagementFeatures; + this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; } export function PublicSettingsViewModel(settings) { diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index 85f671b60..ad055583f 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -13,8 +13,9 @@ angular.module('portainer.app') update: { method: 'PUT', params: { id: '@id' } }, updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } }, remove: { method: 'DELETE', params: { id: '@id'} }, - snapshots: { method: 'POST', params: { action: 'snapshot' }}, - snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' }}, - executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } } + snapshots: { method: 'POST', params: { action: 'snapshot' } }, + snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } }, + executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } }, + status: { method: 'GET', params: { id: '@id', action: 'status' } } }); }]); diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 9ee3db98d..7a43ee5b2 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -66,7 +66,12 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { service.createRemoteEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { var deferred = $q.defer(); - FileUploadService.createEndpoint(name, type, 'tcp://' + URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + var endpointURL = URL; + if (type !== 4) { + endpointURL = 'tcp://' + URL; + } + + FileUploadService.createEndpoint(name, type, endpointURL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) .then(function success(response) { deferred.resolve(response.data); }) diff --git a/app/portainer/services/api/groupService.js b/app/portainer/services/api/groupService.js index 073b4d122..88eb38774 100644 --- a/app/portainer/services/api/groupService.js +++ b/app/portainer/services/api/groupService.js @@ -1,8 +1,4 @@ -import { - EndpointGroupModel, - EndpointGroupCreateRequest, - EndpointGroupUpdateRequest -} from '../../models/group'; +import {EndpointGroupCreateRequest, EndpointGroupModel, EndpointGroupUpdateRequest} from '../../models/group'; angular.module('portainer.app') .factory('GroupService', ['$q', 'EndpointGroups', diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 83a472648..69ceecb0c 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -171,6 +171,7 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin var endpointAPIVersion = parseFloat(data.version.ApiVersion); state.endpoint.mode = endpointMode; state.endpoint.name = endpoint.Name; + state.endpoint.type = endpoint.Type; state.endpoint.apiVersion = endpointAPIVersion; state.endpoint.extensions = assignExtensions(extensions); diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index 2572b70e2..b2fe0dcee 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -1,4 +1,4 @@ -import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel'; +import {EndpointSecurityFormData} from '../../../components/endpointSecurity/porEndpointSecurityModel'; angular.module('portainer.app') .controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'Notifications', @@ -27,6 +27,14 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, $('#copyNotification').fadeOut(2000); }; + $scope.setDefaultPortainerInstanceURL = function() { + $scope.formValues.URL = window.location.origin; + }; + + $scope.resetEndpointURL = function() { + $scope.formValues.URL = ''; + }; + $scope.addDockerEndpoint = function() { var name = $scope.formValues.Name; var URL = $filter('stripprotocol')($scope.formValues.URL); @@ -56,6 +64,15 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, addEndpoint(name, 2, URL, publicURL, groupId, tags, true, true, true, null, null, null); }; + $scope.addEdgeAgentEndpoint = function() { + var name = $scope.formValues.Name; + var groupId = $scope.formValues.GroupId; + var tags = $scope.formValues.Tags; + var URL = $scope.formValues.URL; + + addEndpoint(name, 4, URL, "", groupId, tags, false, false, false, null, null, null); + }; + $scope.addAzureEndpoint = function() { var name = $scope.formValues.Name; var applicationId = $scope.formValues.AzureApplicationId; @@ -85,9 +102,13 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, function addEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { $scope.state.actionInProgress = true; EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) - .then(function success() { + .then(function success(data) { Notifications.success('Endpoint created', name); - $state.go('portainer.endpoints', {}, {reload: true}); + if (type === 4) { + $state.go('portainer.endpoints.endpoint', { id: data.Id }); + } else { + $state.go('portainer.endpoints', {}, {reload: true}); + } }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to create endpoint'); diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 3563a99b9..a011472ea 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -16,7 +16,7 @@
-
+
{{ item.Name }}
{{ item.Name }}
Loading...
{{ $ctrl.emptyDatasetMessage }}
Service webhook diff --git a/app/extensions/rbac/components/access-viewer/accessViewerController.js b/app/extensions/rbac/components/access-viewer/accessViewerController.js index 7b4fc5d4d..297e9f39c 100644 --- a/app/extensions/rbac/components/access-viewer/accessViewerController.js +++ b/app/extensions/rbac/components/access-viewer/accessViewerController.js @@ -1,7 +1,7 @@ -import _ from "lodash-es"; -import angular from "angular"; +import _ from 'lodash-es'; +import angular from 'angular'; -import AccessViewerPolicyModel from '../../models/access' +import AccessViewerPolicyModel from '../../models/access'; class AccessViewerController { /* @ngInject */ diff --git a/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.html b/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.html index 818b978a5..00b395086 100644 --- a/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.html +++ b/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.html @@ -51,15 +51,19 @@
{{ item.Endpoint.Name }} + Download logs - {{ item.Id | truncate: 32 }} + {{ item.Id | truncate: 32 }} + - - {{ item.Status }} + {{ item.Status }} + - - {{ item.Created | getisodatefromtimestamp}} + {{ item.Created | getisodatefromtimestamp}} + -