feat(registries): remove registry extension (#4155)

* feat(registries): remove client extension code

* feat(registry): remove server registry code

* refactor(registry): remove extension related code

* feat(extensions): remove registry extension type
pull/4163/head
Chaim Lev-Ari 2020-08-05 13:23:19 +03:00 committed by GitHub
parent 7e90bf11b7
commit 82064152ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 13 additions and 2377 deletions

View File

@ -26,8 +26,7 @@ var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/"
var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`)
var extensionBinaryMap = map[portainer.ExtensionID]string{
portainer.RegistryManagementExtension: "extension-registry-management",
portainer.RBACExtension: "extension-rbac",
portainer.RBACExtension: "extension-rbac",
}
// ExtensionManager represents a service used to
@ -111,8 +110,6 @@ func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension
}
switch extension.ID {
case portainer.RegistryManagementExtension:
extension.Name = "Registry Manager"
case portainer.RBACExtension:
extension.Name = "Role-Based Access Control"
}

View File

@ -43,10 +43,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AdminAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost)
h.Handle("/registries/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
h.PathPrefix("/registries/{id}/v2").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
h.PathPrefix("/registries/{id}/proxies/gitlab").Handler(
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithRegistry)))
h.PathPrefix("/registries/proxies/gitlab").Handler(
bouncer.AdminAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithoutRegistry)))
return h

View File

@ -1,85 +0,0 @@
package registries
import (
"encoding/json"
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/errors"
)
// request on /api/registries/:id/v2
func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
}
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
err = handler.requestBouncer.RegistryAccess(r, registry)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied}
}
extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
}
}
managementConfiguration := registry.ManagementConfiguration
if managementConfiguration == nil {
managementConfiguration = createDefaultManagementConfiguration(registry)
}
encodedConfiguration, err := json.Marshal(managementConfiguration)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err}
}
id := strconv.Itoa(int(registryID))
r.Header.Set("X-RegistryManagement-Key", id)
r.Header.Set("X-RegistryManagement-URI", registry.URL)
r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration))
r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey)
http.StripPrefix("/registries/"+id, proxy).ServeHTTP(w, r)
return nil
}
func createDefaultManagementConfiguration(registry *portainer.Registry) *portainer.RegistryManagementConfiguration {
config := &portainer.RegistryManagementConfiguration{
Type: registry.Type,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
}
if registry.Authentication {
config.Authentication = true
config.Username = registry.Username
config.Password = registry.Password
}
return config
}

View File

@ -1,68 +0,0 @@
package registries
import (
"encoding/json"
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/errors"
)
// request on /api/registries/{id}/proxies/gitlab
func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
}
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
err = handler.requestBouncer.RegistryAccess(r, registry)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied}
}
extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
}
}
config := &portainer.RegistryManagementConfiguration{
Type: portainer.GitlabRegistry,
Password: registry.Password,
}
encodedConfiguration, err := json.Marshal(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err}
}
id := strconv.Itoa(int(registryID))
r.Header.Set("X-RegistryManagement-Key", id+"-gitlab")
r.Header.Set("X-RegistryManagement-URI", registry.Gitlab.InstanceURL)
r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration))
r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey)
http.StripPrefix("/registries/"+id+"/proxies/gitlab", proxy).ServeHTTP(w, r)
return nil
}

View File

@ -17,8 +17,7 @@ import (
const azureAPIBaseURL = "https://management.azure.com"
var extensionPorts = map[portainer.ExtensionID]string{
portainer.RegistryManagementExtension: "7001",
portainer.RBACExtension: "7003",
portainer.RBACExtension: "7003",
}
type (

View File

@ -1255,7 +1255,7 @@ const (
const (
_ ExtensionID = iota
// RegistryManagementExtension represents the registry management extension
// RegistryManagementExtension represents the registry management extension (removed)
RegistryManagementExtension
// OAuthAuthenticationExtension represents the OAuth authentication extension (Deprecated)
OAuthAuthenticationExtension

View File

@ -1,7 +1,7 @@
import angular from 'angular';
import _ from 'lodash-es';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
import { RegistryTypes } from '@/portainer/models/registryTypes';
class porImageRegistryController {
/* @ngInject */

View File

@ -1,5 +1,5 @@
import _ from 'lodash-es';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
import { RegistryTypes } from '@/portainer/models/registryTypes';
angular.module('portainer.docker').factory('ImageHelper', [
function ImageHelperFactory() {

View File

@ -1 +1 @@
angular.module('portainer.extensions', ['portainer.extensions.registrymanagement', 'portainer.extensions.rbac']);
angular.module('portainer.extensions', ['portainer.extensions.rbac']);

View File

@ -1,55 +0,0 @@
angular.module('portainer.extensions.registrymanagement', []).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
var registryConfiguration = {
name: 'portainer.registries.registry.configure',
url: '/configure',
views: {
'content@': {
templateUrl: './views/configure/configureregistry.html',
controller: 'ConfigureRegistryController',
},
},
};
var registryRepositories = {
name: 'portainer.registries.registry.repositories',
url: '/repositories',
views: {
'content@': {
templateUrl: './views/repositories/registryRepositories.html',
controller: 'RegistryRepositoriesController',
},
},
};
var registryRepositoryTags = {
name: 'portainer.registries.registry.repository',
url: '/:repository',
views: {
'content@': {
templateUrl: './views/repositories/edit/registryRepository.html',
controller: 'RegistryRepositoryController',
},
},
};
var registryRepositoryTag = {
name: 'portainer.registries.registry.repository.tag',
url: '/:tag',
views: {
'content@': {
templateUrl: './views/repositories/tag/registryRepositoryTag.html',
controller: 'RegistryRepositoryTagController',
controllerAs: 'ctrl',
},
},
};
$stateRegistryProvider.register(registryConfiguration);
$stateRegistryProvider.register(registryRepositories);
$stateRegistryProvider.register(registryRepositoryTags);
$stateRegistryProvider.register(registryRepositoryTag);
},
]);

View File

@ -1,76 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
/>
</div>
<div class="table-responsive">
<table class="table table-hover table-filters nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Repository
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
Tags count
</th>
</tr>
</thead>
<tbody>
<tr
ng-hide="$ctrl.loading"
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>
<a ui-sref="portainer.registries.registry.repository({repository: item.Name})" title="{{ item.Name }}">{{ item.Name }}</a>
</td>
<td>{{ item.TagsCount }}</td>
</tr>
<tr ng-if="!$ctrl.dataset || $ctrl.loading">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No repository available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -1,14 +0,0 @@
angular.module('portainer.extensions.registrymanagement').component('registryRepositoriesDatatable', {
templateUrl: './registryRepositoriesDatatable.html',
controller: 'RegistryRepositoriesDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
paginationAction: '<',
loading: '<',
},
});

View File

@ -1,41 +0,0 @@
import _ from 'lodash-es';
angular.module('portainer.app').controller('RegistryRepositoriesDatatableController', [
'$scope',
'$controller',
function ($scope, $controller) {
var ctrl = this;
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state.orderBy = this.orderBy;
function areDifferent(a, b) {
if (!a || !b) {
return true;
}
var namesA = a
.map(function (x) {
return x.Name;
})
.sort();
var namesB = b
.map(function (x) {
return x.Name;
})
.sort();
return namesA.join(',') !== namesB.join(',');
}
$scope.$watch(
function () {
return ctrl.state.filteredDataSet;
},
function (newValue, oldValue) {
if (newValue && areDifferent(oldValue, newValue)) {
ctrl.paginationAction(_.filter(newValue, { TagsCount: 0 }));
}
},
true
);
},
]);

View File

@ -1,108 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="actionBar" ng-if="$ctrl.advancedFeaturesAvailable">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>Os/Architecture</th>
<th>Image ID</th>
<th>Compressed size</th>
<th ng-if="$ctrl.advancedFeaturesAvailable">Actions</th>
</tr>
</thead>
<tbody>
<tr
ng-hide="$ctrl.loading"
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.registries.registry.repository.tag({tag: item.Name})" title="{{ item.Name }}">{{ item.Name }}</a>
</td>
<td>{{ item.Os }}/{{ item.Architecture }}</td>
<td>{{ item.ImageId | trimshasum }}</td>
<td>{{ item.Size | humansize }}</td>
<td ng-if="$ctrl.advancedFeaturesAvailable">
<span ng-if="!item.Modified">
<a class="interactive" ng-click="item.Modified = true; item.NewName = item.Name; $event.stopPropagation();">
<i class="fa fa-tag" aria-hidden="true"></i> Retag
</a>
</span>
<span ng-if="item.Modified">
<portainer-tooltip
position="bottom"
message="Tag can only contain alphanumeric (a-zA-Z0-9) and special _ . - characters. Tag must not start with . - characters."
></portainer-tooltip>
<input class="input-sm" type="text" ng-model="item.NewName" on-enter-key="$ctrl.retagAction(item)" auto-focus ng-click="$event.stopPropagation();" />
<a class="interactive" ng-click="item.Modified = false; $event.stopPropagation();"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="$ctrl.retagAction(item); $event.stopPropagation();"><i class="fa fa-check-square"></i></a>
</span>
</td>
</tr>
<tr ng-if="$ctrl.loading">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="!$ctrl.loading && $ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No tag available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -1,17 +0,0 @@
angular.module('portainer.extensions.registrymanagement').component('registriesRepositoryTagsDatatable', {
templateUrl: './registriesRepositoryTagsDatatable.html',
controller: 'RegistryRepositoriesTagsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
removeAction: '<',
retagAction: '<',
advancedFeaturesAvailable: '<',
paginationAction: '<',
loading: '<',
},
});

View File

@ -1,37 +0,0 @@
import _ from 'lodash-es';
angular.module('portainer.app').controller('RegistryRepositoriesTagsDatatableController', [
'$scope',
'$controller',
function ($scope, $controller) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
var ctrl = this;
this.state.orderBy = this.orderBy;
function diff(item) {
return item.Name + item.ImageDigest;
}
function areDifferent(a, b) {
if (!a || !b) {
return true;
}
var namesA = _.sortBy(_.map(a, diff));
var namesB = _.sortBy(_.map(b, diff));
return namesA.join(',') !== namesB.join(',');
}
$scope.$watch(
function () {
return ctrl.state.filteredDataSet;
},
function (newValue, oldValue) {
if (newValue && newValue.length && areDifferent(oldValue, newValue)) {
ctrl.paginationAction(_.filter(newValue, { ImageId: '' }));
ctrl.resetSelectionState();
}
},
true
);
},
]);

View File

@ -1,35 +0,0 @@
import _ from 'lodash-es';
import { RepositoryTagViewModel } from '../models/repositoryTag';
angular.module('portainer.extensions.registrymanagement').factory('RegistryV2Helper', [
function RegistryV2HelperFactory() {
'use strict';
var helper = {};
function historyRawToParsed(rawHistory) {
return _.map(rawHistory, (item) => angular.fromJson(item.v1Compatibility));
}
helper.manifestsToTag = function (manifests) {
var v1 = manifests.v1;
var v2 = manifests.v2;
var history = historyRawToParsed(v1.history);
var name = v1.tag;
var os = history[0].os;
var arch = v1.architecture;
var size = v2.layers.reduce(function (a, b) {
return {
size: a.size + b.size,
};
}).size;
var imageId = v2.config.digest;
var imageDigest = v2.digest;
return new RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2, history);
};
return helper;
},
]);

View File

@ -1,14 +0,0 @@
export function RegistryImageDetailsViewModel(data) {
this.Id = data.id;
this.Parent = data.parent;
this.Created = data.created;
this.DockerVersion = data.docker_version;
this.Os = data.os;
this.Architecture = data.architecture;
this.Author = data.author;
this.Command = data.config.Cmd;
this.Entrypoint = data.container_config.Entrypoint ? data.container_config.Entrypoint : '';
this.ExposedPorts = data.container_config.ExposedPorts ? Object.keys(data.container_config.ExposedPorts) : [];
this.Volumes = data.container_config.Volumes ? Object.keys(data.container_config.Volumes) : [];
this.Env = data.container_config.Env ? data.container_config.Env : [];
}

View File

@ -1,8 +0,0 @@
import _ from 'lodash-es';
export function RegistryImageLayerViewModel(order, data) {
this.Order = order;
this.Id = data.id;
this.Created = data.created;
this.CreatedBy = _.join(data.container_config.Cmd, ' ');
}

View File

@ -1,22 +0,0 @@
export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2, history) {
this.Name = name;
this.Os = os || '';
this.Architecture = arch || '';
this.Size = size || 0;
this.ImageDigest = imageDigest || '';
this.ImageId = imageId || '';
this.ManifestV2 = v2 || {};
this.History = history || [];
}
export function RepositoryShortTag(name, imageId, imageDigest, manifest) {
this.Name = name;
this.ImageId = imageId;
this.ImageDigest = imageDigest;
this.ManifestV2 = manifest;
}
export function RepositoryAddTagPayload(tag, manifest) {
this.Tag = tag;
this.Manifest = manifest;
}

View File

@ -1,34 +0,0 @@
import linkGetResponse from './transform/linkGetResponse';
angular.module('portainer.extensions.registrymanagement').factory('RegistryCatalog', [
'$resource',
'API_ENDPOINT_REGISTRIES',
function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return $resource(
API_ENDPOINT_REGISTRIES + '/:id/v2/:action',
{},
{
get: {
method: 'GET',
params: { id: '@id', action: '_catalog' },
transformResponse: linkGetResponse,
},
ping: {
method: 'GET',
params: { id: '@id' },
timeout: 3500,
},
pingWithForceNew: {
method: 'GET',
params: { id: '@id' },
timeout: 3500,
headers: { 'X-RegistryManagement-ForceNew': '1' },
},
},
{
stripTrailingSlashes: false,
}
);
},
]);

View File

@ -1,90 +0,0 @@
/**
* This service has been created to request the docker registry API
* without triggering AngularJS digest cycles
* For more information, see https://github.com/portainer/portainer/pull/2648#issuecomment-505644913
*/
import $ from 'jquery';
angular.module('portainer.extensions.registrymanagement').factory('RegistryManifestsJquery', [
'API_ENDPOINT_REGISTRIES',
function RegistryManifestsJqueryFactory(API_ENDPOINT_REGISTRIES) {
'use strict';
function buildUrl(params) {
return API_ENDPOINT_REGISTRIES + '/' + params.id + '/v2/' + params.repository + '/manifests/' + params.tag;
}
function _get(params) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'GET',
dataType: 'JSON',
url: buildUrl(params),
headers: {
'Cache-Control': 'no-cache',
'If-Modified-Since': 'Mon, 26 Jul 1997 05:00:00 GMT',
},
success: (result) => resolve(result),
error: (error) => reject(error),
});
});
}
function _getV2(params) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'GET',
dataType: 'JSON',
url: buildUrl(params),
headers: {
Accept: 'application/vnd.docker.distribution.manifest.v2+json',
'Cache-Control': 'no-cache',
'If-Modified-Since': 'Mon, 26 Jul 1997 05:00:00 GMT',
},
success: (result, status, request) => {
result.digest = request.getResponseHeader('Docker-Content-Digest');
resolve(result);
},
error: (error) => reject(error),
});
});
}
function _put(params, data) {
const transformRequest = (d) => {
return angular.toJson(d, 3);
};
return new Promise((resolve, reject) => {
$.ajax({
type: 'PUT',
url: buildUrl(params),
headers: {
'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json',
},
data: transformRequest(data),
success: (result) => resolve(result),
error: (error) => reject(error),
});
});
}
function _delete(params) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'DELETE',
url: buildUrl(params),
success: (result) => resolve(result),
error: (error) => reject(error),
});
});
}
return {
get: _get,
getV2: _getV2,
put: _put,
delete: _delete,
};
},
]);

View File

@ -1,20 +0,0 @@
import linkGetResponse from './transform/linkGetResponse';
angular.module('portainer.extensions.registrymanagement').factory('RegistryTags', [
'$resource',
'API_ENDPOINT_REGISTRIES',
function RegistryTagsFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return $resource(
API_ENDPOINT_REGISTRIES + '/:id/v2/:repository/tags/list',
{},
{
get: {
method: 'GET',
params: { id: '@id', repository: '@repository' },
transformResponse: linkGetResponse,
},
}
);
},
]);

View File

@ -1,13 +0,0 @@
export default function linkGetResponse(data, headers) {
var response = angular.fromJson(data);
var link = headers('link');
if (link) {
var queryString = link.substring(link.indexOf('?') + 1).split('>;')[0];
var queries = queryString.split('&');
for (var i = 0; i < queries.length; i++) {
var kv = queries[i].split('=');
response[kv[0]] = kv[1];
}
}
return response;
}

View File

@ -1,34 +0,0 @@
import _ from 'lodash-es';
function findBestStep(length) {
let step = Math.trunc(length / 10);
if (step < 10) {
step = 10;
} else if (step > 100) {
step = 100;
}
return step;
}
export default async function* genericAsyncGenerator($q, list, func, params) {
const step = findBestStep(list.length);
let start = 0;
let end = start + step;
let results = [];
while (start < list.length) {
const batch = _.slice(list, start, end);
const promises = [];
for (let i = 0; i < batch.length; i++) {
promises.push(func(...params, batch[i]));
}
yield start;
const res = await $q.all(promises);
for (let i = 0; i < res.length; i++) {
results.push(res[i]);
}
start = end;
end += step;
}
yield list.length;
yield results;
}

View File

@ -1,84 +0,0 @@
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
angular.module('portainer.extensions.registrymanagement').factory('RegistryServiceSelector', [
'$q',
'RegistryV2Service',
'RegistryGitlabService',
function RegistryServiceSelector($q, RegistryV2Service, RegistryGitlabService) {
'use strict';
const service = {};
service.ping = ping;
service.repositories = repositories;
service.getRepositoriesDetails = getRepositoriesDetails;
service.tags = tags;
service.getTagsDetails = getTagsDetails;
service.tag = tag;
service.addTag = addTag;
service.deleteManifest = deleteManifest;
service.shortTagsWithProgress = shortTagsWithProgress;
service.deleteTagsWithProgress = deleteTagsWithProgress;
service.retagWithProgress = retagWithProgress;
function ping(registry, forceNewConfig) {
let service = RegistryV2Service;
return service.ping(registry, forceNewConfig);
}
function repositories(registry) {
let service = RegistryV2Service;
if (registry.Type === RegistryTypes.GITLAB) {
service = RegistryGitlabService;
}
return service.repositories(registry);
}
function getRepositoriesDetails(registry, repositories) {
let service = RegistryV2Service;
return service.getRepositoriesDetails(registry, repositories);
}
function tags(registry, repository) {
let service = RegistryV2Service;
return service.tags(registry, repository);
}
function getTagsDetails(registry, repository, tags) {
let service = RegistryV2Service;
return service.getTagsDetails(registry, repository, tags);
}
function tag(registry, repository, tag) {
let service = RegistryV2Service;
return service.tag(registry, repository, tag);
}
function addTag(registry, repository, tag, manifest) {
let service = RegistryV2Service;
return service.addTag(registry, repository, tag, manifest);
}
function deleteManifest(registry, repository, digest) {
let service = RegistryV2Service;
return service.deleteManifest(registry, repository, digest);
}
function shortTagsWithProgress(registry, repository, tagsList) {
let service = RegistryV2Service;
return service.shortTagsWithProgress(registry, repository, tagsList);
}
function deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
let service = RegistryV2Service;
return service.deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags);
}
function retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags) {
let service = RegistryV2Service;
return service.retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags);
}
return service;
},
]);

View File

@ -1,310 +0,0 @@
import _ from 'lodash-es';
import { RepositoryAddTagPayload, RepositoryShortTag } from '../models/repositoryTag';
import { RegistryRepositoryViewModel } from '../models/registryRepository';
import genericAsyncGenerator from './genericAsyncGenerator';
angular.module('portainer.extensions.registrymanagement').factory('RegistryV2Service', [
'$q',
'$async',
'RegistryCatalog',
'RegistryTags',
'RegistryManifestsJquery',
'RegistryV2Helper',
function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, RegistryManifestsJquery, RegistryV2Helper) {
'use strict';
var service = {};
/**
* PING
*/
function ping(registry, forceNewConfig) {
const id = registry.Id;
if (forceNewConfig) {
return RegistryCatalog.pingWithForceNew({ id: id }).$promise;
}
return RegistryCatalog.ping({ id: id }).$promise;
}
/**
* END PING
*/
/**
* REPOSITORIES
*/
function _getCatalogPage(params, deferred, repositories) {
RegistryCatalog.get(params).$promise.then(function (data) {
repositories = _.concat(repositories, data.repositories);
if (data.last && data.n) {
_getCatalogPage({ id: params.id, n: data.n, last: data.last }, deferred, repositories);
} else {
deferred.resolve(repositories);
}
});
}
function _getCatalog(id) {
var deferred = $q.defer();
var repositories = [];
_getCatalogPage({ id: id }, deferred, repositories);
return deferred.promise;
}
function repositories(registry) {
const deferred = $q.defer();
const id = registry.Id;
_getCatalog(id)
.then(function success(data) {
const repositories = _.map(data, (repositoryName) => new RegistryRepositoryViewModel(repositoryName));
deferred.resolve(repositories);
})
.catch(function error(err) {
deferred.reject({
msg: 'Unable to retrieve repositories',
err: err,
});
});
return deferred.promise;
}
function getRepositoriesDetails(registry, repositories) {
const deferred = $q.defer();
const promises = _.map(repositories, (repository) => tags(registry, repository.Name));
$q.all(promises)
.then(function success(data) {
var repositories = data.map(function (item) {
return new RegistryRepositoryViewModel(item);
});
repositories = _.without(repositories, undefined);
deferred.resolve(repositories);
})
.catch(function error(err) {
deferred.reject({
msg: 'Unable to retrieve repositories',
err: err,
});
});
return deferred.promise;
}
/**
* END REPOSITORIES
*/
/**
* TAGS
*/
function _getTagsPage(params, deferred, previousTags) {
RegistryTags.get(params)
.$promise.then(function (data) {
previousTags.name = data.name;
previousTags.tags = _.concat(previousTags.tags, data.tags);
if (data.last && data.n) {
_getTagsPage({ id: params.id, repository: params.repository, n: data.n, last: data.last }, deferred, previousTags);
} else {
deferred.resolve(previousTags);
}
})
.catch(function error(err) {
deferred.reject({
msg: 'Unable to retrieve tags',
err: err,
});
});
}
function tags(registry, repository) {
const deferred = $q.defer();
const id = registry.Id;
_getTagsPage({ id: id, repository: repository }, deferred, { tags: [] });
return deferred.promise;
}
function getTagsDetails(registry, repository, tags) {
const promises = _.map(tags, (t) => tag(registry, repository, t.Name));
return $q.all(promises);
}
function tag(registry, repository, tag) {
const deferred = $q.defer();
const id = registry.Id;
var promises = {
v1: RegistryManifestsJquery.get({
id: id,
repository: repository,
tag: tag,
}),
v2: RegistryManifestsJquery.getV2({
id: id,
repository: repository,
tag: tag,
}),
};
$q.all(promises)
.then(function success(data) {
var tag = RegistryV2Helper.manifestsToTag(data);
deferred.resolve(tag);
})
.catch(function error(err) {
deferred.reject({
msg: 'Unable to retrieve tag ' + tag,
err: err,
});
});
return deferred.promise;
}
/**
* END TAGS
*/
/**
* ADD TAG
*/
// tag: RepositoryAddTagPayload
function _addTagFromGenerator(registry, repository, tag) {
return addTag(registry, repository, tag.Tag, tag.Manifest);
}
function addTag(registry, repository, tag, manifest) {
const id = registry.Id;
delete manifest.digest;
return RegistryManifestsJquery.put(
{
id: id,
repository: repository,
tag: tag,
},
manifest
);
}
async function* _addTagsWithProgress(registry, repository, tagsList, progression = 0) {
for await (const partialResult of genericAsyncGenerator($q, tagsList, _addTagFromGenerator, [registry, repository])) {
if (typeof partialResult === 'number') {
yield progression + partialResult;
} else {
yield partialResult;
}
}
}
/**
* END ADD TAG
*/
/**
* DELETE MANIFEST
*/
function deleteManifest(registry, repository, imageDigest) {
const id = registry.Id;
return RegistryManifestsJquery.delete({
id: id,
repository: repository,
tag: imageDigest,
});
}
async function* _deleteManifestsWithProgress(registry, repository, manifests) {
for await (const partialResult of genericAsyncGenerator($q, manifests, deleteManifest, [registry, repository])) {
yield partialResult;
}
}
/**
* END DELETE MANIFEST
*/
/**
* SHORT TAG
*/
function _shortTagFromGenerator(id, repository, tag) {
return new Promise((resolve, reject) => {
RegistryManifestsJquery.getV2({ id: id, repository: repository, tag: tag })
.then((data) => resolve(new RepositoryShortTag(tag, data.config.digest, data.digest, data)))
.catch((err) => reject(err));
});
}
async function* shortTagsWithProgress(registry, repository, tagsList) {
const id = registry.Id;
yield* genericAsyncGenerator($q, tagsList, _shortTagFromGenerator, [id, repository]);
}
/**
* END SHORT TAG
*/
/**
* RETAG
*/
async function* retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags) {
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
const newTags = _.map(impactedTags, (item) => {
const tagFromTable = _.find(modifiedTags, { Name: item.Name });
const name = tagFromTable && tagFromTable.Name !== tagFromTable.NewName ? tagFromTable.NewName : item.Name;
return new RepositoryAddTagPayload(name, item.ManifestV2);
});
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
}
/**
* END RETAG
*/
/**
* DELETE TAGS
*/
async function* deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
const newTags = _.map(impactedTags, (item) => new RepositoryAddTagPayload(item.Name, item.ManifestV2));
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
}
/**
* END DELETE TAGS
*/
/**
* SERVICE FUNCTIONS DECLARATION
*/
service.ping = ping;
service.repositories = repositories;
service.getRepositoriesDetails = getRepositoriesDetails;
service.tags = tags;
service.tag = tag;
service.getTagsDetails = getTagsDetails;
service.shortTagsWithProgress = shortTagsWithProgress;
service.addTag = addTag;
service.deleteManifest = deleteManifest;
service.deleteTagsWithProgress = deleteTagsWithProgress;
service.retagWithProgress = retagWithProgress;
return service;
},
]);

View File

@ -1,75 +0,0 @@
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
import { RegistryManagementConfigurationDefaultModel } from '../../../../portainer/models/registry';
angular.module('portainer.extensions.registrymanagement').controller('ConfigureRegistryController', [
'$scope',
'$state',
'$transition$',
'RegistryService',
'RegistryServiceSelector',
'Notifications',
function ($scope, $state, $transition$, RegistryService, RegistryServiceSelector, Notifications) {
$scope.state = {
testInProgress: false,
updateInProgress: false,
validConfiguration: false,
};
$scope.testConfiguration = testConfiguration;
$scope.updateConfiguration = updateConfiguration;
function testConfiguration() {
$scope.state.testInProgress = true;
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
.then(function success() {
return RegistryServiceSelector.ping($scope.registry, true);
})
.then(function success() {
Notifications.success('Success', 'Valid management configuration');
$scope.state.validConfiguration = true;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Invalid management configuration');
})
.finally(function final() {
$scope.state.testInProgress = false;
});
}
function updateConfiguration() {
$scope.state.updateInProgress = true;
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
.then(function success() {
Notifications.success('Success', 'Registry management configuration updated');
$state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, { reload: true });
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update registry management configuration');
})
.finally(function final() {
$scope.state.updateInProgress = false;
});
}
function initView() {
var registryId = $transition$.params().id;
$scope.RegistryTypes = RegistryTypes;
RegistryService.registry(registryId)
.then(function success(data) {
var registry = data;
var model = new RegistryManagementConfigurationDefaultModel(registry);
$scope.registry = registry;
$scope.model = model;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
});
}
initView();
},
]);

View File

@ -1,170 +0,0 @@
<rd-header>
<rd-header-title title-text="Configure registry"></rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> &gt; Management configuration
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
The following configuration will be used to access this <a href="https://docs.docker.com/registry/spec/api/" target="_blank">registry API</a> to provide Portainer
management features.
</span>
</div>
<div class="col-sm-12 form-section-title">
Registry details
</div>
<!-- registry-url-input -->
<div class="form-group">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" ng-model="registry.URL" disabled />
</div>
</div>
<!-- !registry-url-input -->
<!-- authentication-checkbox -->
<div class="form-group" ng-if="registry.Type === RegistryTypes.CUSTOM || registry.Type === RegistryTypes.GITLAB">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="model.Authentication" /><i></i> </label>
</div>
</div>
<!-- !authentication-checkbox -->
<!-- authentication-credentials -->
<div ng-if="model.Authentication">
<!-- credentials-user -->
<div class="form-group">
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="credentials_username" ng-model="model.Username" />
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="credentials_password" ng-model="model.Password" placeholder="*******" />
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- !authentication-credentials -->
<!-- tls -->
<div ng-if="registry.Type === RegistryTypes.CUSTOM || registry.Type === RegistryTypes.GITLAB">
<!-- tls-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to connect to the registry API with TLS."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="model.TLS" /><i></i> </label>
</div>
</div>
<!-- !tls-checkbox -->
<!-- tls-skip-verify -->
<div class="form-group" ng-if="model.TLS">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
Skip certificate verification
<portainer-tooltip position="bottom" message="Skip the verification of the server TLS certificate. Not recommended on unsecured networks."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="model.TLSSkipVerify" /><i></i> </label>
</div>
</div>
<!-- !tls-skip-verify -->
<div class="col-sm-12 form-section-title" ng-if="model.TLS && !model.TLSSkipVerify">
Required TLS files
</div>
<!-- tls-file-upload -->
<div ng-if="model.TLS && !model.TLSSkipVerify">
<!-- tls-ca-file-cert -->
<div class="form-group">
<label for="tls_cert" class="col-sm-3 col-lg-2 control-label text-left">TLS CA certificate</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="model.TLSCACertFile">Select file</button>
<span style="margin-left: 5px;">
{{ model.TLSCACertFile.name }}
<i
class="fa fa-check green-icon"
ng-if="model.TLSCACertFile && model.TLSCACertFile === registry.ManagementConfiguration.TLSConfig.TLSCACertFile"
aria-hidden="true"
></i>
<i class="fa fa-times red-icon" ng-if="!model.TLSCACertFile" aria-hidden="true"></i>
</span>
</div>
</div>
<!-- !tls-ca-file-cert -->
<!-- tls-file-cert -->
<div class="form-group">
<label for="tls_cert" class="col-sm-3 col-lg-2 control-label text-left">TLS certificate</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="model.TLSCertFile">Select file</button>
<span style="margin-left: 5px;">
{{ model.TLSCertFile.name }}
<i
class="fa fa-check green-icon"
ng-if="model.TLSCertFile && model.TLSCertFile === registry.ManagementConfiguration.TLSConfig.TLSCertFile"
aria-hidden="true"
></i>
<i class="fa fa-times red-icon" ng-if="!model.TLSCertFile" aria-hidden="true"></i>
</span>
</div>
</div>
<!-- !tls-file-cert -->
<!-- tls-file-key -->
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">TLS key</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="model.TLSKeyFile">Select file</button>
<span style="margin-left: 5px;">
{{ model.TLSKeyFile.name }}
<i class="fa fa-check green-icon" ng-if="model.TLSKeyFile && model.TLSKeyFile === registry.ManagementConfiguration.TLSConfig.TLSKeyFile" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!model.TLSKeyFile" aria-hidden="true"></i>
</span>
</div>
</div>
<!-- !tls-file-key -->
</div>
</div>
<!-- !tls -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.testInProgress" ng-click="testConfiguration()" button-spinner="state.testInProgress">
<span ng-hide="state.testInProgress">Test configuration</span>
<span ng-show="state.testInProgress">Test in progress...</span>
</button>
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.updateInProgress || !state.validConfiguration"
ng-click="updateConfiguration()"
button-spinner="state.updateInProgress"
>
<span ng-hide="state.updateInProgress">Save configuration</span>
<span ng-show="state.updateInProgress">Saving configuration...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -1,11 +0,0 @@
<rd-widget>
<rd-widget-body>
<span class="small text-muted">
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ $ctrl.resolve.message }}
</p>
</span>
<span> &nbsp; {{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime | number: 0 }}s </span>
</rd-widget-body>
</rd-widget>

View File

@ -1,6 +0,0 @@
angular.module('portainer.extensions.registrymanagement').component('progressionModal', {
templateUrl: './progressionModal.html',
bindings: {
resolve: '<',
},
});

View File

@ -1,134 +0,0 @@
<rd-header>
<rd-header-title title-text="Repository">
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.registries.registry.repository" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry.repositories({id: registry.Id})">{{ registry.Name }}</a> &gt;
<a ui-sref="portainer.registries.registry.repository()">{{ repository.Name }} </a>
</rd-header-content>
</rd-header>
<div class="row">
<information-panel ng-if="!state.tagsRetrieval.auto" title-text="Information regarding repository size">
<span class="small text-muted">
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer needs to retrieve additional information to enable <code>tags modifications (addition, removal, rename)</code> and <code>repository removal</code> features.<br />
As this repository contains more than <code>{{ state.tagsRetrieval.limit }}</code> tags, the additional retrieval wasn't started automatically.<br />
Once started you can still navigate this page, leaving the page will cancel the retrieval process.<br />
<br />
<span style="font-weight: 700;">Note:</span> on very large repositories or high latency environments the retrieval process can take a few minutes.
</p>
<button class="btn btn-sm btn-primary" ng-if="!state.tagsRetrieval.running && short.Tags.length === 0" ng-click="startStopRetrieval()">Start</button>
<button class="btn btn-sm btn-danger" ng-if="state.tagsRetrieval.running" ng-click="startStopRetrieval()">Cancel</button>
</span>
<span ng-if="state.tagsRetrieval.running && state.tagsRetrieval.progression !== '100'">
&nbsp; Retrieval progress : {{ state.tagsRetrieval.progression }}% - {{ state.tagsRetrieval.elapsedTime | number: 0 }}s
</span>
<span ng-if="!state.tagsRetrieval.running && state.tagsRetrieval.progression === '100'">
<i class="fa fa-check-circle green-icon"></i> Retrieval completed in {{ state.tagsRetrieval.elapsedTime | number: 0 }}s
</span>
</information-panel>
</div>
<div class="row">
<div class="col-sm-8">
<rd-widget>
<rd-widget-header icon="fa-info" title-text="Repository information"> </rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Repository</td>
<td>
{{ repository.Name }}
</td>
<td>
<button class="btn btn-xs btn-danger" ng-if="!state.tagsRetrieval.running && state.tagsRetrieval.progression !== 0" ng-click="removeRepository()">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this repository
</button>
</td>
</tr>
<tr>
<td>Tags count</td>
<td>{{ repository.Tags.length }}</td>
<td></td>
</tr>
<tr ng-if="short.Images.length">
<td>Images count</td>
<td>{{ short.Images.length }}</td>
<td></td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-sm-4" ng-if="short.Images.length > 0">
<rd-widget>
<rd-widget-header icon="fa-plus" title-text="Add tag"> </rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<label for="tag" class="col-sm-3 col-lg-2 control-label text-left"
>Tag
<portainer-tooltip
position="bottom"
message="Tag can only contain alphanumeric (a-zA-Z0-9) and special _ . - characters. Tag must not start with . - characters."
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="tag" ng-model="formValues.Tag" />
</div>
</div>
<div class="form-group">
<label for="image" class="col-sm-3 col-lg-2 control-label text-left">Image</label>
<ui-select class="col-sm-9 col-lg-10" ng-model="formValues.SelectedImage" id="image">
<ui-select-match placeholder="Select an image" allow-clear="true">
<span>{{ $select.selected | trimshasum }}</span>
</ui-select-match>
<ui-select-choices repeat="image in (short.Images | filter: $select.search)">
<span>{{ image | trimshasum }}</span>
</ui-select-choices>
</ui-select>
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.Tag || !formValues.SelectedImage"
ng-click="addTag()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Add tag</span>
<span ng-show="state.actionInProgress">Adding tag...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<registries-repository-tags-datatable
title-text="Tags"
title-icon="fa-tags"
dataset="tags"
table-key="registryRepositoryTags"
order-by="Name"
remove-action="removeTags"
retag-action="retagAction"
advanced-features-available="short.Images.length > 0"
pagination-action="paginationAction"
loading="state.loading"
>
</registries-repository-tags-datatable>
</div>
</div>

View File

@ -1,423 +0,0 @@
import _ from 'lodash-es';
import { RepositoryShortTag, RepositoryTagViewModel } from '../../../models/repositoryTag';
angular.module('portainer.app').controller('RegistryRepositoryController', [
'$q',
'$async',
'$scope',
'$uibModal',
'$interval',
'$transition$',
'$state',
'RegistryServiceSelector',
'RegistryService',
'ModalService',
'Notifications',
'ImageHelper',
function ($q, $async, $scope, $uibModal, $interval, $transition$, $state, RegistryServiceSelector, RegistryService, ModalService, Notifications, ImageHelper) {
$scope.state = {
actionInProgress: false,
loading: false,
tagsRetrieval: {
auto: true,
running: false,
limit: 100,
progression: 0,
elapsedTime: 0,
asyncGenerator: null,
clock: null,
},
tagsRetag: {
running: false,
progression: 0,
elapsedTime: 0,
asyncGenerator: null,
clock: null,
},
tagsDelete: {
running: false,
progression: 0,
elapsedTime: 0,
asyncGenerator: null,
clock: null,
},
};
$scope.formValues = {
Tag: '', // new tag name on add feature
};
$scope.tags = []; // RepositoryTagViewModel (for datatable)
$scope.short = {
Tags: [], // RepositoryShortTag
Images: [], // strings extracted from short.Tags
};
$scope.repository = {
Name: '',
Tags: [], // string list
};
function toSeconds(time) {
return time / 1000;
}
function toPercent(progress, total) {
return ((progress / total) * 100).toFixed();
}
function openModal(resolve) {
return $uibModal.open({
component: 'progressionModal',
backdrop: 'static',
keyboard: false,
resolve: resolve,
});
}
$scope.paginationAction = function (tags) {
$scope.state.loading = true;
RegistryServiceSelector.getTagsDetails($scope.registry, $scope.repository.Name, tags)
.then(function success(data) {
for (var i = 0; i < data.length; i++) {
var idx = _.findIndex($scope.tags, { Name: data[i].Name });
if (idx !== -1) {
$scope.tags[idx] = data[i];
}
}
$scope.state.loading = false;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve tags details');
});
};
/**
* RETRIEVAL SECTION
*/
function updateRetrievalClock(startTime) {
$scope.state.tagsRetrieval.elapsedTime = toSeconds(Date.now() - startTime);
}
function createRetrieveAsyncGenerator() {
$scope.state.tagsRetrieval.asyncGenerator = RegistryServiceSelector.shortTagsWithProgress($scope.registry, $scope.repository.Name, $scope.repository.Tags);
}
function resetTagsRetrievalState() {
$scope.state.tagsRetrieval.running = false;
$scope.state.tagsRetrieval.progression = 0;
$scope.state.tagsRetrieval.elapsedTime = 0;
$scope.state.tagsRetrieval.clock = null;
}
function computeImages() {
const images = _.map($scope.short.Tags, 'ImageId');
$scope.short.Images = _.without(_.uniq(images), '');
}
$scope.startStopRetrieval = function () {
if ($scope.state.tagsRetrieval.running) {
$scope.state.tagsRetrieval.asyncGenerator.return();
$interval.cancel($scope.state.tagsRetrieval.clock);
} else {
retrieveTags().then(() => {
createRetrieveAsyncGenerator();
if ($scope.short.Tags.length === 0) {
resetTagsRetrievalState();
} else {
computeImages();
}
});
}
};
function retrieveTags() {
return $async(retrieveTagsAsync);
}
async function retrieveTagsAsync() {
$scope.state.tagsRetrieval.running = true;
const startTime = Date.now();
$scope.state.tagsRetrieval.clock = $interval(updateRetrievalClock, 1000, 0, true, startTime);
for await (const partialResult of $scope.state.tagsRetrieval.asyncGenerator) {
if (typeof partialResult === 'number') {
$scope.state.tagsRetrieval.progression = toPercent(partialResult, $scope.repository.Tags.length);
} else {
$scope.short.Tags = _.sortBy(partialResult, 'Name');
}
}
$scope.state.tagsRetrieval.running = false;
$interval.cancel($scope.state.tagsRetrieval.clock);
}
/**
* !END RETRIEVAL SECTION
*/
/**
* ADD TAG SECTION
*/
async function addTagAsync() {
try {
$scope.state.actionInProgress = true;
if (!ImageHelper.isValidTag($scope.formValues.Tag)) {
throw { msg: 'Invalid tag pattern, see info for more details on format.' };
}
const tag = $scope.short.Tags.find((item) => item.ImageId === $scope.formValues.SelectedImage);
const manifest = tag.ManifestV2;
await RegistryServiceSelector.addTag($scope.registry, $scope.repository.Name, $scope.formValues.Tag, manifest);
Notifications.success('Success', 'Tag successfully added');
$scope.short.Tags.push(new RepositoryShortTag($scope.formValues.Tag, tag.ImageId, tag.ImageDigest, tag.ManifestV2));
await loadRepositoryDetails();
$scope.formValues.Tag = '';
delete $scope.formValues.SelectedImage;
} catch (err) {
Notifications.error('Failure', err, 'Unable to add tag');
} finally {
$scope.state.actionInProgress = false;
}
}
$scope.addTag = function () {
return $async(addTagAsync);
};
/**
* !END ADD TAG SECTION
*/
/**
* RETAG SECTION
*/
function updateRetagClock(startTime) {
$scope.state.tagsRetag.elapsedTime = toSeconds(Date.now() - startTime);
}
function createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags) {
$scope.state.tagsRetag.asyncGenerator = RegistryServiceSelector.retagWithProgress($scope.registry, $scope.repository.Name, modifiedTags, modifiedDigests, impactedTags);
}
async function retagActionAsync() {
let modal = null;
try {
$scope.state.tagsRetag.running = true;
const modifiedTags = _.filter($scope.tags, (item) => item.Modified === true);
for (const tag of modifiedTags) {
if (!ImageHelper.isValidTag(tag.NewName)) {
throw { msg: 'Invalid tag pattern, see info for more details on format.' };
}
}
modal = await openModal({
message: () => 'Retag is in progress! Closing your browser or refreshing the page while this operation is in progress will result in loss of tags.',
progressLabel: () => 'Retag progress',
context: () => $scope.state.tagsRetag,
});
const modifiedDigests = _.uniq(_.map(modifiedTags, 'ImageDigest'));
const impactedTags = _.filter($scope.short.Tags, (item) => _.includes(modifiedDigests, item.ImageDigest));
const totalOps = modifiedDigests.length + impactedTags.length;
createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags);
const startTime = Date.now();
$scope.state.tagsRetag.clock = $interval(updateRetagClock, 1000, 0, true, startTime);
for await (const partialResult of $scope.state.tagsRetag.asyncGenerator) {
if (typeof partialResult === 'number') {
$scope.state.tagsRetag.progression = toPercent(partialResult, totalOps);
}
}
_.map(modifiedTags, (item) => {
const idx = _.findIndex($scope.short.Tags, (i) => i.Name === item.Name);
$scope.short.Tags[idx].Name = item.NewName;
});
Notifications.success('Success', 'Tags successfully renamed');
await loadRepositoryDetails();
} catch (err) {
Notifications.error('Failure', err, 'Unable to rename tags');
} finally {
$interval.cancel($scope.state.tagsRetag.clock);
$scope.state.tagsRetag.running = false;
if (modal) {
modal.close();
}
}
}
$scope.retagAction = function () {
return $async(retagActionAsync);
};
/**
* !END RETAG SECTION
*/
/**
* REMOVE TAGS SECTION
*/
function updateDeleteClock(startTime) {
$scope.state.tagsDelete.elapsedTime = toSeconds(Date.now() - startTime);
}
function createDeleteAsyncGenerator(modifiedDigests, impactedTags) {
$scope.state.tagsDelete.asyncGenerator = RegistryServiceSelector.deleteTagsWithProgress($scope.registry, $scope.repository.Name, modifiedDigests, impactedTags);
}
async function removeTagsAsync(selectedTags) {
let modal = null;
try {
$scope.state.tagsDelete.running = true;
modal = await openModal({
message: () => 'Tag delete is in progress! Closing your browser or refreshing the page while this operation is in progress will result in loss of tags.',
progressLabel: () => 'Deletion progress',
context: () => $scope.state.tagsDelete,
});
const deletedTagNames = _.map(selectedTags, 'Name');
const deletedShortTags = _.filter($scope.short.Tags, (item) => _.includes(deletedTagNames, item.Name));
const modifiedDigests = _.uniq(_.map(deletedShortTags, 'ImageDigest'));
const impactedTags = _.filter($scope.short.Tags, (item) => _.includes(modifiedDigests, item.ImageDigest));
const tagsToKeep = _.without(impactedTags, ...deletedShortTags);
const totalOps = modifiedDigests.length + tagsToKeep.length;
createDeleteAsyncGenerator(modifiedDigests, tagsToKeep);
const startTime = Date.now();
$scope.state.tagsDelete.clock = $interval(updateDeleteClock, 1000, 0, true, startTime);
for await (const partialResult of $scope.state.tagsDelete.asyncGenerator) {
if (typeof partialResult === 'number') {
$scope.state.tagsDelete.progression = toPercent(partialResult, totalOps);
}
}
_.pull($scope.short.Tags, ...deletedShortTags);
$scope.short.Images = _.map(_.uniqBy($scope.short.Tags, 'ImageId'), 'ImageId');
Notifications.success('Success', 'Tags successfully deleted');
if ($scope.short.Tags.length === 0) {
$state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, { reload: true });
}
await loadRepositoryDetails();
} catch (err) {
Notifications.error('Failure', err, 'Unable to delete tags');
} finally {
$interval.cancel($scope.state.tagsDelete.clock);
$scope.state.tagsDelete.running = false;
modal.close();
}
}
$scope.removeTags = function (selectedItems) {
ModalService.confirmDeletion('Are you sure you want to remove the selected tags ?', (confirmed) => {
if (!confirmed) {
return;
}
return $async(removeTagsAsync, selectedItems);
});
};
/**
* !END REMOVE TAGS SECTION
*/
/**
* REMOVE REPOSITORY SECTION
*/
async function removeRepositoryAsync() {
try {
const digests = _.uniqBy($scope.short.Tags, 'ImageDigest');
const promises = [];
_.map(digests, (item) => promises.push(RegistryServiceSelector.deleteManifest($scope.registry, $scope.repository.Name, item.ImageDigest)));
await Promise.all(promises);
Notifications.success('Success', 'Repository sucessfully removed');
$state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, { reload: true });
} catch (err) {
Notifications.error('Failure', err, 'Unable to delete repository');
}
}
$scope.removeRepository = function () {
ModalService.confirmDeletion(
'This action will only remove the manifests linked to this repository. You need to manually trigger a garbage collector pass on your registry to remove orphan layers and really remove the images content. THIS ACTION CAN NOT BE UNDONE',
function onConfirm(confirmed) {
if (!confirmed) {
return;
}
return $async(removeRepositoryAsync);
}
);
};
/**
* !END REMOVE REPOSITORY SECTION
*/
/**
* INIT SECTION
*/
async function loadRepositoryDetails() {
try {
const registry = $scope.registry;
const repository = $scope.repository.Name;
const tags = await RegistryServiceSelector.tags(registry, repository);
$scope.tags = [];
$scope.repository.Tags = [];
$scope.repository.Tags = _.sortBy(_.concat($scope.repository.Tags, _.without(tags.tags, null)));
_.map($scope.repository.Tags, (item) => $scope.tags.push(new RepositoryTagViewModel(item)));
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve tags details');
}
}
async function initView() {
try {
const registryId = $transition$.params().id;
$scope.repository.Name = $transition$.params().repository;
$scope.state.loading = true;
$scope.registry = await RegistryService.registry(registryId);
await loadRepositoryDetails();
if ($scope.repository.Tags.length > $scope.state.tagsRetrieval.limit) {
$scope.state.tagsRetrieval.auto = false;
}
createRetrieveAsyncGenerator();
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve repository information');
} finally {
$scope.state.loading = false;
}
}
$scope.$on('$destroy', () => {
if ($scope.state.tagsRetrieval.asyncGenerator) {
$scope.state.tagsRetrieval.asyncGenerator.return();
}
if ($scope.state.tagsRetrieval.clock) {
$interval.cancel($scope.state.tagsRetrieval.clock);
}
if ($scope.state.tagsRetag.asyncGenerator) {
$scope.state.tagsRetag.asyncGenerator.return();
}
if ($scope.state.tagsRetag.clock) {
$interval.cancel($scope.state.tagsRetag.clock);
}
if ($scope.state.tagsDelete.asyncGenerator) {
$scope.state.tagsDelete.asyncGenerator.return();
}
if ($scope.state.tagsDelete.clock) {
$interval.cancel($scope.state.tagsDelete.clock);
}
});
this.$onInit = function () {
return $async(initView).then(() => {
if ($scope.state.tagsRetrieval.auto) {
$scope.startStopRetrieval();
}
});
};
/**
* !END INIT SECTION
*/
},
]);

View File

@ -1,45 +0,0 @@
<rd-header>
<rd-header-title title-text="Registry repositories">
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.registries.registry.repositories" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt;
<a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})" ui-sref-opts="{reload:true}">{{ registry.Name }}</a
><span ng-if="!isAdmin">{{ registry.Name }}</span> &gt; Repositories
</rd-header-content>
</rd-header>
<div class="row">
<information-panel ng-if="state.displayInvalidConfigurationMessage" title-text="Registry management configuration required">
<span class="small text-muted">
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer was not able to use this registry management features. You might need to update the configuration used by Portainer to access this registry.
</p>
<p
>Note: Portainer registry management features are only supported with registries exposing the
<a href="https://docs.docker.com/registry/spec/api/" target="_blank">v2 registry API</a>.</p
>
<div style="margin-top: 7px;">
<a ui-sref="portainer.registries.registry.configure({id: registry.Id})"> <i class="fa fa-wrench" aria-hidden="true"></i> Configure this registry </a>
</div>
</span>
</information-panel>
</div>
<div class="row" ng-if="repositories">
<div class="col-sm-12">
<registry-repositories-datatable
title-text="Repositories"
title-icon="fa-book"
dataset="repositories"
table-key="registryRepositories"
order-by="Name"
pagination-action="paginationAction"
loading="state.loading"
>
</registry-repositories-datatable>
</div>
</div>

View File

@ -1,68 +0,0 @@
import _ from 'lodash-es';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
angular.module('portainer.extensions.registrymanagement').controller('RegistryRepositoriesController', [
'$transition$',
'$scope',
'RegistryService',
'RegistryServiceSelector',
'Notifications',
'Authentication',
function ($transition$, $scope, RegistryService, RegistryServiceSelector, Notifications, Authentication) {
$scope.state = {
displayInvalidConfigurationMessage: false,
loading: false,
};
$scope.paginationAction = function (repositories) {
if ($scope.registry.Type === RegistryTypes.GITLAB) {
return;
}
$scope.state.loading = true;
RegistryServiceSelector.getRepositoriesDetails($scope.registry, repositories)
.then(function success(data) {
for (var i = 0; i < data.length; i++) {
var idx = _.findIndex($scope.repositories, { Name: data[i].Name });
if (idx !== -1) {
if (data[i].TagsCount === 0) {
$scope.repositories.splice(idx, 1);
} else {
$scope.repositories[idx].TagsCount = data[i].TagsCount;
}
}
}
$scope.state.loading = false;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve repositories details');
});
};
function initView() {
const registryId = $transition$.params().id;
$scope.isAdmin = Authentication.isAdmin();
RegistryService.registry(registryId)
.then(function success(data) {
$scope.registry = data;
RegistryServiceSelector.ping($scope.registry, false)
.then(function success() {
return RegistryServiceSelector.repositories($scope.registry);
})
.then(function success(data) {
$scope.repositories = data;
})
.catch(function error() {
$scope.state.displayInvalidConfigurationMessage = true;
});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
});
}
initView();
},
]);

View File

@ -1,177 +0,0 @@
<rd-header>
<rd-header-title title-text="Tag">
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.registries.registry.repository.tag" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry.repositories()">{{ ctrl.registry.Name }}</a> &gt;
<a ui-sref="portainer.registries.registry.repository()">{{ ctrl.context.repository }} </a> &gt;
{{ ctrl.context.tag }}
</rd-header-content>
</rd-header>
<div class="row" ng-if="ctrl.details.RepoTags.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa fa-tags" title-text="Image tags"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="row">
<div class="pull-left" ng-repeat="tag in ctrl.details.RepoTags" style="display: table;">
<div class="input-group col-md-1" style="padding: 0 15px;">
<span class="input-group-addon">{{ tag }}</span>
</div>
</div>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="ctrl.details">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title-text="Image details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>ID</td>
<td>
{{ ctrl.details.Id }}
</td>
</tr>
<tr ng-if="ctrl.details.Parent">
<td>Parent</td>
<td>{{ ctrl.details.Parent }}</td>
</tr>
<tr>
<td>Created</td>
<td>{{ ctrl.details.Created | getisodate }}</td>
</tr>
<tr>
<td>Build</td>
<td>Docker {{ ctrl.details.DockerVersion }} on {{ ctrl.details.Os }}, {{ ctrl.details.Architecture }}</td>
</tr>
<tr ng-if="ctrl.details.Author">
<td>Author</td>
<td>{{ ctrl.details.Author }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="ctrl.details">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title-text="Dockerfile details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>CMD</td>
<td
><code>{{ ctrl.details.Command | command }}</code></td
>
</tr>
<tr ng-if="ctrl.details.Entrypoint">
<td>ENTRYPOINT</td>
<td
><code>{{ ctrl.details.Entrypoint | command }}</code></td
>
</tr>
<tr ng-if="ctrl.details.ExposedPorts.length > 0">
<td>EXPOSE</td>
<td>
<span class="label label-default space-right" ng-repeat="port in ctrl.details.ExposedPorts">
{{ port }}
</span>
</td>
</tr>
<tr ng-if="ctrl.details.Volumes.length > 0">
<td>VOLUME</td>
<td>
<span class="label label-default space-right" ng-repeat="volume in ctrl.details.Volumes">
{{ volume }}
</span>
</td>
</tr>
<tr ng-if="ctrl.details.Env.length > 0">
<td>ENV</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in ctrl.details.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="ctrl.tag">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title-text="Image layers"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table id="image-layers" class="table">
<thead>
<th style="white-space: nowrap;">
<a ng-click="ctrl.order('Order')">
Order
<span ng-show="ctrl.Sort.Type == 'Order' && !ctrl.Sort.Reverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="ctrl.Sort.Type == 'Order' && ctrl.Sort.Reverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="ctrl.order('CreatedBy')">
Layer
<span ng-show="ctrl.Sort.Type == 'CreatedBy' && !ctrl.Sort.Reverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="ctrl.Sort.Type == 'CreatedBy' && ctrl.Sort.Reverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead>
<tbody>
<tr ng-repeat="layer in ctrl.history | orderBy:ctrl.Sort.Type:ctrl.Sort.Reverse">
<td style="white-space: nowrap;">
{{ layer.Order }}
</td>
<td class="expand">
<div ng-if="layer.CreatedBy.length > 130">
<span id="layer-command-{{ $index }}-full" style="display: none;">
{{ layer.CreatedBy }}
</span>
<span id="layer-command-{{ $index }}-short">
{{ layer.CreatedBy | truncate: 130 }}
<span ng-if="layer.CreatedBy.length > 130" style="margin-left: 5px;">
<a id="layer-command-expander{{ $index }}" class="btn" ng-click="ctrl.toggleLayerCommand($index)">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
</a>
</span>
</span>
</div>
<div ng-if="layer.CreatedBy.length <= 130">
<span id="layer-command-{{ $index }}-full">
{{ layer.CreatedBy }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -1,56 +0,0 @@
import _ from 'lodash-es';
import angular from 'angular';
import { RegistryImageLayerViewModel } from 'Extensions/registry-management/models/registryImageLayer';
import { RegistryImageDetailsViewModel } from 'Extensions/registry-management/models/registryImageDetails';
class RegistryRepositoryTagController {
/* @ngInject */
constructor($transition$, $async, Notifications, RegistryService, RegistryServiceSelector, imagelayercommandFilter) {
this.$transition$ = $transition$;
this.$async = $async;
this.Notifications = Notifications;
this.RegistryService = RegistryService;
this.RegistryServiceSelector = RegistryServiceSelector;
this.imagelayercommandFilter = imagelayercommandFilter;
this.context = {};
this.onInit = this.onInit.bind(this);
}
toggleLayerCommand(layerId) {
$('#layer-command-expander' + layerId + ' span').toggleClass('glyphicon-plus-sign glyphicon-minus-sign');
$('#layer-command-' + layerId + '-short').toggle();
$('#layer-command-' + layerId + '-full').toggle();
}
order(sortType) {
this.Sort.Reverse = this.Sort.Type === sortType ? !this.Sort.Reverse : false;
this.Sort.Type = sortType;
}
async onInit() {
this.context.registryId = this.$transition$.params().id;
this.context.repository = this.$transition$.params().repository;
this.context.tag = this.$transition$.params().tag;
this.Sort = {
Type: 'Order',
Reverse: false,
};
try {
this.registry = await this.RegistryService.registry(this.context.registryId);
this.tag = await this.RegistryServiceSelector.tag(this.registry, this.context.repository, this.context.tag);
const length = this.tag.History.length;
this.history = _.map(this.tag.History, (layer, idx) => new RegistryImageLayerViewModel(length - idx, layer));
_.forEach(this.history, (item) => (item.CreatedBy = this.imagelayercommandFilter(item.CreatedBy)));
this.details = new RegistryImageDetailsViewModel(this.tag.History[0]);
} catch (error) {
this.Notifications.error('Failure', error, 'Unable to retrieve tag');
}
}
$onInit() {
return this.$async(this.onInit);
}
}
export default RegistryRepositoryTagController;
angular.module('portainer.extensions.registrymanagement').controller('RegistryRepositoryTagController', RegistryRepositoryTagController);

View File

@ -66,21 +66,6 @@
</td>
<td>
<a ui-sref="portainer.registries.registry.access({id: item.Id})" ng-if="$ctrl.accessManagement"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
<a ui-sref="portainer.registries.registry.repositories({id: item.Id})" ng-if="$ctrl.registryManagement && $ctrl.canBrowse(item)" class="space-left">
<i class="fa fa-search" aria-hidden="true"></i> Browse
</a>
<a
ui-sref="portainer.extensions.extension({id: 1})"
ng-if="!$ctrl.registryManagement && $ctrl.canBrowse(item)"
class="space-left"
style="color: #767676;"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Feature available via an extension"
>
<i class="fa fa-search" aria-hidden="true"></i> Browse (extension)
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">

View File

@ -10,7 +10,6 @@ angular.module('portainer.app').component('registriesDatatable', {
reverseOrder: '<',
accessManagement: '<',
removeAction: '<',
registryManagement: '<',
canBrowse: '<',
},
});

View File

@ -8,10 +8,6 @@
For information on how to generate a Gitlab Personal Access Token, follow the
<a href="https://gitlab.com/help/user/profile/personal_access_tokens.md" target="_blank">gitlab guide</a>.
</p>
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> You must provide a token with <code>api</code> scope. Failure to do so
will mean you can only push/pull from your registry but not manage it using the <a ui-sref="portainer.extensions.extension({id: 1})">registry management (extension)</a>.
</p>
</span>
</div>
<div class="col-sm-12 form-section-title">

View File

@ -1,5 +1,5 @@
import _ from 'lodash-es';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
import { RegistryTypes } from '@/portainer/models/registryTypes';
export function RegistryViewModel(data) {
this.Id = data.Id;

View File

@ -1,6 +1,6 @@
import gitlabResponseGetLink from './transform/gitlabResponseGetLink';
angular.module('portainer.extensions.registrymanagement').factory('Gitlab', [
angular.module('portainer.app').factory('Gitlab', [
'$resource',
'API_ENDPOINT_REGISTRIES',
function GitlabFactory($resource, API_ENDPOINT_REGISTRIES) {

View File

@ -1,6 +1,6 @@
import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import { RegistryCreateRequest, RegistryViewModel } from '../../models/registry';
angular.module('portainer.app').factory('RegistryService', [

View File

@ -2,7 +2,7 @@ import _ from 'lodash-es';
import { RegistryGitlabProject } from '../models/gitlabRegistry';
import { RegistryRepositoryGitlabViewModel } from '../models/registryRepository';
angular.module('portainer.extensions.registrymanagement').factory('RegistryGitlabService', [
angular.module('portainer.app').factory('RegistryGitlabService', [
'$async',
'Gitlab',
function RegistryGitlabServiceFactory($async, Gitlab) {

View File

@ -1,4 +1,4 @@
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import { RegistryDefaultModel } from '../../../models/registry';
angular.module('portainer.app').controller('CreateRegistryController', [
@ -7,8 +7,7 @@ angular.module('portainer.app').controller('CreateRegistryController', [
'RegistryService',
'Notifications',
'RegistryGitlabService',
'ExtensionService',
function ($scope, $state, RegistryService, Notifications, RegistryGitlabService, ExtensionService) {
function ($scope, $state, RegistryService, Notifications, RegistryGitlabService) {
$scope.selectQuayRegistry = selectQuayRegistry;
$scope.selectAzureRegistry = selectAzureRegistry;
$scope.selectCustomRegistry = selectCustomRegistry;
@ -106,7 +105,6 @@ angular.module('portainer.app').controller('CreateRegistryController', [
function initView() {
$scope.RegistryTypes = RegistryTypes;
$scope.model = new RegistryDefaultModel();
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.REGISTRY_MANAGEMENT).then((data) => ($scope.registryExtensionEnabled = data));
}
initView();

View File

@ -81,7 +81,6 @@
order-by="Name"
access-management="isAdmin"
remove-action="removeAction"
registry-management="registryManagementAvailable"
can-browse="canBrowse"
></registries-datatable>
</div>

View File

@ -8,9 +8,8 @@ angular.module('portainer.app').controller('RegistriesController', [
'DockerHubService',
'ModalService',
'Notifications',
'ExtensionService',
'Authentication',
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService, Authentication) {
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, Authentication) {
$scope.state = {
actionInProgress: false,
};
@ -75,12 +74,10 @@ angular.module('portainer.app').controller('RegistriesController', [
$q.all({
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub(),
registryManagement: ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.REGISTRY_MANAGEMENT),
})
.then(function success(data) {
$scope.registries = data.registries;
$scope.dockerhub = data.dockerhub;
$scope.registryManagementAvailable = data.registryManagement;
$scope.isAdmin = Authentication.isAdmin();
})
.catch(function error(err) {