mirror of
https://github.com/portainer/portainer.git
synced 2025-08-08 07:15:23 +02:00
fix(app): registry push-pull features overhaul (#3393)
* feat(registry): registry or direct url selector
* feat(app): push pull container creation
* feat(app): push pull container duplicate
* feat(app): push pull container details recreate
* feat(app): push pull container details commit
* feat(app): push pull images
* feat(app): push pull image tag
* feat(app): push pull image push
* feat(app): push pull image pull
* feat(app): push pull service creation
* feat(app): push pull templates create container
* feat(app): push pull templates create stacks
* feat(app): push pull template edit
* feat(app): push pull service details update
* fix(app): refactor registry selector + registry auto select
* feat(app): remove autocomplete on registry selector
* style(image-registry): reword simple/advanced mode
* Revert "feat(app): remove autocomplete on registry selector"
This reverts commit 97ec2ddd62
.
* refactor(registry-selector): reverse registry and image fields
* feat(app): autocomplete on registry selector
* feat(registry-selector): change gitlab registry autocomplete
* feat(registry-selector): autocomplete for dockerhub
* feat(registry-selector): gitlab url based on locked value instead of name
* fix(registry-selector): gitlab registries URL are not modified anymore
* fix(registry-selector): change gitlab image autofill on duplicate
* fix(registry-selector): gitlab registries now only suggest their own images and not all from gitlab
* fix(registry-selector): psuh pull issues with gitlab registries
* fix(registry-selector): dockerhub registry selection on duplicate for dockerhub images
* fix(templates): registry retrieval for template
* feat(images): add autocomplete on image pull panel
* fix(registry-selector): add latest tag when no tag is specified
* fix(registry-selector): latest tag now applied for non gitlab registries
This commit is contained in:
parent
61c38534a7
commit
e19bc8abc7
32 changed files with 525 additions and 349 deletions
|
@ -1,58 +1,6 @@
|
|||
angular.module('portainer.app').component('templateForm', {
|
||||
templateUrl: './templateForm.html',
|
||||
controller: 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;
|
||||
};
|
||||
},
|
||||
controller: 'TemplateFormController',
|
||||
bindings: {
|
||||
model: '=',
|
||||
categories: '<',
|
||||
|
|
|
@ -203,10 +203,9 @@
|
|||
<!-- container-details -->
|
||||
<div uib-collapse="$ctrl.state.collapseContainer">
|
||||
<por-image-registry
|
||||
image="$ctrl.model.Image"
|
||||
registry="$ctrl.model.Registry"
|
||||
model="$ctrl.model.RegistryModel"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-2" input-class="col-sm-10 col-md-4"
|
||||
label-class="col-sm-1" input-class="col-sm-11"
|
||||
></por-image-registry>
|
||||
<!-- command -->
|
||||
<div class="form-group">
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
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,18 +0,0 @@
|
|||
angular.module('portainer.app')
|
||||
.factory('RegistryHelper', [function RegistryHelperFactory() {
|
||||
'use strict';
|
||||
|
||||
var helper = {};
|
||||
|
||||
helper.getRegistryByURL = function(registries, url) {
|
||||
for (var i = 0; i < registries.length; i++) {
|
||||
if (registries[i].URL === url) {
|
||||
return registries[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return helper;
|
||||
}]);
|
|
@ -59,7 +59,8 @@ export function RegistryCreateRequest(model) {
|
|||
if (model.Type === RegistryTypes.GITLAB) {
|
||||
this.Gitlab = {
|
||||
ProjectId: model.Gitlab.ProjectId,
|
||||
InstanceURL: model.Gitlab.InstanceURL
|
||||
InstanceURL: model.Gitlab.InstanceURL,
|
||||
ProjectPath: model.Gitlab.ProjectPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
import _ from 'lodash-es';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
export function TemplateDefaultModel() {
|
||||
this.Type = 1;
|
||||
this.AdministratorOnly = false;
|
||||
this.Title = '';
|
||||
this.Image = '';
|
||||
this.Description = '';
|
||||
this.Volumes = [];
|
||||
this.Ports = [];
|
||||
this.Env = [];
|
||||
this.Labels = [];
|
||||
this.RestartPolicy = 'always';
|
||||
this.Registry = {};
|
||||
this.RegistryModel = new PorImageRegistryModel();
|
||||
}
|
||||
|
||||
export function TemplateCreateRequest(model) {
|
||||
|
@ -24,8 +24,8 @@ export function TemplateCreateRequest(model) {
|
|||
this.Categories = model.Categories;
|
||||
this.Platform = model.Platform;
|
||||
this.Logo = model.Logo;
|
||||
this.Image = model.Image;
|
||||
this.Registry = model.Registry.URL;
|
||||
this.Image = model.RegistryModel.Image;
|
||||
this.Registry = model.RegistryModel.Registry.URL;
|
||||
this.Command = model.Command;
|
||||
this.Network = model.Network && model.Network.Name;
|
||||
this.Privileged = model.Privileged;
|
||||
|
@ -66,9 +66,9 @@ export function TemplateViewModel(data) {
|
|||
this.Logo = data.logo;
|
||||
this.Repository = data.repository;
|
||||
this.Hostname = data.hostname;
|
||||
this.Registry = data.registry ? { URL: data.registry } : {};
|
||||
this.Image = data.image;
|
||||
this.Registry = data.registry ? data.registry : '';
|
||||
this.RegistryModel = new PorImageRegistryModel();
|
||||
this.RegistryModel.Image = data.image;
|
||||
this.RegistryModel.Registry.URL = data.registry || '';
|
||||
this.Command = data.command ? data.command : '';
|
||||
this.Network = data.network ? data.network : '';
|
||||
this.Privileged = data.privileged ? data.privileged : false;
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import _ from 'lodash-es';
|
||||
import { RegistryViewModel, RegistryCreateRequest } from '../../models/registry';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.factory('RegistryService', ['$q', 'Registries', 'DockerHubService', 'RegistryHelper', 'ImageHelper', 'FileUploadService', function RegistryServiceFactory($q, Registries, DockerHubService, RegistryHelper, ImageHelper, FileUploadService) {
|
||||
.factory('RegistryService', ['$q', '$async', 'Registries', 'DockerHubService', 'ImageHelper', 'FileUploadService',
|
||||
function RegistryServiceFactory($q, $async, Registries, DockerHubService, ImageHelper, FileUploadService) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
|
@ -71,6 +74,7 @@ angular.module('portainer.app')
|
|||
_.forEach(projects, (p) => {
|
||||
const m = model;
|
||||
m.Name = p.PathWithNamespace;
|
||||
m.Gitlab.ProjectPath = _.toLower(p.PathWithNamespace);
|
||||
m.Gitlab.ProjectId = p.Id;
|
||||
m.Password = m.Token;
|
||||
const payload = new RegistryCreateRequest(m);
|
||||
|
@ -79,23 +83,52 @@ angular.module('portainer.app')
|
|||
return $q.all(promises);
|
||||
};
|
||||
|
||||
service.retrieveRegistryFromRepository = function(repository) {
|
||||
var deferred = $q.defer();
|
||||
service.retrievePorRegistryModelFromRepositoryWithRegistries = retrievePorRegistryModelFromRepositoryWithRegistries;
|
||||
|
||||
var imageDetails = ImageHelper.extractImageAndRegistryFromRepository(repository);
|
||||
$q.when(imageDetails.registry ? service.registries() : DockerHubService.dockerhub())
|
||||
.then(function success(data) {
|
||||
var registry = data;
|
||||
if (imageDetails.registry) {
|
||||
registry = RegistryHelper.getRegistryByURL(data, imageDetails.registry);
|
||||
function getURL(reg) {
|
||||
let url = reg.URL;
|
||||
if (reg.Type === RegistryTypes.GITLAB) {
|
||||
url = reg.URL + '/' + reg.Gitlab.ProjectPath;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, dockerhub) {
|
||||
const model = new PorImageRegistryModel();
|
||||
const registry = _.find(registries, (reg) => _.includes(repository, getURL(reg)));
|
||||
if (registry) {
|
||||
const url = getURL(registry);
|
||||
const lastIndex = repository.lastIndexOf(url) + url.length;
|
||||
let image = repository.substring(lastIndex);
|
||||
if (!_.startsWith(image, ':')) {
|
||||
image = image.substring(1);
|
||||
}
|
||||
deferred.resolve(registry);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve the registry associated to the repository', err: err });
|
||||
});
|
||||
model.Registry = registry;
|
||||
model.Image = image;
|
||||
} else {
|
||||
if (ImageHelper.imageContainsURL(repository)) {
|
||||
model.UseRegistry = false;
|
||||
}
|
||||
model.Registry = dockerhub;
|
||||
model.Image = repository;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
service.retrievePorRegistryModelFromRepository = function(repository) {
|
||||
return $async(retrievePorRegistryModelFromRepositoryAsync, repository)
|
||||
};
|
||||
|
||||
return service;
|
||||
|
|
|
@ -5,18 +5,26 @@ import {
|
|||
} from '../../models/template';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.factory('TemplateService', ['$q', 'Templates', 'TemplateHelper', 'ImageHelper', 'ContainerHelper',
|
||||
function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, ContainerHelper) {
|
||||
.factory('TemplateService', ['$q', 'Templates', 'TemplateHelper', 'RegistryService', 'DockerHubService', 'ImageHelper', 'ContainerHelper',
|
||||
function TemplateServiceFactory($q, Templates, TemplateHelper, RegistryService, DockerHubService, ImageHelper, ContainerHelper) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.templates = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Templates.query().$promise
|
||||
$q.all({
|
||||
templates: Templates.query().$promise,
|
||||
registries: RegistryService.registries(),
|
||||
dockerhub: DockerHubService.dockerhub()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var templates = data.map(function (item) {
|
||||
return new TemplateViewModel(item);
|
||||
const templates = data.templates.map(function (item) {
|
||||
const res = new TemplateViewModel(item);
|
||||
const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(res.RegistryModel.Registry.URL, data.registries, data.dockerhub);
|
||||
registry.Image = res.RegistryModel.Image;
|
||||
res.RegistryModel = registry;
|
||||
return res;
|
||||
});
|
||||
deferred.resolve(templates);
|
||||
})
|
||||
|
@ -29,10 +37,15 @@ function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, Cont
|
|||
|
||||
service.template = function(id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
let template;
|
||||
Templates.get({ id: id }).$promise
|
||||
.then(function success(data) {
|
||||
var template = new TemplateViewModel(data);
|
||||
template = new TemplateViewModel(data);
|
||||
return RegistryService.retrievePorRegistryModelFromRepository(template.RegistryModel.Registry.URL);
|
||||
})
|
||||
.then((registry) => {
|
||||
registry.Image = template.RegistryModel.Image;
|
||||
template.RegistryModel = registry;
|
||||
deferred.resolve(template);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -58,13 +71,13 @@ function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, Cont
|
|||
};
|
||||
|
||||
service.createTemplateConfiguration = function(template, containerName, network) {
|
||||
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
|
||||
var containerConfiguration = service.createContainerConfiguration(template, containerName, network);
|
||||
containerConfiguration.Image = imageConfiguration.fromImage + ':' + imageConfiguration.tag;
|
||||
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel);
|
||||
var containerConfiguration = createContainerConfiguration(template, containerName, network);
|
||||
containerConfiguration.Image = imageConfiguration.fromImage;
|
||||
return containerConfiguration;
|
||||
};
|
||||
|
||||
service.createContainerConfiguration = function(template, containerName, network) {
|
||||
function createContainerConfiguration(template, containerName, network) {
|
||||
var configuration = TemplateHelper.getDefaultContainerConfiguration();
|
||||
configuration.HostConfig.NetworkMode = network.Name;
|
||||
configuration.HostConfig.Privileged = template.Privileged;
|
||||
|
@ -72,7 +85,6 @@ function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, Cont
|
|||
configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : [];
|
||||
configuration.name = containerName;
|
||||
configuration.Hostname = template.Hostname;
|
||||
configuration.Image = template.Image;
|
||||
configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
|
||||
configuration.Cmd = ContainerHelper.commandStringToArray(template.Command);
|
||||
var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
|
||||
|
@ -83,7 +95,7 @@ function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, Cont
|
|||
configuration.Tty = consoleConfiguration.tty;
|
||||
configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels);
|
||||
return configuration;
|
||||
};
|
||||
}
|
||||
|
||||
service.updateContainerConfigurationWithVolumes = function(configuration, template, generatedVolumesPile) {
|
||||
var volumes = template.Volumes;
|
||||
|
|
|
@ -73,7 +73,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima
|
|||
generatedVolumeIds.push(volumeId);
|
||||
});
|
||||
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration, template, data);
|
||||
return ImageService.pullImage(template.Image, { URL: template.Registry }, true);
|
||||
return ImageService.pullImage(template.RegistryModel, true);
|
||||
})
|
||||
.then(function success() {
|
||||
return ContainerService.createAndStartContainer(templateConfiguration);
|
||||
|
@ -112,7 +112,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima
|
|||
var endpointId = EndpointProvider.endpointID();
|
||||
StackService.createComposeStackFromGitRepository(stackName, repositoryOptions, template.Env, endpointId)
|
||||
.then(function success(data) {
|
||||
const resourceControl = data.Portainer.ResourceControl;
|
||||
const resourceControl = data.ResourceControl;
|
||||
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||
})
|
||||
.then(function success() {
|
||||
|
@ -150,7 +150,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima
|
|||
var endpointId = EndpointProvider.endpointID();
|
||||
StackService.createSwarmStackFromGitRepository(stackName, repositoryOptions, env, endpointId)
|
||||
.then(function success(data) {
|
||||
const resourceControl = data.Portainer.ResourceControl;
|
||||
const resourceControl = data.ResourceControl;
|
||||
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||
})
|
||||
.then(function success() {
|
||||
|
@ -158,7 +158,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima
|
|||
$state.go('portainer.stacks');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.warning('Deployment error', err.err.data.err);
|
||||
Notifications.warning('Deployment error', err.data.err);
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue