1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-10 00:05:24 +02:00

feat(app): rework private registries and support private registries in kubernetes EE-30 (#5131)

* feat(app): rework private registries and support private registries in kubernetes

[EE-30]

feat(api): backport private registries backend changes (#5072)

* feat(api/bolt): backport bolt changes

* feat(api/exec): backport exec changes

* feat(api/http): backport http/handler/dockerhub changes

* feat(api/http): backport http/handler/endpoints changes

* feat(api/http): backport http/handler/registries changes

* feat(api/http): backport http/handler/stacks changes

* feat(api/http): backport http/handler changes

* feat(api/http): backport http/proxy/factory/azure changes

* feat(api/http): backport http/proxy/factory/docker changes

* feat(api/http): backport http/proxy/factory/utils changes

* feat(api/http): backport http/proxy/factory/kubernetes changes

* feat(api/http): backport http/proxy/factory changes

* feat(api/http): backport http/security changes

* feat(api/http): backport http changes

* feat(api/internal): backport internal changes

* feat(api): backport api changes

* feat(api/kubernetes): backport kubernetes changes

* fix(api/http): changes on backend following backport

feat(app): backport private registries frontend changes (#5056)

* feat(app/docker): backport docker/components changes

* feat(app/docker): backport docker/helpers changes

* feat(app/docker): backport docker/views/container changes

* feat(app/docker): backport docker/views/images changes

* feat(app/docker): backport docker/views/registries changes

* feat(app/docker): backport docker/views/services changes

* feat(app/docker): backport docker changes

* feat(app/kubernetes): backport kubernetes/components changes

* feat(app/kubernetes): backport kubernetes/converters changes

* feat(app/kubernetes): backport kubernetes/models changes

* feat(app/kubernetes): backport kubernetes/registries changes

* feat(app/kubernetes): backport kubernetes/services changes

* feat(app/kubernetes): backport kubernetes/views/applications changes

* feat(app/kubernetes): backport kubernetes/views/configurations changes

* feat(app/kubernetes): backport kubernetes/views/configure changes

* feat(app/kubernetes): backport kubernetes/views/resource-pools changes

* feat(app/kubernetes): backport kubernetes/views changes

* feat(app/portainer): backport portainer/components/accessManagement changes

* feat(app/portainer): backport portainer/components/datatables changes

* feat(app/portainer): backport portainer/components/forms changes

* feat(app/portainer): backport portainer/components/registry-details changes

* feat(app/portainer): backport portainer/models changes

* feat(app/portainer): backport portainer/rest changes

* feat(app/portainer): backport portainer/services changes

* feat(app/portainer): backport portainer/views changes

* feat(app/portainer): backport portainer changes

* feat(app): backport app changes

* config(project): gitignore + jsconfig changes

gitignore all files under api/cmd/portainer but main.go and enable Code Editor autocomplete on import ... from '@/...'

fix(app): fix pull rate limit checker

fix(app/registries): sidebar menus and registry accesses users filtering

fix(api): add missing kube client factory

fix(kube): fetch dockerhub pull limits (#5133)

fix(app): pre review fixes (#5142)

* fix(app/registries): remove checkbox for endpointRegistries view

* fix(endpoints): allow access to default namespace

* fix(docker): fetch pull limits

* fix(kube/ns): show selected registries for non admin

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>

chore(webpack): ignore missing sourcemaps

fix(registries): fetch registry config from url

feat(kube/registries): ignore not found when deleting secret

feat(db): move migration to db 31

fix(registries): fix bugs in PR EE-869 (#5169)

* fix(registries): hide role

* fix(endpoints): set empty access policy to edge endpoint

* fix(registry): remove double arguments

* fix(admin): ignore warning

* feat(kube/configurations): tag registry secrets (#5157)

* feat(kube/configurations): tag registry secrets

* feat(kube/secrets): show registry secrets for admins

* fix(registries): move dockerhub to beginning

* refactor(registries): use endpoint scoped registries

feat(registries): filter by namespace if supplied

feat(access-managment): filter users for registry (#5191)

* refactor(access-manage): move users selector to component

* feat(access-managment): filter users for registry

refactor(registries): sync code with CE (#5200)

* refactor(registry): add inspect handler under endpoints

* refactor(endpoint): sync endpoint_registries_list

* refactor(endpoints): sync registry_access

* fix(db): rename migration functions

* fix(registries): show accesses for admin

* fix(kube): set token on transport

* refactor(kube): move secret help to bottom

* fix(kuberentes): remove shouldLog parameter

* style(auth): add description of security.IsAdmin

* feat(security): allow admin access to registry

* feat(edge): connect to edge endpoint when creating client

* style(portainer): change deprecation version

* refactor(sidebar): hide manage

* refactor(containers): revert changes

* style(container): remove whitespace

* fix(endpoint): add handler to registy on endpointService

* refactor(image): use endpointService.registries

* fix(kueb/namespaces): rename resource pool to namespace

* fix(kube/namespace): move selected registries

* fix(api/registries): hide accesses on registry creation

Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>

refactor(api): remove code duplication after rebase

fix(app/registries): replace last registry api usage by endpoint registry api

fix(api/endpoints): update registry access policies on endpoint deletion (#5226)

[EE-1027]

fix(db): update db version

* fix(dockerhub): fetch rate limits

* fix(registry/tests): supply restricred context

* fix(registries): show proget registry only when selected

* fix(registry): create dockerhub registry

* feat(db): move migrations to db 32

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
This commit is contained in:
LP B 2021-07-14 11:15:21 +02:00 committed by GitHub
parent 0f5407da40
commit 179df06267
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
175 changed files with 3757 additions and 2544 deletions

View file

@ -295,24 +295,12 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([
},
};
var registryCreation = {
const registryCreation = {
name: 'portainer.registries.new',
url: '/new',
views: {
'content@': {
templateUrl: './views/registries/create/createregistry.html',
controller: 'CreateRegistryController',
},
},
};
var registryAccess = {
name: 'portainer.registries.registry.access',
url: '/access',
views: {
'content@': {
templateUrl: './views/registries/access/registryAccess.html',
controller: 'RegistryAccessController',
component: 'createRegistry',
},
},
};
@ -425,7 +413,6 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([
$stateRegistryProvider.register(initAdmin);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registry);
$stateRegistryProvider.register(registryAccess);
$stateRegistryProvider.register(registryCreation);
$stateRegistryProvider.register(settings);
$stateRegistryProvider.register(settingsAuthentication);

View file

@ -0,0 +1,9 @@
import angular from 'angular';
import { porAccessManagement } from './por-access-management';
import { porAccessManagementUsersSelector } from './por-access-management-users-selector';
export default angular
.module('portainer.app.component.access-management', [])
.component('porAccessManagement', porAccessManagement)
.component('porAccessManagementUsersSelector', porAccessManagementUsersSelector).name;

View file

@ -0,0 +1,7 @@
export const porAccessManagementUsersSelector = {
templateUrl: './por-access-management-users-selector.html',
bindings: {
options: '<',
value: '=',
},
};

View file

@ -0,0 +1,23 @@
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">
Select user(s) and/or team(s)
</label>
<div class="col-sm-9 col-lg-4">
<span class="small text-muted" ng-if="$ctrl.options.length === 0">
No users or teams available.
</span>
<span
isteven-multi-select
ng-if="$ctrl.options.length > 0"
input-model="$ctrl.options"
output-model="$ctrl.value"
button-label="icon '-' Name"
item-label="icon '-' Name"
tick-property="ticked"
helper-elements="filter"
search-property="Name"
translation="{nothingSelected: 'Select one or more users and/or teams', search: 'Search...'}"
>
</span>
</div>
</div>

View file

@ -1,4 +1,4 @@
angular.module('portainer.app').component('porAccessManagement', {
export const porAccessManagement = {
templateUrl: './porAccessManagement.html',
controller: 'porAccessManagementController',
controllerAs: 'ctrl',
@ -8,5 +8,6 @@ angular.module('portainer.app').component('porAccessManagement', {
entityType: '@',
updateAccess: '<',
actionInProgress: '<',
filterUsers: '<',
},
});
};

View file

@ -4,31 +4,9 @@
<rd-widget-header icon="fa-user-lock" title-text="Create access"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">
Select user(s) and/or team(s)
</label>
<div class="col-sm-9 col-lg-4">
<span class="small text-muted" ng-if="ctrl.availableUsersAndTeams.length === 0">
No users or teams available.
</span>
<span
isteven-multi-select
ng-if="ctrl.availableUsersAndTeams.length > 0"
input-model="ctrl.availableUsersAndTeams"
output-model="ctrl.formValues.multiselectOutput"
button-label="icon '-' Name"
item-label="icon '-' Name"
tick-property="ticked"
helper-elements="filter"
search-property="Name"
translation="{nothingSelected: 'Select one or more users and/or teams', search: 'Search...'}"
>
</span>
</div>
</div>
<por-access-management-users-selector options="ctrl.availableUsersAndTeams" value="ctrl.formValues.multiselectOutput"></por-access-management-users-selector>
<div class="form-group">
<div class="form-group" ng-if="ctrl.entityType != 'registry'">
<label class="col-sm-3 col-lg-2 control-label text-left">
Role
</label>

View file

@ -44,6 +44,7 @@ class PorAccessManagementController {
const teamAccessPolicies = entity.TeamAccessPolicies;
const selectedUserAccesses = _.filter(selectedAccesses, (access) => access.Type === 'user');
const selectedTeamAccesses = _.filter(selectedAccesses, (access) => access.Type === 'team');
_.forEach(selectedUserAccesses, (access) => delete userAccessPolicies[access.Id]);
_.forEach(selectedTeamAccesses, (access) => delete teamAccessPolicies[access.Id]);
this.updateAccess();
@ -55,6 +56,11 @@ class PorAccessManagementController {
const parent = this.inheritFrom;
const data = await this.AccessService.accesses(entity, parent, this.roles);
if (this.filterUsers) {
data.availableUsersAndTeams = this.filterUsers(data.availableUsersAndTeams);
}
this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc');
this.authorizedUsersAndTeams = data.authorizedUsersAndTeams;
} catch (err) {

View file

@ -4,8 +4,14 @@
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="actionBar" ng-if="$ctrl.accessManagement">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<div class="actionBar" ng-if="$ctrl.isAdmin">
<button
ng-if="!$ctrl.endpointType"
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.registries.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add registry </button>
@ -27,7 +33,7 @@
<thead>
<tr>
<th>
<span class="md-checkbox" ng-if="$ctrl.accessManagement">
<span class="md-checkbox" ng-if="$ctrl.isAdmin && !$ctrl.endpointType">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
@ -53,22 +59,29 @@
ng-class="{ active: item.Checked }"
>
<td>
<span class="md-checkbox" ng-if="$ctrl.accessManagement">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
<span class="md-checkbox" ng-if="$ctrl.isAdmin && !$ctrl.endpointType">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.registries.registry({id: item.Id})" ng-if="$ctrl.accessManagement">{{ item.Name }}</a>
<span ng-if="!$ctrl.accessManagement">{{ item.Name }}</span>
<a ui-sref="portainer.registries.registry({id: item.Id})" ng-if="$ctrl.enableGoToLink(item)">{{ item.Name }}</a>
<span ng-if="!$ctrl.enableGoToLink(item)">{{ item.Name }}</span>
<span ng-if="item.Authentication" style="margin-left: 5px;" class="label label-info image-tag">authentication-enabled</span>
</td>
<td>
{{ item.URL }}
</td>
<td>
<a ui-sref="portainer.registries.registry.access({id: item.Id})" ng-if="$ctrl.accessManagement"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
<span class="text-muted space-left" style="cursor: pointer;" data-toggle="tooltip" title="This feature is available in Portainer Business Edition">
<a ng-if="$ctrl.canManageAccess(item)" ng-click="$ctrl.redirectToManageAccess(item)"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
<span
ng-if="$ctrl.canBrowse(item)"
class="text-muted space-left"
style="cursor: pointer;"
data-toggle="tooltip"
title="This feature is available in Portainer Business Edition"
>
<i class="fa fa-search" aria-hidden="true"></i> Browse</span
>
<span ng-if="!$ctrl.canBrowse(item) && !$ctrl.canManageAccess(item)"> - </span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">

View file

@ -1,6 +1,6 @@
angular.module('portainer.app').component('registriesDatatable', {
templateUrl: './registriesDatatable.html',
controller: 'GenericDatatableController',
controller: 'RegistriesDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
@ -8,8 +8,9 @@ angular.module('portainer.app').component('registriesDatatable', {
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
accessManagement: '<',
removeAction: '<',
canBrowse: '<',
endpointType: '<',
canManageAccess: '<',
},
});

View file

@ -0,0 +1,85 @@
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
angular.module('portainer.docker').controller('RegistriesDatatableController', RegistriesDatatableController);
/* @ngInject */
function RegistriesDatatableController($scope, $controller, $state, Authentication, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.allowSelection = function (item) {
return item.Id;
};
this.enableGoToLink = (item) => {
return this.isAdmin && item.Id && !this.endpointType;
};
this.goToRegistry = function (item) {
if (
this.endpointType === PortainerEndpointTypes.KubernetesLocalEnvironment ||
this.endpointType === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
this.endpointType === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
) {
$state.go('kubernetes.registries.registry', { id: item.Id });
} else if (
this.endpointType === PortainerEndpointTypes.DockerEnvironment ||
this.endpointType === PortainerEndpointTypes.AgentOnDockerEnvironment ||
this.endpointType === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment
) {
$state.go('docker.registries.registry', { id: item.Id });
} else {
$state.go('portainer.registries.registry', { id: item.Id });
}
};
this.redirectToManageAccess = function (item) {
if (
this.endpointType === PortainerEndpointTypes.KubernetesLocalEnvironment ||
this.endpointType === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
this.endpointType === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
) {
$state.go('kubernetes.registries.access', { id: item.Id });
} else {
$state.go('docker.registries.access', { id: item.Id });
}
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.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();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
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();
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
if (storedColumnVisibility !== null) {
this.columnVisibility = storedColumnVisibility;
}
};
}

View file

@ -0,0 +1,20 @@
import angular from 'angular';
// import controller from './strings-datatable.controller.js';
export const stringsDatatable = {
templateUrl: './strings-datatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
emptyDatasetMessage: '@',
columnHeader: '@',
tableKey: '@',
onRemove: '<',
},
};
angular.module('portainer.app').component('stringsDatatable', stringsDatatable);

View file

@ -0,0 +1,65 @@
<div class="datatable">
<rd-widget>
<rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="{{ $ctrl.titleText }}"> </rd-widget-header>
<rd-widget-body classes="no-padding">
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.onRemove($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
{{ $ctrl.columnHeader }}
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit)) track by $index"
ng-class="{ active: $ctrl.state.selectedItems.includes(item) }"
>
<td>
<span class="md-checkbox">
<input
id="select_{{ $index }}"
type="checkbox"
ng-checked="$ctrl.state.selectedItems.includes(item)"
ng-disabled="$ctrl.disableRemove(item)"
ng-click="$ctrl.selectItem(item, $event)"
/>
<label for="select_{{ $index }}"></label>
</span>
{{ item.value }}
</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td class="text-center text-muted">{{ $ctrl.emptyDatasetMessage }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,64 @@
<form class="form-horizontal" name="registryFormDockerhub" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title">
DockerHub account details
</div>
<!-- name-input -->
<div class="form-group">
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="dockerhub-prod-us" required />
</div>
</div>
<div class="form-group" ng-show="registryFormDockerhub.registry_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormDockerhub.registry_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !name-input -->
<!-- credentials-user -->
<div class="form-group">
<label for="registry_username" class="col-sm-3 col-lg-2 control-label text-left">DockerHub username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required />
</div>
</div>
<div class="form-group" ng-show="registryFormDockerhub.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormDockerhub.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="registry_password" class="col-sm-3 col-lg-2 control-label text-left">DockerHub password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required />
</div>
</div>
<div class="form-group" ng-show="registryFormDockerhub.registry_password.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormDockerhub.registry_password.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-password -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid" button-spinner="$ctrl.actionInProgress">
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>

View file

@ -0,0 +1,9 @@
angular.module('portainer.app').component('registryFormDockerhub', {
templateUrl: './registry-form-dockerhub.html',
bindings: {
model: '=',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
},
});

View file

@ -1,13 +0,0 @@
angular.module('portainer.app').component('templateForm', {
templateUrl: './templateForm.html',
controller: 'TemplateFormController',
bindings: {
model: '=',
categories: '<',
networks: '<',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
showTypeSelector: '<',
},
});

View file

@ -1,580 +0,0 @@
<form class="form-horizontal" name="templateForm">
<!-- title-input -->
<div class="form-group" ng-class="{ 'has-error': templateForm.template_title.$invalid }">
<label for="template_title" class="col-sm-3 col-lg-2 control-label text-left">Title</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_title" ng-model="$ctrl.model.Title" placeholder="e.g. my-template" required auto-focus />
</div>
</div>
<div class="form-group" ng-show="templateForm.template_title.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="templateForm.template_title.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !title-input -->
<!-- description-input -->
<div class="form-group" ng-class="{ 'has-error': templateForm.template_description.$invalid }">
<label for="template_description" class="col-sm-3 col-lg-2 control-label text-left">Description</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_description" ng-model="$ctrl.model.Description" placeholder="e.g. template description..." required />
</div>
</div>
<div class="form-group" ng-show="templateForm.template_description.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="templateForm.template_description.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !description-input -->
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseTemplate = !$ctrl.state.collapseTemplate">
Template
<span class="small space-left">
<a ng-if="$ctrl.state.collapseTemplate"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
<a ng-if="!$ctrl.state.collapseTemplate"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
</span>
</div>
<!-- template-details -->
<div uib-collapse="$ctrl.state.collapseTemplate">
<div ng-if="$ctrl.showTypeSelector">
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="template_container" ng-model="$ctrl.model.Type" ng-value="1" />
<label for="template_container">
<div class="boxselector_header">
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
Container
</div>
<p>Container template</p>
</label>
</div>
<div>
<input type="radio" id="template_swarm_stack" ng-model="$ctrl.model.Type" ng-value="2" />
<label for="template_swarm_stack">
<div class="boxselector_header">
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
Swarm stack
</div>
<p>Stack template (Swarm)</p>
</label>
</div>
<div>
<input type="radio" id="template_compose_stack" ng-model="$ctrl.model.Type" ng-value="3" />
<label for="template_compose_stack">
<div class="boxselector_header">
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
Compose stack
</div>
<p>Stack template (Compose)</p>
</label>
</div>
</div>
</div>
</div>
<!-- name -->
<div class="form-group">
<label for="template_name" class="col-sm-3 col-lg-2 control-label text-left">
Name
<portainer-tooltip position="bottom" message="Default name that will be associated to the template"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_name" ng-model="$ctrl.model.Name" placeholder="e.g. myApp" />
</div>
</div>
<!-- !name -->
<!-- logo -->
<div class="form-group">
<label for="template_logo" class="col-sm-3 col-lg-2 control-label text-left">
Logo URL
<portainer-tooltip position="bottom" message="Recommended size: 60x60"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_logo" ng-model="$ctrl.model.Logo" placeholder="e.g. https://portainer.io/images/logos/nginx.png" />
</div>
</div>
<!-- !logo -->
<!-- note -->
<div class="form-group">
<label for="template_note" class="col-sm-3 col-lg-2 control-label text-left">
Note
<portainer-tooltip position="bottom" message="Usage/extra information about the template. Supports HTML."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<textarea
class="form-control"
name="template_note"
ng-model="$ctrl.model.Note"
placeholder="You can use this field to specify extra information. <br/> It supports <b>HTML</b>."
></textarea>
</div>
</div>
<!-- !note -->
<!-- platform -->
<div class="form-group">
<label for="template_platform" class="col-sm-3 col-lg-2 control-label text-left">
Platform
</label>
<div class="col-sm-9 col-lg-10">
<select class="form-control" name="template_platform" ng-model="$ctrl.model.Platform">
<option value="">Multi-platform</option>
<option value="linux">Linux</option>
<option value="windows">Windows</option>
</select>
</div>
</div>
<!-- !platform -->
<!-- categories -->
<div class="form-group">
<label for="template_categories" class="col-sm-3 col-lg-2 control-label text-left">
Categories
</label>
<div class="col-sm-9 col-lg-10">
<ui-select multiple tagging tagging-label="(new category)" ng-model="$ctrl.model.Categories" sortable="true" style="width: 300px;" title="Choose a category">
<ui-select-match placeholder="Select categories...">{{ $item }}</ui-select-match>
<ui-select-choices repeat="category in $ctrl.categories | filter:$select.search">
{{ category }}
</ui-select-choices>
</ui-select>
</div>
</div>
<!-- !categories -->
<!-- administrator-only -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
Administrator template
<portainer-tooltip position="bottom" message="This template will only be available to administrator users."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.model.AdministratorOnly" /><i></i> </label>
</div>
</div>
<!-- administrator-only -->
</div>
<!-- !template-details -->
<div ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseStack = !$ctrl.state.collapseStack">
Stack
<span class="small space-left">
<a ng-if="$ctrl.state.collapseStack"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
<a ng-if="!$ctrl.state.collapseStack"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
</span>
</div>
<!-- stack-details -->
<div uib-collapse="$ctrl.state.collapseStack">
<!-- repository-url -->
<div class="form-group" ng-class="{ 'has-error': templateForm.template_repository_url.$invalid }">
<label for="template_repository_url" class="col-sm-3 col-lg-2 control-label text-left">Repository URL</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
name="template_repository_url"
ng-model="$ctrl.model.Repository.url"
placeholder="https://github.com/portainer/portainer-compose"
required
/>
</div>
</div>
<div class="form-group" ng-show="templateForm.template_repository_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="templateForm.template_repository_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !repository-url -->
<!-- composefile-path -->
<div class="form-group">
<label for="template_repository_path" class="col-sm-3 col-lg-2 control-label text-left">
Compose file path
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_repository_path" ng-model="$ctrl.model.Repository.stackfile" placeholder="docker-compose.yml" />
</div>
</div>
<!-- !composefile-path -->
</div>
<!-- !stack-details -->
</div>
<div ng-if="$ctrl.model.Type === 1">
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseContainer = !$ctrl.state.collapseContainer">
Container
<span class="small space-left">
<a ng-if="$ctrl.state.collapseContainer"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
<a ng-if="!$ctrl.state.collapseContainer"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
</span>
</div>
<!-- container-details -->
<div uib-collapse="$ctrl.state.collapseContainer">
<por-image-registry model="$ctrl.model.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<!-- command -->
<div class="form-group">
<label for="template_command" class="col-sm-3 col-lg-2 control-label text-left">
Command
<portainer-tooltip
position="bottom"
message="The command to run in the container. If not specified, the container will use the default command specified in its Dockerfile."
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_command" ng-model="$ctrl.model.Command" placeholder='/bin/bash -c \"echo hello\" && exit 777' />
</div>
</div>
<!-- !command -->
<!-- hostname -->
<div class="form-group">
<label for="template_hostname" class="col-sm-3 col-lg-2 control-label text-left">
Hostname
<portainer-tooltip position="bottom" message="Set the hostname of the container. Will use Docker default if not specified."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_hostname" ng-model="$ctrl.model.Hostname" placeholder="mycontainername" />
</div>
</div>
<!-- !hostname -->
<!-- network -->
<div class="form-group">
<label for="template_network" class="col-sm-3 col-lg-2 control-label text-left">
Network
</label>
<div class="col-sm-10">
<select class="form-control" ng-options="net.Name for net in $ctrl.networks" ng-model="$ctrl.model.Network">
<option disabled hidden value="">Select a network</option>
</select>
</div>
</div>
<!-- !network -->
<!-- port-mapping -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span>
</div>
<div class="col-sm-12" style="margin-top: 10px;" ng-if="$ctrl.model.Ports.length > 0">
<span class="small text-muted">Portainer will automatically assign a port if you leave the host port empty.</span>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in $ctrl.model.Ports" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)" />
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80" />
</div>
<!-- !container-port -->
<!-- protocol-actions -->
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div>
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<!-- volumes -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Volume mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
</span>
</div>
<div class="col-sm-12" style="margin-top: 10px;" ng-if="$ctrl.model.Volumes.length > 0">
<span class="small text-muted">Portainer will automatically create and map a local volume when using the <b>auto</b> option.</span>
</div>
<div ng-repeat="volume in $ctrl.model.Volumes">
<div class="col-sm-12" style="margin-top: 10px;">
<!-- volume-line1 -->
<div class="col-sm-12 form-inline">
<!-- container-path -->
<div class="input-group input-group-sm col-sm-6">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.container" placeholder="e.g. /path/in/container" />
</div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'auto'" ng-click="volume.bind = ''">Auto</label>
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.bind = ''">Bind</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeVolume($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !volume-type -->
</div>
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="col-sm-12 form-inline" style="margin-top: 5px;" ng-if="volume.type !== 'auto'">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
<!-- bind -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'bind'">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.bind" placeholder="e.g. /path/on/host" />
</div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="true">Read-only</label>
</div>
</div>
<!-- !read-only -->
</div>
<!-- !volume-line2 -->
</div>
</div>
</div>
<!-- !volumes -->
<!-- labels -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Labels</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in $ctrl.model.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo" />
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" />
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels -->
<!-- restart_policy -->
<div class="form-group">
<label for="template_restart_policy" class="col-sm-3 col-lg-2 control-label text-left">
Restart policy
</label>
<div class="col-sm-9 col-lg-10">
<select class="form-control" name="template_platform" ng-model="$ctrl.model.RestartPolicy">
<option value="always">Always</option>
<option value="unless-stopped">Unless stopped</option>
<option value="on-failure">On failure</option>
<option value="no">None</option>
</select>
</div>
</div>
<!-- !restart_policy -->
<!-- privileged-mode -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
Privileged mode
<portainer-tooltip position="bottom" message="Start the container in privileged mode."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.model.Privileged" /><i></i> </label>
</div>
</div>
<!-- !privileged-mode -->
<!-- interactive-mode -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
Interactive mode
<portainer-tooltip position="bottom" message="Start the container in foreground (equivalent of -i -t flags)."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.model.Interactive" /><i></i> </label>
</div>
</div>
<!-- !interactive-mode -->
</div>
<!-- !container-details -->
</div>
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseEnv = !$ctrl.state.collapseEnv">
Environment
<span class="small space-left">
<a ng-if="$ctrl.state.collapseEnv"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
<a ng-if="!$ctrl.state.collapseEnv"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
</span>
</div>
<!-- environment-details -->
<div uib-collapse="$ctrl.state.collapseEnv">
<!-- env -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Environment variables</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addEnvVar()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add variable
</span>
</div>
<!-- env-var-list -->
<div style="margin-top: 10px;">
<div class="col-sm-12 template-envvar" ng-repeat="var in $ctrl.model.Env" style="margin-top: 10px;">
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="preset_var_{{ $index }}" ng-model="var.type" ng-value="1" ng-change="$ctrl.changeEnvVarType(var)" />
<label for="preset_var_{{ $index }}">
<div class="boxselector_header">
<i class="fa fa-user-slash" aria-hidden="true" style="margin-right: 2px;"></i>
Preset
</div>
<p>Preset variable</p>
</label>
</div>
<div>
<input type="radio" id="text_var_{{ $index }}" ng-model="var.type" ng-value="2" ng-change="$ctrl.changeEnvVarType(var)" />
<label for="text_var_{{ $index }}">
<div class="boxselector_header">
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
Text
</div>
<p>Free text value</p>
</label>
</div>
<div>
<input type="radio" id="select_var_{{ $index }}" ng-model="var.type" ng-value="3" />
<label for="select_var_{{ $index }}">
<div class="boxselector_header">
<i class="fa fa-list-ol" aria-hidden="true" style="margin-right: 2px;"></i>
Select
</div>
<p>Choose value from list</p>
</label>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label text-left">
Name
</label>
<div class="col-sm-8">
<input type="text" class="form-control" ng-model="var.name" placeholder="env_var" />
</div>
<div class="col-sm-2">
<button class="btn btn-sm btn-danger space-left" type="button" ng-click="$ctrl.removeEnvVar($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<div ng-if="var.type == 2 || var.type == 3">
<div class="form-group">
<label class="col-sm-2 control-label text-left">
Label
</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="var.label" placeholder="Choose a label" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label text-left" style="margin-top: 2px;">
Description
</label>
<div class="col-sm-10" style="margin-top: 2px;">
<input type="text" class="form-control" ng-model="var.description" placeholder="Tooltip" />
</div>
</div>
</div>
<div class="form-group" ng-if="var.type === 1 || var.type === 2">
<label class="col-sm-2 control-label text-left">
Default value
</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="var.default" placeholder="default_value" />
</div>
</div>
<div ng-if="var.type === 3" style="margin-bottom: 5px;" class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Values</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addEnvVarValue(var)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add allowed value
</span>
</div>
<!-- envvar-values-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="val in var.select" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="val.text" placeholder="Yes, I agree" />
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="val.value" placeholder="Y" />
</div>
<div class="input-group col-sm-1 input-group-sm">
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeEnvVarValue(var, $index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
<input style="margin-left: 5px;" type="checkbox" ng-model="val.default" id="val_default_{{ $index }}" /><label for="val_default_{{ $index }}" class="space-left"
>Default</label
>
</div>
</div>
</div>
</div>
<!-- envvar-values-list -->
</div>
<div class="col-sm-12" ng-show="$ctrl.model.Env.length > 1">
<div class="line-separator"></div>
</div>
</div>
</div>
<!-- !env-var-list -->
</div>
<!-- !env -->
</div>
<!-- !environment-details -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="$ctrl.formAction()"
ng-disabled="$ctrl.actionInProgress || !templateForm.$valid"
button-spinner="$ctrl.actionInProgress"
>
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>

View file

@ -1,55 +0,0 @@
angular.module('portainer.app').controller('TemplateFormController', [
function () {
this.state = {
collapseTemplate: false,
collapseContainer: false,
collapseStack: false,
collapseEnv: false,
};
this.addPortBinding = function () {
this.model.Ports.push({ containerPort: '', protocol: 'tcp' });
};
this.removePortBinding = function (index) {
this.model.Ports.splice(index, 1);
};
this.addVolume = function () {
this.model.Volumes.push({ container: '', bind: '', readonly: false, type: 'auto' });
};
this.removeVolume = function (index) {
this.model.Volumes.splice(index, 1);
};
this.addLabel = function () {
this.model.Labels.push({ name: '', value: '' });
};
this.removeLabel = function (index) {
this.model.Labels.splice(index, 1);
};
this.addEnvVar = function () {
this.model.Env.push({ type: 1, name: '', label: '', description: '', default: '', preset: true, select: [] });
};
this.removeEnvVar = function (index) {
this.model.Env.splice(index, 1);
};
this.addEnvVarValue = function (env) {
env.select = env.select || [];
env.select.push({ name: '', value: '' });
};
this.removeEnvVarValue = function (env, index) {
env.select.splice(index, 1);
};
this.changeEnvVarType = function (env) {
env.preset = env.type === 1;
};
},
]);

View file

@ -1,5 +1,6 @@
import angular from 'angular';
import gitFormModule from './forms/git-form';
import porAccessManagementModule from './accessManagement';
export default angular.module('portainer.app.components', [gitFormModule]).name;
export default angular.module('portainer.app.components', [gitFormModule, porAccessManagementModule]).name;

View file

@ -0,0 +1,10 @@
import angular from 'angular';
export const registryDetails = {
templateUrl: './registry-details.html',
bindings: {
registry: '<',
},
};
angular.module('portainer.app').component('registryDetails', registryDetails);

View file

@ -0,0 +1,25 @@
<div class="row" ng-if="$ctrl.registry">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title-text="Registry"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
{{ $ctrl.registry.Name }}
</td>
</tr>
<tr>
<td>URL</td>
<td>
{{ $ctrl.registry.URL }}
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,36 +1,33 @@
import _ from 'lodash-es';
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
angular.module('portainer.app').factory('EndpointHelper', [
function EndpointHelperFactory() {
'use strict';
var helper = {};
function findAssociatedGroup(endpoint, groups) {
return _.find(groups, function (group) {
return group.Id === endpoint.GroupId;
});
}
function findAssociatedGroup(endpoint, groups) {
return _.find(groups, function (group) {
return group.Id === endpoint.GroupId;
});
}
export default class EndpointHelper {
static isLocalEndpoint(endpoint) {
return endpoint.URL.includes('unix://') || endpoint.URL.includes('npipe://') || endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment;
}
helper.isLocalEndpoint = isLocalEndpoint;
function isLocalEndpoint(endpoint) {
return endpoint.URL.includes('unix://') || endpoint.URL.includes('npipe://') || endpoint.Type === 5;
}
static isAgentEndpoint(endpoint) {
return [
PortainerEndpointTypes.AgentOnDockerEnvironment,
PortainerEndpointTypes.EdgeAgentOnDockerEnvironment,
PortainerEndpointTypes.AgentOnKubernetesEnvironment,
PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment,
].includes(endpoint.Type);
}
helper.isAgentEndpoint = isAgentEndpoint;
function isAgentEndpoint(endpoint) {
return [2, 4, 6, 7].includes(endpoint.Type);
}
helper.mapGroupNameToEndpoint = function (endpoints, groups) {
for (var i = 0; i < endpoints.length; i++) {
var endpoint = endpoints[i];
var group = findAssociatedGroup(endpoint, groups);
if (group) {
endpoint.GroupName = group.Name;
}
static mapGroupNameToEndpoint(endpoints, groups) {
for (var i = 0; i < endpoints.length; i++) {
var endpoint = endpoints[i];
var group = findAssociatedGroup(endpoint, groups);
if (group) {
endpoint.GroupName = group.Name;
}
};
return helper;
},
]);
}
}
}

View file

@ -1,7 +1,8 @@
export function DockerHubViewModel(data) {
this.Name = 'DockerHub';
this.URL = '';
this.Authentication = data.Authentication;
this.Username = data.Username;
this.Password = data.Password;
import { RegistryTypes } from './registryTypes';
export function DockerHubViewModel() {
this.Id = 0;
this.Type = RegistryTypes.ANONYMOUS;
this.Name = 'DockerHub (anonymous)';
this.URL = 'docker.io';
}

View file

@ -10,10 +10,7 @@ export function RegistryViewModel(data) {
this.Authentication = data.Authentication;
this.Username = data.Username;
this.Password = data.Password;
this.AuthorizedUsers = data.AuthorizedUsers;
this.AuthorizedTeams = data.AuthorizedTeams;
this.UserAccessPolicies = data.UserAccessPolicies;
this.TeamAccessPolicies = data.TeamAccessPolicies;
this.RegistryAccesses = data.RegistryAccesses; // map[EndpointID]{UserAccessPolicies, TeamAccessPolicies, NamespaceAccessPolicies}
this.Checked = false;
this.Gitlab = data.Gitlab;
this.Quay = data.Quay;
@ -40,7 +37,7 @@ export function RegistryManagementConfigurationDefaultModel(registry) {
}
}
export function RegistryDefaultModel() {
export function RegistryCreateFormValues() {
this.Type = RegistryTypes.CUSTOM;
this.URL = '';
this.Name = '';

View file

@ -1,7 +1,9 @@
export const RegistryTypes = Object.freeze({
ANONYMOUS: 0, // not backend replicated, only for frontend
QUAY: 1,
AZURE: 2,
CUSTOM: 3,
GITLAB: 4,
PROGET: 5,
DOCKERHUB: 6,
});

View file

@ -1,15 +0,0 @@
angular.module('portainer.app').factory('DockerHub', [
'$resource',
'API_ENDPOINT_DOCKERHUB',
function DockerHubFactory($resource, API_ENDPOINT_DOCKERHUB) {
'use strict';
return $resource(
API_ENDPOINT_DOCKERHUB,
{},
{
get: { method: 'GET' },
update: { method: 'PUT' },
}
);
},
]);

View file

@ -22,7 +22,26 @@ angular.module('portainer.app').factory('Endpoints', [
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } },
status: { method: 'GET', params: { id: '@id', action: 'status' } },
updateSecuritySettings: { method: 'PUT', params: { id: '@id', action: 'settings' } },
dockerhubLimits: { method: 'GET', params: { id: '@id', action: 'dockerhub' } },
dockerhubLimits: {
method: 'GET',
url: `${API_ENDPOINT_ENDPOINTS}/:id/dockerhub/:registryId`,
},
registries: {
method: 'GET',
url: `${API_ENDPOINT_ENDPOINTS}/:id/registries`,
params: { id: '@id', namespace: '@namespace' },
isArray: true,
},
registry: {
url: `${API_ENDPOINT_ENDPOINTS}/:id/registries/:registryId`,
method: 'GET',
params: { id: '@id', namespace: '@namespace', registryId: '@registryId' },
},
updateRegistryAccess: {
method: 'PUT',
url: `${API_ENDPOINT_ENDPOINTS}/:id/registries/:registryId`,
params: { id: '@id', registryId: '@registryId' },
},
}
);
},

View file

@ -9,9 +9,8 @@ angular.module('portainer.app').factory('Registries', [
{
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
get: { method: 'GET', params: { id: '@id', action: '', endpointId: '@endpointId' } },
update: { method: 'PUT', params: { id: '@id' } },
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
remove: { method: 'DELETE', params: { id: '@id' } },
configure: { method: 'POST', params: { id: '@id', action: 'configure' } },
}

View file

@ -9,7 +9,10 @@ angular.module('portainer.app').factory('AccessService', [
'TeamService',
function AccessServiceFactory($q, $async, UserService, TeamService) {
'use strict';
var service = {};
return {
accesses,
generateAccessPolicies,
};
function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies) {
var availableAccesses = [];
@ -76,7 +79,7 @@ angular.module('portainer.app').factory('AccessService', [
async function accessesAsync(entity, parent) {
try {
if (!entity) {
throw { msg: 'Unable to retrieve accesses' };
throw new Error('Unable to retrieve accesses');
}
if (!entity.UserAccessPolicies) {
entity.UserAccessPolicies = {};
@ -100,9 +103,7 @@ angular.module('portainer.app').factory('AccessService', [
return $async(accessesAsync, entity, parent);
}
service.accesses = accesses;
service.generateAccessPolicies = function (userAccessPolicies, teamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId) {
function generateAccessPolicies(userAccessPolicies, teamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId) {
const newUserPolicies = _.clone(userAccessPolicies);
const newTeamPolicies = _.clone(teamAccessPolicies);
@ -115,8 +116,6 @@ angular.module('portainer.app').factory('AccessService', [
};
return accessPolicies;
};
return service;
}
},
]);

View file

@ -1,51 +1,27 @@
import { DockerHubViewModel } from '../../models/dockerhub';
import EndpointHelper from 'Portainer/helpers/endpointHelper';
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
angular.module('portainer.app').factory('DockerHubService', [
'$q',
'DockerHub',
'Endpoints',
'AgentDockerhub',
'EndpointHelper',
function DockerHubServiceFactory($q, DockerHub, Endpoints, AgentDockerhub, EndpointHelper) {
'use strict';
var service = {};
angular.module('portainer.app').factory('DockerHubService', DockerHubService);
service.dockerhub = function () {
var deferred = $q.defer();
/* @ngInject */
function DockerHubService(Endpoints, AgentDockerhub) {
return {
checkRateLimits,
};
DockerHub.get()
.$promise.then(function success(data) {
var dockerhub = new DockerHubViewModel(data);
deferred.resolve(dockerhub);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve DockerHub details', err: err });
});
return deferred.promise;
};
service.update = function (dockerhub) {
return DockerHub.update({}, dockerhub).$promise;
};
service.checkRateLimits = checkRateLimits;
function checkRateLimits(endpoint) {
if (EndpointHelper.isLocalEndpoint(endpoint)) {
return Endpoints.dockerhubLimits({ id: endpoint.Id }).$promise;
}
switch (endpoint.Type) {
case 2: //AgentOnDockerEnvironment
case 4: //EdgeAgentOnDockerEnvironment
return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'docker' }).$promise;
case 6: //AgentOnKubernetesEnvironment
case 7: //EdgeAgentOnKubernetesEnvironment
return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'kubernetes' }).$promise;
}
function checkRateLimits(endpoint, registryId) {
if (EndpointHelper.isLocalEndpoint(endpoint)) {
return Endpoints.dockerhubLimits({ id: endpoint.Id, registryId }).$promise;
}
return service;
},
]);
switch (endpoint.Type) {
case PortainerEndpointTypes.AgentOnDockerEnvironment:
case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'docker', registryId }).$promise;
case PortainerEndpointTypes.AgentOnKubernetesEnvironment:
case PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment:
return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'kubernetes', registryId }).$promise;
}
}
}

View file

@ -8,6 +8,9 @@ angular.module('portainer.app').factory('EndpointService', [
'use strict';
var service = {
updateSecuritySettings,
registries,
registry,
updateRegistryAccess,
};
service.endpoint = function (endpointID) {
@ -157,10 +160,22 @@ angular.module('portainer.app').factory('EndpointService', [
return deferred.promise;
};
function updateRegistryAccess(id, registryId, endpointAccesses) {
return Endpoints.updateRegistryAccess({ registryId, id }, endpointAccesses).$promise;
}
function registries(id, namespace) {
return Endpoints.registries({ namespace, id }).$promise;
}
return service;
function updateSecuritySettings(id, securitySettings) {
return Endpoints.updateSecuritySettings({ id }, securitySettings).$promise;
}
function registry(endpointId, registryId) {
return Endpoints.registry({ registryId, id: endpointId }).$promise;
}
},
]);

View file

@ -1,20 +1,30 @@
import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import { RegistryCreateRequest, RegistryViewModel } from '../../models/registry';
import { RegistryTypes } from 'Portainer/models/registryTypes';
import { RegistryCreateRequest, RegistryViewModel } from 'Portainer/models/registry';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
angular.module('portainer.app').factory('RegistryService', [
'$q',
'$async',
'EndpointService',
'Registries',
'DockerHubService',
'ImageHelper',
'FileUploadService',
function RegistryServiceFactory($q, $async, Registries, DockerHubService, ImageHelper, FileUploadService) {
'use strict';
var service = {};
function RegistryServiceFactory($q, $async, EndpointService, Registries, ImageHelper, FileUploadService) {
return {
registries,
registry,
encodedCredentials,
deleteRegistry,
updateRegistry,
configureRegistry,
createRegistry,
createGitlabRegistries,
retrievePorRegistryModelFromRepository,
};
service.registries = function () {
function registries() {
var deferred = $q.defer();
Registries.query()
@ -29,12 +39,12 @@ angular.module('portainer.app').factory('RegistryService', [
});
return deferred.promise;
};
}
service.registry = function (id) {
function registry(id, endpointId) {
var deferred = $q.defer();
Registries.get({ id: id })
Registries.get({ id, endpointId })
.$promise.then(function success(data) {
var registry = new RegistryViewModel(data);
deferred.resolve(registry);
@ -44,39 +54,35 @@ angular.module('portainer.app').factory('RegistryService', [
});
return deferred.promise;
};
}
service.encodedCredentials = function (registry) {
function encodedCredentials(registry) {
var credentials = {
serveraddress: registry.URL,
registryId: registry.Id,
};
return btoa(JSON.stringify(credentials));
};
}
service.updateAccess = function (id, userAccessPolicies, teamAccessPolicies) {
return Registries.updateAccess({ id: id }, { UserAccessPolicies: userAccessPolicies, TeamAccessPolicies: teamAccessPolicies }).$promise;
};
service.deleteRegistry = function (id) {
function deleteRegistry(id) {
return Registries.remove({ id: id }).$promise;
};
}
service.updateRegistry = function (registry) {
function updateRegistry(registry) {
registry.URL = _.replace(registry.URL, /^https?\:\/\//i, '');
registry.URL = _.replace(registry.URL, /\/$/, '');
return Registries.update({ id: registry.Id }, registry).$promise;
};
}
service.configureRegistry = function (id, registryManagementConfigurationModel) {
function configureRegistry(id, registryManagementConfigurationModel) {
return FileUploadService.configureRegistry(id, registryManagementConfigurationModel);
};
}
service.createRegistry = function (model) {
function createRegistry(model) {
var payload = new RegistryCreateRequest(model);
return Registries.create(payload).$promise;
};
}
service.createGitlabRegistries = function (model, projects) {
function createGitlabRegistries(model, projects) {
const promises = [];
_.forEach(projects, (p) => {
const m = model;
@ -88,9 +94,7 @@ angular.module('portainer.app').factory('RegistryService', [
promises.push(Registries.create(payload).$promise);
});
return $q.all(promises);
};
service.retrievePorRegistryModelFromRepositoryWithRegistries = retrievePorRegistryModelFromRepositoryWithRegistries;
}
function getURL(reg) {
let url = reg.URL;
@ -103,14 +107,23 @@ angular.module('portainer.app').factory('RegistryService', [
return url;
}
function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, dockerhub) {
function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, registryId) {
const model = new PorImageRegistryModel();
const registry = _.find(registries, (reg) => _.includes(repository, getURL(reg)));
const registry = registries.find((reg) => {
if (registryId) {
return reg.Id === registryId;
}
if (reg.Type === RegistryTypes.DOCKERHUB) {
return _.includes(repository, reg.Username);
}
return _.includes(repository, getURL(reg));
});
if (registry) {
const url = getURL(registry);
const lastIndex = repository.lastIndexOf(url) + url.length;
let lastIndex = repository.lastIndexOf(url);
lastIndex = lastIndex === -1 ? 0 : lastIndex + url.length;
let image = repository.substring(lastIndex);
if (!_.startsWith(image, ':')) {
if (_.startsWith(image, '/')) {
image = image.substring(1);
}
model.Registry = registry;
@ -119,25 +132,21 @@ angular.module('portainer.app').factory('RegistryService', [
if (ImageHelper.imageContainsURL(repository)) {
model.UseRegistry = false;
}
model.Registry = dockerhub;
model.Registry = new DockerHubViewModel();
model.Image = repository;
}
return model;
}
async function retrievePorRegistryModelFromRepositoryAsync(repository) {
try {
let [registries, dockerhub] = await Promise.all([service.registries(), DockerHubService.dockerhub()]);
return retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, dockerhub);
} catch (err) {
throw { msg: 'Unable to retrieve the registry associated to the repository', err: err };
}
function retrievePorRegistryModelFromRepository(repository, endpointId, registryId, namespace) {
return $async(async () => {
try {
const regs = await EndpointService.registries(endpointId, namespace);
return retrievePorRegistryModelFromRepositoryWithRegistries(repository, regs, registryId);
} catch (err) {
throw { msg: 'Unable to retrieve the registry associated to the repository', err: err };
}
});
}
service.retrievePorRegistryModelFromRepository = function (repository) {
return $async(retrievePorRegistryModelFromRepositoryAsync, repository);
};
return service;
},
]);

View file

@ -1,91 +1,85 @@
import _ from 'lodash-es';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
import { TemplateViewModel } from '../../models/template';
angular.module('portainer.app').factory('TemplateService', [
'$q',
'Templates',
'TemplateHelper',
'RegistryService',
'DockerHubService',
'ImageHelper',
'ContainerHelper',
function TemplateServiceFactory($q, Templates, TemplateHelper, RegistryService, DockerHubService, ImageHelper, ContainerHelper) {
'use strict';
var service = {};
angular.module('portainer.app').factory('TemplateService', TemplateServiceFactory);
service.templates = function () {
const deferred = $q.defer();
/* @ngInject */
function TemplateServiceFactory($q, Templates, TemplateHelper, EndpointProvider, ImageHelper, ContainerHelper, EndpointService) {
var service = {};
$q.all({
templates: Templates.query().$promise,
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub(),
})
.then(function success(data) {
const version = data.templates.version;
const templates = _.map(data.templates.templates, (item) => {
service.templates = function () {
const deferred = $q.defer();
const endpointId = EndpointProvider.currentEndpoint().Id;
$q.all({
templates: Templates.query().$promise,
registries: EndpointService.registries(endpointId),
})
.then(function success({ templates, registries }) {
const version = templates.version;
deferred.resolve(
templates.templates.map((item) => {
try {
const template = new TemplateViewModel(item, version);
const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(template.RegistryModel.Registry.URL, data.registries, data.dockerhub);
registry.Image = template.RegistryModel.Image;
template.RegistryModel = registry;
const registryURL = template.RegistryModel.Registry.URL;
const registry = registryURL ? registries.find((reg) => reg.URL === registryURL) : new DockerHubViewModel();
template.RegistryModel.Registry = registry;
return template;
} catch (err) {
deferred.reject({ msg: 'Unable to retrieve templates', err: err });
}
});
deferred.resolve(templates);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve templates', err: err });
});
return deferred.promise;
};
service.templateFile = templateFile;
function templateFile(repositoryUrl, composeFilePathInRepository) {
return Templates.file({ repositoryUrl, composeFilePathInRepository }).$promise;
}
service.createTemplateConfiguration = function (template, containerName, network) {
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel);
var containerConfiguration = createContainerConfiguration(template, containerName, network);
containerConfiguration.Image = imageConfiguration.fromImage;
return containerConfiguration;
};
function createContainerConfiguration(template, containerName, network) {
var configuration = TemplateHelper.getDefaultContainerConfiguration();
configuration.HostConfig.NetworkMode = network.Name;
configuration.HostConfig.Privileged = template.Privileged;
configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy };
configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : [];
configuration.name = containerName;
configuration.Hostname = template.Hostname;
configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
configuration.Cmd = ContainerHelper.commandStringToArray(template.Command);
var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
configuration.HostConfig.PortBindings = portConfiguration.bindings;
configuration.ExposedPorts = portConfiguration.exposedPorts;
var consoleConfiguration = TemplateHelper.getConsoleConfiguration(template.Interactive);
configuration.OpenStdin = consoleConfiguration.openStdin;
configuration.Tty = consoleConfiguration.tty;
configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels);
return configuration;
}
service.updateContainerConfigurationWithVolumes = function (configuration, template, generatedVolumesPile) {
var volumes = template.Volumes;
TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
volumes.forEach(function (volume) {
if (volume.binding) {
configuration.Volumes[volume.container] = {};
configuration.HostConfig.Binds.push(volume.binding);
}
})
);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve templates', err: err });
});
};
return service;
},
]);
return deferred.promise;
};
service.templateFile = templateFile;
function templateFile(repositoryUrl, composeFilePathInRepository) {
return Templates.file({ repositoryUrl, composeFilePathInRepository }).$promise;
}
service.createTemplateConfiguration = function (template, containerName, network) {
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel);
var containerConfiguration = createContainerConfiguration(template, containerName, network);
containerConfiguration.Image = imageConfiguration.fromImage;
return containerConfiguration;
};
function createContainerConfiguration(template, containerName, network) {
var configuration = TemplateHelper.getDefaultContainerConfiguration();
configuration.HostConfig.NetworkMode = network.Name;
configuration.HostConfig.Privileged = template.Privileged;
configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy };
configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : [];
configuration.name = containerName;
configuration.Hostname = template.Hostname;
configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
configuration.Cmd = ContainerHelper.commandStringToArray(template.Command);
var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
configuration.HostConfig.PortBindings = portConfiguration.bindings;
configuration.ExposedPorts = portConfiguration.exposedPorts;
var consoleConfiguration = TemplateHelper.getConsoleConfiguration(template.Interactive);
configuration.OpenStdin = consoleConfiguration.openStdin;
configuration.Tty = consoleConfiguration.tty;
configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels);
return configuration;
}
service.updateContainerConfigurationWithVolumes = function (configuration, template, generatedVolumesPile) {
var volumes = template.Volumes;
TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
volumes.forEach(function (volume) {
if (volume.binding) {
configuration.Volumes[volume.container] = {};
configuration.HostConfig.Binds.push(volume.binding);
}
});
};
return service;
}

View file

@ -0,0 +1,21 @@
<rd-header>
<rd-header-title title-text="Environment registries">
<a data-toggle="tooltip" title="Refresh" ui-sref="docker.registries" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Manage registry access inside this environment</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<registries-datatable
title-text="Registries"
title-icon="fa-database"
dataset="$ctrl.registries"
table-key="endpointRegistries"
order-by="Name"
endpoint-type="$ctrl.endpointType"
can-manage-access="$ctrl.canManageAccess"
></registries-datatable>
</div>
</div>

View file

@ -0,0 +1,7 @@
angular.module('portainer.app').component('endpointRegistriesView', {
templateUrl: './registries.html',
controller: 'EndpointRegistriesController',
bindings: {
endpoint: '<',
},
});

View file

@ -0,0 +1,51 @@
import _ from 'lodash-es';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
import { RegistryTypes } from 'Portainer/models/registryTypes';
class EndpointRegistriesController {
/* @ngInject */
constructor($async, Notifications, EndpointService) {
this.$async = $async;
this.Notifications = Notifications;
this.EndpointService = EndpointService;
this.canManageAccess = this.canManageAccess.bind(this);
}
canManageAccess(item) {
return item.Type !== RegistryTypes.ANONYMOUS;
}
getRegistries() {
return this.$async(async () => {
try {
const dockerhub = new DockerHubViewModel();
const registries = await this.EndpointService.registries(this.endpointId);
this.registries = _.concat(dockerhub, registries);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
}
});
}
$onInit() {
return this.$async(async () => {
this.state = {
viewReady: false,
};
try {
this.endpointType = this.endpoint.Type;
this.endpointId = this.endpoint.Id;
await this.getRegistries();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
} finally {
this.state.viewReady = true;
}
});
}
}
export default EndpointRegistriesController;
angular.module('portainer.app').controller('EndpointRegistriesController', EndpointRegistriesController);

View file

@ -1,8 +1,9 @@
import angular from 'angular';
import EndpointHelper from 'Portainer/helpers/endpointHelper';
angular.module('portainer.app').controller('EndpointsController', EndpointsController);
function EndpointsController($q, $scope, $state, $async, EndpointService, GroupService, EndpointHelper, Notifications) {
function EndpointsController($q, $scope, $state, $async, EndpointService, GroupService, Notifications) {
$scope.removeAction = removeAction;
function removeAction(endpoints) {

View file

@ -1,3 +1,5 @@
import EndpointHelper from 'Portainer/helpers/endpointHelper';
angular
.module('portainer.app')
.controller('HomeController', function (
@ -7,7 +9,6 @@ angular
TagService,
Authentication,
EndpointService,
EndpointHelper,
GroupService,
Notifications,
EndpointProvider,

View file

@ -1,5 +1,4 @@
angular.module('portainer.app').controller('InitAdminController', [
'$async',
'$scope',
'$state',
'Notifications',
@ -10,7 +9,7 @@ angular.module('portainer.app').controller('InitAdminController', [
'EndpointService',
'BackupService',
'StatusService',
function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) {
function ($scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) {
$scope.logo = StateManager.getState().application.logo;
$scope.formValues = {
@ -85,7 +84,9 @@ angular.module('portainer.app').controller('InitAdminController', [
if (status && status.Version) {
return;
}
} catch (e) {}
} catch (e) {
// pass
}
}
throw 'Timeout to wait for Portainer restarting';
}

View file

@ -1,35 +0,0 @@
<rd-header>
<rd-header-title title-text="Registry access"></rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> &gt; Access management
</rd-header-content>
</rd-header>
<div class="row" ng-if="registry">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title-text="Registry"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
{{ registry.Name }}
</td>
</tr>
<tr>
<td>URL</td>
<td>
{{ registry.URL }}
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<por-access-management ng-if="registry" access-controlled-entity="registry" entity-type="registry" action-in-progress="state.actionInProgress" update-access="updateAccess">
</por-access-management>

View file

@ -1,34 +0,0 @@
angular.module('portainer.app').controller('RegistryAccessController', [
'$scope',
'$state',
'$transition$',
'RegistryService',
'Notifications',
function ($scope, $state, $transition$, RegistryService, Notifications) {
$scope.updateAccess = function () {
$scope.state.actionInProgress = true;
RegistryService.updateRegistry($scope.registry)
.then(() => {
Notifications.success('Access successfully updated');
$state.reload();
})
.catch((err) => {
$scope.state.actionInProgress = false;
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
function initView() {
$scope.state = { actionInProgress: false };
RegistryService.registry($transition$.params().id)
.then(function success(data) {
$scope.registry = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
});
}
initView();
},
]);

View file

@ -17,8 +17,18 @@
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="registry_quay" ng-model="model.Type" ng-value="RegistryTypes.QUAY" />
<label for="registry_quay" ng-click="selectQuayRegistry()">
<input type="radio" id="registry_dockerhub" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.DOCKERHUB" />
<label for="registry_dockerhub" ng-click="$ctrl.selectDockerHub()">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
DockerHub
</div>
<p>DockerHub authenticated account</p>
</label>
</div>
<div>
<input type="radio" id="registry_quay" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.QUAY" />
<label for="registry_quay" ng-click="$ctrl.selectQuayRegistry()">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
Quay.io
@ -27,8 +37,8 @@
</label>
</div>
<div>
<input type="radio" id="registry_proget" ng-model="model.Type" ng-value="RegistryTypes.PROGET" />
<label for="registry_proget" ng-click="selectProGetRegistry()">
<input type="radio" id="registry_proget" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.PROGET" />
<label for="registry_proget" ng-click="$ctrl.selectProGetRegistry()">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
ProGet
@ -37,8 +47,8 @@
</label>
</div>
<div>
<input type="radio" id="registry_azure" ng-model="model.Type" ng-value="RegistryTypes.AZURE" />
<label for="registry_azure" ng-click="selectAzureRegistry()">
<input type="radio" id="registry_azure" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.AZURE" />
<label for="registry_azure" ng-click="$ctrl.selectAzureRegistry()">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Azure
@ -47,8 +57,8 @@
</label>
</div>
<div>
<input type="radio" id="registry_gitlab" ng-model="model.Type" ng-value="RegistryTypes.GITLAB" />
<label for="registry_gitlab" ng-click="selectGitlabRegistry()">
<input type="radio" id="registry_gitlab" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.GITLAB" />
<label for="registry_gitlab" ng-click="$ctrl.selectGitlabRegistry()">
<div class="boxselector_header">
<i class="fab fa-gitlab" aria-hidden="true" style="margin-right: 2px;"></i>
Gitlab
@ -57,8 +67,8 @@
</label>
</div>
<div>
<input type="radio" id="registry_custom" ng-model="model.Type" ng-value="RegistryTypes.CUSTOM" />
<label for="registry_custom" ng-click="selectCustomRegistry()">
<input type="radio" id="registry_custom" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.CUSTOM" />
<label for="registry_custom" ng-click="$ctrl.selectCustomRegistry()">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
Custom registry
@ -70,47 +80,55 @@
</div>
<registry-form-quay
ng-if="model.Type === RegistryTypes.QUAY"
model="model"
form-action="create"
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.QUAY"
model="$ctrl.model"
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
action-in-progress="$ctrl.state.actionInProgress"
></registry-form-quay>
<registry-form-azure
ng-if="model.Type === RegistryTypes.AZURE"
model="model"
form-action="create"
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.AZURE"
model="$ctrl.model"
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
action-in-progress="$ctrl.state.actionInProgress"
></registry-form-azure>
<registry-form-custom
ng-if="model.Type === RegistryTypes.CUSTOM"
model="model"
form-action="create"
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.CUSTOM"
model="$ctrl.model"
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
action-in-progress="$ctrl.state.actionInProgress"
></registry-form-custom>
<registry-form-proget
ng-if="model.Type === RegistryTypes.PROGET"
model="model"
form-action="create"
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.PROGET"
model="$ctrl.model"
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
action-in-progress="$ctrl.state.actionInProgress"
></registry-form-proget>
<registry-form-gitlab
ng-if="model.Type === RegistryTypes.GITLAB"
model="model"
retrieve-registries="retrieveGitlabRegistries"
create-registries="createGitlabRegistries"
projects="gitlabProjects"
state="state"
action-in-progress="state.actionInProgress"
reset-defaults="useDefaultGitlabConfiguration"
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.GITLAB"
model="$ctrl.model"
retrieve-registries="$ctrl.retrieveGitlabRegistries"
create-registries="$ctrl.createGitlabRegistries"
projects="$ctrl.gitlabProjects"
state="$ctrl.state"
action-in-progress="$ctrl.state.actionInProgress"
reset-defaults="$ctrl.useDefaultGitlabConfiguration"
></registry-form-gitlab>
<registry-form-dockerhub
ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.DOCKERHUB"
model="$ctrl.model"
form-action="$ctrl.createRegistry"
form-action-label="Add registry"
action-in-progress="$ctrl.state.actionInProgress"
></registry-form-dockerhub>
</form>
</rd-widget-body>
</rd-widget>

View file

@ -0,0 +1,10 @@
import angular from 'angular';
import CreateRegistryController from './createRegistryController';
angular.module('portainer.app').component('createRegistry', {
templateUrl: './createRegistry.html',
controller: CreateRegistryController,
bindings: {
$transition$: '<',
},
});

View file

@ -1,24 +1,13 @@
import { RegistryTypes } from '@/portainer/models/registryTypes';
import { RegistryDefaultModel } from '@/portainer/models/registry';
import { RegistryTypes } from 'Portainer/models/registryTypes';
import { RegistryCreateFormValues } from 'Portainer/models/registry';
angular.module('portainer.app').controller('CreateRegistryController', [
'$scope',
'$state',
'RegistryService',
'Notifications',
'RegistryGitlabService',
function ($scope, $state, RegistryService, Notifications, RegistryGitlabService) {
$scope.selectQuayRegistry = selectQuayRegistry;
$scope.selectAzureRegistry = selectAzureRegistry;
$scope.selectCustomRegistry = selectCustomRegistry;
$scope.selectProGetRegistry = selectProGetRegistry;
$scope.selectGitlabRegistry = selectGitlabRegistry;
$scope.create = createRegistry;
$scope.useDefaultGitlabConfiguration = useDefaultGitlabConfiguration;
$scope.retrieveGitlabRegistries = retrieveGitlabRegistries;
$scope.createGitlabRegistries = createGitlabRegistries;
class CreateRegistryController {
/* @ngInject */
constructor($async, $state, EndpointProvider, Notifications, RegistryService, RegistryGitlabService) {
Object.assign(this, { $async, $state, EndpointProvider, Notifications, RegistryService, RegistryGitlabService });
$scope.state = {
this.RegistryTypes = RegistryTypes;
this.state = {
actionInProgress: false,
overrideConfiguration: false,
gitlab: {
@ -27,101 +16,113 @@ angular.module('portainer.app').controller('CreateRegistryController', [
},
selectedItems: [],
},
originViewReference: 'portainer.registries',
};
function useDefaultQuayConfiguration() {
$scope.model.Quay.useOrganisation = false;
$scope.model.Quay.organisationName = '';
}
this.createRegistry = this.createRegistry.bind(this);
this.retrieveGitlabRegistries = this.retrieveGitlabRegistries.bind(this);
this.createGitlabRegistries = this.createGitlabRegistries.bind(this);
}
function selectQuayRegistry() {
$scope.model.Name = 'Quay';
$scope.model.URL = 'quay.io';
$scope.model.Authentication = true;
$scope.model.Quay = {};
useDefaultQuayConfiguration();
}
useDefaultQuayConfiguration() {
this.model.Quay.useOrganisation = false;
this.model.Quay.organisationName = '';
}
function useDefaultGitlabConfiguration() {
$scope.model.URL = 'https://registry.gitlab.com';
$scope.model.Gitlab.InstanceURL = 'https://gitlab.com';
}
selectQuayRegistry() {
this.model.Name = 'Quay';
this.model.URL = 'quay.io';
this.model.Authentication = true;
this.model.Quay = {};
this.useDefaultQuayConfiguration();
}
function selectGitlabRegistry() {
$scope.model.Name = '';
$scope.model.Authentication = true;
$scope.model.Gitlab = {};
useDefaultGitlabConfiguration();
}
useDefaultGitlabConfiguration() {
this.model.URL = 'https://registry.gitlab.com';
this.model.Gitlab.InstanceURL = 'https://gitlab.com';
}
function selectAzureRegistry() {
$scope.model.Name = '';
$scope.model.URL = '';
$scope.model.Authentication = true;
}
selectGitlabRegistry() {
this.model.Name = '';
this.model.Authentication = true;
this.model.Gitlab = {};
this.useDefaultGitlabConfiguration();
}
function selectCustomRegistry() {
$scope.model.Name = '';
$scope.model.URL = '';
$scope.model.Authentication = false;
}
selectAzureRegistry() {
this.model.Name = '';
this.model.URL = '';
this.model.Authentication = true;
}
function selectProGetRegistry() {
$scope.model.Name = '';
$scope.model.URL = '';
$scope.model.BaseURL = '';
$scope.model.Authentication = true;
}
selectProGetRegistry() {
this.model.Name = '';
this.model.URL = '';
this.model.BaseURL = '';
this.model.Authentication = true;
}
function retrieveGitlabRegistries() {
$scope.state.actionInProgress = true;
RegistryGitlabService.projects($scope.model.Gitlab.InstanceURL, $scope.model.Token)
.then((data) => {
$scope.gitlabProjects = data;
})
.catch((err) => {
Notifications.error('Failure', err, 'Unable to retrieve projects');
})
.finally(() => {
$scope.state.actionInProgress = false;
});
}
selectCustomRegistry() {
this.model.Name = '';
this.model.URL = '';
this.model.Authentication = false;
}
function createGitlabRegistries() {
$scope.state.actionInProgress = true;
RegistryService.createGitlabRegistries($scope.model, $scope.state.gitlab.selectedItems)
.then(() => {
Notifications.success('Registries successfully created');
$state.go('portainer.registries');
})
.catch((err) => {
Notifications.error('Failure', err, 'Unable to create registries');
})
.finally(() => {
$scope.state.actionInProgress = false;
});
}
selectDockerHub() {
this.model.Name = '';
this.model.URL = 'docker.io';
this.model.Authentication = true;
}
function createRegistry() {
$scope.state.actionInProgress = true;
RegistryService.createRegistry($scope.model)
.then(function success() {
Notifications.success('Registry successfully created');
$state.go('portainer.registries');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create registry');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
retrieveGitlabRegistries() {
return this.$async(async () => {
this.state.actionInProgress = true;
try {
this.gitlabProjects = await this.RegistryGitlabService.projects(this.model.Gitlab.InstanceURL, this.model.Token);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve projects');
} finally {
this.state.actionInProgress = false;
}
});
}
function initView() {
$scope.RegistryTypes = RegistryTypes;
$scope.model = new RegistryDefaultModel();
}
createGitlabRegistries() {
return this.$async(async () => {
try {
this.state.actionInProgress = true;
await this.RegistryService.createGitlabRegistries(this.model, this.state.gitlab.selectedItems);
this.Notifications.success('Registries successfully created');
this.$state.go(this.state.originViewReference, { endpointId: this.EndpointProvider.endpointID() });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create registries');
this.state.actionInProgress = false;
}
});
}
initView();
},
]);
createRegistry() {
return this.$async(async () => {
try {
this.state.actionInProgress = true;
await this.RegistryService.createRegistry(this.model);
this.Notifications.success('Registry successfully created');
this.$state.go(this.state.originViewReference, { endpointId: this.EndpointProvider.endpointID() });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create registry');
this.state.actionInProgress = false;
}
});
}
$onInit() {
this.model = new RegistryCreateFormValues();
const origin = this.$transition$.originalTransition().from();
if (origin.name && /^[a-z]+\.registries$/.test(origin.name)) {
this.state.originViewReference = origin;
}
}
}
export default CreateRegistryController;

View file

@ -3,11 +3,9 @@ import { RegistryTypes } from '@/portainer/models/registryTypes';
angular.module('portainer.app').controller('RegistryController', [
'$scope',
'$state',
'$transition$',
'$filter',
'RegistryService',
'Notifications',
function ($scope, $state, $transition$, $filter, RegistryService, Notifications) {
function ($scope, $state, RegistryService, Notifications) {
$scope.state = {
actionInProgress: false,
};
@ -36,7 +34,7 @@ angular.module('portainer.app').controller('RegistryController', [
};
function initView() {
var registryID = $transition$.params().id;
var registryID = $state.params.id;
RegistryService.registry(registryID)
.then(function success(data) {
$scope.registry = data;

View file

@ -7,79 +7,6 @@
<rd-header-content>Registry management</rd-header-content>
</rd-header>
<information-panel title-text="Registry usage">
<span class="small">
<p class="text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
DockerHub credentials and registries can only be used with Docker endpoints at the time.
</p>
</span>
</information-panel>
<div class="row" ng-if="dockerhub && isAdmin">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-database" title-text="DockerHub"> </rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- note -->
<div class="form-group">
<span class="col-sm-12 text-muted small">
The DockerHub registry can be used by any user. You can specify the credentials that will be used to push &amp; pull images here.
</span>
</div>
<!-- !note -->
<!-- authentication-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to push/pull private images."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="dockerhub.Authentication" /><i></i> </label>
</div>
</div>
<!-- !authentication-checkbox -->
<!-- authentication-credentials -->
<div ng-if="dockerhub.Authentication">
<!-- credentials-user -->
<div class="form-group">
<label for="hub_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="hub_username" ng-model="dockerhub.Username" />
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="hub_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="hub_password" ng-model="formValues.dockerHubPassword" placeholder="*******" />
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- !authentication-credentials -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || dockerhub.Authentication && (!dockerhub.Username || !formValues.dockerHubPassword)"
ng-click="updateDockerHub()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Update</span>
<span ng-show="state.actionInProgress">Updating DockerHub settings...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<registries-datatable
@ -88,7 +15,6 @@
dataset="registries"
table-key="registries"
order-by="Name"
access-management="isAdmin"
remove-action="removeAction"
can-browse="canBrowse"
></registries-datatable>

View file

@ -1,43 +1,23 @@
import _ from 'lodash-es';
import { RegistryTypes } from 'Portainer/models/registryTypes';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
angular.module('portainer.app').controller('RegistriesController', [
'$q',
'$scope',
'$state',
'RegistryService',
'DockerHubService',
'ModalService',
'Notifications',
'Authentication',
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, Authentication) {
function ($q, $scope, $state, RegistryService, ModalService, Notifications) {
$scope.state = {
actionInProgress: false,
};
$scope.formValues = {
dockerHubPassword: '',
};
const nonBrowsableUrls = ['quay.io'];
const nonBrowsableTypes = [RegistryTypes.ANONYMOUS, RegistryTypes.DOCKERHUB, RegistryTypes.QUAY];
$scope.canBrowse = function (item) {
return !_.includes(nonBrowsableUrls, item.URL);
};
$scope.updateDockerHub = function () {
var dockerhub = $scope.dockerhub;
dockerhub.Password = $scope.formValues.dockerHubPassword;
$scope.state.actionInProgress = true;
DockerHubService.update(dockerhub)
.then(function success() {
Notifications.success('DockerHub registry updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update DockerHub details');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
return !_.includes(nonBrowsableTypes, item.Type);
};
$scope.removeAction = function (selectedItems) {
@ -73,12 +53,9 @@ angular.module('portainer.app').controller('RegistriesController', [
function initView() {
$q.all({
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub(),
})
.then(function success(data) {
$scope.registries = data.registries;
$scope.dockerhub = data.dockerhub;
$scope.isAdmin = Authentication.isAdmin();
$scope.registries = _.concat(new DockerHubViewModel(), data.registries);
})
.catch(function error(err) {
$scope.registries = [];

View file

@ -108,7 +108,7 @@
<li class="sidebar-list" ng-if="isAdmin && applicationState.application.enableEdgeComputeFeatures">
<a ui-sref="edge.jobs" ui-sref-active="active">Edge Jobs <span class="menu-icon fa fa-clock fa-fw"></span></a>
</li>
<li class="sidebar-title">
<li class="sidebar-title" ng-if="isAdmin || isTeamLeader">
<span>Settings</span>
</li>
<li class="sidebar-list" ng-if="isAdmin || isTeamLeader">
@ -181,7 +181,7 @@
<a ui-sref="portainer.tags" ui-sref-active="active">Tags</a>
</div>
</li>
<li class="sidebar-list">
<li class="sidebar-list" ng-if="isAdmin">
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="isAdmin">

View file

@ -19,15 +19,20 @@ angular.module('portainer.app').controller('SidebarController', [
$scope.isTeamLeader = isLeader;
}
function isClusterAdmin() {
return Authentication.isAdmin();
}
async function initView() {
$scope.uiVersion = StateManager.getState().application.version;
$scope.logo = StateManager.getState().application.logo;
$scope.showStacks = await shouldShowStacks();
$scope.endpointId = EndpointProvider.endpointID();
$scope.showStacks = shouldShowStacks();
let userDetails = Authentication.getUserDetails();
let isAdmin = Authentication.isAdmin();
const isAdmin = isClusterAdmin();
$scope.isAdmin = isAdmin;
$scope.endpointId = EndpointProvider.endpointID();
$q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : [])
.then(function success(data) {
@ -36,18 +41,12 @@ angular.module('portainer.app').controller('SidebarController', [
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
});
$transitions.onEnter({}, () => {
$scope.endpointId = EndpointProvider.endpointID();
});
}
initView();
async function shouldShowStacks() {
const isAdmin = Authentication.isAdmin();
if (isAdmin) {
function shouldShowStacks() {
if (isClusterAdmin()) {
return true;
}
@ -60,7 +59,9 @@ angular.module('portainer.app').controller('SidebarController', [
}
$transitions.onEnter({}, async () => {
$scope.showStacks = await shouldShowStacks();
$scope.endpointId = EndpointProvider.endpointID();
$scope.showStacks = shouldShowStacks();
$scope.isAdmin = isClusterAdmin();
if ($scope.applicationState.endpoint.name) {
document.title = `${$rootScope.defaultTitle} | ${$scope.applicationState.endpoint.name}`;