diff --git a/cmd/config/anchor.go b/cmd/config/anchor.go new file mode 100644 index 0000000..fed429a --- /dev/null +++ b/cmd/config/anchor.go @@ -0,0 +1,50 @@ +package config + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" + + "github.com/tlmiller/disttrust/conductor" + "github.com/tlmiller/disttrust/provider" +) + +type Anchor struct { + Action Action `json:"action"` + AltNames []string `json:"altNames"` + CommonName string `json:"cn"` + Dest string `json:"dest"` + DestOptions json.RawMessage `json:"destOpts"` + Name string `json:"name"` + Provider string `json:"provider"` +} + +type ProviderFixer func(string) (provider.Provider, bool) + +func AnchorsToMembers(anchors []Anchor, fixer ProviderFixer) ([]conductor.Member, error) { + members := []conductor.Member{} + for _, anchor := range anchors { + aprovider, exists := fixer(anchor.Provider) + if !exists { + return nil, fmt.Errorf("no provider found for %s", anchor.Provider) + } + + req := provider.Request{} + req.CommonName = anchor.CommonName + req.AltNames = anchor.AltNames + + dest, err := ToDest(anchor.Dest, anchor.DestOptions) + if err != nil { + return nil, errors.Wrap(err, "making dest for anchor") + } + action, err := ToAction(anchor.Action) + if err != nil { + return nil, errors.Wrap(err, "making action for anchor") + } + + members = append(members, conductor.NewMember(anchor.Name, aprovider, + req, conductor.DefaultLeaseHandle(dest, action))) + } + return members, nil +} diff --git a/cmd/config/config.go b/cmd/config/config.go index 8a469f8..577658b 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -6,16 +6,6 @@ import ( "github.com/pkg/errors" ) -type Anchor struct { - Action Action `json:"action"` - AltNames []string `json:"altNames"` - CommonName string `json:"cn"` - Dest string `json:"dest"` - DestOptions json.RawMessage `json:"destOpts"` - Name string `json:"name"` - Provider string `json:"provider"` -} - type Config struct { Api Api `json:"api"` Providers []Provider `json:"providers"` diff --git a/cmd/config/file.go b/cmd/config/file.go index ff32894..5a779e7 100644 --- a/cmd/config/file.go +++ b/cmd/config/file.go @@ -2,6 +2,8 @@ package config import ( "io/ioutil" + "os" + "path/filepath" "github.com/pkg/errors" ) @@ -9,6 +11,19 @@ import ( func FromFiles(files ...string) (*Config, error) { conf := DefaultConfig() for _, file := range files { + finfo, err := os.Stat(file) + if err != nil { + return nil, errors.Wrap(err, "geting config file info") + } + + if finfo.Mode().IsDir() { + conf, err = FromDirectory(conf, file) + if err != nil { + return nil, err + } + continue + } + raw, err := ioutil.ReadFile(file) if err != nil { return nil, errors.Wrap(err, "reading config file") @@ -22,3 +37,28 @@ func FromFiles(files ...string) (*Config, error) { } return conf, nil } + +func FromDirectory(conf *Config, dir string) (*Config, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return conf, errors.Wrap(err, "reading config files from dir") + } + + for _, f := range files { + if f.IsDir() || filepath.Ext(f.Name()) != ".json" { + continue + } + + raw, err := ioutil.ReadFile(filepath.Join(dir, f.Name())) + if err != nil { + return conf, errors.Wrap(err, "reading config file") + } + c, err := New(raw) + if err != nil { + return conf, errors.Wrap(err, "parsing config file") + } + conf = mergeConfigs(conf, c) + } + + return conf, nil +} diff --git a/cmd/config/provider_config_wrap.go b/cmd/config/provider_config_wrap.go new file mode 100644 index 0000000..096139b --- /dev/null +++ b/cmd/config/provider_config_wrap.go @@ -0,0 +1,20 @@ +package config + +import ( + "encoding/json" + + "github.com/tlmiller/disttrust/provider" +) + +type ProviderConfigWrap struct { + config json.RawMessage + p provider.Provider +} + +func (p *ProviderConfigWrap) Issue(r *provider.Request) (provider.Lease, error) { + return p.p.Issue(r) +} + +func (p *ProviderConfigWrap) Renew(l provider.Lease) (provider.Lease, error) { + return p.p.Renew(l) +} diff --git a/cmd/config/provider_factory.go b/cmd/config/provider_factory.go index 22d3101..e51ab7f 100644 --- a/cmd/config/provider_factory.go +++ b/cmd/config/provider_factory.go @@ -3,6 +3,9 @@ package config import ( "encoding/json" "fmt" + "reflect" + + "github.com/pkg/errors" "github.com/tlmiller/disttrust/provider" ) @@ -27,5 +30,64 @@ func ToProvider(id string, opts json.RawMessage) (provider.Provider, error) { return nil, fmt.Errorf("provider mapper does not exists for id '%s'", id) } - return mapper(opts) + p, err := mapper(opts) + return &ProviderConfigWrap{ + config: opts, + p: p, + }, err +} + +func ToProviderOnUpdate(id string, opts json.RawMessage, ex provider.Provider) (provider.Provider, error) { + if cpwrap, ok := ex.(*ProviderConfigWrap); ok { + var j1, j2 interface{} + if err := json.Unmarshal(opts, &j1); err != nil { + return nil, err + } + if err := json.Unmarshal(cpwrap.config, &j2); err != nil { + return nil, err + } + if reflect.DeepEqual(j1, j2) { + return ex, nil + } + } + + mapper, exists := providerMappings[provider.Id(id)] + if exists == false { + return nil, fmt.Errorf("provider mapper does not exists for id '%s'", id) + } + + p, err := mapper(opts) + return &ProviderConfigWrap{ + config: opts, + p: p, + }, err +} + +func ToProviderStore(cnfProviders []Provider, store *provider.Store) (*provider.Store, error) { + nstore := provider.NewStore() + for _, cnfProvider := range cnfProviders { + if len(cnfProvider.Name) == 0 { + return nstore, errors.New("undefined provider name") + } + + var err error + var genProvider provider.Provider + if p, exists := store.Fetch(cnfProvider.Name); exists { + genProvider, err = ToProviderOnUpdate(cnfProvider.Name, + cnfProvider.Options, p) + } else { + genProvider, err = ToProvider(cnfProvider.Id, cnfProvider.Options) + } + + if err != nil { + return nstore, errors.Wrapf(err, "config to provider %s", cnfProvider.Name) + } + + err = nstore.Store(cnfProvider.Name, genProvider) + if err != nil { + return nstore, errors.Wrap(err, "registering provider") + } + } + return nstore, nil + } diff --git a/cmd/disttrust.go b/cmd/disttrust.go index 82dcc9d..7a16d24 100644 --- a/cmd/disttrust.go +++ b/cmd/disttrust.go @@ -3,8 +3,8 @@ package cmd import ( "fmt" "os" - - "github.com/pkg/errors" + "os/signal" + "syscall" log "github.com/sirupsen/logrus" @@ -13,12 +13,11 @@ import ( "github.com/tlmiller/disttrust/cmd/config" "github.com/tlmiller/disttrust/conductor" "github.com/tlmiller/disttrust/provider" - "github.com/tlmiller/disttrust/server" + _ "github.com/tlmiller/disttrust/server" ) var ( configFiles []string - manager *conductor.Conductor ) var disttrustCmd = &cobra.Command{ @@ -29,25 +28,6 @@ var disttrustCmd = &cobra.Command{ Run: Run, } -func applyProviders(cnfProviders []config.Provider, store *provider.Store) error { - for _, cnfProvider := range cnfProviders { - if len(cnfProvider.Name) == 0 { - return errors.New("undefined provider name") - } - - p, err := config.ToProvider(cnfProvider.Id, cnfProvider.Options) - if err != nil { - return errors.Wrapf(err, "config to provider%s", cnfProvider.Name) - } - - err = store.Store(cnfProvider.Name, p) - if err != nil { - return errors.Wrap(err, "registering provider") - } - } - return nil -} - func Execute() { if err := disttrustCmd.Execute(); err != nil { fmt.Println(err) @@ -56,7 +36,6 @@ func Execute() { } func init() { - manager = conductor.NewConductor() disttrustCmd.Flags().StringSliceVarP(&configFiles, "config", "c", []string{}, "Config file(s)") disttrustCmd.MarkFlagRequired("config") @@ -67,61 +46,51 @@ func preRun(cmd *cobra.Command, args []string) { } func Run(cmd *cobra.Command, args []string) { - config, err := config.FromFiles(configFiles...) - if err != nil { - log.Fatalf("making config: %v", err) - } - - log.Debug("applying providers from config") - err = applyProviders(config.Providers, provider.DefaultStore()) - if err != nil { - log.Fatalf("applying providers: %v", err) - } + providers := provider.DefaultStore() - log.Debug("applying anchors from config") - status, err := applyAnchors(config.Anchors, manager, provider.DefaultStore()) - if err != nil { - log.Fatalf("applying anchors: %v", err) - } - - var apiServ *server.ApiServer - if config.Api.Address != "" { - apiServ = server.NewApiServer(config.Api.Address) - for _, s := range status { - apiServ.AddHealthzChecks(s) - } - go apiServ.Serve() - } - - manager.Play().Watch() -} - -func applyAnchors(anchors []config.Anchor, con *conductor.Conductor, - store *provider.Store) ([]*conductor.MemberStatus, error) { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGHUP) - status := []*conductor.MemberStatus{} - for _, cnfAnchor := range anchors { - prv, err := store.Fetch(cnfAnchor.Provider) + for { + userConfig, err := config.FromFiles(configFiles...) if err != nil { - return nil, errors.Wrap(err, "getting anchor provider") + log.Fatalf("making config: %v", err) } - req := provider.Request{} - req.CommonName = cnfAnchor.CommonName - req.AltNames = cnfAnchor.AltNames - - dest, err := config.ToDest(cnfAnchor.Dest, cnfAnchor.DestOptions) + log.Debug("building provider store from config") + providers, err = config.ToProviderStore(userConfig.Providers, providers) if err != nil { - return nil, errors.Wrap(err, "make dest for anchor") + log.Fatalf("building providers: %v", err) } - action, err := config.ToAction(cnfAnchor.Action) + + log.Debug("building anchors from config") + members, err := config.AnchorsToMembers(userConfig.Anchors, providers.Fetch) if err != nil { - return nil, errors.Wrap(err, "make action for anchor") + log.Fatalf("building providers: %v", err) } - memHandle := conductor.DefaultLeaseHandle(dest, action) - member := conductor.NewMember(cnfAnchor.Name, prv, req, memHandle) - status = append(status, con.AddMember(member)) + manager := conductor.NewConductor() + _ = manager.AddMembers(members...) + + //var apiServ *server.ApiServer + //if userConfig.Api.Address != "" { + // apiServ = server.NewApiServer(userConfig.Api.Address) + // for _, s := range mstatuses { + // apiServ.AddHealthzChecks(s) + // } + // go apiServ.Serve() + //} + + manager.Play() + + sig := <-sigCh + log.Infof("recieved signal %s", sig) + + if sysSig, ok := sig.(syscall.Signal); ok && sysSig == syscall.SIGHUP { + manager.Stop() + log.Info("reloading config") + } else { + break + } } - return status, nil } diff --git a/conductor/conductor.go b/conductor/conductor.go index 2e9270b..32ce6ca 100644 --- a/conductor/conductor.go +++ b/conductor/conductor.go @@ -1,6 +1,8 @@ package conductor import ( + "sync" + "github.com/sirupsen/logrus" ) @@ -8,13 +10,17 @@ type Conductor struct { healthErr error members []*MemberStatus memCount int32 + stopCh chan struct{} watchCh chan *MemberStatus + waitGroup sync.WaitGroup } func NewConductor() *Conductor { return &Conductor{ healthErr: nil, + stopCh: make(chan struct{}), watchCh: make(chan *MemberStatus), + waitGroup: sync.WaitGroup{}, } } @@ -24,34 +30,54 @@ func (c *Conductor) AddMember(member Member) *MemberStatus { return mstatus } +func (c *Conductor) AddMembers(members ...Member) []*MemberStatus { + statuses := make([]*MemberStatus, len(members)) + for i, member := range members { + statuses[i] = c.AddMember(member) + } + return statuses +} + func (c *Conductor) Play() *Conductor { - for _, mstatus := range c.members { - if running, err := mstatus.State(); running || err != nil { + for _, fmstatus := range c.members { + if running, err := fmstatus.State(); running || err != nil { continue } - mstatus.setState(true, nil) + fmstatus.setState(true, nil) + gmstatus := fmstatus + + c.waitGroup.Add(1) go func() { log := logrus.WithFields(logrus.Fields{ - "member": mstatus.member.Name(), + "member": gmstatus.member.Name(), }) log.Info("playing member") - go mstatus.member.Play() + go gmstatus.member.Play() Outer: for { select { - case err := <-mstatus.member.DoneCh(): + case err := <-gmstatus.member.DoneCh(): log.Info("member stopped") - mstatus.setState(false, err) - c.watchCh <- mstatus + gmstatus.setState(false, err) + c.watchCh <- gmstatus + c.waitGroup.Done() break Outer + case <-c.stopCh: + gmstatus.member.Stop() } } }() } + go c.Watch() return c } +func (c *Conductor) Stop() { + close(c.stopCh) + c.waitGroup.Wait() +} + func (c *Conductor) Watch() { for { select { diff --git a/provider/dummy_provider.go b/provider/dummy_provider.go new file mode 100644 index 0000000..7b3a435 --- /dev/null +++ b/provider/dummy_provider.go @@ -0,0 +1,12 @@ +package provider + +type DummyProvider struct { +} + +func (p *DummyProvider) Issue(_ *Request) (Lease, error) { + return nil, nil +} + +func (p *DummyProvider) Renew(_ Lease) (Lease, error) { + return nil, nil +} diff --git a/provider/store.go b/provider/store.go index c962efd..8d02475 100644 --- a/provider/store.go +++ b/provider/store.go @@ -16,11 +16,11 @@ func DefaultStore() *Store { return defaultStore } -func (s *Store) Fetch(name string) (Provider, error) { +func (s *Store) Fetch(name string) (Provider, bool) { if p, exists := s.providers[name]; exists { - return p, nil + return p, true } - return nil, fmt.Errorf("no provider registered for '%s'", name) + return nil, false } func init() { @@ -33,6 +33,10 @@ func NewStore() *Store { } } +func (s *Store) Remove(name string) { + delete(s.providers, name) +} + func (s *Store) Store(name string, p Provider) error { if _, exists := s.providers[name]; exists { return fmt.Errorf("provider for name '%s' already exists", name) diff --git a/provider/store_test.go b/provider/store_test.go index eab4291..627427c 100644 --- a/provider/store_test.go +++ b/provider/store_test.go @@ -5,7 +5,7 @@ import ( ) func TestDupeNameProviders(t *testing.T) { - store := DefaultStore() + store := NewStore() err := store.Store("test", nil) if err != nil { t.Fatalf("receeved error for provider store: %v", err) @@ -16,3 +16,27 @@ func TestDupeNameProviders(t *testing.T) { t.Fatal("should have received error for duplicate provider name") } } + +func TestProvidersRemoval(t *testing.T) { + store := NewStore() + p := &DummyProvider{} + err := store.Store("test1", p) + if err != nil { + t.Fatalf("receeved error for provider store: %v", err) + } + + pf, err := store.Fetch("test1") + if err != nil { + t.Fatalf("receeved error for provider fetch: %v", err) + } + + if p != pf { + t.Fatal("provider store returned does not match that stored") + } + + store.Remove("test1") + pf, err = store.Fetch("test1") + if err == nil { + t.Fatal("provider store fetch should have failed for removed povider") + } +}