github.com/esnet/gdg@v0.6.1-0.20240412190737-6b6eba9c14d8/internal/service/folders.go (about)

     1  package service
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/esnet/gdg/internal/config"
     8  	"github.com/esnet/gdg/internal/service/filters"
     9  	"github.com/gosimple/slug"
    10  	"github.com/grafana/grafana-openapi-client-go/client/folder_permissions"
    11  	"github.com/grafana/grafana-openapi-client-go/client/folders"
    12  	"github.com/grafana/grafana-openapi-client-go/client/search"
    13  	"github.com/grafana/grafana-openapi-client-go/models"
    14  	"github.com/tidwall/gjson"
    15  	"golang.org/x/exp/slices"
    16  	"log"
    17  	"log/slog"
    18  	"path/filepath"
    19  	"strings"
    20  )
    21  
    22  func NewFolderFilter() filters.Filter {
    23  	filterObj := filters.NewBaseFilter()
    24  	filterObj.AddValidation(filters.FolderFilter, func(i interface{}) bool {
    25  		val, ok := i.(map[filters.FilterType]string)
    26  		if !ok {
    27  			return ok
    28  		}
    29  		//Check folder
    30  		if folderFilter, ok := val[filters.FolderFilter]; ok {
    31  			return slices.Contains(config.Config().GetDefaultGrafanaConfig().GetMonitoredFolders(), folderFilter)
    32  		} else {
    33  			return true
    34  		}
    35  	})
    36  	return filterObj
    37  
    38  }
    39  
    40  // checkFolderName returns true if folder is valid, otherwise false if special chars are found
    41  // in folder name.
    42  func (s *DashNGoImpl) checkFolderName(folderName string) bool {
    43  	if strings.Contains(folderName, "/") || strings.Contains(folderName, "\\") {
    44  		return false
    45  	}
    46  	return true
    47  }
    48  
    49  // DownloadFolderPermissions downloads all the current folder permissions based on filter.
    50  func (s *DashNGoImpl) DownloadFolderPermissions(filter filters.Filter) []string {
    51  	slog.Info("Downloading folder permissions")
    52  	var (
    53  		dsPacked  []byte
    54  		err       error
    55  		dataFiles []string
    56  	)
    57  	currentPermissions := s.ListFolderPermissions(filter)
    58  	for folder, permission := range currentPermissions {
    59  		if dsPacked, err = json.MarshalIndent(permission, "", "	"); err != nil {
    60  			slog.Error("Unable to marshall file", "err", err, "folderName", folder.Title)
    61  			continue
    62  		}
    63  		dsPath := buildResourcePath(slug.Make(folder.Title), config.FolderPermissionResource)
    64  		if err = s.storage.WriteFile(dsPath, dsPacked); err != nil {
    65  			slog.Error("Unable to write file", "err", err.Error(), "filename", slug.Make(folder.Title))
    66  		} else {
    67  			dataFiles = append(dataFiles, dsPath)
    68  		}
    69  	}
    70  	return dataFiles
    71  
    72  }
    73  
    74  // UploadFolderPermissions update current folder permissions to match local file system.
    75  // Note: This expects all the current users and teams to already exist.
    76  func (s *DashNGoImpl) UploadFolderPermissions(filter filters.Filter) []string {
    77  	var (
    78  		rawFolder []byte
    79  		dataFiles []string
    80  	)
    81  	filesInDir, err := s.storage.FindAllFiles(config.Config().GetDefaultGrafanaConfig().GetPath(config.FolderPermissionResource), false)
    82  	if err != nil {
    83  		log.Fatalf("Failed to read folders permission imports, %v", err)
    84  	}
    85  	for _, file := range filesInDir {
    86  		fileLocation := filepath.Join(config.Config().GetDefaultGrafanaConfig().GetPath(config.FolderPermissionResource), file)
    87  		if strings.HasSuffix(file, ".json") {
    88  			if rawFolder, err = s.storage.ReadFile(fileLocation); err != nil {
    89  				slog.Error("failed to read file", "filename", fileLocation, "err", err)
    90  				continue
    91  			}
    92  		}
    93  		uid := gjson.GetBytes(rawFolder, "0.uid")
    94  
    95  		newEntries := make([]*models.DashboardACLUpdateItem, 0)
    96  		err = json.Unmarshal(rawFolder, &newEntries)
    97  		if err != nil {
    98  			slog.Warn("Failed to Decode payload file", "filename", fileLocation)
    99  			continue
   100  		}
   101  		payload := &models.UpdateDashboardACLCommand{
   102  			Items: newEntries,
   103  		}
   104  
   105  		_, err := s.GetClient().FolderPermissions.UpdateFolderPermissions(uid.String(), payload)
   106  		if err != nil {
   107  			slog.Error("Failed to update folder permissions")
   108  		} else {
   109  			dataFiles = append(dataFiles, fileLocation)
   110  
   111  		}
   112  	}
   113  	slog.Info("Patching server with local folder permissions")
   114  	return dataFiles
   115  }
   116  
   117  // ListFolderPermissions retrieves all current folder permissions
   118  // TODO: add concurrency to folder permissions calls
   119  func (s *DashNGoImpl) ListFolderPermissions(filter filters.Filter) map[*models.Hit][]*models.DashboardACLInfoDTO {
   120  	//get list of folders
   121  	foldersList := s.ListFolder(filter)
   122  
   123  	r := make(map[*models.Hit][]*models.DashboardACLInfoDTO, 0)
   124  
   125  	for ndx, foldersEntry := range foldersList {
   126  		results, err := s.GetClient().FolderPermissions.GetFolderPermissionList(foldersEntry.UID)
   127  		if err != nil {
   128  			msg := fmt.Sprintf("Unable to get folder permissions for folderUID: %s", foldersEntry.UID)
   129  
   130  			var getFolderPermissionListInternalServerError *folder_permissions.GetFolderPermissionListInternalServerError
   131  			switch {
   132  			case errors.As(err, &getFolderPermissionListInternalServerError):
   133  				var castError *folder_permissions.GetFolderPermissionListInternalServerError
   134  				errors.As(err, &castError)
   135  				slog.Error(msg, "message", *castError.GetPayload().Message, "err", err)
   136  			default:
   137  				slog.Error(msg, "err", err)
   138  			}
   139  		} else {
   140  			r[foldersList[ndx]] = results.GetPayload()
   141  		}
   142  	}
   143  
   144  	return r
   145  }
   146  
   147  // ListFolder list the current existing folders that match the given filter.
   148  func (s *DashNGoImpl) ListFolder(filter filters.Filter) []*models.Hit {
   149  	var result = make([]*models.Hit, 0)
   150  	if config.Config().GetDefaultGrafanaConfig().GetFilterOverrides().IgnoreDashboardFilters {
   151  		filter = nil
   152  	}
   153  	p := search.NewSearchParams()
   154  	p.Type = &searchTypeFolder
   155  	folderListing, err := s.GetClient().Search.Search(p)
   156  	if err != nil {
   157  		log.Fatal("unable to retrieve folder list.")
   158  	}
   159  
   160  	for ndx, val := range folderListing.GetPayload() {
   161  		valid := s.checkFolderName(val.Title)
   162  		if filter == nil {
   163  			if !valid {
   164  				slog.Warn("Folder has an invalid character and is not supported. Path separators are not allowed", "folderName", val.Title)
   165  				continue
   166  			}
   167  			result = append(result, folderListing.GetPayload()[ndx])
   168  		} else if filter.ValidateAll(map[filters.FilterType]string{filters.FolderFilter: val.Title}) {
   169  			if !valid {
   170  				slog.Warn("Folder has an invalid character and is not supported. Path separators are not allowed", "folderName", val.Title)
   171  				continue
   172  			}
   173  			result = append(result, folderListing.GetPayload()[ndx])
   174  		}
   175  	}
   176  
   177  	return result
   178  
   179  }
   180  
   181  // DownloadFolders Download all the given folders matching filter
   182  func (s *DashNGoImpl) DownloadFolders(filter filters.Filter) []string {
   183  	var (
   184  		dsPacked  []byte
   185  		err       error
   186  		dataFiles []string
   187  	)
   188  	folderListing := s.ListFolder(filter)
   189  	for _, folder := range folderListing {
   190  		if dsPacked, err = json.MarshalIndent(folder, "", "	"); err != nil {
   191  			slog.Error("Unable to serialize data to JSON", "err", err, "folderName", folder.Title)
   192  			continue
   193  		}
   194  		dsPath := buildResourcePath(slug.Make(folder.Title), config.FolderResource)
   195  		if err = s.storage.WriteFile(dsPath, dsPacked); err != nil {
   196  			slog.Error("Unable to write file.", "err", err.Error(), "folderName", slug.Make(folder.Title))
   197  		} else {
   198  			dataFiles = append(dataFiles, dsPath)
   199  		}
   200  	}
   201  
   202  	return dataFiles
   203  }
   204  
   205  // UploadFolders upload all the given folders to grafana
   206  func (s *DashNGoImpl) UploadFolders(filter filters.Filter) []string {
   207  	var (
   208  		result    []string
   209  		rawFolder []byte
   210  	)
   211  	filesInDir, err := s.storage.FindAllFiles(config.Config().GetDefaultGrafanaConfig().GetPath(config.FolderResource), false)
   212  	if err != nil {
   213  		log.Fatalf("Failed to read folders imports, %v", err)
   214  	}
   215  	folderItems := s.ListFolder(filter)
   216  
   217  	for _, file := range filesInDir {
   218  		fileLocation := filepath.Join(config.Config().GetDefaultGrafanaConfig().GetPath(config.FolderResource), file)
   219  		if strings.HasSuffix(file, ".json") {
   220  			if rawFolder, err = s.storage.ReadFile(fileLocation); err != nil {
   221  				slog.Error("failed to read file", "filename", fileLocation, "err", err)
   222  				continue
   223  			}
   224  		}
   225  
   226  		var newFolder models.CreateFolderCommand
   227  		//var newFolder models.CreateFolderCommand
   228  		if err = json.Unmarshal(rawFolder, &newFolder); err != nil {
   229  			slog.Warn("failed to unmarshall folder", "err", err)
   230  			continue
   231  		}
   232  		if !s.checkFolderName(newFolder.Title) {
   233  			slog.Warn("Folder has an invalid character and is not supported, skipping folder", "folderName", newFolder.Title)
   234  			continue
   235  		}
   236  		skipCreate := false
   237  		for _, existingFolder := range folderItems {
   238  			if existingFolder.UID == newFolder.UID {
   239  				slog.Warn("Folder already exists, skipping", "folderName", existingFolder.Title)
   240  				skipCreate = true
   241  			}
   242  
   243  		}
   244  		if skipCreate {
   245  			continue
   246  		}
   247  		params := folders.NewCreateFolderParams()
   248  		params.Body = &newFolder
   249  		f, err := s.GetClient().Folders.CreateFolder(&newFolder)
   250  		if err != nil {
   251  			slog.Error("failed to create folder.", "folderName", newFolder.Title, "err", err)
   252  			continue
   253  		}
   254  		result = append(result, f.Payload.Title)
   255  
   256  	}
   257  	return result
   258  }
   259  
   260  // DeleteAllFolders deletes all the matching folders from grafana
   261  func (s *DashNGoImpl) DeleteAllFolders(filter filters.Filter) []string {
   262  	var result []string
   263  	folderListing := s.ListFolder(filter)
   264  	for _, folder := range folderListing {
   265  		params := folders.NewDeleteFolderParams()
   266  		params.FolderUID = folder.UID
   267  		_, err := s.GetClient().Folders.DeleteFolder(params)
   268  		if err == nil {
   269  			result = append(result, folder.Title)
   270  		}
   271  	}
   272  	return result
   273  }
   274  
   275  // getFolderNameIDMap helper function to build a mapping for name to folderID
   276  func getFolderNameIDMap(folders []*models.Hit) map[string]int64 {
   277  	var folderMap = make(map[string]int64)
   278  	for _, folder := range folders {
   279  		folderMap[folder.Title] = folder.ID
   280  	}
   281  	return folderMap
   282  }
   283  
   284  // Creates a reverse look up map, where the values are the keys and the keys are the values.
   285  func reverseLookUp[T comparable, Y comparable](m map[T]Y) map[Y]T {
   286  	reverse := make(map[Y]T, 0)
   287  	for key, val := range m {
   288  		reverse[val] = key
   289  	}
   290  
   291  	return reverse
   292  }