mirror of https://github.com/portainer/portainer
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 typepull/4163/head
parent
7e90bf11b7
commit
82064152ec
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1 +1 @@
|
|||
angular.module('portainer.extensions', ['portainer.extensions.registrymanagement', 'portainer.extensions.rbac']);
|
||||
angular.module('portainer.extensions', ['portainer.extensions.rbac']);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
|
@ -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
|
||||
);
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
|
@ -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
|
||||
);
|
||||
},
|
||||
]);
|
|
@ -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;
|
||||
},
|
||||
]);
|
|
@ -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 : [];
|
||||
}
|
|
@ -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, ' ');
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
},
|
||||
]);
|
|
@ -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;
|
||||
},
|
||||
]);
|
|
@ -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();
|
||||
},
|
||||
]);
|
|
@ -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> > <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> > 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>
|
|
@ -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> {{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime | number: 0 }}s </span>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
|
@ -1,6 +0,0 @@
|
|||
angular.module('portainer.extensions.registrymanagement').component('progressionModal', {
|
||||
templateUrl: './progressionModal.html',
|
||||
bindings: {
|
||||
resolve: '<',
|
||||
},
|
||||
});
|
|
@ -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> > <a ui-sref="portainer.registries.registry.repositories({id: registry.Id})">{{ registry.Name }}</a> >
|
||||
<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'">
|
||||
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>
|
|
@ -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
|
||||
*/
|
||||
},
|
||||
]);
|
|
@ -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> >
|
||||
<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> > 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>
|
|
@ -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();
|
||||
},
|
||||
]);
|
|
@ -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> > <a ui-sref="portainer.registries.registry.repositories()">{{ ctrl.registry.Name }}</a> >
|
||||
<a ui-sref="portainer.registries.registry.repository()">{{ ctrl.context.repository }} </a> >
|
||||
{{ 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>
|
|
@ -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);
|
|
@ -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">
|
||||
|
|
|
@ -10,7 +10,6 @@ angular.module('portainer.app').component('registriesDatatable', {
|
|||
reverseOrder: '<',
|
||||
accessManagement: '<',
|
||||
removeAction: '<',
|
||||
registryManagement: '<',
|
||||
canBrowse: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
|
@ -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', [
|
||||
|
|
|
@ -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) {
|
|
@ -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();
|
||||
|
|
|
@ -81,7 +81,6 @@
|
|||
order-by="Name"
|
||||
access-management="isAdmin"
|
||||
remove-action="removeAction"
|
||||
registry-management="registryManagementAvailable"
|
||||
can-browse="canBrowse"
|
||||
></registries-datatable>
|
||||
</div>
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue