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 }