mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 13:55:21 +02:00
feat(docker): show docker pull rate limits (#4666)
* feat(dockerhub): introduce local status endpoint * feat(proxy): rewrite request with dockerhub credentials * feat(endpoint): check env type * feat(endpoint): check for local endpoint * feat(docker): introduce client side service to get limits * feat(container): add info about rate limits in container * feat(dockerhub): load rate limits just for specific endpoints * feat(images): show specific dockerhub messages for admin * feat(service-create): show docker rate limits * feat(service-edit): show rate limit messages * fix(images): fix loading of page * refactor(images): move rate limits check to container * feat(kubernetes): proxy agent requests * feat(kubernetes/apps): show pull limits in application creation * refactor(image-registry): move warning to end of field * fix(image-registry): show right message for admin * fix(images): silently fail when loading rate limits * fix(kube/apps): use new rate limits comp * fix(images): move rate warning to end * fix(registry): move search to right place * fix(service): remove service warning * fix(endpoints): check if kube endpoint is local
This commit is contained in:
parent
d1a21ef6c1
commit
f5aa6c4dc2
29 changed files with 605 additions and 139 deletions
13
app/agent/rest/dockerhub.js
Normal file
13
app/agent/rest/dockerhub.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub);
|
||||
|
||||
function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) {
|
||||
return $resource(
|
||||
`${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub`,
|
||||
{},
|
||||
{
|
||||
limits: { method: 'GET' },
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
export default class porImageRegistryContainerController {
|
||||
/* @ngInject */
|
||||
constructor(EndpointHelper, DockerHubService, Notifications) {
|
||||
this.EndpointHelper = EndpointHelper;
|
||||
this.DockerHubService = DockerHubService;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.pullRateLimits = null;
|
||||
}
|
||||
|
||||
$onChanges({ isDockerHubRegistry }) {
|
||||
if (isDockerHubRegistry && isDockerHubRegistry.currentValue) {
|
||||
this.fetchRateLimits();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
this.setValidity(true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<div class="form-group" ng-if="$ctrl.isDockerHubRegistry && $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>
|
||||
<span ng-if="$ctrl.isAuthenticated">
|
||||
You are currently using a free account to pull images from DockerHub and will be limited to 200 pulls every 6 hours. Remaining pulls:
|
||||
<span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
|
||||
</span>
|
||||
<span ng-if="!$ctrl.isAuthenticated">
|
||||
<span ng-if="$ctrl.isAdmin">
|
||||
You are currently using an anonymous account to pull images from DockerHub and will be limited to 100 pulls every 6 hours. You can configure DockerHub authentication in
|
||||
the
|
||||
<a ui-sref="portainer.registries">Registries View</a>. Remaining pulls:
|
||||
<span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
|
||||
</span>
|
||||
<span ng-if="!$ctrl.isAdmin">
|
||||
You are currently using an anonymous account to pull images from DockerHub and will be limited to 100 pulls every 6 hours. Contact your administrator to configure
|
||||
DockerHub authentication. Remaining pulls: <span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="$ctrl.pullRateLimits.remaining <= 0" class="text-warning">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span ng-if="$ctrl.isAuthenticated">
|
||||
Your authorized pull count quota as a free user is now exceeded.
|
||||
<span ng-transclude="rateLimitExceeded">You will not be able to pull any image from the DockerHub registry.</span>
|
||||
</span>
|
||||
<span ng-if="!$ctrl.isAuthenticated">
|
||||
Your authorized pull count quota as an anonymous user is now exceeded.
|
||||
<span ng-transclude="rateLimitExceeded">You will not be able to pull any image from the DockerHub registry.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import controller from './por-image-registry-rate-limits.controller';
|
||||
|
||||
angular.module('portainer.docker').component('porImageRegistryRateLimits', {
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
setValidity: '<',
|
||||
isAdmin: '<',
|
||||
isDockerHubRegistry: '<',
|
||||
isAuthenticated: '<',
|
||||
},
|
||||
controller,
|
||||
transclude: {
|
||||
rateLimitExceeded: '?porImageRegistryRateLimitExceeded',
|
||||
},
|
||||
templateUrl: './por-image-registry-rate-limits.html',
|
||||
});
|
|
@ -48,7 +48,11 @@ class porImageRegistryController {
|
|||
this.availableImages = images;
|
||||
}
|
||||
|
||||
onRegistryChange() {
|
||||
isDockerHubRegistry() {
|
||||
return this.model.UseRegistry && this.model.Registry.Name === 'DockerHub';
|
||||
}
|
||||
|
||||
async onRegistryChange() {
|
||||
this.prepareAutocomplete();
|
||||
if (this.model.Registry.Type === RegistryTypes.GITLAB && this.model.Image) {
|
||||
this.model.Image = _.replace(this.model.Image, this.model.Registry.Gitlab.ProjectPath, '');
|
97
app/docker/components/imageRegistry/por-image-registry.html
Normal file
97
app/docker/components/imageRegistry/por-image-registry.html
Normal file
|
@ -0,0 +1,97 @@
|
|||
<!-- use registry -->
|
||||
<div ng-if="$ctrl.model.UseRegistry">
|
||||
<div class="form-group">
|
||||
<label for="image_registry" class="control-label text-left" ng-class="$ctrl.labelClass">
|
||||
Registry
|
||||
</label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<select
|
||||
ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
|
||||
ng-model="$ctrl.model.Registry"
|
||||
id="image_registry"
|
||||
selected-item-id="ctrl.selectedItemId"
|
||||
class="form-control"
|
||||
></select>
|
||||
</div>
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="margin-sm-top control-label text-left">Image</label>
|
||||
<div ng-class="$ctrl.inputClass" class="margin-sm-top">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" id="registry-name">{{ $ctrl.displayedRegistryURL() }}</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
aria-describedby="registry-name"
|
||||
uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
|
||||
ng-model="$ctrl.model.Image"
|
||||
name="image_name"
|
||||
placeholder="e.g. myImage:myTag"
|
||||
ng-change="$ctrl.onImageChange()"
|
||||
required
|
||||
/>
|
||||
<span ng-if="$ctrl.isDockerHubRegistry()" class="input-group-btn">
|
||||
<a
|
||||
href="https://hub.docker.com/search?type=image&q={{ $ctrl.model.Image | trimshasum | trimversiontag }}"
|
||||
class="btn btn-default"
|
||||
title="Search image on Docker Hub"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fab fa-docker"></i> Search
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! use registry -->
|
||||
<!-- don't use registry -->
|
||||
<div ng-if="!$ctrl.model.UseRegistry">
|
||||
<div class="form-group">
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-left: 15px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
When using advanced mode, image and repository <b>must be</b> publicly available.
|
||||
</p>
|
||||
</span>
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left">Image </label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! don't use registry -->
|
||||
<!-- info message -->
|
||||
<div class="form-group" ng-show="$ctrl.form.image_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="$ctrl.form.image_name.$error">
|
||||
<p ng-message="required">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Image name is required.
|
||||
<span ng-if="$ctrl.canPull">Tag must be specified otherwise Portainer will pull all tags associated to the image.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! info message -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<p>
|
||||
<a class="small interactive" ng-if="!$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = true;">
|
||||
<i class="fa fa-database space-right" aria-hidden="true"></i> Simple mode
|
||||
</a>
|
||||
<a class="small interactive" ng-if="$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = false;">
|
||||
<i class="fa fa-globe space-right" aria-hidden="true"></i> Advanced mode
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-transclude></div>
|
||||
|
||||
<por-image-registry-rate-limits
|
||||
ng-show="$ctrl.checkRateLimits"
|
||||
is-docker-hub-registry="$ctrl.isDockerHubRegistry()"
|
||||
endpoint="$ctrl.endpoint"
|
||||
set-validity="$ctrl.setValidity"
|
||||
is-authenticated="$ctrl.model.Registry.Authentication"
|
||||
is-admin="$ctrl.isAdmin"
|
||||
>
|
||||
</por-image-registry-rate-limits>
|
||||
</div>
|
|
@ -1,5 +1,5 @@
|
|||
angular.module('portainer.docker').component('porImageRegistry', {
|
||||
templateUrl: './porImageRegistry.html',
|
||||
templateUrl: './por-image-registry.html',
|
||||
controller: 'porImageRegistryController',
|
||||
bindings: {
|
||||
model: '=', // must be of type PorImageRegistryModel
|
||||
|
@ -7,9 +7,14 @@ angular.module('portainer.docker').component('porImageRegistry', {
|
|||
autoComplete: '<',
|
||||
labelClass: '@',
|
||||
inputClass: '@',
|
||||
endpoint: '<',
|
||||
isAdmin: '<',
|
||||
checkRateLimits: '<',
|
||||
onImageChange: '&',
|
||||
setValidity: '<',
|
||||
},
|
||||
require: {
|
||||
form: '^form',
|
||||
},
|
||||
transclude: true,
|
||||
});
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
<!-- use registry -->
|
||||
<div ng-if="$ctrl.model.UseRegistry">
|
||||
<div class="form-group">
|
||||
<label for="image_registry" class="control-label text-left" ng-class="$ctrl.labelClass">
|
||||
Registry
|
||||
</label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<select
|
||||
ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
|
||||
ng-model="$ctrl.model.Registry"
|
||||
id="image_registry"
|
||||
selected-item-id="ctrl.selectedItemId"
|
||||
class="form-control"
|
||||
></select>
|
||||
</div>
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="margin-sm-top control-label text-left">Image</label>
|
||||
<div ng-class="$ctrl.inputClass" class="margin-sm-top">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" id="registry-name">{{ $ctrl.displayedRegistryURL() }}</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
aria-describedby="registry-name"
|
||||
uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
|
||||
ng-model="$ctrl.model.Image"
|
||||
name="image_name"
|
||||
placeholder="e.g. myImage:myTag"
|
||||
ng-change="$ctrl.onImageChange()"
|
||||
required
|
||||
/>
|
||||
<span ng-if="$ctrl.displayedRegistryURL() === 'docker.io'" class="input-group-btn">
|
||||
<a href="https://hub.docker.com/search?type=image&q={{$ctrl.model.Image | trimshasum | trimversiontag}}"
|
||||
class="btn btn-default"
|
||||
title="Search image on Docker Hub"
|
||||
target="_blank">
|
||||
<i class="fab fa-docker"></i> Search
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! use registry -->
|
||||
<!-- don't use registry -->
|
||||
<div ng-if="!$ctrl.model.UseRegistry">
|
||||
<div class="form-group">
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-left: 15px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
When using advanced mode, image and repository <b>must be</b> publicly available.
|
||||
</p>
|
||||
</span>
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left">Image </label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! don't use registry -->
|
||||
<!-- info message -->
|
||||
<div class="form-group" ng-show="$ctrl.form.image_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="$ctrl.form.image_name.$error">
|
||||
<p ng-message="required"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Image name is required.
|
||||
<span ng-if="$ctrl.canPull">Tag must be specified otherwise Portainer will pull all tags associated to the image.</span></p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! info message -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<p>
|
||||
<a class="small interactive" ng-if="!$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = true;">
|
||||
<i class="fa fa-database space-right" aria-hidden="true"></i> Simple mode
|
||||
</a>
|
||||
<a class="small interactive" ng-if="$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = false;">
|
||||
<i class="fa fa-globe space-right" aria-hidden="true"></i> Advanced mode
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -58,6 +58,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
endpoint
|
||||
) {
|
||||
$scope.create = create;
|
||||
$scope.endpoint = endpoint;
|
||||
|
||||
$scope.formValues = {
|
||||
alwaysPull: true,
|
||||
|
@ -90,6 +91,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
mode: '',
|
||||
pullImageValidity: true,
|
||||
};
|
||||
|
||||
$scope.refreshSlider = function () {
|
||||
|
@ -103,6 +105,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||
$scope.formValues.EntrypointMode = 'default';
|
||||
};
|
||||
|
||||
$scope.setPullImageValidity = setPullImageValidity;
|
||||
function setPullImageValidity(validity) {
|
||||
if (!validity) {
|
||||
$scope.formValues.alwaysPull = false;
|
||||
}
|
||||
$scope.state.pullImageValidity = validity;
|
||||
}
|
||||
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Env: [],
|
||||
|
|
|
@ -31,10 +31,10 @@
|
|||
</div>
|
||||
<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
|
||||
>
|
||||
<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.RegistryModel.Registry || !fromContainer">
|
||||
<!-- image-and-registry -->
|
||||
|
@ -45,23 +45,30 @@
|
|||
auto-complete="true"
|
||||
label-class="col-sm-1"
|
||||
input-class="col-sm-11"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
check-rate-limits="formValues.alwaysPull"
|
||||
on-image-change="onImageNameChange()"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- always-pull -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="ownership" class="control-label text-left">
|
||||
Always pull the image
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="When enabled, Portainer will automatically try to pull the specified image before creating the container."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.alwaysPull" /><i></i> </label>
|
||||
set-validity="setPullImageValidity"
|
||||
>
|
||||
<!-- always-pull -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="ownership" class="control-label text-left">
|
||||
Always pull the image
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="When enabled, Portainer will automatically try to pull the specified image before creating the container."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.alwaysPull" ng-disabled="!state.pullImageValidity" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !always-pull -->
|
||||
<!-- !always-pull -->
|
||||
</por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Network ports configuration
|
||||
|
|
|
@ -14,30 +14,41 @@
|
|||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry model="formValues.RegistryModel" auto-complete="true" pull-warning="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
|
||||
<por-image-registry
|
||||
model="formValues.RegistryModel"
|
||||
auto-complete="true"
|
||||
pull-warning="true"
|
||||
label-class="col-sm-1"
|
||||
input-class="col-sm-11"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
set-validity="setPullImageValidity"
|
||||
check-rate-limits="true"
|
||||
>
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<node-selector model="formValues.NodeName"> </node-selector>
|
||||
<!-- !node-selection -->
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || !state.pullRateValid"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<div ng-if="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<node-selector model="formValues.NodeName"> </node-selector>
|
||||
<!-- !node-selection -->
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
|||
angular.module('portainer.docker').controller('ImagesController', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'Authentication',
|
||||
'ImageService',
|
||||
'Notifications',
|
||||
'ModalService',
|
||||
|
@ -11,10 +12,15 @@ angular.module('portainer.docker').controller('ImagesController', [
|
|||
'FileSaver',
|
||||
'Blob',
|
||||
'EndpointProvider',
|
||||
function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider) {
|
||||
'endpoint',
|
||||
function ($scope, $state, Authentication, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider, endpoint) {
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
exportInProgress: false,
|
||||
pullRateValid: false,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
|
@ -140,6 +146,11 @@ angular.module('portainer.docker').controller('ImagesController', [
|
|||
});
|
||||
}
|
||||
|
||||
$scope.setPullImageValidity = setPullImageValidity;
|
||||
function setPullImageValidity(validity) {
|
||||
$scope.state.pullRateValid = validity;
|
||||
}
|
||||
|
||||
function initView() {
|
||||
getImages();
|
||||
}
|
||||
|
|
|
@ -104,6 +104,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||
$scope.state = {
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
pullImageValidity: false,
|
||||
};
|
||||
|
||||
$scope.allowBindMounts = false;
|
||||
|
@ -114,6 +115,11 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||
});
|
||||
};
|
||||
|
||||
$scope.setPullImageValidity = setPullImageValidity;
|
||||
function setPullImageValidity(validity) {
|
||||
$scope.state.pullImageValidity = validity;
|
||||
}
|
||||
|
||||
$scope.addPortBinding = function () {
|
||||
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
|
||||
};
|
||||
|
|
|
@ -20,7 +20,17 @@
|
|||
Image configuration
|
||||
</div>
|
||||
<!-- image-and-registry -->
|
||||
<por-image-registry model="formValues.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
|
||||
<por-image-registry
|
||||
model="formValues.RegistryModel"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1"
|
||||
input-class="col-sm-11"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
check-rate-limits="true"
|
||||
set-validity="setPullImageValidity"
|
||||
>
|
||||
</por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Scheduling
|
||||
|
|
|
@ -3,7 +3,17 @@
|
|||
<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>
|
||||
<por-image-registry
|
||||
model="formValues.RegistryModel"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1"
|
||||
input-class="col-sm-11"
|
||||
endpoint="endpoint"
|
||||
is-admin="isAdmin"
|
||||
check-rate-limits="true"
|
||||
set-validity="setPullImageValidity"
|
||||
>
|
||||
</por-image-registry>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="isUpdating">
|
||||
|
|
|
@ -85,6 +85,8 @@ angular.module('portainer.docker').controller('ServiceController', [
|
|||
NetworkService,
|
||||
endpoint
|
||||
) {
|
||||
$scope.endpoint = endpoint;
|
||||
|
||||
$scope.state = {
|
||||
updateInProgress: false,
|
||||
deletionInProgress: false,
|
||||
|
@ -532,6 +534,11 @@ angular.module('portainer.docker').controller('ServiceController', [
|
|||
});
|
||||
};
|
||||
|
||||
$scope.setPullImageValidity = setPullImageValidity;
|
||||
function setPullImageValidity(validity) {
|
||||
$scope.state.pullImageValidity = validity;
|
||||
}
|
||||
|
||||
$scope.updateService = function updateService(service) {
|
||||
let config = {};
|
||||
service, (config = buildChanges(service));
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
<!-- #endregion -->
|
||||
|
||||
<!-- #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">
|
||||
|
@ -72,6 +73,14 @@
|
|||
</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">
|
||||
|
@ -1516,7 +1525,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled()"
|
||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity"
|
||||
ng-click="ctrl.deployApplication()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
|
|
|
@ -4,5 +4,6 @@ angular.module('portainer.kubernetes').component('kubernetesCreateApplicationVie
|
|||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -40,6 +40,7 @@ class KubernetesCreateApplicationController {
|
|||
Notifications,
|
||||
EndpointProvider,
|
||||
Authentication,
|
||||
DockerHubService,
|
||||
ModalService,
|
||||
KubernetesResourcePoolService,
|
||||
KubernetesApplicationService,
|
||||
|
@ -56,6 +57,7 @@ class KubernetesCreateApplicationController {
|
|||
this.Notifications = Notifications;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.Authentication = Authentication;
|
||||
this.DockerHubService = DockerHubService;
|
||||
this.ModalService = ModalService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
|
@ -77,9 +79,14 @@ class KubernetesCreateApplicationController {
|
|||
|
||||
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
|
||||
this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
|
||||
this.setPullImageValidity = this.setPullImageValidity.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
setPullImageValidity(validity) {
|
||||
this.state.pullImageValidity = validity;
|
||||
}
|
||||
|
||||
onChangeName() {
|
||||
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
|
||||
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
|
||||
|
@ -915,6 +922,7 @@ class KubernetesCreateApplicationController {
|
|||
name: this.$transition$.params().name,
|
||||
},
|
||||
persistedFoldersUseExistingVolumes: false,
|
||||
pullImageValidity: false,
|
||||
};
|
||||
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
|
@ -984,6 +992,9 @@ class KubernetesCreateApplicationController {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -11,6 +11,16 @@ angular.module('portainer.app').factory('EndpointHelper', [
|
|||
});
|
||||
}
|
||||
|
||||
helper.isLocalEndpoint = isLocalEndpoint;
|
||||
function isLocalEndpoint(endpoint) {
|
||||
return endpoint.URL.includes('unix://') || endpoint.URL.includes('npipe://') || endpoint.Type === 5;
|
||||
}
|
||||
|
||||
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];
|
||||
|
|
|
@ -22,6 +22,7 @@ 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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
|
|
|
@ -3,7 +3,10 @@ import { DockerHubViewModel } from '../../models/dockerhub';
|
|||
angular.module('portainer.app').factory('DockerHubService', [
|
||||
'$q',
|
||||
'DockerHub',
|
||||
function DockerHubServiceFactory($q, DockerHub) {
|
||||
'Endpoints',
|
||||
'AgentDockerhub',
|
||||
'EndpointHelper',
|
||||
function DockerHubServiceFactory($q, DockerHub, Endpoints, AgentDockerhub, EndpointHelper) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
|
@ -26,6 +29,23 @@ angular.module('portainer.app').factory('DockerHubService', [
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue