Skip to content

Commit

Permalink
feat: expose healthz endpoint on different port; collect status from …
Browse files Browse the repository at this point in the history
…adguard; fix #131

BREAKING CHANGE: introduce new address and port configuration for healthz endpoint
  • Loading branch information
muhlba91 committed Oct 4, 2024
1 parent a4adb43 commit bb4678f
Show file tree
Hide file tree
Showing 13 changed files with 164 additions and 48 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ However, rules **not matching** above format, for example, `|domain.to.block`, *
> **If** an **upgrade path** between version is **listed here**, please make sure to **follow** those paths **without skipping a version**!
> Otherwise, the correct behaviour cannot be guaranteed, resulting in possible inconsistencies or errors.
### v7 to v8

`v8` introduces the `HEALTHZ_ADDRESS` (default: `0.0.0.0`) and `HEALTHZ_PORT` (default: `8080`) environment variable to introduce compatibility with the official Helm chart.

Attention: if you are using a customized Helm chart, make sure to adjust the probes accordingly.

### v5 to v6

`v6` introduces the `ADGUARD_SET_IMPORTANT_FLAG` environment variable to set the `important` flag for AdGuard rules. This is enabled by default.
Expand Down Expand Up @@ -120,16 +126,18 @@ sidecars:
ports:
- containerPort: 8888
name: http
- containerPort: 8080
name: healthz
livenessProbe:
httpGet:
path: /healthz
port: http
port: healthz
initialDelaySeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /healthz
port: http
port: healthz
initialDelaySeconds: 10
timeoutSeconds: 5
env:
Expand All @@ -150,8 +158,6 @@ sidecars:
secretKeyRef:
name: adguard-configuration
key: password
- name: SERVER_HOST
value: "0.0.0.0"
- name: DRY_RUN
value: "false"
EOF
Expand Down
2 changes: 2 additions & 0 deletions cmd/webhook/init/configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
type Config struct {
ServerHost string `env:"SERVER_HOST" envDefault:"localhost"`
ServerPort int `env:"SERVER_PORT" envDefault:"8888"`
HealthzHost string `env:"HEALTHZ_HOST" envDefault:"0.0.0.0"`
HealthzPort int `env:"HEALTHZ_PORT" envDefault:"8080"`
ServerReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT"`
ServerWriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT"`
DomainFilter []string `env:"DOMAIN_FILTER" envDefault:""`
Expand Down
2 changes: 1 addition & 1 deletion cmd/webhook/init/dnsprovider/dnsprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

type AdguardProviderFactory func(baseProvider *provider.BaseProvider, adguardConfig *adguard.Configuration) provider.Provider

func Init(config configuration.Config) (provider.Provider, error) {
func Init(config configuration.Config) (adguard.Provider, error) {
var domainFilter endpoint.DomainFilter
createMsg := "creating adguard provider with "

Expand Down
2 changes: 1 addition & 1 deletion cmd/webhook/init/dnsprovider/dnsprovider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func TestInit(t *testing.T) {
return
}

assert.Equal(t, tc.expectedFlags, dnsProvider.(*adguard.Provider).Configuration.DNSEntryFlags())
assert.Equal(t, tc.expectedFlags, dnsProvider.(*adguard.AGProvider).Configuration.DNSEntryFlags())

assert.NoErrorf(t, err, "error creating provider")
assert.NotNil(t, dnsProvider)
Expand Down
24 changes: 22 additions & 2 deletions cmd/webhook/init/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
func Init(config configuration.Config, p *webhook.Webhook) *http.Server {
r := chi.NewRouter()

r.Use(webhook.Health)
r.Get("/", p.Negotiate)
r.Get("/records", p.Records)
r.Post("/records", p.ApplyChanges)
Expand All @@ -42,6 +41,24 @@ func Init(config configuration.Config, p *webhook.Webhook) *http.Server {
return srv
}

// Init server initialization function
// The server will respond to the following endpoints:
// - /healthz (GET): health check endpoint
func InitHealthz(config configuration.Config, p *webhook.Webhook) *http.Server {
r := chi.NewRouter()

r.Get("/healthz", p.Health)

srv := createHTTPServer(fmt.Sprintf("%s:%d", config.HealthzHost, config.HealthzPort), r, config.ServerReadTimeout, config.ServerWriteTimeout)
go func() {
log.Infof("starting healthz on addr: '%s' ", srv.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Errorf("can't serve healthz on addr: '%s', error: %v", srv.Addr, err)
}
}()
return srv
}

func createHTTPServer(addr string, hand http.Handler, readTimeout, writeTimeout time.Duration) *http.Server {
return &http.Server{
ReadTimeout: readTimeout,
Expand All @@ -52,7 +69,7 @@ func createHTTPServer(addr string, hand http.Handler, readTimeout, writeTimeout
}

// ShutdownGracefully gracefully shutdown the http server
func ShutdownGracefully(srv *http.Server) {
func ShutdownGracefully(srv *http.Server, healthz *http.Server) {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
sig := <-sigCh
Expand All @@ -62,5 +79,8 @@ func ShutdownGracefully(srv *http.Server) {
if err := srv.Shutdown(ctx); err != nil {
log.Errorf("error shutting down server: %v", err)
}
if err := healthz.Shutdown(ctx); err != nil {
log.Errorf("error shutting down healthz: %v", err)
}
cancel()
}
62 changes: 60 additions & 2 deletions cmd/webhook/init/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ var mockProvider *MockProvider
func TestMain(m *testing.M) {
mockProvider = &MockProvider{}

srv := Init(configuration.Init(), webhook.New(mockProvider))
go ShutdownGracefully(srv)
hook := webhook.New(mockProvider)
srv := Init(configuration.Init(), hook)
healthz := InitHealthz(configuration.Init(), hook)
go ShutdownGracefully(srv, healthz)

time.Sleep(300 * time.Millisecond)

Expand All @@ -51,6 +53,20 @@ func TestMain(m *testing.M) {
}
}

func TestHealth(t *testing.T) {
testCases := []testCase{
{
name: "health ok",
method: http.MethodGet,
path: "/healthz",
body: "",
expectedStatusCode: http.StatusOK,
},
}

executeHealthzTestCases(t, testCases)
}

func TestRecords(t *testing.T) {
testCases := []testCase{
{
Expand Down Expand Up @@ -517,11 +533,53 @@ func executeTestCases(t *testing.T, testCases []testCase) {
}
}

func executeHealthzTestCases(t *testing.T, testCases []testCase) {
log.SetLevel(log.DebugLevel)

for i, tc := range testCases {
t.Run(fmt.Sprintf("%d. %s", i+1, tc.name), func(t *testing.T) {
mockProvider.testCase = tc
mockProvider.t = t

var bodyReader io.Reader
request, err := http.NewRequest(tc.method, "http://localhost:8080"+tc.path, bodyReader)
if err != nil {
t.Error(err)
}

response, err := http.DefaultClient.Do(request)
if err != nil {
t.Error(err)
}

if response.StatusCode != tc.expectedStatusCode {
t.Errorf("expected status code %d, got %d", tc.expectedStatusCode, response.StatusCode)
}

if tc.expectedBody != "" {
body, err := io.ReadAll(response.Body)
if err != nil {
t.Error(err)
}
_ = response.Body.Close()
actualTrimmedBody := strings.TrimSpace(string(body))
if actualTrimmedBody != tc.expectedBody {
t.Errorf("expected body '%s', got '%s'", tc.expectedBody, actualTrimmedBody)
}
}
})
}
}

type MockProvider struct {
t *testing.T
testCase testCase
}

func (d *MockProvider) Health(_ context.Context) bool {
return true
}

func (d *MockProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) {
return d.testCase.returnRecords, d.testCase.hasError
}
Expand Down
6 changes: 4 additions & 2 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func main() {
log.Fatalf("failed to initialize provider: %v", err)
}

srv := server.Init(config, webhook.New(provider))
server.ShutdownGracefully(srv)
hook := webhook.New(provider)
srv := server.Init(config, hook)
healthz := server.InitHealthz(config, hook)
server.ShutdownGracefully(srv, healthz)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.20.4 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.59.1 // indirect
github.com/prometheus/common v0.60.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/net v0.29.0 // indirect
Expand All @@ -44,7 +44,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apimachinery v0.31.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zI
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA=
github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
Expand Down Expand Up @@ -125,8 +125,8 @@ k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA=
k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI=
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/external-dns v0.15.0 h1:4NCSLHONsTmJXD8KReb4hubSz9Cx4goCHz3Dl+pGR+Q=
sigs.k8s.io/external-dns v0.15.0/go.mod h1:QdocdJu3mk9l4u80fu992lZEKqKd1130h17yNisIC78=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
Expand Down
28 changes: 21 additions & 7 deletions internal/adguard/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
type Client interface {
GetFilteringRules(ctx context.Context) ([]string, error)
SetFilteringRules(ctx context.Context, rules []string) error
Status(ctx context.Context) (bool, error)
}

// hhtpClient type used to implement Client with an HTTP client
Expand All @@ -24,6 +25,12 @@ type httpClient struct {
config *Configuration
}

// adguardStatus is the response of retrieving the AdGuard status
type adguardStatus struct {
Version string `json:"version"`
Running bool `json:"running"`
}

// getFilteringRules is the response of retrieving filtering rules
type getFilteringRules struct {
UserRules []string `json:"user_rules"`
Expand All @@ -43,8 +50,8 @@ func newAdguardClient(config *Configuration) (*httpClient, error) {
}

// check validity of the configuration
err := c.status(context.Background())
if err != nil {
s, err := c.Status(context.Background())
if err != nil || !s {
return nil, err
}

Expand Down Expand Up @@ -76,19 +83,26 @@ func (c *httpClient) doRequest(ctx context.Context, method, path string, body io
return resp, nil
}

func (c *httpClient) status(ctx context.Context) error {
func (c *httpClient) Status(ctx context.Context) (bool, error) {
if c.config.DryRun {
log.Info("would check adguard configuration")
return nil
return true, nil
}

r, err := c.doRequest(ctx, http.MethodGet, "status", nil)
if err != nil {
return err
return false, err
}
_ = r.Body.Close()
defer r.Body.Close()

return nil
var resp adguardStatus
err = json.NewDecoder(r.Body).Decode(&resp)
if err != nil {
return false, err
}
log.Debugf("retrieved status: %+v", resp)

return resp.Running, nil
}

// Retrieves all existing filtering rules from Adguard
Expand Down
22 changes: 17 additions & 5 deletions internal/adguard/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ import (
"sigs.k8s.io/external-dns/provider"
)

// Provider interface for interfacing with Adguard
type Provider interface {
provider.Provider
Health(ctx context.Context) bool
}

// Provider type for interfacing with Adguard
type Provider struct {
type AGProvider struct {
provider.BaseProvider

Configuration *Configuration
Expand All @@ -29,7 +35,7 @@ var (
)

// NewAdguardProvider initializes a new provider
func NewAdguardProvider(domainFilter endpoint.DomainFilter, config *Configuration) (provider.Provider, error) {
func NewAdguardProvider(domainFilter endpoint.DomainFilter, config *Configuration) (Provider, error) {
log.Debugf("using adguard at %s", config.URL)

// URL adjustment according to the specification
Expand All @@ -45,7 +51,7 @@ func NewAdguardProvider(domainFilter endpoint.DomainFilter, config *Configuratio
return nil, fmt.Errorf("failed to create the adguard client: %w", err)
}

p := &Provider{
p := &AGProvider{
Configuration: config,
client: c,
domainFilter: domainFilter,
Expand All @@ -54,8 +60,14 @@ func NewAdguardProvider(domainFilter endpoint.DomainFilter, config *Configuratio
return p, nil
}

// Health checks if AdGuard is available
func (p *AGProvider) Health(ctx context.Context) bool {
s, _ := p.client.Status(ctx)
return s
}

// ApplyChanges syncs the desired state with Adguard
func (p *Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
func (p *AGProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugf("received changes: %+v", changes)

or, err := p.client.GetFilteringRules(ctx)
Expand Down Expand Up @@ -139,7 +151,7 @@ func (p *Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) erro
}

// Records reads all endpoints from Adguard
func (p *Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
func (p *AGProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
resp, err := p.client.GetFilteringRules(ctx)
if err != nil {
return nil, err
Expand Down
Loading

0 comments on commit bb4678f

Please sign in to comment.