1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +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

@ -4,10 +4,10 @@ angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub);
function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) {
return $resource(
`${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub`,
`${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub/:registryId`,
{},
{
limits: { method: 'GET' },
limits: { method: 'GET', params: { registryId: '@registryId' } },
}
);
}

View file

@ -1,7 +1,6 @@
angular
.module('portainer')
.constant('API_ENDPOINT_AUTH', 'api/auth')
.constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub')
.constant('API_ENDPOINT_CUSTOM_TEMPLATES', 'api/custom_templates')
.constant('API_ENDPOINT_EDGE_GROUPS', 'api/edge_groups')
.constant('API_ENDPOINT_EDGE_JOBS', 'api/edge_jobs')

View file

@ -591,6 +591,26 @@ angular.module('portainer.docker', ['portainer.app']).config([
},
};
const registries = {
name: 'docker.registries',
url: '/registries',
views: {
'content@': {
component: 'endpointRegistriesView',
},
},
};
const registryAccess = {
name: 'docker.registries.access',
url: '/:id/access',
views: {
'content@': {
component: 'dockerRegistryAccessView',
},
},
};
$stateRegistryProvider.register(configs);
$stateRegistryProvider.register(config);
$stateRegistryProvider.register(configCreation);
@ -641,5 +661,7 @@ angular.module('portainer.docker', ['portainer.app']).config([
$stateRegistryProvider.register(volumeBrowse);
$stateRegistryProvider.register(volumeCreation);
$stateRegistryProvider.register(dockerFeaturesConfiguration);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registryAccess);
},
]);

View file

@ -35,17 +35,19 @@
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess && !$ctrl.offlineMode">
<a ui-sref="docker.events({endpointId: $ctrl.endpointId})" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
<a ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
<li class="sidebar-list">
<a ng-if="$ctrl.standaloneManagement" ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
<a ng-if="$ctrl.swarmManagement" ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.swarm'].includes($ctrl.currentRouteName)">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
</li>
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
<a ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.host'].includes($ctrl.currentRouteName)">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
<div
ng-if="$ctrl.adminAccess && ['docker.swarm', 'docker.host', 'docker.registries', 'docker.registries.access', 'docker.featuresConfiguration'].includes($ctrl.currentRouteName)"
>
<div class="sidebar-sublist">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
<div class="sidebar-sublist">
<a ui-sref="docker.registries({endpointId: $ctrl.endpointId})" ui-sref-active="active">Registries</a>
</div>
</div>
</li>

View file

@ -1,31 +1,41 @@
import EndpointHelper from '@/portainer/helpers/endpointHelper';
export default class porImageRegistryContainerController {
/* @ngInject */
constructor(EndpointHelper, DockerHubService, Notifications) {
this.EndpointHelper = EndpointHelper;
constructor(DockerHubService, Notifications) {
this.DockerHubService = DockerHubService;
this.Notifications = Notifications;
this.pullRateLimits = null;
}
$onChanges({ isDockerHubRegistry }) {
if (isDockerHubRegistry && isDockerHubRegistry.currentValue) {
$onChanges({ registryId }) {
if (registryId) {
this.fetchRateLimits();
}
}
$onInit() {
this.setValidity =
this.setValidity ||
(() => {
/* noop */
});
}
async fetchRateLimits() {
this.pullRateLimits = null;
if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) {
try {
this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint);
this.setValidity(this.pullRateLimits.remaining >= 0);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed loading DockerHub pull rate limits', e);
this.setValidity(true);
}
} else {
if (!EndpointHelper.isAgentEndpoint(this.endpoint) && !EndpointHelper.isLocalEndpoint(this.endpoint)) {
this.setValidity(true);
return;
}
try {
this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint, this.registryId || 0);
this.setValidity(this.pullRateLimits.remaining >= 0);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed loading DockerHub pull rate limits', e);
this.setValidity(true);
}
}

View file

@ -1,4 +1,4 @@
<div class="form-group" ng-if="$ctrl.isDockerHubRegistry && $ctrl.pullRateLimits">
<div class="form-group" ng-if="$ctrl.pullRateLimits">
<div class="col-sm-12 small">
<div ng-if="$ctrl.pullRateLimits.remaining > 0" class="text-muted">
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>

View file

@ -5,10 +5,12 @@ import controller from './por-image-registry-rate-limits.controller';
angular.module('portainer.docker').component('porImageRegistryRateLimits', {
bindings: {
endpoint: '<',
registry: '<',
setValidity: '<',
isAdmin: '<',
isDockerHubRegistry: '<',
isAuthenticated: '<',
registryId: '<',
},
controller,
transclude: {

View file

@ -5,18 +5,21 @@ import { RegistryTypes } from '@/portainer/models/registryTypes';
class porImageRegistryController {
/* @ngInject */
constructor($async, $scope, ImageHelper, RegistryService, DockerHubService, ImageService, Notifications) {
constructor($async, $scope, ImageHelper, RegistryService, EndpointService, ImageService, Notifications) {
this.$async = $async;
this.$scope = $scope;
this.ImageHelper = ImageHelper;
this.RegistryService = RegistryService;
this.DockerHubService = DockerHubService;
this.EndpointService = EndpointService;
this.ImageService = ImageService;
this.Notifications = Notifications;
this.onInit = this.onInit.bind(this);
this.onRegistryChange = this.onRegistryChange.bind(this);
this.registries = [];
this.images = [];
this.defaultRegistry = new DockerHubViewModel();
this.$scope.$watch(() => this.model.Registry, this.onRegistryChange);
}
@ -40,7 +43,7 @@ class porImageRegistryController {
const registryImages = _.filter(this.images, (image) => _.includes(image, url));
images = _.map(registryImages, (image) => _.replace(image, new RegExp(url + '/?'), ''));
} else {
const registries = _.filter(this.availableRegistries, (reg) => this.isKnownRegistry(reg));
const registries = _.filter(this.registries, (reg) => this.isKnownRegistry(reg));
const registryImages = _.flatMap(registries, (registry) => _.filter(this.images, (image) => _.includes(image, registry.URL)));
const imagesWithoutKnown = _.difference(this.images, registryImages);
images = _.filter(imagesWithoutKnown, (image) => !this.ImageHelper.imageContainsURL(image));
@ -49,7 +52,7 @@ class porImageRegistryController {
}
isDockerHubRegistry() {
return this.model.UseRegistry && this.model.Registry.Name === 'DockerHub';
return this.model.UseRegistry && (this.model.Registry.Type === RegistryTypes.DOCKERHUB || this.model.Registry.Type === RegistryTypes.ANONYMOUS);
}
async onRegistryChange() {
@ -63,29 +66,49 @@ class porImageRegistryController {
return this.getRegistryURL(this.model.Registry) || 'docker.io';
}
async onInit() {
try {
const [registries, dockerhub, images] = await Promise.all([
this.RegistryService.registries(),
this.DockerHubService.dockerhub(),
this.autoComplete ? this.ImageService.images() : [],
]);
this.images = this.ImageService.getUniqueTagListFromImages(images);
this.availableRegistries = _.concat(dockerhub, registries);
async reloadRegistries() {
return this.$async(async () => {
try {
const registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
this.registries = _.concat(this.defaultRegistry, registries);
const id = this.model.Registry.Id;
if (!id) {
this.model.Registry = dockerhub;
} else {
this.model.Registry = _.find(this.availableRegistries, { Id: id });
const id = this.model.Registry.Id;
const registry = _.find(this.registries, { Id: id });
if (!registry) {
this.model.Registry = this.defaultRegistry;
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
});
}
async loadImages() {
return this.$async(async () => {
try {
if (!this.autoComplete) {
this.images = [];
return;
}
const images = await this.ImageService.images();
this.images = this.ImageService.getUniqueTagListFromImages(images);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve images');
}
});
}
$onChanges({ namespace, endpoint }) {
if ((namespace || endpoint) && this.endpoint.Id) {
this.reloadRegistries();
}
}
$onInit() {
return this.$async(this.onInit);
return this.$async(async () => {
await this.loadImages();
});
}
}

View file

@ -6,10 +6,9 @@
</label>
<div ng-class="$ctrl.inputClass">
<select
ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Name"
ng-model="$ctrl.model.Registry"
id="image_registry"
selected-item-id="ctrl.selectedItemId"
class="form-control"
></select>
</div>
@ -86,12 +85,13 @@
<div ng-transclude></div>
<por-image-registry-rate-limits
ng-show="$ctrl.checkRateLimits"
is-docker-hub-registry="$ctrl.isDockerHubRegistry()"
ng-if="$ctrl.checkRateLimits && $ctrl.isDockerHubRegistry()"
endpoint="$ctrl.endpoint"
registry="$ctrl.model.Registry"
set-validity="$ctrl.setValidity"
is-authenticated="$ctrl.model.Registry.Authentication"
is-admin="$ctrl.isAdmin"
registry-id="$ctrl.model.Registry.Id"
>
</por-image-registry-rate-limits>
</div>

View file

@ -3,7 +3,6 @@ angular.module('portainer.docker').component('porImageRegistry', {
controller: 'porImageRegistryController',
bindings: {
model: '=', // must be of type PorImageRegistryModel
pullWarning: '<',
autoComplete: '<',
labelClass: '@',
inputClass: '@',
@ -12,6 +11,7 @@ angular.module('portainer.docker').component('porImageRegistry', {
checkRateLimits: '<',
onImageChange: '&',
setValidity: '<',
namespace: '<',
},
require: {
form: '^form',

View file

@ -1,77 +1,85 @@
import _ from 'lodash-es';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import { RegistryTypes } from 'Portainer/models/registryTypes';
angular.module('portainer.docker').factory('ImageHelper', [
function ImageHelperFactory() {
'use strict';
angular.module('portainer.docker').factory('ImageHelper', ImageHelperFactory);
function ImageHelperFactory() {
return {
isValidTag,
createImageConfigForContainer,
getImagesNamesForDownload,
removeDigestFromRepository,
imageContainsURL,
};
var helper = {};
function isValidTag(tag) {
return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
}
helper.isValidTag = isValidTag;
helper.createImageConfigForContainer = createImageConfigForContainer;
helper.getImagesNamesForDownload = getImagesNamesForDownload;
helper.removeDigestFromRepository = removeDigestFromRepository;
helper.imageContainsURL = imageContainsURL;
function getImagesNamesForDownload(images) {
var names = images.map(function (image) {
return image.RepoTags[0] !== '<none>:<none>' ? image.RepoTags[0] : image.Id;
});
return {
names: names,
};
}
function isValidTag(tag) {
return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
/**
*
* @param {PorImageRegistryModel} registry
*/
function createImageConfigForContainer(imageModel) {
return {
fromImage: buildImageFullURI(imageModel),
};
}
function imageContainsURL(image) {
const split = _.split(image, '/');
const url = split[0];
if (split.length > 1) {
return _.includes(url, '.') || _.includes(url, ':');
}
return false;
}
function getImagesNamesForDownload(images) {
var names = images.map(function (image) {
return image.RepoTags[0] !== '<none>:<none>' ? image.RepoTags[0] : image.Id;
});
return {
names: names,
};
}
function removeDigestFromRepository(repository) {
return repository.split('@sha')[0];
}
}
/**
* builds the complete uri for an image based on its registry
* @param {PorImageRegistryModel} imageModel
*/
export function buildImageFullURI(imageModel) {
if (!imageModel.UseRegistry) {
return imageModel.Image;
}
/**
*
* @param {PorImageRegistryModel} registry
*/
function createImageConfigForContainer(registry) {
const data = {
fromImage: '',
};
let fullImageName = '';
let fullImageName = '';
if (registry.UseRegistry) {
if (registry.Registry.Type === RegistryTypes.GITLAB) {
const slash = _.startsWith(registry.Image, ':') ? '' : '/';
fullImageName = registry.Registry.URL + '/' + registry.Registry.Gitlab.ProjectPath + slash + registry.Image;
} else if (registry.Registry.Type === RegistryTypes.QUAY) {
const name = registry.Registry.Quay.UseOrganisation ? registry.Registry.Quay.OrganisationName : registry.Registry.Username;
const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
fullImageName = url + name + '/' + registry.Image;
} else {
const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
fullImageName = url + registry.Image;
}
if (!_.includes(registry.Image, ':')) {
fullImageName += ':latest';
}
} else {
fullImageName = registry.Image;
}
switch (imageModel.Registry.Type) {
case RegistryTypes.GITLAB:
fullImageName = imageModel.Registry.URL + '/' + imageModel.Registry.Gitlab.ProjectPath + (imageModel.Image.startsWith(':') ? '' : '/') + imageModel.Image;
break;
case RegistryTypes.ANONYMOUS:
fullImageName = imageModel.Image;
break;
case RegistryTypes.QUAY:
fullImageName =
(imageModel.Registry.URL ? imageModel.Registry.URL + '/' : '') +
(imageModel.Registry.Quay.UseOrganisation ? imageModel.Registry.Quay.OrganisationName : imageModel.Registry.Username) +
'/' +
imageModel.Image;
break;
default:
fullImageName = imageModel.Registry.URL + '/' + imageModel.Image;
break;
}
data.fromImage = fullImageName;
return data;
}
if (!imageModel.Image.includes(':')) {
fullImageName += ':latest';
}
function imageContainsURL(image) {
const split = _.split(image, '/');
const url = split[0];
if (split.length > 1) {
return _.includes(url, '.') || _.includes(url, ':');
}
return false;
}
function removeDigestFromRepository(repository) {
return repository.split('@sha')[0];
}
return helper;
},
]);
return fullImageName;
}

View file

@ -575,7 +575,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
function loadFromContainerImageConfig() {
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image)
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image, endpoint.Id)
.then((model) => {
$scope.formValues.RegistryModel = model;
})

View file

@ -40,15 +40,14 @@
<!-- image-and-registry -->
<por-image-registry
model="formValues.RegistryModel"
pull-warning="formValues.alwaysPull"
ng-if="formValues.RegistryModel.Registry"
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
on-image-change="onImageNameChange()"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="formValues.alwaysPull"
on-image-change="onImageNameChange()"
set-validity="setPullImageValidity"
>
<!-- always-pull -->

View file

@ -190,7 +190,7 @@
</div>
<!-- !tag-description -->
<!-- image-and-registry -->
<por-image-registry model="config.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<por-image-registry model="config.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11" endpoint="endpoint"></por-image-registry>
<!-- !image-and-registry -->
<!-- tag-note -->
<div class="form-group">

View file

@ -21,7 +21,6 @@ angular.module('portainer.docker').controller('ContainerController', [
'ImageService',
'HttpRequestHelper',
'Authentication',
'StateManager',
'endpoint',
function (
$q,
@ -42,9 +41,9 @@ angular.module('portainer.docker').controller('ContainerController', [
ImageService,
HttpRequestHelper,
Authentication,
StateManager,
endpoint
) {
$scope.endpoint = endpoint;
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.displayRecreateButton = false;
@ -295,7 +294,7 @@ angular.module('portainer.docker').controller('ContainerController', [
if (!pullImage) {
return $q.when();
}
return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image).then(function pullImage(registryModel) {
return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image, endpoint.Id).then((registryModel) => {
return ImageService.pullImage(registryModel, true);
});
}

View file

@ -63,7 +63,7 @@
<rd-widget-body>
<form class="form-horizontal">
<!-- image-and-registry -->
<por-image-registry model="formValues.RegistryModel" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<por-image-registry model="formValues.RegistryModel" endpoint="endpoint" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<!-- !image-and-registry -->
<!-- tag-note -->
<div class="form-group">

View file

@ -2,11 +2,12 @@ import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
angular.module('portainer.docker').controller('ImageController', [
'$async',
'$q',
'$scope',
'$transition$',
'$state',
'$timeout',
'endpoint',
'ImageService',
'ImageHelper',
'RegistryService',
@ -15,7 +16,8 @@ angular.module('portainer.docker').controller('ImageController', [
'ModalService',
'FileSaver',
'Blob',
function ($q, $scope, $transition$, $state, $timeout, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) {
function ($async, $q, $scope, $transition$, $state, endpoint, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) {
$scope.endpoint = endpoint;
$scope.formValues = {
RegistryModel: new PorImageRegistryModel(),
};
@ -53,39 +55,38 @@ angular.module('portainer.docker').controller('ImageController', [
});
};
$scope.pushTag = function (repository) {
$('#uploadResourceHint').show();
RegistryService.retrievePorRegistryModelFromRepository(repository)
.then(function success(registryModel) {
return ImageService.pushImage(registryModel);
})
.then(function success() {
Notifications.success('Image successfully pushed', repository);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to push image to repository');
})
.finally(function final() {
$('#uploadResourceHint').hide();
});
};
$scope.pushTag = pushTag;
$scope.pullTag = function (repository) {
$('#downloadResourceHint').show();
RegistryService.retrievePorRegistryModelFromRepository(repository)
.then(function success(registryModel) {
return ImageService.pullImage(registryModel, false);
})
.then(function success() {
Notifications.success('Image successfully pulled', repository);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to pull image');
})
.finally(function final() {
async function pushTag(repository) {
return $async(async () => {
$('#uploadResourceHint').show();
try {
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
await ImageService.pushImage(registryModel);
Notifications.success('Image successfully pushed', repository);
} catch (err) {
Notifications.error('Failure', err, 'Unable to push image to repository');
} finally {
$('#uploadResourceHint').hide();
}
});
}
$scope.pullTag = pullTag;
async function pullTag(repository) {
return $async(async () => {
$('#downloadResourceHint').show();
try {
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
await ImageService.pullImage(registryModel);
Notifications.success('Image successfully pushed', repository);
} catch (err) {
Notifications.error('Failure', err, 'Unable to push image to repository');
} finally {
$('#downloadResourceHint').hide();
});
};
}
});
}
$scope.removeTag = function (repository) {
ImageService.deleteImage(repository, false)

View file

@ -17,7 +17,6 @@
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
pull-warning="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"

View file

@ -0,0 +1,16 @@
<rd-header>
<rd-header-title title-text="Registry access"></rd-header-title>
<rd-header-content> <a ui-sref="docker.registries">Registries</a> &gt; {{ $ctrl.registry.Name }} &gt; Access management </rd-header-content>
</rd-header>
<registry-details registry="$ctrl.registry" ng-if="$ctrl.registry"></registry-details>
<por-access-management
ng-if="$ctrl.registry && $ctrl.endpointGroup"
access-controlled-entity="$ctrl.registryEndpointAccesses"
entity-type="registry"
action-in-progress="$ctrl.state.actionInProgress"
update-access="$ctrl.updateAccess"
filter-users="$ctrl.filterUsers"
>
</por-access-management>

View file

@ -0,0 +1,7 @@
angular.module('portainer.docker').component('dockerRegistryAccessView', {
templateUrl: './registryAccess.html',
controller: 'DockerRegistryAccessController',
bindings: {
endpoint: '<',
},
});

View file

@ -0,0 +1,67 @@
import { TeamAccessViewModel, UserAccessViewModel } from 'Portainer/models/access';
class DockerRegistryAccessController {
/* @ngInject */
constructor($async, $state, Notifications, EndpointService, GroupService) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.updateAccess = this.updateAccess.bind(this);
this.filterUsers = this.filterUsers.bind(this);
}
updateAccess() {
return this.$async(async () => {
this.state.actionInProgress = true;
try {
await this.EndpointService.updateRegistryAccess(this.state.endpointId, this.state.registryId, this.registryEndpointAccesses);
this.Notifications.success('Access successfully updated');
this.$state.reload();
} catch (err) {
this.state.actionInProgress = false;
this.Notifications.error('Failure', err, 'Unable to update accesses');
}
});
}
filterUsers(users) {
const endpointUsers = this.endpoint.UserAccessPolicies;
const endpointTeams = this.endpoint.TeamAccessPolicies;
const endpointGroupUsers = this.endpointGroup.UserAccessPolicies;
const endpointGroupTeams = this.endpointGroup.TeamAccessPolicies;
return users.filter((userOrTeam) => {
const userRole = userOrTeam instanceof UserAccessViewModel && (endpointUsers[userOrTeam.Id] || endpointGroupUsers[userOrTeam.Id]);
const teamRole = userOrTeam instanceof TeamAccessViewModel && (endpointTeams[userOrTeam.Id] || endpointGroupTeams[userOrTeam.Id]);
return userRole || teamRole;
});
}
$onInit() {
return this.$async(async () => {
try {
this.state = {
viewReady: false,
actionInProgress: false,
endpointId: this.$state.params.endpointId,
registryId: this.$state.params.id,
};
this.registry = await this.EndpointService.registry(this.state.endpointId, this.state.registryId);
this.registryEndpointAccesses = this.registry.RegistryAccesses[this.state.endpointId] || {};
this.endpointGroup = await this.GroupService.group(this.endpoint.GroupId);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registry details');
} finally {
this.state.viewReady = true;
}
});
}
}
export default DockerRegistryAccessController;
angular.module('portainer.docker').controller('DockerRegistryAccessController', DockerRegistryAccessController);

View file

@ -50,7 +50,6 @@ angular.module('portainer.docker').controller('ServiceController', [
'VolumeService',
'ImageHelper',
'WebhookService',
'EndpointProvider',
'clipboard',
'WebhookHelper',
'NetworkService',
@ -82,7 +81,6 @@ angular.module('portainer.docker').controller('ServiceController', [
VolumeService,
ImageHelper,
WebhookService,
EndpointProvider,
clipboard,
WebhookHelper,
NetworkService,
@ -337,7 +335,7 @@ angular.module('portainer.docker').controller('ServiceController', [
Notifications.error('Failure', err, 'Unable to delete webhook');
});
} else {
WebhookService.createServiceWebhook(service.Id, EndpointProvider.endpointID())
WebhookService.createServiceWebhook(service.Id, endpoint.Id)
.then(function success(data) {
$scope.WebhookExists = true;
$scope.webhookID = data.Id;
@ -688,7 +686,7 @@ angular.module('portainer.docker').controller('ServiceController', [
availableImages: ImageService.images(),
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25),
webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
webhooks: WebhookService.webhooks(service.Id, endpoint.Id),
});
})
.then(async function success(data) {

View file

@ -1,6 +1,6 @@
export class EditEdgeGroupController {
/* @ngInject */
constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async, EndpointService, EndpointHelper) {
constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async, EndpointService) {
this.EdgeGroupService = EdgeGroupService;
this.GroupService = GroupService;
this.TagService = TagService;
@ -8,7 +8,6 @@ export class EditEdgeGroupController {
this.$state = $state;
this.$async = $async;
this.EndpointService = EndpointService;
this.EndpointHelper = EndpointHelper;
this.state = {
actionInProgress: false,

View file

@ -1,4 +1,6 @@
angular.module('portainer.kubernetes', ['portainer.app']).config([
import registriesModule from './registries';
angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
@ -272,6 +274,26 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
},
};
const registries = {
name: 'kubernetes.registries',
url: '/registries',
views: {
'content@': {
component: 'endpointRegistriesView',
},
},
};
const registriesAccess = {
name: 'kubernetes.registries.access',
url: '/:id/access',
views: {
'content@': {
component: 'kubernetesRegistryAccessView',
},
},
};
$stateRegistryProvider.register(kubernetes);
$stateRegistryProvider.register(applications);
$stateRegistryProvider.register(applicationCreation);
@ -297,5 +319,7 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
$stateRegistryProvider.register(resourcePoolAccess);
$stateRegistryProvider.register(volumes);
$stateRegistryProvider.register(volume);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registriesAccess);
},
]);

View file

@ -28,7 +28,7 @@ angular.module('portainer.docker').controller('KubernetesConfigurationsDatatable
};
this.isSystemConfig = function (item) {
return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item);
return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item) || item.IsRegistrySecret;
};
this.isExternalConfiguration = function (item) {

View file

@ -15,7 +15,17 @@
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.cluster({endpointId: $ctrl.endpointId})" ui-sref-active="active">Cluster <span class="menu-icon fa fa-server fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ($ctrl.currentState === 'kubernetes.cluster' || $ctrl.currentState === 'portainer.endpoints.endpoint.kubernetesConfig')">
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
<div
ng-if="
$ctrl.adminAccess &&
['kubernetes.cluster', 'portainer.endpoints.endpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access'].includes($ctrl.currentState)
"
>
<div class="sidebar-sublist">
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
<div class="sidebar-sublist">
<a ui-sref="kubernetes.registries({endpointId: $ctrl.endpointId})" ui-sref-active="active">Registries</a>
</div>
</div>
</li>

View file

@ -62,6 +62,9 @@ class KubernetesApplicationConverter {
if (containers.length) {
res.Image = containers[0].image;
}
if (data.spec.template && data.spec.template.spec && data.spec.template.spec.imagePullSecrets && data.spec.template.spec.imagePullSecrets.length) {
res.RegistryId = parseInt(data.spec.template.spec.imagePullSecrets[0].name.replace('registry-', ''), 10);
}
res.CreationDate = data.metadata.creationTimestamp;
res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data];
@ -268,7 +271,8 @@ class KubernetesApplicationConverter {
res.Name = app.Name;
res.StackName = app.StackName;
res.ApplicationOwner = app.ApplicationOwner;
res.Image = app.Image;
res.ImageModel.Image = app.Image;
res.ImageModel.Registry.Id = app.RegistryId;
res.ReplicaCount = app.TotalPodsCount;
res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(app.Limits.Memory);
res.CpuLimit = app.Limits.Cpu;
@ -292,7 +296,10 @@ class KubernetesApplicationConverter {
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
}
KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels);
if (app.Pods && app.Pods.length) {
KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels);
}
return res;
}

View file

@ -14,6 +14,7 @@ class KubernetesConfigurationConverter {
res.Data[entry.Key] = entry.Value;
});
res.ConfigurationOwner = secret.ConfigurationOwner;
res.IsRegistrySecret = secret.IsRegistrySecret;
return res;
}

View file

@ -10,10 +10,11 @@ import {
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
import { buildImageFullURI } from 'Docker/helpers/imageHelper';
class KubernetesDaemonSetConverter {
/**
* Generate KubernetesDaemonSet from KubenetesApplicationFormValues
* Generate KubernetesDaemonSet from KubernetesApplicationFormValues
* @param {KubernetesApplicationFormValues} formValues
*/
static applicationFormValuesToDaemonSet(formValues, volumeClaims) {
@ -23,7 +24,7 @@ class KubernetesDaemonSetConverter {
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
res.Image = formValues.Image;
res.ImageModel = formValues.ImageModel;
res.CpuLimit = formValues.CpuLimit;
res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
@ -35,7 +36,7 @@ class KubernetesDaemonSetConverter {
/**
* Generate CREATE payload from DaemonSet
* @param {KubernetesDaemonSetPayload} model DaemonSet to genereate payload from
* @param {KubernetesDaemonSetPayload} model DaemonSet to generate payload from
*/
static createPayload(daemonSet) {
const payload = new KubernetesDaemonSetCreatePayload();
@ -50,7 +51,10 @@ class KubernetesDaemonSetConverter {
payload.spec.template.metadata.labels.app = daemonSet.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
payload.spec.template.spec.containers[0].name = daemonSet.Name;
payload.spec.template.spec.containers[0].image = daemonSet.Image;
payload.spec.template.spec.containers[0].image = buildImageFullURI(daemonSet.ImageModel);
if (daemonSet.ImageModel.Registry && daemonSet.ImageModel.Registry.Authentication) {
payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${daemonSet.ImageModel.Registry.Id}` }];
}
payload.spec.template.spec.affinity = daemonSet.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', daemonSet.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts);

View file

@ -11,6 +11,7 @@ import {
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
import { buildImageFullURI } from 'Docker/helpers/imageHelper';
class KubernetesDeploymentConverter {
/**
@ -25,7 +26,7 @@ class KubernetesDeploymentConverter {
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
res.ReplicaCount = formValues.ReplicaCount;
res.Image = formValues.Image;
res.ImageModel = formValues.ImageModel;
res.CpuLimit = formValues.CpuLimit;
res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
@ -53,7 +54,10 @@ class KubernetesDeploymentConverter {
payload.spec.template.metadata.labels.app = deployment.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
payload.spec.template.spec.containers[0].name = deployment.Name;
payload.spec.template.spec.containers[0].image = deployment.Image;
payload.spec.template.spec.containers[0].image = buildImageFullURI(deployment.ImageModel);
if (deployment.ImageModel.Registry && deployment.ImageModel.Registry.Authentication) {
payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${deployment.ImageModel.Registry.Id}` }];
}
payload.spec.template.spec.affinity = deployment.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts);

View file

@ -28,7 +28,16 @@ class KubernetesResourcePoolConverter {
}
});
const ingresses = _.without(ingMap, undefined);
return [namespace, quota, ingresses];
const registries = _.map(formValues.Registries, (r) => {
if (!r.RegistryAccesses[formValues.EndpointId]) {
r.RegistryAccesses[formValues.EndpointId] = { Namespaces: [] };
}
if (!_.includes(r.RegistryAccesses[formValues.EndpointId].Namespaces, formValues.Name)) {
r.RegistryAccesses[formValues.EndpointId].Namespaces = [...r.RegistryAccesses[formValues.EndpointId].Namespaces, formValues.Name];
}
return r;
});
return [namespace, quota, ingresses, registries];
}
}

View file

@ -56,6 +56,9 @@ class KubernetesSecretConverter {
res.Namespace = payload.metadata.namespace;
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = payload.metadata.creationTimestamp;
res.IsRegistrySecret = payload.metadata.annotations && !!payload.metadata.annotations['portainer.io/registry.id'];
res.Yaml = yaml ? yaml.data : '';
res.Data = _.map(payload.data, (value, key) => {

View file

@ -12,6 +12,7 @@ import {
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
import { buildImageFullURI } from 'Docker/helpers/imageHelper';
import KubernetesPersistentVolumeClaimConverter from './persistentVolumeClaim';
class KubernetesStatefulSetConverter {
@ -27,7 +28,7 @@ class KubernetesStatefulSetConverter {
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
res.ReplicaCount = formValues.ReplicaCount;
res.Image = formValues.Image;
res.ImageModel = formValues.ImageModel;
res.CpuLimit = formValues.CpuLimit;
res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
@ -56,7 +57,12 @@ class KubernetesStatefulSetConverter {
payload.spec.template.metadata.labels.app = statefulSet.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
payload.spec.template.spec.containers[0].name = statefulSet.Name;
payload.spec.template.spec.containers[0].image = statefulSet.Image;
if (statefulSet.ImageModel.Image) {
payload.spec.template.spec.containers[0].image = buildImageFullURI(statefulSet.ImageModel);
if (statefulSet.ImageModel.Registry && statefulSet.ImageModel.Registry.Authentication) {
payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${statefulSet.ImageModel.Registry.Id}` }];
}
}
payload.spec.template.spec.affinity = statefulSet.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', statefulSet.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts);

View file

@ -1,37 +1,34 @@
import { PorImageRegistryModel } from '@/docker/models/porImageRegistry';
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationPlacementTypes } from './models';
/**
* KubernetesApplicationFormValues Model
*/
const _KubernetesApplicationFormValues = Object.freeze({
ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation)
ResourcePool: {},
Name: '',
StackName: '',
ApplicationOwner: '',
Image: '',
Note: '',
MemoryLimit: 0,
CpuLimit: 0,
DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED,
ReplicaCount: 1,
AutoScaler: {},
Containers: [],
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.ISOLATED,
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
Configurations: [], // KubernetesApplicationConfigurationFormValue list
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
PlacementType: KubernetesApplicationPlacementTypes.MANDATORY,
Placements: [], // KubernetesApplicationPlacementFormValue list
OriginalIngresses: undefined,
});
export class KubernetesApplicationFormValues {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationFormValues)));
}
export function KubernetesApplicationFormValues() {
return {
ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation)
ResourcePool: {},
Name: '',
StackName: '',
ApplicationOwner: '',
ImageModel: new PorImageRegistryModel(),
Note: '',
MemoryLimit: 0,
CpuLimit: 0,
DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED,
ReplicaCount: 1,
AutoScaler: {},
Containers: [],
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
Configurations: [], // KubernetesApplicationConfigurationFormValue list
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
PlacementType: KubernetesApplicationPlacementTypes.PREFERRED,
Placements: [], // KubernetesApplicationPlacementFormValue list
OriginalIngresses: undefined,
};
}
export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({

View file

@ -5,7 +5,7 @@ const _KubernetesDaemonSet = Object.freeze({
Namespace: '',
Name: '',
StackName: '',
Image: '',
ImageModel: null,
Env: [],
CpuLimit: 0,
MemoryLimit: 0,

View file

@ -6,7 +6,7 @@ const _KubernetesDeployment = Object.freeze({
Name: '',
StackName: '',
ReplicaCount: 0,
Image: '',
ImageModel: null,
Env: [],
CpuLimit: 0,
MemoryLimit: 0,

View file

@ -1,9 +1,13 @@
export function KubernetesResourcePoolFormValues(defaults) {
this.Name = '';
this.MemoryLimit = defaults.MemoryLimit;
this.CpuLimit = defaults.CpuLimit;
this.HasQuota = false;
this.IngressClasses = []; // KubernetesResourcePoolIngressClassFormValue
return {
Name: '',
MemoryLimit: defaults.MemoryLimit,
CpuLimit: defaults.CpuLimit,
HasQuota: false,
IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue
Registries: [], // RegistryViewModel
EndpointId: 0,
};
}
/**

View file

@ -6,7 +6,7 @@ const _KubernetesStatefulSet = Object.freeze({
Name: '',
StackName: '',
ReplicaCount: 0,
Image: '',
ImageModel: null,
Env: [],
CpuLimit: '',
MemoryLimit: '',

View file

@ -0,0 +1,5 @@
import angular from 'angular';
import { kubernetesRegistryAccessView } from './kube-registry-access-view';
export default angular.module('portainer.kubernetes.registries', []).component('kubernetesRegistryAccessView', kubernetesRegistryAccessView).name;

View file

@ -0,0 +1,9 @@
import controller from './kube-registry-access-view.controller';
export const kubernetesRegistryAccessView = {
templateUrl: './kube-registry-access-view.html',
controller,
bindings: {
endpoint: '<',
},
};

View file

@ -0,0 +1,70 @@
export default class KubernetesRegistryAccessController {
/* @ngInject */
constructor($async, $state, EndpointService, Notifications, KubernetesResourcePoolService, KubernetesNamespaceHelper) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.EndpointService = EndpointService;
this.state = {
actionInProgress: false,
};
this.selectedResourcePools = [];
this.resourcePools = [];
this.savedResourcePools = [];
this.handleRemove = this.handleRemove.bind(this);
}
async submit() {
return this.updateNamespaces([...this.savedResourcePools.map(({ value }) => value), ...this.selectedResourcePools.map((pool) => pool.name)]);
}
handleRemove(namespaces) {
const removeNamespaces = namespaces.map(({ value }) => value);
return this.updateNamespaces(this.savedResourcePools.map(({ value }) => value).filter((value) => !removeNamespaces.includes(value)));
}
updateNamespaces(namespaces) {
return this.$async(async () => {
try {
await this.EndpointService.updateRegistryAccess(this.endpoint.Id, this.registry.Id, {
namespaces,
});
this.$state.reload();
} catch (err) {
this.Notifications.error('Failure', err, 'Failed saving registry access');
}
});
}
$onInit() {
return this.$async(async () => {
try {
this.state = {
registryId: this.$state.params.id,
};
this.registry = await this.EndpointService.registry(this.endpoint.Id, this.state.registryId);
if (this.registry.RegistryAccesses && this.registry.RegistryAccesses[this.endpoint.Id]) {
this.savedResourcePools = this.registry.RegistryAccesses[this.endpoint.Id].Namespaces.map((value) => ({ value }));
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registry details');
}
try {
const resourcePools = await this.KubernetesResourcePoolService.get();
this.resourcePools = resourcePools
.filter((pool) => !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name) && !this.savedResourcePools.find(({ value }) => value === pool.Namespace.Name))
.map((pool) => ({ name: pool.Namespace.Name, id: pool.Namespace.Id }));
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');
}
});
}
}

View file

@ -0,0 +1,72 @@
<rd-header>
<rd-header-title title-text="Registry access"></rd-header-title>
<rd-header-content> <a ui-sref="kubernetes.registries">Registries</a> &gt; {{ $ctrl.registry.Name }} &gt; Access management </rd-header-content>
</rd-header>
<registry-details registry="$ctrl.registry" ng-if="$ctrl.registry"></registry-details>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<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" style="padding-top: 0;">
Select namespaces
</label>
<div class="col-sm-9 col-lg-4">
<span class="small text-muted" ng-if="!$ctrl.resourcePools.length">
No namespaces available.
</span>
<span
isteven-multi-select
ng-if="$ctrl.resourcePools.length"
input-model="$ctrl.resourcePools"
output-model="$ctrl.selectedResourcePools"
button-label="name"
item-label="name"
tick-property="ticked"
helper-elements="filter"
search-property="name"
translation="{nothingSelected: 'Select one or more namespaces', search: 'Search...'}"
>
</span>
</div>
</div>
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.selectedResourcePools.length === 0 || $ctrl.state.actionInProgress"
ng-click="$ctrl.submit()"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create access</span>
<span ng-show="$ctrl.state.actionInProgress">Creating access...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<strings-datatable
title-text="Access"
title-icon="fa-user-lock"
table-key="access_registry_resourcepools"
dataset="$ctrl.savedResourcePools"
empty-dataset-message="No namespace has been authorized yet."
on-remove="($ctrl.handleRemove)"
column-header="Namespace"
>
</strings-datatable>
</div>
</div>

View file

@ -5,7 +5,7 @@ import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool'
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
/* @ngInject */
export function KubernetesResourcePoolService($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) {
export function KubernetesResourcePoolService($async, EndpointService, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) {
return {
get,
create,
@ -59,7 +59,7 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
function create(formValues) {
return $async(async () => {
try {
const [namespace, quota, ingresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues);
const [namespace, quota, ingresses, registries] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues);
await KubernetesNamespaceService.create(namespace);
if (quota) {
@ -67,6 +67,10 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
}
const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i));
await Promise.all(ingressPromises);
const endpointId = formValues.EndpointId;
const registriesPromises = _.map(registries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]));
await Promise.all(registriesPromises);
} catch (err) {
throw err;
}
@ -76,8 +80,8 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
function patch(oldFormValues, newFormValues) {
return $async(async () => {
try {
const [oldNamespace, oldQuota, oldIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
const [newNamespace, newQuota, newIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
const [oldNamespace, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
const [newNamespace, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
void oldNamespace, newNamespace;
if (oldQuota && newQuota) {
@ -103,6 +107,18 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
const promises = _.flatten([createPromises, delPromises, patchPromises]);
await Promise.all(promises);
const endpointId = newFormValues.EndpointId;
const keptRegistries = _.intersectionBy(oldRegistries, newRegistries, 'Id');
const removedRegistries = _.without(oldRegistries, ...keptRegistries);
const newRegistriesPromises = _.map(newRegistries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]));
const removedRegistriesPromises = _.map(removedRegistries, (r) => {
_.pull(r.RegistryAccesses[endpointId].Namespaces, newFormValues.Name);
return EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]);
});
await Promise.all(_.concat(newRegistriesPromises, removedRegistriesPromises));
} catch (err) {
throw err;
}

View file

@ -93,6 +93,7 @@ class KubernetesStatefulSetService {
if (!payload.length) {
return;
}
const data = await this.KubernetesStatefulSets(namespace).patch(params, payload).$promise;
return data;
} catch (err) {

View file

@ -16,6 +16,40 @@
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
<div class="col-sm-12 form-section-title">
Namespace
</div>
<!-- #region NAMESPACE -->
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
<div class="col-sm-11">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
ng-disabled="ctrl.state.isEdit"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-danger">
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-muted">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
You do not have access to any namespace. Contact your administrator to get access to a namespace.
</div>
</div>
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Application
</div>
<!-- #region NAME FIELD -->
<div class="form-group">
<label for="application_name" class="col-sm-1 control-label text-left">Name</label>
@ -53,61 +87,20 @@
<!-- #region IMAGE FIELD -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
name="container_image"
ng-model="ctrl.formValues.Image"
placeholder="nginx:latest"
required
ng-disabled="ctrl.formValues.Containers.length > 1"
/>
<div class="col-sm-12">
<por-image-registry
model="ctrl.formValues.ImageModel"
auto-complete="false"
label-class="col-sm-1"
input-class="col-sm-11"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
endpoint="ctrl.endpoint"
is-admin="ctrl.isAdmin"
check-rate-limits="true"
set-validity="ctrl.setPullImageValidity"
></por-image-registry>
</div>
</div>
<div class="form-group" ng-show="kubernetesApplicationCreationForm.container_image.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="kubernetesApplicationCreationForm.container_image.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<por-image-registry-rate-limits
is-docker-hub-registry="true"
endpoint="ctrl.endpoint"
is-authenticated="ctrl.state.isDockerAuthenticated"
is-admin="ctrl.isAdmin"
set-validity="ctrl.setPullImageValidity"
>
</por-image-registry-rate-limits>
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Namespace
</div>
<!-- #region NAMESPACE -->
<div class="form-group">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
<div class="col-sm-11">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
ng-disabled="ctrl.state.isEdit"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
<div class="col-sm-12 small text-danger">
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Stack

View file

@ -3,7 +3,6 @@ angular.module('portainer.kubernetes').component('kubernetesCreateApplicationVie
controller: 'KubernetesCreateApplicationController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',
endpoint: '<',
},
});

View file

@ -38,9 +38,7 @@ class KubernetesCreateApplicationController {
$async,
$state,
Notifications,
EndpointProvider,
Authentication,
DockerHubService,
ModalService,
KubernetesResourcePoolService,
KubernetesApplicationService,
@ -50,14 +48,13 @@ class KubernetesCreateApplicationController {
KubernetesIngressService,
KubernetesPersistentVolumeClaimService,
KubernetesNamespaceHelper,
KubernetesVolumeService
KubernetesVolumeService,
RegistryService
) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.EndpointProvider = EndpointProvider;
this.Authentication = Authentication;
this.DockerHubService = DockerHubService;
this.ModalService = ModalService;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesApplicationService = KubernetesApplicationService;
@ -68,6 +65,7 @@ class KubernetesCreateApplicationController {
this.KubernetesIngressService = KubernetesIngressService;
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.RegistryService = RegistryService;
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
@ -77,6 +75,56 @@ class KubernetesCreateApplicationController {
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
this.ServiceTypes = KubernetesServiceTypes;
this.state = {
actionInProgress: false,
useLoadBalancer: false,
useServerMetrics: false,
sliders: {
cpu: {
min: 0,
max: 0,
},
memory: {
min: 0,
max: 0,
},
},
nodes: {
memory: 0,
cpu: 0,
},
resourcePoolHasQuota: false,
viewReady: false,
availableSizeUnits: ['MB', 'GB', 'TB'],
alreadyExists: false,
duplicates: {
environmentVariables: new KubernetesFormValidationReferences(),
persistedFolders: new KubernetesFormValidationReferences(),
configurationPaths: new KubernetesFormValidationReferences(),
existingVolumes: new KubernetesFormValidationReferences(),
publishedPorts: {
containerPorts: new KubernetesFormValidationReferences(),
nodePorts: new KubernetesFormValidationReferences(),
ingressRoutes: new KubernetesFormValidationReferences(),
loadBalancerPorts: new KubernetesFormValidationReferences(),
},
placements: new KubernetesFormValidationReferences(),
},
isEdit: this.$state.params.namespace && this.$state.params.name,
persistedFoldersUseExistingVolumes: false,
pullImageValidity: false,
};
this.isAdmin = this.Authentication.isAdmin();
this.editChanges = [];
this.storageClasses = [];
this.state.useLoadBalancer = false;
this.state.useServerMetrics = false;
this.formValues = new KubernetesApplicationFormValues();
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
this.setPullImageValidity = this.setPullImageValidity.bind(this);
@ -869,9 +917,9 @@ class KubernetesCreateApplicationController {
getApplication() {
return this.$async(async () => {
try {
const namespace = this.state.params.namespace;
const namespace = this.$state.params.namespace;
[this.application, this.persistentVolumeClaims] = await Promise.all([
this.KubernetesApplicationService.get(namespace, this.state.params.name),
this.KubernetesApplicationService.get(namespace, this.$state.params.name),
this.KubernetesPersistentVolumeClaimService.get(namespace),
]);
} catch (err) {
@ -879,71 +927,26 @@ class KubernetesCreateApplicationController {
}
});
}
async parseImageConfiguration(imageModel) {
return this.$async(async () => {
try {
return await this.RegistryService.retrievePorRegistryModelFromRepository(imageModel.Image, this.endpoint.Id, imageModel.Registry.Id, this.$state.params.namespace);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registry');
return imageModel;
}
});
}
/* #endregion */
/* #region ON INIT */
$onInit() {
return this.$async(async () => {
try {
this.state = {
actionInProgress: false,
useLoadBalancer: false,
useServerMetrics: false,
sliders: {
cpu: {
min: 0,
max: 0,
},
memory: {
min: 0,
max: 0,
},
},
nodes: {
memory: 0,
cpu: 0,
},
resourcePoolHasQuota: false,
viewReady: false,
availableSizeUnits: ['MB', 'GB', 'TB'],
alreadyExists: false,
duplicates: {
environmentVariables: new KubernetesFormValidationReferences(),
persistedFolders: new KubernetesFormValidationReferences(),
configurationPaths: new KubernetesFormValidationReferences(),
existingVolumes: new KubernetesFormValidationReferences(),
publishedPorts: {
containerPorts: new KubernetesFormValidationReferences(),
nodePorts: new KubernetesFormValidationReferences(),
ingressRoutes: new KubernetesFormValidationReferences(),
loadBalancerPorts: new KubernetesFormValidationReferences(),
},
placements: new KubernetesFormValidationReferences(),
},
isEdit: false,
params: {
namespace: this.$transition$.params().namespace,
name: this.$transition$.params().name,
},
persistedFoldersUseExistingVolumes: false,
pullImageValidity: false,
};
this.isAdmin = this.Authentication.isAdmin();
this.editChanges = [];
if (this.state.params.namespace && this.state.params.name) {
this.state.isEdit = true;
}
const endpoint = this.EndpointProvider.currentEndpoint();
this.endpoint = endpoint;
this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer;
this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics;
this.formValues = new KubernetesApplicationFormValues();
this.storageClasses = this.endpoint.Kubernetes.Configuration.StorageClasses;
this.state.useLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
const [resourcePools, nodes, ingresses] = await Promise.all([
this.KubernetesResourcePoolService.get(),
@ -964,7 +967,7 @@ class KubernetesCreateApplicationController {
});
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
const namespace = this.state.isEdit ? this.$state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
await this.refreshNamespaceData(namespace);
if (this.state.isEdit) {
@ -978,6 +981,7 @@ class KubernetesCreateApplicationController {
this.filteredIngresses
);
this.formValues.OriginalIngresses = this.filteredIngresses;
this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel);
this.savedFormValues = angular.copy(this.formValues);
delete this.formValues.ApplicationType;
@ -995,11 +999,7 @@ class KubernetesCreateApplicationController {
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
}
this.updateSliders();
const dockerHub = await this.DockerHubService.dockerhub();
this.state.isDockerAuthenticated = dockerHub.Authentication;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {

View file

@ -21,6 +21,7 @@
<td>Name</td>
<td>
{{ ctrl.configuration.Name }}
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.configuration.IsRegistrySecret">system</span>
</td>
</tr>
<tr>
@ -76,7 +77,7 @@
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form ng-if="!ctrl.isSystemNamespace()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<form ng-if="!ctrl.isSystemConfig()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<kubernetes-configuration-data
ng-if="ctrl.formValues"
form-values="ctrl.formValues"
@ -111,7 +112,7 @@
</div>
<!-- !actions -->
</form>
<div ng-if="ctrl.isSystemNamespace()">
<div ng-if="ctrl.isSystemConfig()">
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
Data
</div>

View file

@ -55,6 +55,10 @@ class KubernetesConfigurationController {
return this.KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace);
}
isSystemConfig() {
return this.isSystemNamespace() || this.configuration.IsRegistrySecret;
}
selectTab(index) {
this.LocalStorage.storeActiveTab('configuration', index);
}
@ -134,6 +138,10 @@ class KubernetesConfigurationController {
const name = this.$transition$.params().name;
const namespace = this.$transition$.params().namespace;
const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]);
if (secret.status === 'rejected' && secret.reason.err.status === 403) {
this.$state.go('kubernetes.configurations');
throw new Error('Not authorized to edit secret');
}
if (secret.status === 'fulfilled') {
this.configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value);
this.formValues.Data = secret.value.Data;
@ -146,6 +154,8 @@ class KubernetesConfigurationController {
this.formValues.Name = this.configuration.Name;
this.formValues.Type = this.configuration.Type;
this.oldDataYaml = this.formValues.DataYaml;
return this.configuration;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configuration');
} finally {
@ -251,11 +261,12 @@ class KubernetesConfigurationController {
this.formValues = new KubernetesConfigurationFormValues();
this.resourcePools = await this.KubernetesResourcePoolService.get();
await this.getConfiguration();
await this.getApplications(this.configuration.Namespace);
await this.getEvents(this.configuration.Namespace);
await this.getConfigurations();
const configuration = await this.getConfiguration();
if (configuration) {
await this.getApplications(this.configuration.Namespace);
await this.getEvents(this.configuration.Namespace);
await this.getConfigurations();
}
this.tagUsedDataKeys();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');

View file

@ -9,18 +9,10 @@ import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
class KubernetesConfigureController {
/* #region CONSTRUCTOR */
// TODO: technical debt
// $transition$ cannot be injected as bindings: { $transition$: '<' } inside app/portainer/__module.js
// because this view is not using a component (https://ui-router.github.io/guide/ng1/route-to-component#accessing-transition)
// and will cause
// >> Error: Cannot combine: component|bindings|componentProvider
// >> with: templateProvider|templateUrl|template|notify|async|controller|controllerProvider|controllerAs|resolveAs
// >> in stateview: 'content@@portainer.endpoints.endpoint.kubernetesConfig'
/* @ngInject */
constructor(
$async,
$state,
$transition$,
Notifications,
KubernetesStorageService,
EndpointService,
@ -33,7 +25,6 @@ class KubernetesConfigureController {
) {
this.$async = $async;
this.$state = $state;
this.$transition$ = $transition$;
this.Notifications = Notifications;
this.KubernetesStorageService = KubernetesStorageService;
this.EndpointService = EndpointService;
@ -253,7 +244,7 @@ class KubernetesConfigureController {
actionInProgress: false,
displayConfigureClassPanel: {},
viewReady: false,
endpointId: this.$transition$.params().id,
endpointId: this.$state.params.id,
duplicates: {
ingressClasses: new KubernetesFormValidationReferences(),
},

View file

@ -2,9 +2,9 @@
<a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt; Create a namespace
</kubernetes-view-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<kubernetes-view-loading view-ready="$ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div ng-if="$ctrl.state.viewReady">
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
@ -18,16 +18,16 @@
type="text"
class="form-control"
name="pool_name"
ng-model="ctrl.formValues.Name"
ng-model="$ctrl.formValues.Name"
ng-pattern="/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/"
ng-change="ctrl.onChangeName()"
ng-change="$ctrl.onChangeName()"
placeholder="my-project"
required
auto-focus
/>
</div>
</div>
<div class="form-group" ng-show="resourcePoolCreationForm.pool_name.$invalid || ctrl.state.isAlreadyExist">
<div class="form-group" ng-show="resourcePoolCreationForm.pool_name.$invalid || $ctrl.state.isAlreadyExist">
<div class="col-sm-12 small text-warning">
<div ng-messages="resourcePoolCreationForm.pool_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
@ -36,7 +36,7 @@
with an alphanumeric character.</p
>
</div>
<p ng-if="ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A namespace with the same name already exists.</p>
<p ng-if="$ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A namespace with the same name already exists.</p>
</div>
</div>
<!-- #endregion -->
@ -58,16 +58,16 @@
<label class="control-label text-left">
Resource assignment
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.HasQuota" /><i></i> </label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.formValues.HasQuota" /><i></i> </label>
</div>
</div>
<div class="form-group" ng-if="ctrl.formValues.HasQuota && !ctrl.isQuotaValid()">
<div class="form-group" ng-if="$ctrl.formValues.HasQuota && !$ctrl.isQuotaValid()">
<span class="col-sm-12 text-warning small">
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
</span>
</div>
<!-- !quotas-switch -->
<div ng-if="ctrl.formValues.HasQuota">
<div ng-if="$ctrl.formValues.HasQuota">
<div class="col-sm-12 form-section-title">
Resource limits
</div>
@ -78,17 +78,23 @@
Memory
</label>
<div class="col-sm-3">
<slider model="ctrl.formValues.MemoryLimit" floor="ctrl.defaults.MemoryLimit" ceil="ctrl.state.sliderMaxMemory" step="128" ng-if="ctrl.state.sliderMaxMemory">
<slider
model="$ctrl.formValues.MemoryLimit"
floor="$ctrl.defaults.MemoryLimit"
ceil="$ctrl.state.sliderMaxMemory"
step="128"
ng-if="$ctrl.state.sliderMaxMemory"
>
</slider>
</div>
<div class="col-sm-2">
<input
name="memory_limit"
type="number"
min="{{ ctrl.defaults.MemoryLimit }}"
max="{{ ctrl.state.sliderMaxMemory }}"
min="{{ $ctrl.defaults.MemoryLimit }}"
max="{{ $ctrl.state.sliderMaxMemory }}"
class="form-control"
ng-model="ctrl.formValues.MemoryLimit"
ng-model="$ctrl.formValues.MemoryLimit"
id="memory-limit"
required
/>
@ -103,7 +109,7 @@
<div class="col-sm-12 small text-warning">
<div ng-messages="resourcePoolCreationForm.pool_name.$error">
<p
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.defaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ $ctrl.defaults.MemoryLimit }} and {{ $ctrl.state.sliderMaxMemory }}
</p>
</div>
</div>
@ -115,7 +121,14 @@
CPU
</label>
<div class="col-sm-5">
<slider model="ctrl.formValues.CpuLimit" floor="ctrl.defaults.CpuLimit" ceil="ctrl.state.sliderMaxCpu" step="0.1" precision="2" ng-if="ctrl.state.sliderMaxCpu">
<slider
model="$ctrl.formValues.CpuLimit"
floor="$ctrl.defaults.CpuLimit"
ceil="$ctrl.state.sliderMaxCpu"
step="0.1"
precision="2"
ng-if="$ctrl.state.sliderMaxCpu"
>
</slider>
</div>
<div class="col-sm-4" style="margin-top: 20px;">
@ -186,20 +199,20 @@
</div>
<!-- #endregion -->
<div ng-if="ctrl.state.canUseIngress">
<div ng-if="$ctrl.state.canUseIngress">
<div class="col-sm-12 form-section-title">
Ingresses
</div>
<!-- #region INGRESSES -->
<div class="form-group" ng-if="ctrl.formValues.IngressClasses.length === 0">
<div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length === 0">
<div class="col-sm-12 small text-muted">
The ingress feature must be enabled in the
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: ctrl.endpoint.Id})">endpoint configuration view</a> to be able to register ingresses inside this
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpoint.Id})">endpoint configuration view</a> to be able to register ingresses inside this
namespace.
</div>
</div>
<div class="form-group" ng-if="ctrl.formValues.IngressClasses.length > 0">
<div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length > 0">
<div class="col-sm-12 small text-muted">
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -208,7 +221,7 @@
</div>
</div>
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
<div class="form-group" ng-repeat-start="ic in $ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
<div class="text-muted col-sm-12" style="width: 100%;">
<div style="border-bottom: 1px solid #cdcdcd; padding-bottom: 5px;">
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i> {{ ic.IngressClass.Name }}
@ -234,7 +247,7 @@
>
</portainer-tooltip>
</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addHostname(ic)">
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addHostname(ic)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add hostname
</span>
</div>
@ -248,13 +261,13 @@
class="form-control"
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
ng-model="item.Host"
ng-change="ctrl.onChangeIngressHostname()"
ng-change="$ctrl.onChangeIngressHostname()"
placeholder="foo"
required
/>
</div>
<div class="col-sm-1 input-group input-group-sm" ng-if="$index > 0">
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeHostname(ic, $index)">
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeHostname(ic, $index)">
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
</div>
@ -264,20 +277,20 @@
style="margin-top: 5px;"
ng-show="
resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid ||
ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined
$ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined
"
>
<ng-messages for="resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Hostname is required.</p>
</ng-messages>
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
<p ng-if="$ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This hostname is already used.
</p>
</div>
</div>
</div>
</div>
<div class="form-group" ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX">
<div class="form-group" ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX">
<div class="col-sm-12">
<label class="control-label text-left">
Redirect published routes to / in application
@ -312,7 +325,7 @@
<div class="col-sm-12" ng-if="ic.AdvancedConfig">
<label class="control-label text-left">Annotations</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addAnnotation(ic)">
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addAnnotation(ic)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add annotation
</span>
</div>
@ -328,7 +341,7 @@
<input type="text" class="form-control" ng-model="annotation.Value" placeholder="/$1" required />
</div>
<div class="col-sm-1 input-group input-group-sm">
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeAnnotation(ic, $index)">
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeAnnotation(ic, $index)">
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
</div>
@ -338,8 +351,49 @@
<!-- #endregion -->
</div>
<!-- #region REGISTRIES -->
<div class="col-sm-12 form-section-title">
Registries
</div>
<div class="form-group">
<div class="col-sm-12 small text-muted">
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Define which registry can be used by users who have access to this namespace.
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
Select registries
</label>
<div class="col-sm-9 col-lg-4">
<span class="small text-muted" ng-if="!$ctrl.registries.length && $ctrl.state.isAdmin">
No registries available. Head over <a ui-sref="portainer.registries">registry view</a> to define container registry.
</span>
<span class="small text-muted" ng-if="!$ctrl.registries.length && !$ctrl.state.isAdmin">
No registries available. Contact your administrator to create a container registry.
</span>
<span
isteven-multi-select
ng-if="$ctrl.registries.length"
input-model="$ctrl.registries"
output-model="$ctrl.formValues.Registries"
button-label="Name"
item-label="Name"
tick-property="Checked"
helper-elements="filter"
search-property="Name"
translation="{nothingSelected: 'Select one or more registry', search: 'Search...'}"
>
</span>
</div>
</div>
<!-- #endregion -->
<!-- summary -->
<kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !ctrl.isCreateButtonDisabled()" form-values="ctrl.formValues"></kubernetes-summary-view>
<kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !$ctrl.isCreateButtonDisabled()" form-values="$ctrl.formValues"></kubernetes-summary-view>
<!-- !summary -->
<div class="col-sm-12 form-section-title">
@ -351,12 +405,12 @@
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="!resourcePoolCreationForm.$valid || ctrl.isCreateButtonDisabled()"
ng-click="ctrl.createResourcePool()"
button-spinner="ctrl.state.actionInProgress"
ng-disabled="!resourcePoolCreationForm.$valid || $ctrl.isCreateButtonDisabled()"
ng-click="$ctrl.createResourcePool()"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="ctrl.state.actionInProgress">Create namespace</span>
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
<span ng-hide="$ctrl.state.actionInProgress">Create namespace</span>
<span ng-show="$ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>
</div>

View file

@ -1,5 +1,10 @@
import angular from 'angular';
import KubernetesCreateResourcePoolController from './createResourcePoolController';
angular.module('portainer.kubernetes').component('kubernetesCreateResourcePoolView', {
templateUrl: './createResourcePool.html',
controller: 'KubernetesCreateResourcePoolController',
controllerAs: 'ctrl',
controller: KubernetesCreateResourcePoolController,
bindings: {
endpoint: '<',
},
});

View file

@ -1,4 +1,3 @@
import angular from 'angular';
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
@ -16,23 +15,19 @@ import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
class KubernetesCreateResourcePoolController {
/* #region CONSTRUCTOR */
/* @ngInject */
constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointProvider) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.Authentication = Authentication;
this.EndpointProvider = EndpointProvider;
this.KubernetesNodeService = KubernetesNodeService;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesIngressService = KubernetesIngressService;
constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointService) {
Object.assign(this, {
$async,
$state,
Notifications,
KubernetesNodeService,
KubernetesResourcePoolService,
KubernetesIngressService,
Authentication,
EndpointService,
});
this.IngressClassTypes = KubernetesIngressClassTypes;
this.onInit = this.onInit.bind(this);
this.createResourcePoolAsync = this.createResourcePoolAsync.bind(this);
this.getResourcePoolsAsync = this.getResourcePoolsAsync.bind(this);
this.getIngressesAsync = this.getIngressesAsync.bind(this);
}
/* #endregion */
@ -116,106 +111,111 @@ class KubernetesCreateResourcePoolController {
}
/* #region CREATE NAMESPACE */
async createResourcePoolAsync() {
this.state.actionInProgress = true;
try {
this.checkDefaults();
const owner = this.Authentication.getUserDetails().username;
this.formValues.Owner = owner;
await this.KubernetesResourcePoolService.create(this.formValues);
this.Notifications.success('Namespace successfully created', this.formValues.Name);
this.$state.go('kubernetes.resourcePools');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create namespace');
} finally {
this.state.actionInProgress = false;
}
}
createResourcePool() {
return this.$async(this.createResourcePoolAsync);
return this.$async(async () => {
this.state.actionInProgress = true;
try {
this.checkDefaults();
this.formValues.Owner = this.Authentication.getUserDetails().username;
await this.KubernetesResourcePoolService.create(this.formValues);
this.Notifications.success('Namespace successfully created', this.formValues.Name);
this.$state.go('kubernetes.resourcePools');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create namespace');
} finally {
this.state.actionInProgress = false;
}
});
}
/* #endregion */
/* #region GET INGRESSES */
async getIngressesAsync() {
try {
this.allIngresses = await this.KubernetesIngressService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
}
}
getIngresses() {
return this.$async(this.getIngressesAsync);
return this.$async(async () => {
try {
this.allIngresses = await this.KubernetesIngressService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
}
});
}
/* #endregion */
/* #region GET NAMESPACES */
async getResourcePoolsAsync() {
try {
this.resourcePools = await this.KubernetesResourcePoolService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');
}
}
getResourcePools() {
return this.$async(this.getResourcePoolsAsync);
return this.$async(async () => {
try {
this.resourcePools = await this.KubernetesResourcePoolService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');
}
});
}
/* #endregion */
/* #region GET REGISTRIES */
getRegistries() {
return this.$async(async () => {
try {
this.registries = await this.EndpointService.registries(this.endpoint.Id);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
}
});
}
/* #endregion */
/* #region ON INIT */
async onInit() {
try {
const endpoint = this.EndpointProvider.currentEndpoint();
this.endpoint = endpoint;
this.defaults = KubernetesResourceQuotaDefaults;
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
this.formValues.HasQuota = true;
this.state = {
actionInProgress: false,
sliderMaxMemory: 0,
sliderMaxCpu: 0,
viewReady: false,
isAlreadyExist: false,
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
duplicates: {
ingressHosts: new KubernetesFormValidationReferences(),
},
};
const nodes = await this.KubernetesNodeService.get();
_.forEach(nodes, (item) => {
this.state.sliderMaxMemory += filesizeParser(item.Memory);
this.state.sliderMaxCpu += item.CPU;
});
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
await this.getResourcePools();
if (this.state.canUseIngress) {
await this.getIngresses();
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
}
_.forEach(this.formValues.IngressClasses, (ic) => {
if (ic.Hosts.length === 0) {
ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
}
});
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
}
$onInit() {
return this.$async(this.onInit);
return this.$async(async () => {
try {
const endpoint = this.endpoint;
this.defaults = KubernetesResourceQuotaDefaults;
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
this.formValues.EndpointId = this.endpoint.Id;
this.formValues.HasQuota = true;
this.state = {
actionInProgress: false,
sliderMaxMemory: 0,
sliderMaxCpu: 0,
viewReady: false,
isAlreadyExist: false,
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
duplicates: {
ingressHosts: new KubernetesFormValidationReferences(),
},
isAdmin: this.Authentication.isAdmin(),
};
const nodes = await this.KubernetesNodeService.get();
_.forEach(nodes, (item) => {
this.state.sliderMaxMemory += filesizeParser(item.Memory);
this.state.sliderMaxCpu += item.CPU;
});
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
await this.getResourcePools();
if (this.state.canUseIngress) {
await this.getIngresses();
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
}
_.forEach(this.formValues.IngressClasses, (ic) => {
if (ic.Hosts.length === 0) {
ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
}
});
await this.getRegistries();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
});
}
/* #endregion */
}
export default KubernetesCreateResourcePoolController;
angular.module('portainer.kubernetes').controller('KubernetesCreateResourcePoolController', KubernetesCreateResourcePoolController);

View file

@ -298,6 +298,61 @@
</div>
<!-- #endregion -->
</div>
<!-- #region REGISTRIES -->
<div>
<div class="col-sm-12 form-section-title">
Registries
</div>
<div class="form-group" ng-if="!ctrl.isAdmin">
<label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
Selected registries
</label>
<div class="col-sm-9 col-lg-4">
{{ ctrl.selectedRegistries }}
</div>
</div>
<div ng-if="ctrl.isAdmin">
<div class="form-group">
<div class="col-sm-12 small text-muted">
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Define which registry can be used by users who have access to this namespace.
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
Select registries
</label>
<div class="col-sm-9 col-lg-4">
<span class="small text-muted" ng-if="!ctrl.registries.length && ctrl.state.isAdmin">
No registries available. Head over <a ui-sref="portainer.registries">registry view</a> to define container registry.
</span>
<span class="small text-muted" ng-if="!ctrl.registries.length && !ctrl.state.isAdmin">
No registries available. Contact your administrator to create a container registry.
</span>
<span
isteven-multi-select
ng-if="ctrl.registries.length"
input-model="ctrl.registries"
output-model="ctrl.formValues.Registries"
button-label="Name"
item-label="Name"
tick-property="Checked"
helper-elements="filter"
search-property="Name"
translation="{nothingSelected: 'Select one or more registry', search: 'Search...'}"
>
</span>
</div>
</div>
</div>
</div>
<!-- #endregion -->
<!-- #region STORAGES -->
<div class="col-sm-12 form-section-title">
Storages

View file

@ -3,6 +3,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolView', {
controller: 'KubernetesResourcePoolController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',
endpoint: '<',
},
});

View file

@ -1,7 +1,7 @@
import angular from 'angular';
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import {
@ -24,7 +24,7 @@ class KubernetesResourcePoolController {
Authentication,
Notifications,
LocalStorage,
EndpointProvider,
EndpointService,
ModalService,
KubernetesNodeService,
KubernetesResourceQuotaService,
@ -36,33 +36,30 @@ class KubernetesResourcePoolController {
KubernetesIngressService,
KubernetesVolumeService
) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.Authentication = Authentication;
this.LocalStorage = LocalStorage;
this.EndpointProvider = EndpointProvider;
this.ModalService = ModalService;
this.KubernetesNodeService = KubernetesNodeService;
this.KubernetesResourceQuotaService = KubernetesResourceQuotaService;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesEventService = KubernetesEventService;
this.KubernetesPodService = KubernetesPodService;
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesIngressService = KubernetesIngressService;
this.KubernetesVolumeService = KubernetesVolumeService;
Object.assign(this, {
$async,
$state,
Authentication,
Notifications,
LocalStorage,
EndpointService,
ModalService,
KubernetesNodeService,
KubernetesResourceQuotaService,
KubernetesResourcePoolService,
KubernetesEventService,
KubernetesPodService,
KubernetesApplicationService,
KubernetesNamespaceHelper,
KubernetesIngressService,
KubernetesVolumeService,
});
this.IngressClassTypes = KubernetesIngressClassTypes;
this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults;
this.onInit = this.onInit.bind(this);
this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this);
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
this.getEvents = this.getEvents.bind(this);
this.getApplications = this.getApplications.bind(this);
this.getIngresses = this.getIngresses.bind(this);
}
/* #endregion */
@ -159,15 +156,6 @@ class KubernetesResourcePoolController {
this.selectTab(2);
}
async createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit) {
const quota = new KubernetesResourceQuota(namespace);
quota.CpuLimit = cpuLimit;
quota.MemoryLimit = memoryLimit;
quota.ResourcePoolName = namespace;
quota.ResourcePoolOwner = owner;
await this.KubernetesResourceQuotaService.create(quota);
}
hasResourceQuotaBeenReduced() {
if (this.formValues.HasQuota && this.oldQuota) {
const cpuLimit = this.formValues.CpuLimit;
@ -285,88 +273,119 @@ class KubernetesResourcePoolController {
}
/* #endregion */
/* #region GET REGISTRIES */
getRegistries() {
return this.$async(async () => {
try {
const namespace = this.$state.params.id;
if (this.isAdmin) {
this.registries = await this.EndpointService.registries(this.endpoint.Id);
this.registries.forEach((reg) => {
if (reg.RegistryAccesses && reg.RegistryAccesses[this.endpoint.Id] && reg.RegistryAccesses[this.endpoint.Id].Namespaces.includes(namespace)) {
reg.Checked = true;
this.formValues.Registries.push(reg);
}
});
return;
}
const registries = await this.EndpointService.registries(this.endpoint.Id, namespace);
this.selectedRegistries = registries.map((r) => r.Name).join(', ');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
}
});
}
/* #endregion */
/* #region ON INIT */
async onInit() {
try {
const endpoint = this.EndpointProvider.currentEndpoint();
this.endpoint = endpoint;
this.isAdmin = this.Authentication.isAdmin();
this.state = {
actionInProgress: false,
sliderMaxMemory: 0,
sliderMaxCpu: 0,
cpuUsage: 0,
cpuUsed: 0,
memoryUsage: 0,
memoryUsed: 0,
activeTab: 0,
currentName: this.$state.$current.name,
showEditorTab: false,
eventsLoading: true,
applicationsLoading: true,
ingressesLoading: true,
viewReady: false,
eventWarningCount: 0,
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
duplicates: {
ingressHosts: new KubernetesFormValidationReferences(),
},
};
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
const name = this.$transition$.params().id;
const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
this.pool = _.find(pools, { Namespace: { Name: name } });
this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
this.formValues.Name = this.pool.Namespace.Name;
_.forEach(nodes, (item) => {
this.state.sliderMaxMemory += filesizeParser(item.Memory);
this.state.sliderMaxCpu += item.CPU;
});
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
const quota = this.pool.Quota;
if (quota) {
this.oldQuota = angular.copy(quota);
this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota);
this.state.cpuUsed = quota.CpuLimitUsed;
this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed);
}
this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name);
if (this.pool.Namespace.Name === 'default') {
this.isEditable = false;
}
await this.getEvents();
await this.getApplications();
if (this.state.canUseIngress) {
await this.getIngresses();
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
_.forEach(this.formValues.IngressClasses, (ic) => {
if (ic.Hosts.length === 0) {
ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
}
});
}
this.savedFormValues = angular.copy(this.formValues);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
}
$onInit() {
return this.$async(this.onInit);
return this.$async(async () => {
try {
const endpoint = this.endpoint;
this.isAdmin = this.Authentication.isAdmin();
this.state = {
actionInProgress: false,
sliderMaxMemory: 0,
sliderMaxCpu: 0,
cpuUsage: 0,
cpuUsed: 0,
memoryUsage: 0,
memoryUsed: 0,
activeTab: 0,
currentName: this.$state.$current.name,
showEditorTab: false,
eventsLoading: true,
applicationsLoading: true,
ingressesLoading: true,
viewReady: false,
eventWarningCount: 0,
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
duplicates: {
ingressHosts: new KubernetesFormValidationReferences(),
},
};
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
const name = this.$state.params.id;
const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
this.pool = _.find(pools, { Namespace: { Name: name } });
this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
this.formValues.Name = this.pool.Namespace.Name;
this.formValues.EndpointId = this.endpoint.Id;
_.forEach(nodes, (item) => {
this.state.sliderMaxMemory += filesizeParser(item.Memory);
this.state.sliderMaxCpu += item.CPU;
});
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
const quota = this.pool.Quota;
if (quota) {
this.oldQuota = angular.copy(quota);
this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota);
this.formValues.EndpointId = this.endpoint.Id;
this.state.cpuUsed = quota.CpuLimitUsed;
this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed);
}
this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name);
if (this.pool.Namespace.Name === 'default') {
this.isEditable = false;
}
await this.getEvents();
await this.getApplications();
if (this.state.canUseIngress) {
await this.getIngresses();
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
_.forEach(this.formValues.IngressClasses, (ic) => {
if (ic.Hosts.length === 0) {
ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
}
});
}
await this.getRegistries();
this.savedFormValues = angular.copy(this.formValues);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
});
}
/* #endregion */
$onDestroy() {

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;

Some files were not shown because too many files have changed in this diff Show more