feat(registry): gitlab support (#3107)

* feat(api): gitlab registry type

* feat(registries): early support for gitlab registries

* feat(app): registry service selector

* feat(registry): gitlab support : list repositories and tags - remove features missing

* feat(registry): gitlab registry remove features

* feat(registry): gitlab switch to registry V2 API for repositories and tags

* feat(api): use development extension binary

* fix(registry): avoid 401 on gitlab retrieve to disconnect the user

* feat(registry): gitlab browse projects without extension

* style(app): code cleaning

* refactor(app): PR review changes + refactor on types

* fix(gitlab): remove gitlab info from registrymanagementconfig and force gitlab type

* style(api): go fmt

* feat(api): update APIVersion and ExtensionDefinitionsURL

* fix(api): fix invalid RM extension URL

* feat(registry): PAT scope help

* feat(registry): defaults on registry creation

* style(registry-creation): update layout and text for Gitlab registry

* feat(registry-creation): update gitlab notice
pull/3349/head
xAt0mZ 2019-11-12 04:28:31 +01:00 committed by Anthony Lapenna
parent 03d9d6afbb
commit 198e92c734
38 changed files with 1022 additions and 160 deletions

View File

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

View File

@ -41,7 +41,7 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt
if proxy == nil {
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register registry proxy", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
}
}

View File

@ -0,0 +1,23 @@
package registries
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
)
// request on /api/registries/proxies/gitlab
func (handler *Handler) proxyRequestsToGitlabAPIWithoutRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
domain := r.Header.Get("X-Gitlab-Domain")
if domain == "" {
return &httperror.HandlerError{http.StatusBadRequest, "No Gitlab domain provided", nil}
}
proxy, err := handler.ProxyManager.CreateGitlabProxy(domain)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create gitlab proxy", err}
}
http.StripPrefix("/registries/proxies/gitlab", proxy).ServeHTTP(w, r)
return nil
}

View File

@ -0,0 +1,66 @@
package registries
import (
"encoding/json"
"net/http"
"strconv"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
)
// 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.RegistryService.Registry(portainer.RegistryID(registryID))
if err == portainer.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", portainer.ErrEndpointAccessDenied}
}
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
}
var proxy http.Handler
proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
}
}
config := &portainer.RegistryManagementConfiguration{
Type: portainer.GitlabRegistry,
Password: registry.Password,
}
encodedConfiguration, err := json.Marshal(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err}
}
id := strconv.Itoa(int(registryID))
r.Header.Set("X-RegistryManagement-Key", id+"-gitlab")
r.Header.Set("X-RegistryManagement-URI", registry.Gitlab.InstanceURL)
r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration))
r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey)
http.StripPrefix("/registries/"+id+"/proxies/gitlab", proxy).ServeHTTP(w, r)
return nil
}

View File

@ -12,11 +12,12 @@ import (
type registryCreatePayload struct {
Name string
Type int
Type portainer.RegistryType
URL string
Authentication bool
Username string
Password string
Gitlab portainer.GitlabRegistryData
}
func (payload *registryCreatePayload) Validate(r *http.Request) error {
@ -29,8 +30,8 @@ func (payload *registryCreatePayload) Validate(r *http.Request) error {
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
}
if payload.Type != 1 && payload.Type != 2 && payload.Type != 3 {
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)")
if payload.Type != portainer.QuayRegistry && payload.Type != portainer.AzureRegistry && payload.Type != portainer.CustomRegistry && payload.Type != portainer.GitlabRegistry {
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)")
}
return nil
}
@ -42,16 +43,6 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
registries, err := handler.RegistryService.Registries()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
for _, r := range registries {
if r.URL == payload.URL {
return &httperror.HandlerError{http.StatusConflict, "A registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
}
}
registry := &portainer.Registry{
Type: portainer.RegistryType(payload.Type),
Name: payload.Name,
@ -61,6 +52,7 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
Password: payload.Password,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Gitlab: payload.Gitlab,
}
err = handler.RegistryService.CreateRegistry(registry)

38
api/http/proxy/gitlab.go Normal file
View File

@ -0,0 +1,38 @@
package proxy
import (
"errors"
"net/http"
"net/url"
)
type gitlabTransport struct {
httpTransport *http.Transport
}
func newGitlabProxy(uri string) (http.Handler, error) {
url, err := url.Parse(uri)
if err != nil {
return nil, err
}
proxy := newSingleHostReverseProxyWithHostHeader(url)
proxy.Transport = &gitlabTransport{
httpTransport: &http.Transport{},
}
return proxy, nil
}
func (transport *gitlabTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token := request.Header.Get("Private-Token")
if token == "" {
return nil, errors.New("No gitlab token provided")
}
r, err := http.NewRequest(request.Method, request.URL.String(), nil)
if err != nil {
return nil, err
}
r.Header.Set("Private-Token", token)
return transport.httpTransport.RoundTrip(r)
}

View File

@ -182,3 +182,8 @@ func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler,
return manager.createDockerProxy(endpoint)
}
// CreateGitlabProxy creates a new HTTP reverse proxy that can be used to send requests to the Gitlab API..
func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) {
return newGitlabProxy(url)
}

View File

@ -191,6 +191,12 @@ type (
// RegistryType represents a type of registry
RegistryType int
// GitlabRegistryData represents data required for gitlab registry to work
GitlabRegistryData struct {
ProjectID int `json:"ProjectId"`
InstanceURL string `json:"InstanceURL"`
}
// Registry represents a Docker registry with all the info required
// to connect to it
Registry struct {
@ -202,6 +208,7 @@ type (
Username string `json:"Username"`
Password string `json:"Password,omitempty"`
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
Gitlab GitlabRegistryData `json:"Gitlab"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
@ -903,7 +910,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "1.22.1"
APIVersion = "1.23.0-dev"
// DBVersion is the version number of the Portainer database
DBVersion = 21
// AssetsServerURL represents the URL of the Portainer asset server
@ -913,7 +920,7 @@ const (
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.1.json"
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-" + APIVersion + ".json"
// SupportProductsURL represents the URL where Portainer support products can be retrieved
SupportProductsURL = AssetsServerURL + "/support.json"
// PortainerAgentHeader represents the name of the header available in any agent response
@ -1076,6 +1083,8 @@ const (
AzureRegistry
// CustomRegistry represents a custom registry
CustomRegistry
// GitlabRegistry represents a gitlab registry
GitlabRegistry
)
const (

View File

@ -68,7 +68,7 @@ function initAuthentication(authManager, Authentication, $rootScope, $state) {
// authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector
// to have more controls on which URL should trigger the unauthenticated state.
$rootScope.$on('unauthenticated', function (event, data) {
if (!_.includes(data.config.url, '/v2/')) {
if (!_.includes(data.config.url, '/v2/') && !_.includes(data.config.url, '/api/v4/')) {
$state.go('portainer.auth', { error: 'Your session has expired' });
}
});

View File

@ -30,7 +30,7 @@
<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})" class="monospaced"
<a ui-sref="portainer.registries.registry.repository({repository: item.Name})"
title="{{ item.Name }}">{{ item.Name }}</a>
</td>
<td>{{ item.TagsCount }}</td>

View File

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

View File

@ -0,0 +1,8 @@
export function RegistryGitlabProject(project) {
this.Id = project.id;
this.Description = project.description;
this.Name = project.name;
this.Namespace = project.namespace ? project.namespace.name : '';
this.PathWithNamespace = project.path_with_namespace;
this.RegistryEnabled = project.container_registry_enabled;
}

View File

@ -1,5 +1,5 @@
import _ from 'lodash-es';
export default function RegistryRepositoryViewModel(item) {
export function RegistryRepositoryViewModel(item) {
if (item.name && item.tags) {
this.Name = item.name;
this.TagsCount = _.without(item.tags, null).length;
@ -8,3 +8,8 @@ export default function RegistryRepositoryViewModel(item) {
this.TagsCount = 0;
}
}
export function RegistryRepositoryGitlabViewModel(data) {
this.Name = data.path;
this.TagsCount = data.tags.length;
}

View File

@ -0,0 +1,6 @@
export const RegistryTypes = Object.freeze({
'QUAY': 1,
'AZURE': 2,
'CUSTOM': 3,
'GITLAB': 4
})

View File

@ -1,4 +1,4 @@
export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2) {
export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2, history) {
this.Name = name;
this.Os = os || '';
this.Architecture = arch || '';
@ -6,6 +6,7 @@ export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageI
this.ImageDigest = imageDigest || '';
this.ImageId = imageId || '';
this.ManifestV2 = v2 || {};
this.History = history || [];
}
export function RepositoryShortTag(name, imageId, imageDigest, manifest) {
@ -14,3 +15,8 @@ export function RepositoryShortTag(name, imageId, imageDigest, manifest) {
this.ImageDigest = imageDigest;
this.ManifestV2 = manifest;
}
export function RepositoryAddTagPayload(tag, manifest) {
this.Tag = tag;
this.Manifest = manifest;
}

View File

@ -0,0 +1,33 @@
import gitlabResponseGetLink from './transform/gitlabResponseGetLink'
angular.module('portainer.extensions.registrymanagement')
.factory('Gitlab', ['$resource', 'API_ENDPOINT_REGISTRIES',
function GitlabFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return function(env) {
const headers = {};
if (env) {
headers['Private-Token'] = env.token;
headers['X-Gitlab-Domain'] = env.url
}
const baseUrl = API_ENDPOINT_REGISTRIES + '/:id/proxies/gitlab/api/v4/projects';
return $resource(baseUrl, {id:'@id'},
{
projects: {
method: 'GET',
params: { membership: 'true' },
transformResponse: gitlabResponseGetLink,
headers: headers
},
repositories :{
method: 'GET',
url: baseUrl + '/:projectId/registry/repositories',
params: { tags: true },
headers: headers,
transformResponse: gitlabResponseGetLink
}
});
};
}]);

View File

@ -0,0 +1,10 @@
export default function gitlabResponseGetLink(data, headers) {
let response = {};
try {
response.data = angular.fromJson(data);
response.next = headers('X-Next-Page');
} catch (error) {
response = data;
}
return response;
}

View File

@ -0,0 +1,86 @@
import _ from 'lodash-es';
import { RegistryGitlabProject } from '../models/gitlabRegistry';
import { RegistryRepositoryGitlabViewModel } from '../models/registryRepository';
angular.module('portainer.extensions.registrymanagement')
.factory('RegistryGitlabService', ['$async', 'Gitlab',
function RegistryGitlabServiceFactory($async, Gitlab) {
'use strict';
var service = {};
/**
* PROJECTS
*/
async function _getProjectsPage(env, params, projects) {
const response = await Gitlab(env).projects(params).$promise;
projects = _.concat(projects, response.data);
if (response.next) {
params.page = response.next;
projects = await _getProjectsPage(env, params, projects);
}
return projects;
}
async function projectsAsync(url, token) {
try {
const data = await _getProjectsPage({url: url, token: token}, {page: 1}, []);
return _.map(data, (project) => new RegistryGitlabProject(project));
} catch (error) {
throw {msg: 'Unable to retrieve projects', err: error};
}
}
/**
* END PROJECTS
*/
/**
* REPOSITORIES
*/
async function _getRepositoriesPage(params, repositories) {
const response = await Gitlab().repositories(params).$promise;
repositories = _.concat(repositories, response.data);
if (response.next) {
params.page = response.next;
repositories = await _getRepositoriesPage(params, repositories);
}
return repositories;
}
async function repositoriesAsync(registry) {
try {
const params = {
id: registry.Id,
projectId: registry.Gitlab.ProjectId,
page: 1
};
const data = await _getRepositoriesPage(params, []);
return _.map(data, (r) => new RegistryRepositoryGitlabViewModel(r));
} catch (error) {
throw {msg: 'Unable to retrieve repositories', err: error};
}
}
/**
* END REPOSITORIES
*/
/**
* SERVICE FUNCTIONS DECLARATION
*/
function projects(url, token) {
return $async(projectsAsync, url, token);
}
function repositories(registry) {
return $async(repositoriesAsync, registry);
}
service.projects = projects;
service.repositories = repositories;
return service;
}
]);

View File

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

View File

@ -1,6 +1,7 @@
import _ from 'lodash-es';
import { RepositoryShortTag } from '../models/repositoryTag';
import RegistryRepositoryViewModel from '../models/registryRepository';
import { RepositoryAddTagPayload } from '../models/repositoryTag'
import { RegistryRepositoryViewModel } from '../models/registryRepository';
import genericAsyncGenerator from './genericAsyncGenerator';
angular.module('portainer.extensions.registrymanagement')
@ -9,12 +10,24 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
'use strict';
var service = {};
service.ping = function(id, forceNewConfig) {
/**
* 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) {
@ -27,7 +40,7 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
});
}
function getCatalog(id) {
function _getCatalog(id) {
var deferred = $q.defer();
var repositories = [];
@ -35,13 +48,12 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
return deferred.promise;
}
service.catalog = function (id) {
var deferred = $q.defer();
function repositories(registry) {
const deferred = $q.defer();
const id = registry.Id;
getCatalog(id).then(function success(data) {
var repositories = data.map(function (repositoryName) {
return new RegistryRepositoryViewModel(repositoryName);
});
_getCatalog(id).then(function success(data) {
const repositories = _.map(data, (repositoryName) => new RegistryRepositoryViewModel(repositoryName));
deferred.resolve(repositories);
})
.catch(function error(err) {
@ -52,14 +64,37 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
});
return deferred.promise;
};
}
service.tags = function (id, repository) {
var deferred = $q.defer();
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
});
});
_getTagsPage({id: id, repository: repository}, deferred, {tags:[]});
return deferred.promise;
};
}
/**
* END REPOSITORIES
*/
/**
* TAGS
*/
function _getTagsPage(params, deferred, previousTags) {
RegistryTags.get(params).$promise.then(function(data) {
@ -78,45 +113,23 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
});
}
service.getRepositoriesDetails = function (id, repositories) {
var deferred = $q.defer();
var promises = [];
for (var i = 0; i < repositories.length; i++) {
var repository = repositories[i].Name;
promises.push(service.tags(id, repository));
}
$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
});
});
function tags(registry, repository) {
const deferred = $q.defer();
const id = registry.Id;
_getTagsPage({id: id, repository: repository}, deferred, {tags:[]});
return deferred.promise;
};
service.getTagsDetails = function (id, repository, tags) {
var promises = [];
for (var i = 0; i < tags.length; i++) {
var tag = tags[i].Name;
promises.push(service.tag(id, repository, tag));
}
function getTagsDetails(registry, repository, tags) {
const promises = _.map(tags, (t) => tag(registry, repository, t.Name));
return $q.all(promises);
};
}
service.tag = function (id, repository, tag) {
var deferred = $q.defer();
function tag(registry, repository, tag) {
const deferred = $q.defer();
const id = registry.Id;
var promises = {
v1: RegistryManifestsJquery.get({
@ -142,35 +155,33 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
});
return deferred.promise;
};
}
service.addTag = function (id, repository, {tag, manifest}) {
/**
* 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);
};
}
service.deleteManifest = function (id, repository, imageDigest) {
return RegistryManifestsJquery.delete({
id: id,
repository: repository,
tag: imageDigest
});
};
service.shortTag = function(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* addTagsWithProgress(id, repository, tagsList, progression = 0) {
for await (const partialResult of genericAsyncGenerator($q, tagsList, service.addTag, [id, repository])) {
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 {
@ -179,36 +190,110 @@ function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, Reg
}
}
service.shortTagsWithProgress = async function* (id, repository, tagsList) {
yield* genericAsyncGenerator($q, tagsList, service.shortTag, [id, repository]);
/**
* 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(id, repository, manifests) {
for await (const partialResult of genericAsyncGenerator($q, manifests, service.deleteManifest, [id, repository])) {
async function* _deleteManifestsWithProgress(registry, repository, manifests) {
for await (const partialResult of genericAsyncGenerator($q, manifests, deleteManifest, [registry, repository])) {
yield partialResult;
}
}
service.retagWithProgress = async function* (id, repository, modifiedTags, modifiedDigests, impactedTags){
yield* deleteManifestsWithProgress(id, repository, modifiedDigests);
/**
* 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 { tag: name, manifest: item.ManifestV2 };
return new RepositoryAddTagPayload(name, item.ManifestV2);
});
yield* addTagsWithProgress(id, repository, newTags, modifiedDigests.length);
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
}
service.deleteTagsWithProgress = async function* (id, repository, modifiedDigests, impactedTags) {
yield* deleteManifestsWithProgress(id, repository, modifiedDigests);
/**
* END RETAG
*/
const newTags = _.map(impactedTags, (item) => {return {tag: item.Name, manifest: item.ManifestV2}})
/**
* DELETE TAGS
*/
yield* addTagsWithProgress(id, repository, newTags, modifiedDigests.length);
async function* deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
const newTags = _.map(impactedTags, (item) => new RepositoryAddTagPayload(item.Name, item.ManifestV2));
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
}
/**
* END DELETE TAGS
*/
/**
* SERVICE FUNCTIONS DECLARATION
*/
service.ping = ping;
service.repositories = repositories;
service.getRepositoriesDetails = getRepositoriesDetails;
service.tags = tags;
service.tag = tag;
service.getTagsDetails = getTagsDetails;
service.shortTagsWithProgress = shortTagsWithProgress;
service.addTag = addTag;
service.deleteManifest = deleteManifest;
service.deleteTagsWithProgress = deleteTagsWithProgress;
service.retagWithProgress = retagWithProgress;
return service;
}
]);

View File

@ -1,8 +1,9 @@
import { RegistryManagementConfigurationDefaultModel } from '../../../../portainer/models/registry';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
angular.module('portainer.extensions.registrymanagement')
.controller('ConfigureRegistryController', ['$scope', '$state', '$transition$', 'RegistryService', 'RegistryV2Service', 'Notifications',
function ($scope, $state, $transition$, RegistryService, RegistryV2Service, Notifications) {
.controller('ConfigureRegistryController', ['$scope', '$state', '$transition$', 'RegistryService', 'RegistryServiceSelector', 'Notifications',
function ($scope, $state, $transition$, RegistryService, RegistryServiceSelector, Notifications) {
$scope.state = {
testInProgress: false,
@ -18,7 +19,7 @@ function ($scope, $state, $transition$, RegistryService, RegistryV2Service, Noti
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
.then(function success() {
return RegistryV2Service.ping($scope.registry.Id, true);
return RegistryServiceSelector.ping($scope.registry, true);
})
.then(function success() {
Notifications.success('Success', 'Valid management configuration');
@ -50,6 +51,7 @@ function ($scope, $state, $transition$, RegistryService, RegistryV2Service, Noti
function initView() {
var registryId = $transition$.params().id;
$scope.RegistryTypes = RegistryTypes;
RegistryService.registry(registryId)
.then(function success(data) {

View File

@ -32,7 +32,7 @@
</div>
<!-- !registry-url-input -->
<!-- authentication-checkbox -->
<div class="form-group" ng-if="registry.Type === 3">
<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
@ -65,7 +65,7 @@
</div>
<!-- !authentication-credentials -->
<!-- tls -->
<div ng-if="registry.Type === 3">
<div ng-if="registry.Type === RegistryTypes.CUSTOM || registry.Type === RegistryTypes.GITLAB">
<!-- tls-checkbox -->
<div class="form-group">
<div class="col-sm-12">

View File

@ -48,6 +48,8 @@
<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>
@ -56,10 +58,12 @@
<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>

View File

@ -2,8 +2,8 @@ import _ from 'lodash-es';
import { RepositoryTagViewModel, RepositoryShortTag } from '../../../models/repositoryTag';
angular.module('portainer.app')
.controller('RegistryRepositoryController', ['$q', '$async', '$scope', '$uibModal', '$interval', '$transition$', '$state', 'RegistryV2Service', 'RegistryService', 'ModalService', 'Notifications', 'ImageHelper',
function ($q, $async, $scope, $uibModal, $interval, $transition$, $state, RegistryV2Service, RegistryService, ModalService, Notifications, ImageHelper) {
.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,
@ -63,7 +63,7 @@ angular.module('portainer.app')
$scope.paginationAction = function (tags) {
$scope.state.loading = true;
RegistryV2Service.getTagsDetails($scope.registryId, $scope.repository.Name, tags)
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});
@ -86,7 +86,7 @@ angular.module('portainer.app')
function createRetrieveAsyncGenerator() {
$scope.state.tagsRetrieval.asyncGenerator =
RegistryV2Service.shortTagsWithProgress($scope.registryId, $scope.repository.Name, $scope.repository.Tags);
RegistryServiceSelector.shortTagsWithProgress($scope.registry, $scope.repository.Name, $scope.repository.Tags);
}
function resetTagsRetrievalState() {
@ -151,7 +151,7 @@ angular.module('portainer.app')
}
const tag = $scope.short.Tags.find((item) => item.ImageId === $scope.formValues.SelectedImage);
const manifest = tag.ManifestV2;
await RegistryV2Service.addTag($scope.registryId, $scope.repository.Name, {tag: $scope.formValues.Tag, manifest: manifest})
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));
@ -182,7 +182,7 @@ angular.module('portainer.app')
function createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags) {
$scope.state.tagsRetag.asyncGenerator =
RegistryV2Service.retagWithProgress($scope.registryId, $scope.repository.Name, modifiedTags, modifiedDigests, impactedTags);
RegistryServiceSelector.retagWithProgress($scope.registry, $scope.repository.Name, modifiedTags, modifiedDigests, impactedTags);
}
async function retagActionAsync() {
@ -252,7 +252,7 @@ angular.module('portainer.app')
function createDeleteAsyncGenerator(modifiedDigests, impactedTags) {
$scope.state.tagsDelete.asyncGenerator =
RegistryV2Service.deleteTagsWithProgress($scope.registryId, $scope.repository.Name, modifiedDigests, impactedTags);
RegistryServiceSelector.deleteTagsWithProgress($scope.registry, $scope.repository.Name, modifiedDigests, impactedTags);
}
async function removeTagsAsync(selectedTags) {
@ -289,7 +289,7 @@ angular.module('portainer.app')
Notifications.success('Success', 'Tags successfully deleted');
if ($scope.short.Tags.length === 0) {
$state.go('portainer.registries.registry.repositories', {id: $scope.registryId}, {reload: true});
$state.go('portainer.registries.registry.repositories', {id: $scope.registry.Id}, {reload: true});
}
await loadRepositoryDetails();
} catch (err) {
@ -322,10 +322,10 @@ angular.module('portainer.app')
try {
const digests = _.uniqBy($scope.short.Tags, 'ImageDigest');
const promises = [];
_.map(digests, (item) => promises.push(RegistryV2Service.deleteManifest($scope.registryId, $scope.repository.Name, item.ImageDigest)));
_.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.registryId}, {reload: true});
$state.go('portainer.registries.registry.repositories', {id: $scope.registry.Id}, {reload: true});
} catch (err) {
Notifications.error('Failure', err, 'Unable to delete repository');
}
@ -351,9 +351,9 @@ angular.module('portainer.app')
*/
async function loadRepositoryDetails() {
try {
const registryId = $scope.registryId;
const registry = $scope.registry;
const repository = $scope.repository.Name;
const tags = await RegistryV2Service.tags(registryId, repository);
const tags = await RegistryServiceSelector.tags(registry, repository);
$scope.tags = [];
$scope.repository.Tags = [];
$scope.repository.Tags = _.sortBy(_.concat($scope.repository.Tags, _.without(tags.tags, null)));
@ -365,7 +365,7 @@ angular.module('portainer.app')
async function initView() {
try {
const registryId = $scope.registryId = $transition$.params().id;
const registryId = $transition$.params().id;
$scope.repository.Name = $transition$.params().repository;
$scope.state.loading = true;

View File

@ -1,8 +1,10 @@
import _ from 'lodash-es';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
angular.module('portainer.extensions.registrymanagement')
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications', 'Authentication',
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications, Authentication) {
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryServiceSelector', 'Notifications', 'Authentication',
function ($transition$, $scope, RegistryService, RegistryServiceSelector, Notifications, Authentication) {
$scope.state = {
displayInvalidConfigurationMessage: false,
@ -10,8 +12,11 @@ function ($transition$, $scope, RegistryService, RegistryV2Service, Notification
};
$scope.paginationAction = function (repositories) {
if ($scope.registry.Type === RegistryTypes.GITLAB) {
return;
}
$scope.state.loading = true;
RegistryV2Service.getRepositoriesDetails($scope.state.registryId, repositories)
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});
@ -30,20 +35,19 @@ function ($transition$, $scope, RegistryService, RegistryV2Service, Notification
};
function initView() {
$scope.state.registryId = $transition$.params().id;
const registryId = $transition$.params().id;
var authenticationEnabled = $scope.applicationState.application.authentication;
if (authenticationEnabled) {
$scope.isAdmin = Authentication.isAdmin();
}
RegistryService.registry($scope.state.registryId)
RegistryService.registry(registryId)
.then(function success(data) {
$scope.registry = data;
RegistryV2Service.ping($scope.state.registryId, false)
RegistryServiceSelector.ping($scope.registry, false)
.then(function success() {
return RegistryV2Service.catalog($scope.state.registryId);
return RegistryServiceSelector.repositories($scope.registry);
})
.then(function success(data) {
$scope.repositories = data;

View File

@ -6,12 +6,12 @@ import { RegistryImageDetailsViewModel } from 'Extensions/registry-management/mo
class RegistryRepositoryTagController {
/* @ngInject */
constructor($transition$, $async, Notifications, RegistryService, RegistryV2Service, imagelayercommandFilter) {
constructor($transition$, $async, Notifications, RegistryService, RegistryServiceSelector, imagelayercommandFilter) {
this.$transition$ = $transition$;
this.$async = $async;
this.Notifications = Notifications;
this.RegistryService = RegistryService;
this.RegistryV2Service = RegistryV2Service;
this.RegistryServiceSelector = RegistryServiceSelector;
this.imagelayercommandFilter = imagelayercommandFilter;
this.context = {};
@ -39,7 +39,7 @@ class RegistryRepositoryTagController {
}
try {
this.registry = await this.RegistryService.registry(this.context.registryId);
this.tag = await this.RegistryV2Service.tag(this.context.registryId, this.context.repository, this.context.tag);
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))

View File

@ -18,7 +18,7 @@
</div>
<!-- !name-input -->
<!-- url-input -->
<div class="form-group" ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
<div class="form-group">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip position="bottom" message="URL of an Azure Container Registry. Any protocol will be stripped."></portainer-tooltip>

View File

@ -27,7 +27,7 @@
</div>
<!-- !name-input -->
<!-- url-input -->
<div class="form-group" ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
<div class="form-group">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry. Any protocol will be stripped."></portainer-tooltip>

View File

@ -0,0 +1,94 @@
<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-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('Namespace')">
Namespace
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<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>
<a ng-click="$ctrl.changeOrderBy('PathWithNamespace')">
Path with namespace
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PathWithNamespace' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PathWithNamespace' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr 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)" ng-disabled="$ctrl.disableSelection(item)"/>
<label for="select_{{ $index }}"></label>
</span>
{{ item.Namespace }}
</td>
<td>
{{ item.Name }}
</td>
<td>
{{ item.PathWithNamespace }}
</td>
<td>
{{ item.Description }}
</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No projects 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="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -0,0 +1,13 @@
angular.module('portainer.app').component('gitlabProjectsDatatable', {
templateUrl: './gitlabProjectsDatatable.html',
controller: 'GitlabProjectsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
state: '='
}
});

View File

@ -0,0 +1,48 @@
angular.module('portainer.app')
.controller('GitlabProjectsDatatableController', ['$scope', '$controller', 'DatatableService',
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
this.disableSelection = function(item) {
return !this.allowSelection(item);
}
// based on RegistryGitlabProject model
this.allowSelection = function(item) {
return item.RegistryEnabled;
};
this.$onInit = function() {
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
}
]);

View File

@ -0,0 +1,139 @@
<form class="form-horizontal" name="registryFormGitlab" ng-submit="$ctrl.retrieveRegistries()">
<div class="col-sm-12 form-section-title">
Important notice
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<p>
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">
Gitlab registry connection details
</div>
<!-- credentials-user -->
<div class="form-group">
<label for="registry_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="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required>
</div>
</div>
<div class="form-group" ng-show="registryFormGitlab.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormGitlab.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-pat -->
<div class="form-group">
<label for="registry_perso_acc_token" class="col-sm-3 col-lg-2 control-label text-left">Personal Access Token
</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="registry_perso_acc_token" name="registry_perso_acc_token" ng-model="$ctrl.model.Token" required>
</div>
</div>
<div class="form-group" ng-show="registryFormGitlab.registry_perso_acc_token.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormGitlab.registry_perso_acc_token.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-pat -->
<div class="form-group">
<div class="col-sm-12">
<a class="small interactive" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;">
<i class="fa fa-wrench space-right" aria-hidden="true"></i> Override default configuration
</a>
<a class="small interactive" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = false; $ctrl.resetDefaults()">
<i class="fa fa-cogs space-right" aria-hidden="true"></i> Use default configuration
</a>
</div>
</div>
<!-- url-input -->
<div class="form-group" ng-if="$ctrl.state.overrideConfiguration">
<label for="instance_url" class="col-sm-3 col-lg-2 control-label text-left">
Instance URL
<portainer-tooltip position="bottom" message="URL of Gitlab instance."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="instance_url" name="instance_url" ng-model="$ctrl.model.Gitlab.InstanceURL" placeholder="https://gitlab.com" required>
</div>
</div>
<div class="form-group" ng-show="registryFormGitlab.instance_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormGitlab.instance_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !url-input -->
<!-- url-input -->
<div class="form-group" ng-if="$ctrl.state.overrideConfiguration">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip position="bottom" message="URL of Gitlab registry instance."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" name="registry_url" ng-model="$ctrl.model.URL" placeholder="https://registry.gitlab.com" required>
</div>
</div>
<div class="form-group" ng-show="registryFormGitlab.registry_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormGitlab.registry_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !url-input -->
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormGitlab.$valid" button-spinner="$ctrl.actionInProgress">
<span ng-hide="$ctrl.actionInProgress">Retrieve projects</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>
</div>
</div>
</form>
<div class="form-horizontal" ng-if="$ctrl.projects">
<div class="col-sm-12 form-section-title">
Gitlab projects
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Select the project's registries you want to manage. Portainer will create one registry for each selected project.
</span>
<span class="col-sm-12 text-muted small">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
If you can't select a project, make sure that registry feature is activated on it.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<gitlab-projects-datatable
title-text="Gitlab projects" title-icon="fa-project-diagram"
dataset="$ctrl.projects" table-key="gitlab_projects"
state="$ctrl.state.gitlab"
order-by="Name"
></gitlab-projects-datatable>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button ng-click="$ctrl.createRegistries()" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !$ctrl.state.gitlab.selectedItemCount" button-spinner="$ctrl.actionInProgress">
<span ng-hide="$ctrl.actionInProgress">Create registries</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</div>

View File

@ -0,0 +1,12 @@
angular.module('portainer.app').component('registryFormGitlab', {
templateUrl: './registry-form-gitlab.html',
bindings: {
model: '=',
retrieveRegistries: '<',
createRegistries: '<',
actionInProgress: '<',
projects: '=',
state: '=',
resetDefaults: '<'
}
});

View File

@ -1,3 +1,6 @@
import _ from 'lodash-es';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
export function RegistryViewModel(data) {
this.Id = data.Id;
this.Type = data.Type;
@ -11,6 +14,7 @@ export function RegistryViewModel(data) {
this.UserAccessPolicies = data.UserAccessPolicies;
this.TeamAccessPolicies = data.TeamAccessPolicies;
this.Checked = false;
this.Gitlab = data.Gitlab;
}
export function RegistryManagementConfigurationDefaultModel(registry) {
@ -22,20 +26,20 @@ export function RegistryManagementConfigurationDefaultModel(registry) {
this.TLSCertFile = null;
this.TLSKeyFile = null;
if (registry.Type === 1 || registry.Type === 2 ) {
if (registry.Type === RegistryTypes.QUAY || registry.Type === RegistryTypes.AZURE ) {
this.Authentication = true;
this.Username = registry.Username;
this.TLS = true;
}
if (registry.Type === 3 && registry.Authentication) {
if (registry.Type === RegistryTypes.CUSTOM && registry.Authentication) {
this.Authentication = true;
this.Username = registry.Username;
}
}
export function RegistryDefaultModel() {
this.Type = 3;
this.Type = RegistryTypes.CUSTOM;
this.URL = '';
this.Name = '';
this.Authentication = false;
@ -46,10 +50,16 @@ export function RegistryDefaultModel() {
export function RegistryCreateRequest(model) {
this.Name = model.Name;
this.Type = model.Type;
this.URL = model.URL;
this.URL = _.replace(model.URL, /^https?\:\/\//i, '');
this.Authentication = model.Authentication;
if (model.Authentication) {
this.Username = model.Username;
this.Password = model.Password;
}
if (model.Type === RegistryTypes.GITLAB) {
this.Gitlab = {
ProjectId: model.Gitlab.ProjectId,
InstanceURL: model.Gitlab.InstanceURL
}
}
}

View File

@ -1,3 +1,4 @@
import _ from 'lodash-es';
import { RegistryViewModel, RegistryCreateRequest } from '../../models/registry';
angular.module('portainer.app')
@ -65,6 +66,19 @@ angular.module('portainer.app')
return Registries.create(payload).$promise;
};
service.createGitlabRegistries = function(model, projects) {
const promises = [];
_.forEach(projects, (p) => {
const m = model;
m.Name = p.PathWithNamespace;
m.Gitlab.ProjectId = p.Id;
m.Password = m.Token;
const payload = new RegistryCreateRequest(m);
promises.push(Registries.create(payload).$promise);
});
return $q.all(promises);
};
service.retrieveRegistryFromRepository = function(repository) {
var deferred = $q.defer();

View File

@ -1,16 +1,23 @@
import { RegistryDefaultModel } from '../../../models/registry';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
angular.module('portainer.app')
.controller('CreateRegistryController', ['$scope', '$state', 'RegistryService', 'Notifications',
function ($scope, $state, RegistryService, Notifications) {
.controller('CreateRegistryController', ['$scope', '$state', 'RegistryService', 'Notifications', 'RegistryGitlabService', 'ExtensionService',
function ($scope, $state, RegistryService, Notifications, RegistryGitlabService, ExtensionService) {
$scope.selectQuayRegistry = selectQuayRegistry;
$scope.selectAzureRegistry = selectAzureRegistry;
$scope.selectCustomRegistry = selectCustomRegistry;
$scope.selectGitlabRegistry = selectGitlabRegistry;
$scope.create = createRegistry;
$scope.useDefaultGitlabConfiguration = useDefaultGitlabConfiguration;
$scope.retrieveGitlabRegistries = retrieveGitlabRegistries;
$scope.createGitlabRegistries = createGitlabRegistries;
$scope.state = {
actionInProgress: false
actionInProgress: false,
overrideConfiguration: false,
gitlab: {}
};
function selectQuayRegistry() {
@ -19,6 +26,18 @@ function ($scope, $state, RegistryService, Notifications) {
$scope.model.Authentication = true;
}
function useDefaultGitlabConfiguration() {
$scope.model.URL = 'https://registry.gitlab.com';
$scope.model.Gitlab.InstanceURL = 'https://gitlab.com';
}
function selectGitlabRegistry() {
$scope.model.Name = '';
$scope.model.Authentication = true;
$scope.model.Gitlab = {};
useDefaultGitlabConfiguration();
}
function selectAzureRegistry() {
$scope.model.Name = '';
$scope.model.URL = '';
@ -31,9 +50,32 @@ function ($scope, $state, RegistryService, Notifications) {
$scope.model.Authentication = false;
}
function createRegistry() {
$scope.model.URL = $scope.model.URL.replace(/^https?\:\/\//i, '');
function retrieveGitlabRegistries() {
$scope.state.actionInProgress = true;
RegistryGitlabService.projects($scope.model.Gitlab.InstanceURL, $scope.model.Token)
.then((data) => {
$scope.gitlabProjects = data;
}).catch((err) => {
Notifications.error('Failure', err, 'Unable to retrieve projects');
}).finally(() => {
$scope.state.actionInProgress = false;
});
}
function createGitlabRegistries() {
$scope.state.actionInProgress = true;
RegistryService.createGitlabRegistries($scope.model, $scope.state.gitlab.selectedItems)
.then(() => {
Notifications.success('Registries successfully created');
$state.go('portainer.registries');
}).catch((err) => {
Notifications.error('Failure', err, 'Unable to create registries');
}).finally(() => {
$scope.state.actionInProgress = false;
});
}
function createRegistry() {
$scope.state.actionInProgress = true;
RegistryService.createRegistry($scope.model)
.then(function success() {
@ -49,7 +91,10 @@ function ($scope, $state, RegistryService, Notifications) {
}
function initView() {
$scope.RegistryTypes = RegistryTypes;
$scope.model = new RegistryDefaultModel();
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.REGISTRY_MANAGEMENT)
.then((data) => $scope.registryExtensionEnabled = data);
}
initView();

View File

@ -18,9 +18,9 @@
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-click="selectQuayRegistry()">
<input type="radio" id="registry_quay" ng-model="model.Type" ng-value="1">
<label for="registry_quay">
<div>
<input type="radio" id="registry_quay" ng-model="model.Type" ng-value="RegistryTypes.QUAY">
<label for="registry_quay" ng-click="selectQuayRegistry()">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
Quay.io
@ -28,9 +28,9 @@
<p>Quay container registry</p>
</label>
</div>
<div ng-click="selectAzureRegistry()">
<input type="radio" id="registry_azure" ng-model="model.Type" ng-value="2">
<label for="registry_azure">
<div>
<input type="radio" id="registry_azure" ng-model="model.Type" ng-value="RegistryTypes.AZURE">
<label for="registry_azure" ng-click="selectAzureRegistry()">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Azure
@ -38,9 +38,19 @@
<p>Azure container registry</p>
</label>
</div>
<div ng-click="selectCustomRegistry()">
<input type="radio" id="registry_custom" ng-model="model.Type" ng-value="3">
<label for="registry_custom">
<div>
<input type="radio" id="registry_gitlab" ng-model="model.Type" ng-value="RegistryTypes.GITLAB">
<label for="registry_gitlab" ng-click="selectGitlabRegistry()">
<div class="boxselector_header">
<i class="fab fa-gitlab" aria-hidden="true" style="margin-right: 2px;"></i>
Gitlab
</div>
<p>Gitlab container registry</p>
</label>
</div>
<div>
<input type="radio" id="registry_custom" ng-model="model.Type" ng-value="RegistryTypes.CUSTOM">
<label for="registry_custom" ng-click="selectCustomRegistry()">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
Custom registry
@ -51,27 +61,36 @@
</div>
</div>
<registry-form-quay ng-if="model.Type === 1"
<registry-form-quay ng-if="model.Type === RegistryTypes.QUAY"
model="model"
form-action="create"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
></registry-form-quay>
<registry-form-azure ng-if="model.Type === 2"
<registry-form-azure ng-if="model.Type === RegistryTypes.AZURE"
model="model"
form-action="create"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
></registry-form-azure>
<registry-form-custom ng-if="model.Type === 3"
<registry-form-custom ng-if="model.Type === RegistryTypes.CUSTOM"
model="model"
form-action="create"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
></registry-form-custom>
<registry-form-gitlab ng-if="model.Type === RegistryTypes.GITLAB"
model="model"
retrieve-registries="retrieveGitlabRegistries"
create-registries="createGitlabRegistries"
projects="gitlabProjects"
state="state"
action-in-progress="state.actionInProgress"
reset-defaults="useDefaultGitlabConfiguration"
></registry-form-gitlab>
</form>
</rd-widget-body>
</rd-widget>