Initial setup (#11)

automatically setup new AdGuardHome instances #9
This commit is contained in:
Marc Brugger 2021-04-18 22:03:57 +02:00 committed by GitHub
parent d58c8f115e
commit 3edb5222d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 174 additions and 15 deletions

View file

@ -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:

View file

@ -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)
}

View file

@ -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"))
}

View file

@ -15,7 +15,8 @@ import (
)
var (
l = log.GetLogger("client")
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")
}

View file

@ -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
}

View file

@ -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()

View file

@ -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 {

View file

@ -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

View file

@ -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"`
}