mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
206 lines
4.5 KiB
Go
206 lines
4.5 KiB
Go
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
|
|
// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License-AGPL.txt in the project root for license information.
|
|
|
|
package config
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/sirupsen/logrus"
|
|
"gopkg.in/yaml.v2"
|
|
|
|
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
|
|
)
|
|
|
|
// ConfigInterface provides access to the gitpod config file.
|
|
type ConfigInterface interface {
|
|
// Watch starts the config watching
|
|
Watch(ctx context.Context)
|
|
// Observe provides channels triggered whenever the config is changed
|
|
Observe(ctx context.Context) <-chan *gitpod.GitpodConfig
|
|
}
|
|
|
|
// ConfigService provides access to the gitpod config file.
|
|
type ConfigService struct {
|
|
location string
|
|
locationReady <-chan struct{}
|
|
|
|
cond *sync.Cond
|
|
config *gitpod.GitpodConfig
|
|
|
|
pollTimer *time.Timer
|
|
|
|
log *logrus.Entry
|
|
|
|
ready chan struct{}
|
|
readyOnce sync.Once
|
|
|
|
debounceDuration time.Duration
|
|
}
|
|
|
|
// NewConfigService creates a new instance of ConfigService.
|
|
func NewConfigService(configLocation string, locationReady <-chan struct{}, log *logrus.Entry) *ConfigService {
|
|
return &ConfigService{
|
|
location: configLocation,
|
|
locationReady: locationReady,
|
|
cond: sync.NewCond(&sync.Mutex{}),
|
|
log: log.WithField("location", configLocation),
|
|
ready: make(chan struct{}),
|
|
debounceDuration: 100 * time.Millisecond,
|
|
}
|
|
}
|
|
|
|
// Observe provides channels triggered whenever the config is changed.
|
|
func (service *ConfigService) Observe(ctx context.Context) <-chan *gitpod.GitpodConfig {
|
|
configs := make(chan *gitpod.GitpodConfig)
|
|
go func() {
|
|
defer close(configs)
|
|
|
|
<-service.ready
|
|
|
|
service.cond.L.Lock()
|
|
defer service.cond.L.Unlock()
|
|
for {
|
|
configs <- service.config
|
|
|
|
service.cond.Wait()
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return configs
|
|
}
|
|
|
|
// Watch starts the config watching.
|
|
func (service *ConfigService) Watch(ctx context.Context) {
|
|
service.log.Info("gitpod config watcher: starting...")
|
|
|
|
select {
|
|
case <-service.locationReady:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
|
|
_, err := os.Stat(service.location)
|
|
if os.IsNotExist(err) {
|
|
service.poll(ctx)
|
|
}
|
|
service.watch(ctx)
|
|
}
|
|
|
|
func (service *ConfigService) markReady() {
|
|
service.readyOnce.Do(func() {
|
|
close(service.ready)
|
|
})
|
|
}
|
|
|
|
func (service *ConfigService) watch(ctx context.Context) {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
defer func() {
|
|
if err != nil {
|
|
service.log.WithError(err).Error("gitpod config watcher: failed to start")
|
|
return
|
|
}
|
|
|
|
service.log.Info("gitpod config watcher: started")
|
|
}()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = watcher.Add(service.location)
|
|
if err != nil {
|
|
watcher.Close()
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
defer service.log.Info("gitpod config watcher: stopped")
|
|
defer watcher.Close()
|
|
|
|
polling := make(chan struct{}, 1)
|
|
service.scheduleUpdateConfig(ctx, polling)
|
|
for {
|
|
select {
|
|
case <-polling:
|
|
return
|
|
case <-ctx.Done():
|
|
return
|
|
case err := <-watcher.Errors:
|
|
service.log.WithError(err).Error("gitpod config watcher: failed to watch")
|
|
case <-watcher.Events:
|
|
service.scheduleUpdateConfig(ctx, polling)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (service *ConfigService) scheduleUpdateConfig(ctx context.Context, polling chan<- struct{}) {
|
|
service.cond.L.Lock()
|
|
defer service.cond.L.Unlock()
|
|
if service.pollTimer != nil {
|
|
service.pollTimer.Stop()
|
|
}
|
|
service.pollTimer = time.AfterFunc(service.debounceDuration, func() {
|
|
err := service.updateConfig()
|
|
if os.IsNotExist(err) {
|
|
polling <- struct{}{}
|
|
go service.poll(ctx)
|
|
} else if err != nil {
|
|
service.log.WithError(err).Error("gitpod config watcher: failed to parse")
|
|
}
|
|
})
|
|
}
|
|
|
|
func (service *ConfigService) poll(ctx context.Context) {
|
|
service.markReady()
|
|
|
|
timer := time.NewTicker(2 * time.Second)
|
|
defer timer.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-timer.C:
|
|
}
|
|
|
|
if _, err := os.Stat(service.location); !os.IsNotExist(err) {
|
|
service.watch(ctx)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (service *ConfigService) updateConfig() error {
|
|
service.cond.L.Lock()
|
|
defer service.cond.L.Unlock()
|
|
|
|
config, err := service.parse()
|
|
if err == nil || os.IsNotExist(err) {
|
|
service.config = config
|
|
service.markReady()
|
|
service.cond.Broadcast()
|
|
|
|
service.log.WithField("config", service.config).Debug("gitpod config watcher: updated")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (service *ConfigService) parse() (*gitpod.GitpodConfig, error) {
|
|
data, err := os.ReadFile(service.location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var config *gitpod.GitpodConfig
|
|
err = yaml.Unmarshal(data, &config)
|
|
return config, err
|
|
}
|