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:
parent
0f5407da40
commit
179df06267
175 changed files with 3757 additions and 2544 deletions
|
@ -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' } },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
16
app/docker/views/registries/access/registryAccess.html
Normal file
16
app/docker/views/registries/access/registryAccess.html
Normal 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> > {{ $ctrl.registry.Name }} > 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>
|
7
app/docker/views/registries/access/registryAccess.js
Normal file
7
app/docker/views/registries/access/registryAccess.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.docker').component('dockerRegistryAccessView', {
|
||||
templateUrl: './registryAccess.html',
|
||||
controller: 'DockerRegistryAccessController',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
|
@ -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);
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ class KubernetesConfigurationConverter {
|
|||
res.Data[entry.Key] = entry.Value;
|
||||
});
|
||||
res.ConfigurationOwner = secret.ConfigurationOwner;
|
||||
res.IsRegistrySecret = secret.IsRegistrySecret;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -5,7 +5,7 @@ const _KubernetesDaemonSet = Object.freeze({
|
|||
Namespace: '',
|
||||
Name: '',
|
||||
StackName: '',
|
||||
Image: '',
|
||||
ImageModel: null,
|
||||
Env: [],
|
||||
CpuLimit: 0,
|
||||
MemoryLimit: 0,
|
||||
|
|
|
@ -6,7 +6,7 @@ const _KubernetesDeployment = Object.freeze({
|
|||
Name: '',
|
||||
StackName: '',
|
||||
ReplicaCount: 0,
|
||||
Image: '',
|
||||
ImageModel: null,
|
||||
Env: [],
|
||||
CpuLimit: 0,
|
||||
MemoryLimit: 0,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@ const _KubernetesStatefulSet = Object.freeze({
|
|||
Name: '',
|
||||
StackName: '',
|
||||
ReplicaCount: 0,
|
||||
Image: '',
|
||||
ImageModel: null,
|
||||
Env: [],
|
||||
CpuLimit: '',
|
||||
MemoryLimit: '',
|
||||
|
|
5
app/kubernetes/registries/index.js
Normal file
5
app/kubernetes/registries/index.js
Normal 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;
|
|
@ -0,0 +1,9 @@
|
|||
import controller from './kube-registry-access-view.controller';
|
||||
|
||||
export const kubernetesRegistryAccessView = {
|
||||
templateUrl: './kube-registry-access-view.html',
|
||||
controller,
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
};
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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> > {{ $ctrl.registry.Name }} > 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>
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ class KubernetesStatefulSetService {
|
|||
if (!payload.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.KubernetesStatefulSets(namespace).patch(params, payload).$promise;
|
||||
return data;
|
||||
} catch (err) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,6 @@ angular.module('portainer.kubernetes').component('kubernetesCreateApplicationVie
|
|||
controller: 'KubernetesCreateApplicationController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<a ui-sref="kubernetes.resourcePools">Namespaces</a> > 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>
|
||||
|
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,6 +3,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolView', {
|
|||
controller: 'KubernetesResourcePoolController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
9
app/portainer/components/accessManagement/index.js
Normal file
9
app/portainer/components/accessManagement/index.js
Normal 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;
|
|
@ -0,0 +1,7 @@
|
|||
export const porAccessManagementUsersSelector = {
|
||||
templateUrl: './por-access-management-users-selector.html',
|
||||
bindings: {
|
||||
options: '<',
|
||||
value: '=',
|
||||
},
|
||||
};
|
|
@ -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>
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
angular.module('portainer.app').component('registryFormDockerhub', {
|
||||
templateUrl: './registry-form-dockerhub.html',
|
||||
bindings: {
|
||||
model: '=',
|
||||
formAction: '<',
|
||||
formActionLabel: '@',
|
||||
actionInProgress: '<',
|
||||
},
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
angular.module('portainer.app').component('templateForm', {
|
||||
templateUrl: './templateForm.html',
|
||||
controller: 'TemplateFormController',
|
||||
bindings: {
|
||||
model: '=',
|
||||
categories: '<',
|
||||
networks: '<',
|
||||
formAction: '<',
|
||||
formActionLabel: '@',
|
||||
actionInProgress: '<',
|
||||
showTypeSelector: '<',
|
||||
},
|
||||
});
|
|
@ -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>
|
|
@ -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;
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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;
|
||||
|
|
10
app/portainer/components/registry-details/index.js
Normal file
10
app/portainer/components/registry-details/index.js
Normal 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);
|
|
@ -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>
|
|
@ -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;
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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' },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
|
@ -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' },
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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' } },
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
21
app/portainer/views/endpoint-registries/registries.html
Normal file
21
app/portainer/views/endpoint-registries/registries.html
Normal 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>
|
7
app/portainer/views/endpoint-registries/registries.js
Normal file
7
app/portainer/views/endpoint-registries/registries.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.app').component('endpointRegistriesView', {
|
||||
templateUrl: './registries.html',
|
||||
controller: 'EndpointRegistriesController',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
|
@ -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);
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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> > <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> > 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>
|
|
@ -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();
|
||||
},
|
||||
]);
|
|
@ -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>
|
10
app/portainer/views/registries/create/createRegistry.js
Normal file
10
app/portainer/views/registries/create/createRegistry.js
Normal 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$: '<',
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue