mirror of
https://github.com/bakito/adguardhome-sync.git
synced 2025-03-01 17:45:28 +08:00
parent
d58c8f115e
commit
3edb5222d6
9 changed files with 174 additions and 15 deletions
11
README.md
11
README.md
|
@ -14,6 +14,13 @@ Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to
|
|||
- Services
|
||||
- Clients
|
||||
|
||||
### Setup of initial instances
|
||||
|
||||
New AdGuardHome instances are automatically installed. During automatic installation, the admin interface will be
|
||||
listening on port 3000 in runtime.
|
||||
|
||||
To skip automatic setup
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
|
@ -22,7 +29,7 @@ go get -u github.com/bakito/adguardhome-sync
|
|||
|
||||
## Prerequisites
|
||||
|
||||
Both the origin and replica mist be initially setup via the Adguard Home installation wizard.
|
||||
Both the origin instance must be initially setup via the AdguardHome installation wizard.
|
||||
|
||||
## Run
|
||||
|
||||
|
@ -92,6 +99,7 @@ services:
|
|||
- REPLICA1_USERNAME=username
|
||||
- REPLICA1_PASSWORD=password
|
||||
- REPLICA1_APIPATH=/some/path/control
|
||||
# - REPLICA1_AUTOSETUP=true # if true, AdGuardHome is automatically initialized.
|
||||
- CRON=*/10 * * * * # run every 10 minutes
|
||||
ports:
|
||||
- 8080:8080
|
||||
|
@ -130,6 +138,7 @@ replicas:
|
|||
- url: http://192.168.1.4
|
||||
username: username
|
||||
password: password
|
||||
# autoSetup: true # if true, AdGuardHome is automatically initialized.
|
||||
|
||||
# Configure the sync API server, disabled if api port is 0
|
||||
api:
|
||||
|
|
|
@ -31,11 +31,13 @@ const (
|
|||
configReplicaUsername = "replica.username"
|
||||
configReplicaPassword = "replica.password"
|
||||
configReplicaInsecureSkipVerify = "replica.insecureSkipVerify"
|
||||
configReplicaAutoSetup = "replica.autoSetup"
|
||||
|
||||
envReplicasUsernameFormat = "REPLICA%s_USERNAME"
|
||||
envReplicasPasswordFormat = "REPLICA%s_PASSWORD"
|
||||
envReplicasAPIPathFormat = "REPLICA%s_APIPATH"
|
||||
envReplicasInsecureSkipVerifyFormat = "REPLICA%s_INSECURESKIPVERIFY"
|
||||
envReplicasAutoSetup = "REPLICA%s_AUTOSETUP"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -126,6 +128,7 @@ func collectEnvReplicas() []types.AdGuardInstance {
|
|||
Password: os.Getenv(fmt.Sprintf(envReplicasPasswordFormat, sm[1])),
|
||||
APIPath: os.Getenv(fmt.Sprintf(envReplicasAPIPathFormat, sm[1])),
|
||||
InsecureSkipVerify: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasInsecureSkipVerifyFormat, sm[1])), "true"),
|
||||
AutoSetup: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasAutoSetup, sm[1])), "true"),
|
||||
}
|
||||
replicas = append(replicas, re)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ func init() {
|
|||
_ = viper.BindPFlag(configReplicaUsername, doCmd.PersistentFlags().Lookup("replica-username"))
|
||||
doCmd.PersistentFlags().String("replica-password", "", "Replica instance password")
|
||||
_ = viper.BindPFlag(configReplicaPassword, doCmd.PersistentFlags().Lookup("replica-password"))
|
||||
doCmd.PersistentFlags().String("replica-insecure-skip-verify", "", "Enable Replica instance InsecureSkipVerify")
|
||||
doCmd.PersistentFlags().Bool("replica-insecure-skip-verify", false, "Enable Replica instance InsecureSkipVerify")
|
||||
_ = viper.BindPFlag(configReplicaInsecureSkipVerify, doCmd.PersistentFlags().Lookup("replica-insecure-skip-verify"))
|
||||
doCmd.PersistentFlags().Bool("replica-auto-setup", false, "Enable automatic setup of new AdguardHome instances. This replaces the setup wizard.")
|
||||
_ = viper.BindPFlag(configReplicaAutoSetup, doCmd.PersistentFlags().Lookup("replica-auto-setup"))
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
|
||||
var (
|
||||
l = log.GetLogger("client")
|
||||
SetupNeededError = errors.New("setup needed")
|
||||
)
|
||||
|
||||
// New create a new client
|
||||
|
@ -41,6 +42,9 @@ func New(config types.AdGuardInstance) (Client, error) {
|
|||
cl = cl.SetBasicAuth(config.Username, config.Password)
|
||||
}
|
||||
|
||||
// no redirect
|
||||
cl.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
|
||||
return &client{
|
||||
host: u.Host,
|
||||
client: cl,
|
||||
|
@ -48,7 +52,7 @@ func New(config types.AdGuardInstance) (Client, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Client AdGuard Home API client interface
|
||||
// Client AdguardHome API client interface
|
||||
type Client interface {
|
||||
Host() string
|
||||
|
||||
|
@ -85,6 +89,7 @@ type Client interface {
|
|||
SetQueryLogConfig(enabled bool, interval int, anonymizeClientIP bool) error
|
||||
StatsConfig() (*types.IntervalConfig, error)
|
||||
SetStatsConfig(interval int) error
|
||||
Setup() error
|
||||
}
|
||||
|
||||
type client struct {
|
||||
|
@ -105,6 +110,12 @@ func (cl *client) doGet(req *resty.Request, url string) error {
|
|||
rl.Debug("do get")
|
||||
resp, err := req.Get(url)
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode() == http.StatusFound {
|
||||
loc := resp.Header().Get("Location")
|
||||
if loc == "/install.html" {
|
||||
return SetupNeededError
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
rl.With("status", resp.StatusCode(), "body", string(resp.Body())).Debug("got response")
|
||||
|
@ -346,3 +357,29 @@ func (cl *client) SetStatsConfig(interval int) error {
|
|||
cl.log.With("interval", interval).Info("Set stats config")
|
||||
return cl.doPost(cl.client.R().EnableTrace().SetBody(&types.IntervalConfig{Interval: interval}), "/stats_config")
|
||||
}
|
||||
|
||||
func (cl *client) Setup() error {
|
||||
cl.log.Info("Setup new AdguardHome instance")
|
||||
cfg := &types.InstallConfig{
|
||||
Web: types.InstallPort{
|
||||
IP: "0.0.0.0",
|
||||
Port: 3000,
|
||||
Status: "",
|
||||
CanAutofix: false,
|
||||
},
|
||||
DNS: types.InstallPort{
|
||||
IP: "0.0.0.0",
|
||||
Port: 53,
|
||||
Status: "",
|
||||
CanAutofix: false,
|
||||
},
|
||||
}
|
||||
|
||||
if cl.client.UserInfo != nil {
|
||||
cfg.Username = cl.client.UserInfo.Username
|
||||
cfg.Password = cl.client.UserInfo.Password
|
||||
}
|
||||
req := cl.client.R().EnableTrace().SetBody(cfg)
|
||||
req.UserInfo = nil
|
||||
return cl.doPost(req, "/install/configure")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package client_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -11,6 +12,12 @@ import (
|
|||
|
||||
"github.com/bakito/adguardhome-sync/pkg/client"
|
||||
"github.com/bakito/adguardhome-sync/pkg/types"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
username = uuid.NewString()
|
||||
password = uuid.NewString()
|
||||
)
|
||||
|
||||
var _ = Describe("Client", func() {
|
||||
|
@ -100,6 +107,25 @@ bar`)
|
|||
Ω(fs.DNSAddresses[0]).Should(Equal("192.168.1.2"))
|
||||
Ω(fs.Version).Should(Equal("v0.105.2"))
|
||||
})
|
||||
It("should return SetupNeededError", func() {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", "/install.html")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}))
|
||||
cl, err := client.New(types.AdGuardInstance{URL: ts.URL})
|
||||
Ω(err).ShouldNot(HaveOccurred())
|
||||
_, err = cl.Status()
|
||||
Ω(err).Should(HaveOccurred())
|
||||
Ω(err).Should(Equal(client.SetupNeededError))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Setup", func() {
|
||||
It("should add setup the instance", func() {
|
||||
ts, cl = ClientPost("/install/configure", fmt.Sprintf(`{"web":{"ip":"0.0.0.0","port":3000,"status":"","can_autofix":false},"dns":{"ip":"0.0.0.0","port":53,"status":"","can_autofix":false},"username":"%s","password":"%s"}`, username, password))
|
||||
err := cl.Setup()
|
||||
Ω(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("RewriteList", func() {
|
||||
|
@ -317,7 +343,7 @@ func ClientPost(path string, content ...string) (*httptest.Server, client.Client
|
|||
index++
|
||||
}))
|
||||
|
||||
cl, err := client.New(types.AdGuardInstance{URL: ts.URL})
|
||||
cl, err := client.New(types.AdGuardInstance{URL: ts.URL, Username: username, Password: password})
|
||||
Ω(err).ShouldNot(HaveOccurred())
|
||||
return ts, cl
|
||||
}
|
||||
|
|
|
@ -348,6 +348,20 @@ func (mr *MockClientMockRecorder) SetStatsConfig(arg0 interface{}) *gomock.Call
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStatsConfig", reflect.TypeOf((*MockClient)(nil).SetStatsConfig), arg0)
|
||||
}
|
||||
|
||||
// Setup mocks base method.
|
||||
func (m *MockClient) Setup() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Setup")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Setup indicates an expected call of Setup.
|
||||
func (mr *MockClientMockRecorder) Setup() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setup", reflect.TypeOf((*MockClient)(nil).Setup))
|
||||
}
|
||||
|
||||
// StatsConfig mocks base method.
|
||||
func (m *MockClient) StatsConfig() (*types.IntervalConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/bakito/adguardhome-sync/pkg/client"
|
||||
|
@ -26,6 +27,8 @@ func Sync(cfg *types.Config) error {
|
|||
return fmt.Errorf("no replicas configured")
|
||||
}
|
||||
|
||||
cfg.Origin.AutoSetup = false
|
||||
|
||||
w := &worker{
|
||||
cfg: cfg,
|
||||
createClient: func(ai types.AdGuardInstance) (client.Client, error) {
|
||||
|
@ -153,53 +156,68 @@ func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardIn
|
|||
rl := l.With("to", rc.Host())
|
||||
rl.Info("Start sync")
|
||||
|
||||
rs, err := rc.Status()
|
||||
rs, err := w.statusWithSetup(rl, replica, rc)
|
||||
if err != nil {
|
||||
l.With("error", err).Error("Error getting replica status")
|
||||
rl.With("error", err).Error("Error getting replica status")
|
||||
return
|
||||
}
|
||||
|
||||
if o.status.Version != rs.Version {
|
||||
l.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match")
|
||||
rl.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match")
|
||||
}
|
||||
|
||||
err = w.syncGeneralSettings(o, rs, rc)
|
||||
if err != nil {
|
||||
l.With("error", err).Error("Error syncing general settings")
|
||||
rl.With("error", err).Error("Error syncing general settings")
|
||||
return
|
||||
}
|
||||
|
||||
err = w.syncConfigs(o, rc)
|
||||
if err != nil {
|
||||
l.With("error", err).Error("Error syncing configs")
|
||||
rl.With("error", err).Error("Error syncing configs")
|
||||
return
|
||||
}
|
||||
|
||||
err = w.syncRewrites(o.rewrites, rc)
|
||||
if err != nil {
|
||||
l.With("error", err).Error("Error syncing rewrites")
|
||||
rl.With("error", err).Error("Error syncing rewrites")
|
||||
return
|
||||
}
|
||||
err = w.syncFilters(o.filters, rc)
|
||||
if err != nil {
|
||||
l.With("error", err).Error("Error syncing filters")
|
||||
rl.With("error", err).Error("Error syncing filters")
|
||||
return
|
||||
}
|
||||
|
||||
err = w.syncServices(o.services, rc)
|
||||
if err != nil {
|
||||
l.With("error", err).Error("Error syncing services")
|
||||
rl.With("error", err).Error("Error syncing services")
|
||||
return
|
||||
}
|
||||
|
||||
if err = w.syncClients(o.clients, rc); err != nil {
|
||||
l.With("error", err).Error("Error syncing clients")
|
||||
rl.With("error", err).Error("Error syncing clients")
|
||||
return
|
||||
}
|
||||
|
||||
rl.Info("Sync done")
|
||||
}
|
||||
|
||||
func (w *worker) statusWithSetup(rl *zap.SugaredLogger, replica types.AdGuardInstance, rc client.Client) (*types.Status, error) {
|
||||
rs, err := rc.Status()
|
||||
if err != nil {
|
||||
if replica.AutoSetup && errors.Is(err, client.SetupNeededError) {
|
||||
if serr := rc.Setup(); serr != nil {
|
||||
rl.With("error", serr).Error("Error setup AdGuardHome")
|
||||
return nil, err
|
||||
}
|
||||
return rc.Status()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return rs, err
|
||||
}
|
||||
|
||||
func (w *worker) syncServices(os types.Services, replica client.Client) error {
|
||||
rs, err := replica.Services()
|
||||
if err != nil {
|
||||
|
|
|
@ -266,6 +266,39 @@ var _ = Describe("Sync", func() {
|
|||
Ω(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
Context("statusWithSetup", func() {
|
||||
var (
|
||||
status *types.Status
|
||||
inst types.AdGuardInstance
|
||||
)
|
||||
BeforeEach(func() {
|
||||
status = &types.Status{}
|
||||
inst = types.AdGuardInstance{
|
||||
AutoSetup: true,
|
||||
}
|
||||
})
|
||||
It("should get the replica status", func() {
|
||||
cl.EXPECT().Status().Return(status, nil)
|
||||
st, err := w.statusWithSetup(l, inst, cl)
|
||||
Ω(err).ShouldNot(HaveOccurred())
|
||||
Ω(st).Should(Equal(status))
|
||||
})
|
||||
It("should runs setup before getting replica status", func() {
|
||||
cl.EXPECT().Status().Return(nil, client.SetupNeededError)
|
||||
cl.EXPECT().Setup()
|
||||
cl.EXPECT().Status().Return(status, nil)
|
||||
st, err := w.statusWithSetup(l, inst, cl)
|
||||
Ω(err).ShouldNot(HaveOccurred())
|
||||
Ω(st).Should(Equal(status))
|
||||
})
|
||||
It("should fail on setup", func() {
|
||||
cl.EXPECT().Status().Return(nil, client.SetupNeededError)
|
||||
cl.EXPECT().Setup().Return(te)
|
||||
st, err := w.statusWithSetup(l, inst, cl)
|
||||
Ω(err).Should(HaveOccurred())
|
||||
Ω(st).Should(BeNil())
|
||||
})
|
||||
})
|
||||
Context("syncServices", func() {
|
||||
var (
|
||||
os types.Services
|
||||
|
|
|
@ -49,13 +49,14 @@ func (cfg *Config) UniqueReplicas() []AdGuardInstance {
|
|||
return r
|
||||
}
|
||||
|
||||
// AdGuardInstance adguard home config instance
|
||||
// AdGuardInstance AdguardHome config instance
|
||||
type AdGuardInstance struct {
|
||||
URL string `json:"url" yaml:"url"`
|
||||
APIPath string `json:"apiPath,omitempty" yaml:"apiPath,omitempty"`
|
||||
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
||||
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"`
|
||||
AutoSetup bool `json:"autoSetup" yaml:"autoSetup"`
|
||||
}
|
||||
|
||||
// Key AdGuardInstance key
|
||||
|
@ -337,3 +338,19 @@ func equals(a []string, b []string) bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// InstallConfig AdguardHome install config
|
||||
type InstallConfig struct {
|
||||
Web InstallPort `json:"web"`
|
||||
DNS InstallPort `json:"dns"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// InstallPort AdguardHome install config port
|
||||
type InstallPort struct {
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
Status string `json:"status"`
|
||||
CanAutofix bool `json:"can_autofix"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue