mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +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
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue