Skip to content

Commit

Permalink
feat: add upstream dns validation (#102)
Browse files Browse the repository at this point in the history
## Issue
#84 

## Description
- added service for validating that at least a certain number of
upstream DNS servers are configured
- added unit tests
- expanded integration tests to include new rule

---------

Signed-off-by: Artur Shad Nik <[email protected]>
Co-authored-by: Tyler Gillson <[email protected]>
  • Loading branch information
arturshadnik and TylerGillson authored Jul 24, 2024
1 parent ba024df commit 50b317e
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 32 deletions.
17 changes: 14 additions & 3 deletions internal/controller/maasvalidator_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (

"github.com/validator-labs/validator-plugin-maas/api/v1alpha1"
"github.com/validator-labs/validator-plugin-maas/internal/constants"
dnsval "github.com/validator-labs/validator-plugin-maas/internal/validators/dns"
osval "github.com/validator-labs/validator-plugin-maas/internal/validators/os"
vapi "github.com/validator-labs/validator/api/v1alpha1"
"github.com/validator-labs/validator/pkg/types"
Expand Down Expand Up @@ -117,13 +118,23 @@ func (r *MaasValidatorReconciler) Reconcile(ctx context.Context, req ctrl.Reques
ValidationRuleErrors: make([]error, 0, vr.Spec.ExpectedResults),
}

maasRuleService := osval.NewImageRulesService(r.Log, maasClient.BootResources)
imageRulesService := osval.NewImageRulesService(r.Log, maasClient.BootResources)
upstreamDNSRulesService := dnsval.NewUpstreamDNSRulesService(r.Log, maasClient.MAASServer)

// MAAS Instance image rules
for _, rule := range validator.Spec.ImageRules {
vrr, err := maasRuleService.ReconcileMaasInstanceImageRule(rule)
vrr, err := imageRulesService.ReconcileMaasInstanceImageRule(rule)
if err != nil {
r.Log.V(0).Error(err, "failed to reconcile MAAS instance rule")
r.Log.V(0).Error(err, "failed to reconcile MAAS image rule")
}
resp.AddResult(vrr, err)
}

// MAAS Instance upstream DNS rules
for _, rule := range validator.Spec.UpstreamDNSRules {
vrr, err := upstreamDNSRulesService.ReconcileMaasInstanceUpstreamDNSRules(rule)
if err != nil {
r.Log.V(0).Error(err, "failed to reconcile MAAS upstream DNS rule")
}
resp.AddResult(vrr, err)
}
Expand Down
9 changes: 9 additions & 0 deletions internal/controller/maasvalidator_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type MockBootResourcesService struct {
api.BootResources
}

type MockUDNSRulesService struct {
api.MAASServer
}

func (b *MockBootResourcesService) Get(params *entity.BootResourcesReadParams) ([]entity.BootResource, error) {
return []entity.BootResource{
{
Expand All @@ -33,6 +37,10 @@ func (b *MockBootResourcesService) Get(params *entity.BootResourcesReadParams) (
}, nil
}

func (u *MockUDNSRulesService) Get(string) ([]byte, error) {
return []byte("8.8.8.8"), nil
}

var _ = Describe("MaaSValidator controller", Ordered, func() {

BeforeEach(func() {
Expand All @@ -44,6 +52,7 @@ var _ = Describe("MaaSValidator controller", Ordered, func() {
SetUpClient = func(maasURL, massToken string) (*maasclient.Client, error) {
c := &maasclient.Client{}
c.BootResources = &MockBootResourcesService{}
c.MAASServer = &MockUDNSRulesService{}
return c, nil
}
})
Expand Down
35 changes: 35 additions & 0 deletions internal/utils/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package utils provides utility functions for the MAAS validator
package utils

import (
"fmt"

vapi "github.com/validator-labs/validator/api/v1alpha1"
vapiconstants "github.com/validator-labs/validator/pkg/constants"
"github.com/validator-labs/validator/pkg/types"
"github.com/validator-labs/validator/pkg/util"
)

// BuildValidationResult builds a default ValidationResult for a given validation type
func BuildValidationResult(ruleName, ruleType string) *types.ValidationRuleResult {
state := vapi.ValidationSucceeded
latestCondition := vapi.DefaultValidationCondition()
latestCondition.Details = make([]string, 0)
latestCondition.Failures = make([]string, 0)
latestCondition.Message = fmt.Sprintf("All %s checks passed", ruleType)
latestCondition.ValidationRule = fmt.Sprintf("%s-%s", vapiconstants.ValidationRulePrefix, util.Sanitize(ruleName))
latestCondition.ValidationType = ruleType
return &types.ValidationRuleResult{Condition: &latestCondition, State: &state}
}

// UpdateResult updates a ValidationRuleResult with a list of errors and details
func UpdateResult(vr *types.ValidationRuleResult, errs []error, errMsg string, details ...string) {
if len(errs) > 0 {
vr.State = util.Ptr(vapi.ValidationFailed)
vr.Condition.Message = errMsg
for _, err := range errs {
vr.Condition.Failures = append(vr.Condition.Failures, err.Error())
}
}
vr.Condition.Details = append(vr.Condition.Details, details...)
}
68 changes: 68 additions & 0 deletions internal/validators/dns/upstream_dns_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Package dns contains the logic for validating MAAS instance DNS rules
package dns

import (
"fmt"
"strings"

"github.com/canonical/gomaasclient/api"
"github.com/go-logr/logr"

"github.com/validator-labs/validator-plugin-maas/api/v1alpha1"
"github.com/validator-labs/validator-plugin-maas/internal/constants"
"github.com/validator-labs/validator-plugin-maas/internal/utils"
"github.com/validator-labs/validator/pkg/types"
)

// UpstreamDNSRulesService is the service for validating MAAS instance upstream DNS rules
type UpstreamDNSRulesService struct {
log logr.Logger
api api.MAASServer
}

// NewUpstreamDNSRulesService creates a new UpstreamDNSRulesService
func NewUpstreamDNSRulesService(log logr.Logger, api api.MAASServer) *UpstreamDNSRulesService {
return &UpstreamDNSRulesService{
log: log,
api: api,
}
}

// ReconcileMaasInstanceUpstreamDNSRules reconciles a MAAS instance upstream DNS rule
func (s *UpstreamDNSRulesService) ReconcileMaasInstanceUpstreamDNSRules(rule v1alpha1.UpstreamDNSRule) (*types.ValidationRuleResult, error) {

vr := utils.BuildValidationResult(rule.Name, constants.ValidationTypeUDNS)

details, errs := s.findDNSServers(rule.NumDNSServers)

utils.UpdateResult(vr, errs, constants.ErrUDNSNotConfigured, details...)

if len(errs) > 0 {
return vr, errs[0]
}

return vr, nil
}

func (s *UpstreamDNSRulesService) findDNSServers(expected int) ([]string, []error) {
details := make([]string, 0)
errs := make([]error, 0)

ns, err := s.api.Get("upstream_dns")
if err != nil {
return nil, []error{err}
}
nameservers := strings.Split(string(ns), " ")
numServers := len(nameservers)

if nameservers[0] == "" {
numServers = 0
}

if numServers < expected {
errs = append(errs, fmt.Errorf("expected %d DNS server(s), got %d", expected, numServers))
} else {
details = append(details, fmt.Sprintf("Found %d DNS server(s)", len(nameservers)))
}
return details, errs
}
94 changes: 94 additions & 0 deletions internal/validators/dns/upstream_dns_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package dns

import (
"testing"

"github.com/canonical/gomaasclient/api"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"

"github.com/validator-labs/validator-plugin-maas/api/v1alpha1"
)

type DummyMAASServer struct {
api.MAASServer
upstreamDNS string
}

func (d *DummyMAASServer) Get(string) ([]byte, error) {
return []byte(d.upstreamDNS), nil
}

func TestReconcileMaasInstanceImageRule(t *testing.T) {

testCases := []struct {
Name string
ruleService *UpstreamDNSRulesService
upstreamDNSRules []v1alpha1.UpstreamDNSRule
errors []string
details []string
}{
{
Name: "Enough DNS servers are found in MAAS",
ruleService: NewUpstreamDNSRulesService(
logr.Logger{},
&DummyMAASServer{
upstreamDNS: "8.8.8.8",
},
),
upstreamDNSRules: []v1alpha1.UpstreamDNSRule{
{Name: "Upstream DNS rule 1", NumDNSServers: 1},
},
errors: nil,
details: []string{"Found 1 DNS server(s)"},
},
{
Name: "Not enough DNS servers are found in MAAS",
ruleService: NewUpstreamDNSRulesService(
logr.Logger{},
&DummyMAASServer{
upstreamDNS: "8.8.8.8",
}),
upstreamDNSRules: []v1alpha1.UpstreamDNSRule{
{Name: "Upstream DNS rule 2", NumDNSServers: 2},
},
errors: []string{"expected 2 DNS server(s), got 1"},
details: nil,
},
{
Name: "No DNS servers are found in MAAS",
ruleService: NewUpstreamDNSRulesService(
logr.Logger{},
&DummyMAASServer{
upstreamDNS: "",
}),
upstreamDNSRules: []v1alpha1.UpstreamDNSRule{
{Name: "Upstream DNS rule 3", NumDNSServers: 1},
},
errors: []string{"expected 1 DNS server(s), got 0"},
details: nil,
},
}

for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var details []string
var errors []string

for _, rule := range tc.upstreamDNSRules {
vr, _ := tc.ruleService.ReconcileMaasInstanceUpstreamDNSRules(rule)
details = append(details, vr.Condition.Details...)
errors = append(errors, vr.Condition.Failures...)
}

assert.Equal(t, len(tc.errors), len(errors), "Number of errors should match")
for _, expectedError := range tc.errors {
assert.Contains(t, errors, expectedError, "Expected error should be present")
}
assert.Equal(t, len(tc.details), len(details), "Number of details should match")
for _, expectedDetail := range tc.details {
assert.Contains(t, details, expectedDetail, "Expected detail should be present")
}
})
}
}
32 changes: 3 additions & 29 deletions internal/validators/os/os_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ import (

"github.com/validator-labs/validator-plugin-maas/api/v1alpha1"
"github.com/validator-labs/validator-plugin-maas/internal/constants"
vapi "github.com/validator-labs/validator/api/v1alpha1"
vapiconstants "github.com/validator-labs/validator/pkg/constants"
"github.com/validator-labs/validator-plugin-maas/internal/utils"
"github.com/validator-labs/validator/pkg/types"
"github.com/validator-labs/validator/pkg/util"
)

// ImageRulesService is a service for reconciling OS image rules
Expand All @@ -34,42 +32,18 @@ func NewImageRulesService(log logr.Logger, api api.BootResources) *ImageRulesSer
// ReconcileMaasInstanceImageRule reconciles a MAAS instance image rule from the MaasValidator config
func (s *ImageRulesService) ReconcileMaasInstanceImageRule(rule v1alpha1.ImageRule) (*types.ValidationRuleResult, error) {

vr := buildValidationResult(rule)
vr := utils.BuildValidationResult(rule.Name, constants.ValidationTypeImage)

errs, details := s.findBootResources(rule)

s.updateResult(vr, errs, constants.ErrImageNotFound, details...)
utils.UpdateResult(vr, errs, constants.ErrImageNotFound, details...)

if len(errs) > 0 {
return vr, errs[0]
}
return vr, nil
}

// buildValidationResult builds a default ValidationResult for a given validation type
func buildValidationResult(rule v1alpha1.ImageRule) *types.ValidationRuleResult {
state := vapi.ValidationSucceeded
latestCondition := vapi.DefaultValidationCondition()
latestCondition.Details = make([]string, 0)
latestCondition.Failures = make([]string, 0)
latestCondition.Message = fmt.Sprintf("All %s checks passed", constants.ValidationTypeImage)
latestCondition.ValidationRule = fmt.Sprintf("%s-%s", vapiconstants.ValidationRulePrefix, util.Sanitize(rule.Name))
latestCondition.ValidationType = constants.ValidationTypeImage
return &types.ValidationRuleResult{Condition: &latestCondition, State: &state}
}

// updateResult updates a ValidationRuleResult with a list of errors and details
func (s *ImageRulesService) updateResult(vr *types.ValidationRuleResult, errs []error, errMsg string, details ...string) {
if len(errs) > 0 {
vr.State = util.Ptr(vapi.ValidationFailed)
vr.Condition.Message = errMsg
for _, err := range errs {
vr.Condition.Failures = append(vr.Condition.Failures, err.Error())
}
}
vr.Condition.Details = append(vr.Condition.Details, details...)
}

// convertBootResourceToOSImage formats a list of BootResources as a list of OSImages
func convertBootResourceToOSImage(images []entity.BootResource) []v1alpha1.Image {
converted := make([]v1alpha1.Image, len(images))
Expand Down

0 comments on commit 50b317e

Please sign in to comment.