1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 15:59:41 +02:00

refactor(tags): refactor tag management (#3628)

* refactor(tags): replace tags with tag ids

* refactor(tags): revert tags to be strings and add tagids

* refactor(tags): enable search by tag in home view

* refactor(tags): show endpoint tags

* refactor(endpoints): expect tagIds on create payload

* refactor(endpoints): expect tagIds on update payload

* refactor(endpoints): replace TagIds to TagIDs

* refactor(endpoints): set endpoint group to get TagIDs

* refactor(endpoints): refactor tag-selector to receive tag-ids

* refactor(endpoints): show tags in multi-endpoint-selector

* chore(tags): revert reformat

* refactor(endpoints): remove unneeded bind

* refactor(endpoints): change param tags to tagids in endpoint create

* refactor(endpoints): remove console.log

* refactor(tags): remove deleted tag from endpoint and endpoint group

* fix(endpoints): show loading label while loading tags

* chore(go): remove obsolete import labels

* chore(db): add db version comment

* fix(db): add tag service to migrator

* refactor(db): add error checks in migrator

* style(db): sort props in alphabetical order

* style(tags): fix typo

Co-Authored-By: Anthony Lapenna <anthony.lapenna@portainer.io>

* refactor(endpoints): replace tagsMap with tag string representation

* refactor(tags): rewrite tag delete to be more readable

* refactor(home): rearange code to match former style

* refactor(tags): guard against missing model in tag-selector

* refactor(tags): rename vars in tag_delete

* refactor(tags): allow any authenticated user to fetch tag list

* refactor(endpoints): replace controller function with class

* refactor(endpoints): replace function with helper

* refactor(endpoints): replace controller with class

* refactor(tags): revert tags-selector to use 1 way bindings

* refactor(endpoints): load empty tag array instead of nil

* refactor(endpoints): revert default tag ids

* refactor(endpoints): use function in place

* refactor(tags): use lodash

* style(tags): use parens in arrow functions

* fix(tags): remove tag from tag model

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
This commit is contained in:
Chaim Lev-Ari 2020-03-29 12:54:14 +03:00 committed by GitHub
parent fe89a4fc01
commit edd86f2506
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 404 additions and 171 deletions

View file

@ -128,6 +128,7 @@ func (store *Store) MigrateData() error {
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
TemplateService: store.TemplateService,
UserService: store.UserService,

View file

@ -16,7 +16,7 @@ func (store *Store) Init() error {
Labels: []portainer.Pair{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Tags: []string{},
TagIDs: []portainer.TagID{},
}
err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)

View file

@ -0,0 +1,57 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateEndointsAndEndpointsGroupsToDBVersion23() error {
tags, err := m.tagService.Tags()
if err != nil {
return err
}
tagsNameMap := make(map[string]portainer.TagID)
for _, tag := range tags {
tagsNameMap[tag.Name] = tag.ID
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
endpointTags := make([]portainer.TagID, 0)
for _, tagName := range endpoint.Tags {
tagID, ok := tagsNameMap[tagName]
if ok {
endpointTags = append(endpointTags, tagID)
}
}
endpoint.TagIDs = endpointTags
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
endpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
endpointGroupTags := make([]portainer.TagID, 0)
for _, tagName := range endpointGroup.Tags {
tagID, ok := tagsNameMap[tagName]
if ok {
endpointGroupTags = append(endpointGroupTags, tagID)
}
}
endpointGroup.TagIDs = endpointGroupTags
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
}
return nil
}

View file

@ -12,6 +12,7 @@ import (
"github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/template"
"github.com/portainer/portainer/api/bolt/user"
@ -32,6 +33,7 @@ type (
scheduleService *schedule.Service
settingsService *settings.Service
stackService *stack.Service
tagService *tag.Service
teamMembershipService *teammembership.Service
templateService *template.Service
userService *user.Service
@ -52,6 +54,7 @@ type (
ScheduleService *schedule.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TemplateService *template.Service
UserService *user.Service
@ -73,6 +76,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
roleService: parameters.RoleService,
scheduleService: parameters.ScheduleService,
settingsService: parameters.SettingsService,
tagService: parameters.TagService,
teamMembershipService: parameters.TeamMembershipService,
templateService: parameters.TemplateService,
stackService: parameters.StackService,
@ -301,5 +305,13 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 1.24.0-dev
if m.currentDBVersion < 23 {
err := m.updateEndointsAndEndpointsGroupsToDBVersion23()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View file

@ -52,6 +52,19 @@ func (service *Service) Tags() ([]portainer.Tag, error) {
return tags, err
}
// Tag returns a tag by ID.
func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
var tag portainer.Tag
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &tag)
if err != nil {
return nil, err
}
return &tag, nil
}
// CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error {

View file

@ -259,7 +259,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
LogoURL: *flags.Logo,
AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
@ -397,7 +397,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@ -440,7 +440,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}

View file

@ -14,15 +14,15 @@ type endpointGroupCreatePayload struct {
Name string
Description string
AssociatedEndpoints []portainer.EndpointID
Tags []string
TagIDs []portainer.TagID
}
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid endpoint group name")
}
if payload.Tags == nil {
payload.Tags = []string{}
if payload.TagIDs == nil {
payload.TagIDs = []portainer.TagID{}
}
return nil
}
@ -40,7 +40,7 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
Description: payload.Description,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
}
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)

View file

@ -13,7 +13,7 @@ import (
type endpointGroupUpdatePayload struct {
Name string
Description string
Tags []string
TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
}
@ -50,8 +50,8 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
endpointGroup.Description = payload.Description
}
if payload.Tags != nil {
endpointGroup.Tags = payload.Tags
if payload.TagIDs != nil {
endpointGroup.TagIDs = payload.TagIDs
}
updateAuthorizations := false

View file

@ -32,7 +32,7 @@ type endpointCreatePayload struct {
AzureApplicationID string
AzureTenantID string
AzureAuthenticationKey string
Tags []string
TagIDs []portainer.TagID
}
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
@ -54,14 +54,14 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
}
payload.GroupID = groupID
var tags []string
err = request.RetrieveMultiPartFormJSONValue(r, "Tags", &tags, true)
var tagIDs []portainer.TagID
err = request.RetrieveMultiPartFormJSONValue(r, "TagIds", &tagIDs, true)
if err != nil {
return portainer.Error("Invalid Tags parameter")
return portainer.Error("Invalid TagIds parameter")
}
payload.Tags = tags
if payload.Tags == nil {
payload.Tags = make([]string, 0)
payload.TagIDs = tagIDs
if payload.TagIDs == nil {
payload.TagIDs = make([]portainer.TagID, 0)
}
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
@ -187,7 +187,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
AzureCredentials: credentials,
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@ -232,7 +232,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
EdgeKey: edgeKey,
@ -278,7 +278,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@ -322,7 +322,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload)
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}

View file

@ -5,7 +5,7 @@ import (
"strconv"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/libhttp/request"
@ -52,7 +52,15 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
}
if search != "" {
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, search)
tags, err := handler.TagsService.Tags()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err}
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
}
if endpointType != 0 {
@ -102,17 +110,17 @@ func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID po
return filteredEndpoints
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) []portainer.Endpoint {
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpointMatchSearchCriteria(&endpoint, searchCriteria) {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, searchCriteria) {
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
@ -120,7 +128,7 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro
return filteredEndpoints
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria string) bool {
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
@ -134,8 +142,7 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range endpoint.Tags {
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
@ -144,14 +151,14 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st
return false
}
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) bool {
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
for _, tag := range group.Tags {
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
@ -172,3 +179,11 @@ func filterEndpointsByType(endpoints []portainer.Endpoint, endpointType portaine
}
return filteredEndpoints
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}

View file

@ -24,7 +24,7 @@ type endpointUpdatePayload struct {
AzureApplicationID *string
AzureTenantID *string
AzureAuthenticationKey *string
Tags []string
TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
}
@ -73,8 +73,8 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.GroupID = portainer.EndpointGroupID(*payload.GroupID)
}
if payload.Tags != nil {
endpoint.Tags = payload.Tags
if payload.TagIDs != nil {
endpoint.TagIDs = payload.TagIDs
}
updateAuthorizations := false

View file

@ -37,6 +37,7 @@ type Handler struct {
JobService portainer.JobService
ReverseTunnelService portainer.ReverseTunnelService
SettingsService portainer.SettingsService
TagsService portainer.TagService
AuthorizationService *portainer.AuthorizationService
}

View file

@ -12,7 +12,9 @@ import (
// Handler is the HTTP handler used to handle tag operations.
type Handler struct {
*mux.Router
TagService portainer.TagService
TagService portainer.TagService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
}
// NewHandler creates a handler to manage tag operations.
@ -23,7 +25,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/tags",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
h.Handle("/tags",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet)
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet)
h.Handle("/tags/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)

View file

@ -15,6 +15,39 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid tag identifier route variable", err}
}
tagID := portainer.TagID(id)
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
tagIdx := findTagIndex(endpoint.TagIDs, tagID)
if tagIdx != -1 {
endpoint.TagIDs = removeElement(endpoint.TagIDs, tagIdx)
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpointGroup := range endpointGroups {
tagIdx := findTagIndex(endpointGroup.TagIDs, tagID)
if tagIdx != -1 {
endpointGroup.TagIDs = removeElement(endpointGroup.TagIDs, tagIdx)
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err}
}
}
}
err = handler.TagService.DeleteTag(portainer.TagID(id))
if err != nil {
@ -23,3 +56,21 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe
return response.Empty(w)
}
func findTagIndex(tags []portainer.TagID, searchTagID portainer.TagID) int {
for idx, tagID := range tags {
if searchTagID == tagID {
return idx
}
}
return -1
}
func removeElement(arr []portainer.TagID, index int) []portainer.TagID {
if index < 0 {
return arr
}
lastTagIdx := len(arr) - 1
arr[index] = arr[lastTagIdx]
return arr[:lastTagIdx]
}

View file

@ -7,7 +7,7 @@ import (
"github.com/portainer/portainer/api/http/handler/roles"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/handler"
"github.com/portainer/portainer/api/http/handler/auth"
@ -222,6 +222,8 @@ func (server *Server) Start() error {
var tagHandler = tags.NewHandler(requestBouncer)
tagHandler.TagService = server.TagService
tagHandler.EndpointService = server.EndpointService
tagHandler.EndpointGroupService = server.EndpointGroupService
var teamHandler = teams.NewHandler(requestBouncer)
teamHandler.TeamService = server.TeamService

View file

@ -260,7 +260,7 @@ type (
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
Tags []string `json:"Tags"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []Snapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
@ -277,6 +277,9 @@ type (
// Deprecated in DBVersion == 18
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
// Deprecated in DBVersion == 22
Tags []string `json:"Tags"`
}
// Authorization represents an authorization associated to an operation
@ -426,7 +429,7 @@ type (
Description string `json:"Description"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
Tags []string `json:"Tags"`
TagIDs []TagID `json:"TagIds"`
// Deprecated fields
Labels []Pair `json:"Labels"`
@ -434,6 +437,9 @@ type (
// Deprecated in DBVersion == 18
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
// Deprecated in DBVersion == 22
Tags []string `json:"Tags"`
}
// EndpointExtension represents a deprecated form of Portainer extension
@ -775,6 +781,7 @@ type (
// TagService represents a service for managing tag data
TagService interface {
Tags() ([]Tag, error)
Tag(ID TagID) (*Tag, error)
CreateTag(tag *Tag) error
DeleteTag(ID TagID) error
}
@ -919,7 +926,7 @@ const (
// APIVersion is the version number of the Portainer API
APIVersion = "1.24.0-dev"
// DBVersion is the version number of the Portainer database
DBVersion = 22
DBVersion = 23
// AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved