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
|
@ -2,6 +2,7 @@ import _ from 'lodash-es';
|
|||
import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities';
|
||||
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { ContainerDetailsViewModel } from '../../../models/container';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
|
||||
angular.module('portainer.docker')
|
||||
|
@ -27,7 +28,8 @@ function ($q, $scope, $async, $state, $timeout, $transition$, $filter, Container
|
|||
NodeName: null,
|
||||
capabilities: [],
|
||||
LogDriverName: '',
|
||||
LogDriverOpts: []
|
||||
LogDriverOpts: [],
|
||||
RegistryModel: new PorImageRegistryModel()
|
||||
};
|
||||
|
||||
$scope.extraNetworks = {};
|
||||
|
@ -130,11 +132,8 @@ function ($q, $scope, $async, $state, $timeout, $transition$, $filter, Container
|
|||
$scope.fromContainerMultipleNetworks = false;
|
||||
|
||||
function prepareImageConfig(config) {
|
||||
var image = config.Image;
|
||||
var registry = $scope.formValues.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry.URL);
|
||||
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
$scope.imageConfig = imageConfig;
|
||||
const imageConfig = ImageHelper.createImageConfigForContainer($scope.formValues.RegistryModel);
|
||||
config.Image = imageConfig.fromImage;
|
||||
}
|
||||
|
||||
function preparePortBindings(config) {
|
||||
|
@ -438,13 +437,9 @@ function ($q, $scope, $async, $state, $timeout, $transition$, $filter, Container
|
|||
}
|
||||
|
||||
function loadFromContainerImageConfig() {
|
||||
var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image);
|
||||
RegistryService.retrieveRegistryFromRepository($scope.config.Image)
|
||||
.then(function success(data) {
|
||||
if (data) {
|
||||
$scope.config.Image = imageInfo.image;
|
||||
$scope.formValues.Registry = data;
|
||||
}
|
||||
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image)
|
||||
.then((model) => {
|
||||
$scope.formValues.RegistryModel = model;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrive registry');
|
||||
|
@ -569,7 +564,6 @@ function ($q, $scope, $async, $state, $timeout, $transition$, $filter, Container
|
|||
loadFromContainerSpec();
|
||||
} else {
|
||||
$scope.fromContainer = {};
|
||||
$scope.formValues.Registry = {};
|
||||
$scope.formValues.capabilities = new ContainerCapabilities();
|
||||
}
|
||||
}, function(e) {
|
||||
|
@ -754,7 +748,7 @@ function ($q, $scope, $async, $state, $timeout, $transition$, $filter, Container
|
|||
|
||||
function pullImageIfNeeded() {
|
||||
return $q.when($scope.formValues.alwaysPull &&
|
||||
ImageService.pullImage($scope.config.Image, $scope.formValues.Registry, true));
|
||||
ImageService.pullImage($scope.formValues.RegistryModel, true));
|
||||
}
|
||||
|
||||
function createNewContainer() {
|
||||
|
|
|
@ -21,18 +21,18 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Image configuration
|
||||
</div>
|
||||
<div ng-if="!formValues.Registry && fromContainer">
|
||||
<div ng-if="!formValues.RegistryModel.Registry && fromContainer">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
|
||||
<span class="small text-danger" style="margin-left: 5px;">The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register that registry first.</span>
|
||||
</div>
|
||||
<div ng-if="formValues.Registry || !fromContainer">
|
||||
<div ng-if="formValues.RegistryModel.Registry || !fromContainer">
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry
|
||||
image="config.Image"
|
||||
registry="formValues.Registry"
|
||||
ng-if="formValues.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 col-md-5"
|
||||
label-class="col-sm-1" input-class="col-sm-11"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- always-pull -->
|
||||
|
@ -142,7 +142,9 @@
|
|||
<!-- !autoremove -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !config.Image || (!formValues.Registry && fromContainer)" ng-click="create()" button-spinner="state.actionInProgress">
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || (!formValues.RegistryModel.Registry && fromContainer)"
|
||||
ng-click="create()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">Deploy the container</span>
|
||||
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
||||
</button>
|
||||
|
|
|
@ -155,10 +155,9 @@
|
|||
<!-- !tag-description -->
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry
|
||||
image="config.Image"
|
||||
registry="config.Registry"
|
||||
model="config.RegistryModel"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
label-class="col-sm-1" input-class="col-sm-11"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- tag-note -->
|
||||
|
@ -170,7 +169,7 @@
|
|||
<!-- !tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.RegistryModel.Image || config.commitInProgress" ng-click="commit()">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import moment from 'moment';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', 'HttpRequestHelper', 'Authentication',
|
||||
function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, ContainerService, ImageHelper, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService, HttpRequestHelper, Authentication) {
|
||||
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', '$async', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', 'HttpRequestHelper', 'Authentication',
|
||||
function ($q, $scope, $state, $transition$, $filter, $async, Commit, ContainerHelper, ContainerService, ImageHelper, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService, HttpRequestHelper, Authentication) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
RegistryModel: new PorImageRegistryModel(),
|
||||
commitInProgress: false
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
|
@ -149,20 +150,23 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
|
|||
});
|
||||
};
|
||||
|
||||
$scope.commit = function () {
|
||||
var image = $scope.config.Image;
|
||||
$scope.config.Image = '';
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
|
||||
Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function () {
|
||||
update();
|
||||
async function commitContainerAsync() {
|
||||
$scope.config.commitInProgress = true;
|
||||
const registryModel = $scope.config.RegistryModel;
|
||||
const imageConfig = ImageHelper.createImageConfigForContainer(registryModel);
|
||||
try {
|
||||
await Commit.commitContainer({id: $transition$.params().id, repo: imageConfig.fromImage}).$promise;
|
||||
Notifications.success('Image created', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Notifications.error('Failure', e, 'Unable to create image');
|
||||
});
|
||||
};
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to create image');
|
||||
$scope.config.commitInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.commit = function () {
|
||||
return $async(commitContainerAsync);
|
||||
};
|
||||
|
||||
$scope.confirmRemove = function () {
|
||||
var title = 'You are about to remove a container.';
|
||||
|
@ -225,15 +229,12 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
|
|||
if (!pullImage) {
|
||||
return $q.when();
|
||||
}
|
||||
return getRegistry().then(function pullImage(containerRegistery) {
|
||||
return ImageService.pullImage(container.Config.Image, containerRegistery, true);
|
||||
return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image)
|
||||
.then(function pullImage(registryModel) {
|
||||
return ImageService.pullImage(registryModel, true);
|
||||
});
|
||||
}
|
||||
|
||||
function getRegistry() {
|
||||
return RegistryService.retrieveRegistryFromRepository(container.Config.Image);
|
||||
}
|
||||
|
||||
function setMainNetworkAndCreateContainer() {
|
||||
var networks = config.NetworkingConfig.EndpointsConfig;
|
||||
var networksNames = Object.keys(networks);
|
||||
|
|
|
@ -64,9 +64,8 @@
|
|||
<form class="form-horizontal">
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry
|
||||
image="formValues.Image"
|
||||
registry="formValues.Registry"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
model="formValues.RegistryModel"
|
||||
label-class="col-sm-1" input-class="col-sm-11"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- tag-note -->
|
||||
|
@ -78,7 +77,7 @@
|
|||
<!-- !tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Image" ng-click="tagImage()">Tag</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.RegistryModel.Image" ng-click="tagImage()">Tag</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import _ from 'lodash-es';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('ImageController', ['$q', '$scope', '$transition$', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', 'HttpRequestHelper', 'ModalService', 'FileSaver', 'Blob',
|
||||
function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) {
|
||||
.controller('ImageController', ['$q', '$scope', '$transition$', '$state', '$timeout', 'ImageService', 'ImageHelper', 'RegistryService', 'Notifications', 'HttpRequestHelper', 'ModalService', 'FileSaver', 'Blob',
|
||||
function ($q, $scope, $transition$, $state, $timeout, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) {
|
||||
$scope.formValues = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
RegistryModel: new PorImageRegistryModel()
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
|
@ -27,10 +27,11 @@ function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryServ
|
|||
};
|
||||
|
||||
$scope.tagImage = function() {
|
||||
var image = $scope.formValues.Image;
|
||||
var registry = $scope.formValues.Registry;
|
||||
const registryModel = $scope.formValues.RegistryModel;
|
||||
|
||||
ImageService.tagImage($transition$.params().id, image, registry.URL)
|
||||
const image = ImageHelper.createImageConfigForContainer(registryModel);
|
||||
|
||||
ImageService.tagImage($transition$.params().id, image.fromImage)
|
||||
.then(function success() {
|
||||
Notifications.success('Image successfully tagged');
|
||||
$state.go('docker.images.image', {id: $transition$.params().id}, {reload: true});
|
||||
|
@ -42,10 +43,9 @@ function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryServ
|
|||
|
||||
$scope.pushTag = function(repository) {
|
||||
$('#uploadResourceHint').show();
|
||||
RegistryService.retrieveRegistryFromRepository(repository)
|
||||
.then(function success(data) {
|
||||
var registry = data;
|
||||
return ImageService.pushImage(repository, registry);
|
||||
RegistryService.retrievePorRegistryModelFromRepository(repository)
|
||||
.then(function success(registryModel) {
|
||||
return ImageService.pushImage(registryModel);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Image successfully pushed', repository);
|
||||
|
@ -60,10 +60,9 @@ function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryServ
|
|||
|
||||
$scope.pullTag = function(repository) {
|
||||
$('#downloadResourceHint').show();
|
||||
RegistryService.retrieveRegistryFromRepository(repository)
|
||||
.then(function success(data) {
|
||||
var registry = data;
|
||||
return ImageService.pullImage(repository, registry, false);
|
||||
RegistryService.retrievePorRegistryModelFromRepository(repository)
|
||||
.then(function success(registryModel) {
|
||||
return ImageService.pullImage(registryModel, false);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Image successfully pulled', repository);
|
||||
|
|
|
@ -16,18 +16,12 @@
|
|||
<form class="form-horizontal">
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry
|
||||
image="formValues.Image"
|
||||
registry="formValues.Registry"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
model="formValues.RegistryModel"
|
||||
auto-complete="true"
|
||||
pull-warning="true"
|
||||
label-class="col-sm-1" input-class="col-sm-11"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: if you don't specify the tag in the image name, <span class="label label-default">latest</span> will be used.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tag-note -->
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
|
@ -40,7 +34,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Image" ng-click="pullImage()" button-spinner="state.actionInProgress">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image" ng-click="pullImage()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">Pull the image</span>
|
||||
<span ng-show="state.actionInProgress">Download in progress...</span>
|
||||
</button>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'lodash-es';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper', 'FileSaver', 'Blob', 'EndpointProvider',
|
||||
|
@ -9,22 +10,20 @@ function ($scope, $state, ImageService, Notifications, ModalService, HttpRequest
|
|||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Image: '',
|
||||
Registry: '',
|
||||
RegistryModel: new PorImageRegistryModel(),
|
||||
NodeName: null
|
||||
};
|
||||
|
||||
$scope.pullImage = function() {
|
||||
var image = $scope.formValues.Image;
|
||||
var registry = $scope.formValues.Registry;
|
||||
const registryModel = $scope.formValues.RegistryModel;
|
||||
|
||||
var nodeName = $scope.formValues.NodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
ImageService.pullImage(image, registry, false)
|
||||
ImageService.pullImage(registryModel, false)
|
||||
.then(function success() {
|
||||
Notifications.success('Image successfully pulled', image);
|
||||
Notifications.success('Image successfully pulled', registryModel.Image);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash-es';
|
||||
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
require('./includes/update-restart.html')
|
||||
require('./includes/secret.html')
|
||||
|
@ -12,8 +13,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
Image: '',
|
||||
Registry: {},
|
||||
RegistryModel: new PorImageRegistryModel(),
|
||||
Mode: 'replicated',
|
||||
Replicas: 1,
|
||||
Command: '',
|
||||
|
@ -160,8 +160,8 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
};
|
||||
|
||||
function prepareImageConfig(config, input) {
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry.URL);
|
||||
config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(input.RegistryModel);
|
||||
config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage;
|
||||
}
|
||||
|
||||
function preparePortsConfig(config, input) {
|
||||
|
@ -427,9 +427,8 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
|||
}
|
||||
|
||||
function createNewService(config, accessControlData) {
|
||||
|
||||
var registry = $scope.formValues.Registry;
|
||||
var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : '';
|
||||
const registryModel = $scope.formValues.RegistryModel;
|
||||
var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : '';
|
||||
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
|
||||
|
||||
Service.create(config).$promise
|
||||
|
|
|
@ -23,10 +23,9 @@
|
|||
</div>
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry
|
||||
image="formValues.Image"
|
||||
registry="formValues.Registry"
|
||||
model="formValues.RegistryModel"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
label-class="col-sm-1" input-class="col-sm-11"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
@ -126,7 +125,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Image" ng-click="create()" button-spinner="state.actionInProgress">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image" ng-click="create()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">Create the service</span>
|
||||
<span ng-show="state.actionInProgress">Creating service...</span>
|
||||
</button>
|
||||
|
|
32
app/docker/views/services/edit/includes/image.html
Normal file
32
app/docker/views/services/edit/includes/image.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<div id="service-container-image" authorization="DockerServiceUpdate">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-clone" title-text="Change container image">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="!isUpdating">
|
||||
<form class="form-horizontal">
|
||||
<por-image-registry
|
||||
model="formValues.RegistryModel"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1" input-class="col-sm-11"
|
||||
></por-image-registry>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="isUpdating">
|
||||
<p>Image modification is disabled while service is updating.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer authorization="DockerServiceUpdate">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['Image'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['Image'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -66,10 +66,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" uib-typeahead="image for image in availableImages | filter:$viewValue | limitTo:5"
|
||||
ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" disable-authorization="DockerServiceUpdate">
|
||||
</td>
|
||||
<td>{{ service.Image }}</td>
|
||||
</tr>
|
||||
<tr ng-if="applicationState.endpoint.type !== 4">
|
||||
<td colspan="{{webhookURL ? '1' : '2'}}">
|
||||
|
@ -118,12 +115,12 @@
|
|||
</p>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(service, ['Mode', 'Replicas', 'Image', 'Name', 'Webhooks'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(service, ['Mode', 'Replicas', 'Name', 'Webhooks'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['Mode', 'Replicas', 'Image', 'Name'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service, ['Mode', 'Replicas', 'Name'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -138,6 +135,7 @@
|
|||
<rd-widget-body classes="no-padding">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li><a href ng-click="goToItem('service-env-variables')">Environment variables</a></li>
|
||||
<li><a href ng-click="goToItem('service-container-image')">Container image</a></li>
|
||||
<li><a href ng-click="goToItem('service-container-labels')">Container labels</a></li>
|
||||
<li><a href ng-click="goToItem('service-mounts')">Mounts</a></li>
|
||||
<li><a href ng-click="goToItem('service-network-specs')">Network & published ports</a></li>
|
||||
|
@ -171,6 +169,7 @@
|
|||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<h3 id="container-specs">Container specification</h3>
|
||||
<div id="service-container-spec" class="padding-top" ng-include="'app/docker/views/services/edit/includes/container-specs.html'"></div>
|
||||
<div id="service-container-image" class="padding-top" ng-include="'app/docker/views/services/edit/includes/image.html'"></div>
|
||||
<div id="service-env-variables" class="padding-top" ng-include="'app/docker/views/services/edit/includes/environmentvariables.html'"></div>
|
||||
<div id="service-container-labels" class="padding-top" ng-include="'app/docker/views/services/edit/includes/containerlabels.html'"></div>
|
||||
<div id="service-mounts" class="padding-top" ng-include="'app/docker/views/services/edit/includes/mounts.html'"></div>
|
||||
|
|
|
@ -4,6 +4,7 @@ require('./includes/container-specs.html')
|
|||
require('./includes/containerlabels.html')
|
||||
require('./includes/environmentvariables.html')
|
||||
require('./includes/hosts.html')
|
||||
require('./includes/image.html')
|
||||
require('./includes/logging.html')
|
||||
require('./includes/mounts.html')
|
||||
require('./includes/networks.html')
|
||||
|
@ -16,6 +17,8 @@ require('./includes/servicelabels.html')
|
|||
require('./includes/tasks.html')
|
||||
require('./includes/updateconfig.html')
|
||||
|
||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'ContainerService', 'TaskHelper', 'Notifications', 'ModalService', 'PluginService', 'Authentication', 'SettingsService', 'VolumeService', 'ImageHelper', 'WebhookService', 'EndpointProvider', 'clipboard','WebhookHelper',
|
||||
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, ContainerService, TaskHelper, Notifications, ModalService, PluginService, Authentication, SettingsService, VolumeService, ImageHelper, WebhookService, EndpointProvider, clipboard, WebhookHelper) {
|
||||
|
@ -26,6 +29,10 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
rollbackInProgress: false,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
RegistryModel: new PorImageRegistryModel()
|
||||
};
|
||||
|
||||
$scope.tasks = [];
|
||||
$scope.availableImages = [];
|
||||
|
||||
|
@ -34,20 +41,6 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
var originalService = {};
|
||||
var previousServiceValues = [];
|
||||
|
||||
$scope.renameService = function renameService(service) {
|
||||
updateServiceAttribute(service, 'Name', service.newServiceName || service.name);
|
||||
service.EditName = false;
|
||||
};
|
||||
$scope.changeServiceImage = function changeServiceImage(service) {
|
||||
updateServiceAttribute(service, 'Image', service.newServiceImage || service.image);
|
||||
service.EditImage = false;
|
||||
};
|
||||
$scope.scaleService = function scaleService(service) {
|
||||
var replicas = service.newServiceReplicas === null || isNaN(service.newServiceReplicas) ? service.Replicas : service.newServiceReplicas;
|
||||
updateServiceAttribute(service, 'Replicas', replicas);
|
||||
service.EditReplicas = false;
|
||||
};
|
||||
|
||||
$scope.goToItem = function(hash) {
|
||||
if ($location.hash() !== hash) {
|
||||
$location.hash(hash);
|
||||
|
@ -259,12 +252,17 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
$scope.cancelChanges = function cancelChanges(service, keys) {
|
||||
if (keys) { // clean out the keys only from the list of modified keys
|
||||
keys.forEach(function(key) {
|
||||
var index = previousServiceValues.indexOf(key);
|
||||
if (index >= 0) {
|
||||
previousServiceValues.splice(index, 1);
|
||||
if (key === 'Image') {
|
||||
$scope.formValues.RegistryModel.Image = '';
|
||||
} else {
|
||||
var index = previousServiceValues.indexOf(key);
|
||||
if (index >= 0) {
|
||||
previousServiceValues.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else { // clean out all changes
|
||||
$scope.formValues.RegistryModel.Image = '';
|
||||
keys = Object.keys(service);
|
||||
previousServiceValues = [];
|
||||
}
|
||||
|
@ -277,7 +275,11 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
$scope.hasChanges = function(service, elements) {
|
||||
var hasChanges = false;
|
||||
elements.forEach(function(key) {
|
||||
hasChanges = hasChanges || (previousServiceValues.indexOf(key) >= 0);
|
||||
if (key === 'Image') {
|
||||
hasChanges = hasChanges || $scope.formValues.RegistryModel.Image ? true : false;
|
||||
} else {
|
||||
hasChanges = hasChanges || (previousServiceValues.indexOf(key) >= 0);
|
||||
}
|
||||
});
|
||||
return hasChanges;
|
||||
};
|
||||
|
@ -288,7 +290,14 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels);
|
||||
config.TaskTemplate.ContainerSpec.Env = ServiceHelper.translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
|
||||
config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceContainerLabels);
|
||||
config.TaskTemplate.ContainerSpec.Image = service.Image;
|
||||
|
||||
if ($scope.hasChanges(service, ["Image"])) {
|
||||
const image = ImageHelper.createImageConfigForContainer($scope.formValues.RegistryModel);
|
||||
config.TaskTemplate.ContainerSpec.Image = image.fromImage;
|
||||
} else {
|
||||
config.TaskTemplate.ContainerSpec.Image = service.Image;
|
||||
}
|
||||
|
||||
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
|
||||
config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : [];
|
||||
config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : [];
|
||||
|
@ -468,7 +477,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
|||
function forceUpdateService(service, pullImage) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
if (pullImage) {
|
||||
config.TaskTemplate.ContainerSpec.Image = config.TaskTemplate.ContainerSpec.Image = ImageHelper.removeDigestFromRepository(config.TaskTemplate.ContainerSpec.Image);
|
||||
config.TaskTemplate.ContainerSpec.Image = ImageHelper.removeDigestFromRepository(config.TaskTemplate.ContainerSpec.Image);
|
||||
}
|
||||
|
||||
// As explained in https://github.com/docker/swarmkit/issues/2364 ForceUpdate can accept a random
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue