diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go index 202a81fdc..3b2646dcb 100644 --- a/api/http/handler/registries/handler.go +++ b/api/http/handler/registries/handler.go @@ -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 } diff --git a/api/http/handler/registries/proxy.go b/api/http/handler/registries/proxy.go index d54520508..3f94bed4a 100644 --- a/api/http/handler/registries/proxy.go +++ b/api/http/handler/registries/proxy.go @@ -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} } } diff --git a/api/http/handler/registries/proxy_gitlab.go b/api/http/handler/registries/proxy_gitlab.go new file mode 100644 index 000000000..47b5f4169 --- /dev/null +++ b/api/http/handler/registries/proxy_gitlab.go @@ -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 +} diff --git a/api/http/handler/registries/proxy_management_gitlab.go b/api/http/handler/registries/proxy_management_gitlab.go new file mode 100644 index 000000000..28f1ead12 --- /dev/null +++ b/api/http/handler/registries/proxy_management_gitlab.go @@ -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 +} diff --git a/api/http/handler/registries/registry_create.go b/api/http/handler/registries/registry_create.go index 1b6dbf638..09f6d0a2e 100644 --- a/api/http/handler/registries/registry_create.go +++ b/api/http/handler/registries/registry_create.go @@ -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) diff --git a/api/http/proxy/gitlab.go b/api/http/proxy/gitlab.go new file mode 100644 index 000000000..b809a09a3 --- /dev/null +++ b/api/http/proxy/gitlab.go @@ -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) +} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 7a1f38580..aa4a68bef 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -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) +} diff --git a/api/portainer.go b/api/portainer.go index aac60aad4..078fde6ce 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -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 ( diff --git a/app/app.js b/app/app.js index db35643a2..7b3419893 100644 --- a/app/app.js +++ b/app/app.js @@ -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' }); } }); diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index 107115f21..ab169a4c5 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -121,7 +121,7 @@ angular.module('portainer.docker') if (!portBinding.containerPort) { return; } - + let hostPort = portBinding.hostPort; const containerPortRange = parsePortRange(portBinding.containerPort); if (!isValidPortRange(containerPortRange)) { diff --git a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html index b076acffd..88c085a3d 100644 --- a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html +++ b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html @@ -30,7 +30,7 @@ - {{ item.Name }} {{ item.TagsCount }} diff --git a/app/extensions/registry-management/helpers/localRegistryHelper.js b/app/extensions/registry-management/helpers/localRegistryHelper.js index 5e74f09ab..f68073f41 100644 --- a/app/extensions/registry-management/helpers/localRegistryHelper.js +++ b/app/extensions/registry-management/helpers/localRegistryHelper.js @@ -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; diff --git a/app/extensions/registry-management/models/gitlabRegistry.js b/app/extensions/registry-management/models/gitlabRegistry.js new file mode 100644 index 000000000..7a824873d --- /dev/null +++ b/app/extensions/registry-management/models/gitlabRegistry.js @@ -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; +} diff --git a/app/extensions/registry-management/models/registryRepository.js b/app/extensions/registry-management/models/registryRepository.js index bc509225a..1d7353af1 100644 --- a/app/extensions/registry-management/models/registryRepository.js +++ b/app/extensions/registry-management/models/registryRepository.js @@ -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; @@ -7,4 +7,9 @@ export default function RegistryRepositoryViewModel(item) { this.Name = item; this.TagsCount = 0; } +} + +export function RegistryRepositoryGitlabViewModel(data) { + this.Name = data.path; + this.TagsCount = data.tags.length; } \ No newline at end of file diff --git a/app/extensions/registry-management/models/registryTypes.js b/app/extensions/registry-management/models/registryTypes.js new file mode 100644 index 000000000..2c218f339 --- /dev/null +++ b/app/extensions/registry-management/models/registryTypes.js @@ -0,0 +1,6 @@ +export const RegistryTypes = Object.freeze({ + 'QUAY': 1, + 'AZURE': 2, + 'CUSTOM': 3, + 'GITLAB': 4 +}) \ No newline at end of file diff --git a/app/extensions/registry-management/models/repositoryTag.js b/app/extensions/registry-management/models/repositoryTag.js index 37d7b7cc2..19ffd6965 100644 --- a/app/extensions/registry-management/models/repositoryTag.js +++ b/app/extensions/registry-management/models/repositoryTag.js @@ -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) { @@ -13,4 +14,9 @@ export function RepositoryShortTag(name, imageId, imageDigest, manifest) { this.ImageId = imageId; this.ImageDigest = imageDigest; this.ManifestV2 = manifest; -} \ No newline at end of file +} + +export function RepositoryAddTagPayload(tag, manifest) { + this.Tag = tag; + this.Manifest = manifest; +} diff --git a/app/extensions/registry-management/rest/gitlab.js b/app/extensions/registry-management/rest/gitlab.js new file mode 100644 index 000000000..cb69219aa --- /dev/null +++ b/app/extensions/registry-management/rest/gitlab.js @@ -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 + } + }); + }; +}]); diff --git a/app/extensions/registry-management/rest/transform/gitlabResponseGetLink.js b/app/extensions/registry-management/rest/transform/gitlabResponseGetLink.js new file mode 100644 index 000000000..759bbd681 --- /dev/null +++ b/app/extensions/registry-management/rest/transform/gitlabResponseGetLink.js @@ -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; +} \ No newline at end of file diff --git a/app/extensions/registry-management/services/registryGitlabService.js b/app/extensions/registry-management/services/registryGitlabService.js new file mode 100644 index 000000000..b405e889e --- /dev/null +++ b/app/extensions/registry-management/services/registryGitlabService.js @@ -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; +} +]); diff --git a/app/extensions/registry-management/services/registryServiceSelector.js b/app/extensions/registry-management/services/registryServiceSelector.js new file mode 100644 index 000000000..f8b155188 --- /dev/null +++ b/app/extensions/registry-management/services/registryServiceSelector.js @@ -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; +} +]); diff --git a/app/extensions/registry-management/services/registryV2Service.js b/app/extensions/registry-management/services/registryV2Service.js index f0094f6d6..b0ba0bc11 100644 --- a/app/extensions/registry-management/services/registryV2Service.js +++ b/app/extensions/registry-management/services/registryV2Service.js @@ -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; } ]); diff --git a/app/extensions/registry-management/views/configure/configureRegistryController.js b/app/extensions/registry-management/views/configure/configureRegistryController.js index 75311e01a..cd6d33a1d 100644 --- a/app/extensions/registry-management/views/configure/configureRegistryController.js +++ b/app/extensions/registry-management/views/configure/configureRegistryController.js @@ -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) { diff --git a/app/extensions/registry-management/views/configure/configureregistry.html b/app/extensions/registry-management/views/configure/configureregistry.html index 3325230b3..3bf998f48 100644 --- a/app/extensions/registry-management/views/configure/configureregistry.html +++ b/app/extensions/registry-management/views/configure/configureregistry.html @@ -32,7 +32,7 @@ -
+
-
+
diff --git a/app/extensions/registry-management/views/repositories/edit/registryRepository.html b/app/extensions/registry-management/views/repositories/edit/registryRepository.html index 37da97c6a..ee431749c 100644 --- a/app/extensions/registry-management/views/repositories/edit/registryRepository.html +++ b/app/extensions/registry-management/views/repositories/edit/registryRepository.html @@ -48,6 +48,8 @@ Repository {{ repository.Name }} + + @@ -56,10 +58,12 @@ Tags count {{ repository.Tags.length }} + Images count {{ short.Images.length }} + diff --git a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js index b37856c54..30f9a6c1a 100644 --- a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js +++ b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js @@ -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; diff --git a/app/extensions/registry-management/views/repositories/registryRepositoriesController.js b/app/extensions/registry-management/views/repositories/registryRepositoriesController.js index eace4f9f3..aed77e343 100644 --- a/app/extensions/registry-management/views/repositories/registryRepositoriesController.js +++ b/app/extensions/registry-management/views/repositories/registryRepositoriesController.js @@ -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; diff --git a/app/extensions/registry-management/views/repositories/tag/registryRepositoryTagController.js b/app/extensions/registry-management/views/repositories/tag/registryRepositoryTagController.js index 4f34b89f1..9136302ae 100644 --- a/app/extensions/registry-management/views/repositories/tag/registryRepositoryTagController.js +++ b/app/extensions/registry-management/views/repositories/tag/registryRepositoryTagController.js @@ -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)) diff --git a/app/portainer/components/forms/registry-form-azure/registry-form-azure.html b/app/portainer/components/forms/registry-form-azure/registry-form-azure.html index 9165749f7..1de3fdd9e 100644 --- a/app/portainer/components/forms/registry-form-azure/registry-form-azure.html +++ b/app/portainer/components/forms/registry-form-azure/registry-form-azure.html @@ -18,7 +18,7 @@
-
+
-
+