package cache import ( "fmt" "os" "sync" "time" "github.com/pterm/pterm" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/config" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/projects" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/remotes/remote" "gopkg.in/yaml.v3" ) type Cache struct { Projects []*projects.Project Aliases []*ProjectAlias Updated time.Time config *config.Config readFromFile bool lock *sync.Mutex // Lock the entire cache contentLock *sync.Mutex // Lock projects / aliases ttl time.Duration file string log *pterm.Logger path string gitlabs *remotes.Clients remotes *remote.Remotes } type CacheOpts struct { Path string ProjectsPath string TTL time.Duration Logger *pterm.Logger Gitlabs *remotes.Clients Remotes *remote.Remotes Config *config.Config } // Load cache, if already loaded and up to date, nothing is done. // If the cache is not yet loaded from disk, it is loaded // If the updated timestamp is beyond the set ttl, a refresh is triggered func (c *Cache) Load() error { var err error if !c.readFromFile { c.Read() } if time.Since(c.Updated) > c.ttl { c.log.Info("Project cache stale, updating.") c.Refresh() } return err } func (c *Cache) UnlockCache() { c.log.Info("Unlocking cache") if err := os.Remove(c.file + ".lock"); err != nil { c.log.Fatal("Failed unlocking cache, manual rm may be necessary", c.log.Args("error", err, "lockFile", c.file+".lock"), ) } } func (c *Cache) LockCache() { c.log.Info("Attempting to lock cache") c.checkLock() file, err := os.OpenFile(c.file+".lock", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0640) defer file.Close() if err != nil { c.log.Fatal("Failed to lock cache", c.log.Args("error", err)) } } func (c *Cache) checkLock() { if _, err := os.Stat(c.file + ".lock"); err == nil { c.log.Fatal("Can't manage cache, already locked") } } // Saves the current state of the cache to disk func (c *Cache) write() { file, err := os.OpenFile(c.file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0640) if err != nil { c.log.Error("Failed to write cache to disk", c.log.Args("error", err)) } d := yaml.NewEncoder(file) if err = d.Encode(*c); err != nil { c.log.Error("Failed to Marshal cache to yaml", c.log.Args("error", err)) } else { c.log.Debug("Cache saved to disk") } } func (c *Cache) Write() { c.lock.Lock() defer c.lock.Unlock() c.log.Debug("Saving cache to disk", c.log.Args( "projects", len(c.Projects), "aliases", len(c.Aliases), )) c.write() } // Loads and unmarshals the project cache from disk. func (c *Cache) Read() error { c.lock.Lock() defer c.lock.Unlock() c.log.Debug("Reading project cache from disk", c.log.Args("file", c.file)) file, err := os.Open(c.file) if err != nil { c.log.Error("Failed to read project cache", c.log.Args("error", err)) return err } d := yaml.NewDecoder(file) d.Decode(c) c.readFromFile = true return nil } // Resets projects cache and also optionally clears // project aliase cache. Writes to disk. func (c *Cache) clear(clearAliases bool) { c.log.Info("Clearing project cache") c.Projects = make([]*projects.Project, 0) if clearAliases { c.log.Info("Clearing project alias cache") c.Aliases = make([]*ProjectAlias, 0) } c.setUpdated() } func (c *Cache) Clear(clearAliases bool) { c.lock.Lock() defer c.lock.Unlock() c.clear(clearAliases) } func (c *Cache) refresh() { c.log.Info("Loading project cache, this may take a while\n") defer c.setUpdated() // Fix any dangling aliases // For backwards-compatibility only c.setAliasRemotes() // Retrieve and add/update projects c.LoadGitlabs() } // Iterates through all GitLab projects the user has access to, updating // the project cache where necessary func (c *Cache) Refresh() { c.lock.Lock() defer c.lock.Unlock() c.refresh() } func (c *Cache) String() string { cacheString := fmt.Sprintf("Cache Updated %s: Projects %d, Aliases %d\nRemotes %d:", c.Updated.String(), len(c.Projects), len(c.Aliases), len(*c.gitlabs), ) for _, r := range *c.gitlabs { cacheString += " " + r.Config.Host } return cacheString } func (c *Cache) setUpdated() { c.Updated = time.Now() } func (c *Cache) SetUpdated() { c.lock.Lock() defer c.lock.Unlock() c.setUpdated() } func (c *Cache) GetUpdated() time.Time { return c.Updated } // Returns a new project cache ready to load from // cache file. // Cache.Load() must be called manually func NewProjectCache(opts *CacheOpts) (*Cache, error) { var err error if _, err = os.Stat(opts.Path); err != nil { err = createProjectCache(opts.Path) } // Combine old-and-new gitlabs var gitlabs *remotes.Clients if opts.Gitlabs != nil { gitlabs = opts.Gitlabs } else { gitlabs = remotes.NewCLients() } cache := &Cache{ Projects: make([]*projects.Project, 0), Aliases: make([]*ProjectAlias, 0), config: opts.Config, file: opts.Path, ttl: opts.TTL, lock: &sync.Mutex{}, contentLock: &sync.Mutex{}, log: opts.Logger, gitlabs: gitlabs, remotes: remote.NewRemotes(), path: opts.ProjectsPath, } return cache, err } func createProjectCache(path string) error { return os.WriteFile(path, nil, 0640) }