Skip to content

Commit 50b317e

Browse files
feat: add upstream dns validation (#102)
## 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]>
1 parent ba024df commit 50b317e

File tree

6 files changed

+223
-32
lines changed

6 files changed

+223
-32
lines changed

internal/controller/maasvalidator_controller.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838

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

120-
maasRuleService := osval.NewImageRulesService(r.Log, maasClient.BootResources)
121+
imageRulesService := osval.NewImageRulesService(r.Log, maasClient.BootResources)
122+
upstreamDNSRulesService := dnsval.NewUpstreamDNSRulesService(r.Log, maasClient.MAASServer)
121123

122124
// MAAS Instance image rules
123125
for _, rule := range validator.Spec.ImageRules {
124-
vrr, err := maasRuleService.ReconcileMaasInstanceImageRule(rule)
126+
vrr, err := imageRulesService.ReconcileMaasInstanceImageRule(rule)
125127
if err != nil {
126-
r.Log.V(0).Error(err, "failed to reconcile MAAS instance rule")
128+
r.Log.V(0).Error(err, "failed to reconcile MAAS image rule")
129+
}
130+
resp.AddResult(vrr, err)
131+
}
132+
133+
// MAAS Instance upstream DNS rules
134+
for _, rule := range validator.Spec.UpstreamDNSRules {
135+
vrr, err := upstreamDNSRulesService.ReconcileMaasInstanceUpstreamDNSRules(rule)
136+
if err != nil {
137+
r.Log.V(0).Error(err, "failed to reconcile MAAS upstream DNS rule")
127138
}
128139
resp.AddResult(vrr, err)
129140
}

internal/controller/maasvalidator_controller_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ type MockBootResourcesService struct {
2424
api.BootResources
2525
}
2626

27+
type MockUDNSRulesService struct {
28+
api.MAASServer
29+
}
30+
2731
func (b *MockBootResourcesService) Get(params *entity.BootResourcesReadParams) ([]entity.BootResource, error) {
2832
return []entity.BootResource{
2933
{
@@ -33,6 +37,10 @@ func (b *MockBootResourcesService) Get(params *entity.BootResourcesReadParams) (
3337
}, nil
3438
}
3539

40+
func (u *MockUDNSRulesService) Get(string) ([]byte, error) {
41+
return []byte("8.8.8.8"), nil
42+
}
43+
3644
var _ = Describe("MaaSValidator controller", Ordered, func() {
3745

3846
BeforeEach(func() {
@@ -44,6 +52,7 @@ var _ = Describe("MaaSValidator controller", Ordered, func() {
4452
SetUpClient = func(maasURL, massToken string) (*maasclient.Client, error) {
4553
c := &maasclient.Client{}
4654
c.BootResources = &MockBootResourcesService{}
55+
c.MAASServer = &MockUDNSRulesService{}
4756
return c, nil
4857
}
4958
})

internal/utils/result.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Package utils provides utility functions for the MAAS validator
2+
package utils
3+
4+
import (
5+
"fmt"
6+
7+
vapi "github.com/validator-labs/validator/api/v1alpha1"
8+
vapiconstants "github.com/validator-labs/validator/pkg/constants"
9+
"github.com/validator-labs/validator/pkg/types"
10+
"github.com/validator-labs/validator/pkg/util"
11+
)
12+
13+
// BuildValidationResult builds a default ValidationResult for a given validation type
14+
func BuildValidationResult(ruleName, ruleType string) *types.ValidationRuleResult {
15+
state := vapi.ValidationSucceeded
16+
latestCondition := vapi.DefaultValidationCondition()
17+
latestCondition.Details = make([]string, 0)
18+
latestCondition.Failures = make([]string, 0)
19+
latestCondition.Message = fmt.Sprintf("All %s checks passed", ruleType)
20+
latestCondition.ValidationRule = fmt.Sprintf("%s-%s", vapiconstants.ValidationRulePrefix, util.Sanitize(ruleName))
21+
latestCondition.ValidationType = ruleType
22+
return &types.ValidationRuleResult{Condition: &latestCondition, State: &state}
23+
}
24+
25+
// UpdateResult updates a ValidationRuleResult with a list of errors and details
26+
func UpdateResult(vr *types.ValidationRuleResult, errs []error, errMsg string, details ...string) {
27+
if len(errs) > 0 {
28+
vr.State = util.Ptr(vapi.ValidationFailed)
29+
vr.Condition.Message = errMsg
30+
for _, err := range errs {
31+
vr.Condition.Failures = append(vr.Condition.Failures, err.Error())
32+
}
33+
}
34+
vr.Condition.Details = append(vr.Condition.Details, details...)
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Package dns contains the logic for validating MAAS instance DNS rules
2+
package dns
3+
4+
import (
5+
"fmt"
6+
"strings"
7+
8+
"github.com/canonical/gomaasclient/api"
9+
"github.com/go-logr/logr"
10+
11+
"github.com/validator-labs/validator-plugin-maas/api/v1alpha1"
12+
"github.com/validator-labs/validator-plugin-maas/internal/constants"
13+
"github.com/validator-labs/validator-plugin-maas/internal/utils"
14+
"github.com/validator-labs/validator/pkg/types"
15+
)
16+
17+
// UpstreamDNSRulesService is the service for validating MAAS instance upstream DNS rules
18+
type UpstreamDNSRulesService struct {
19+
log logr.Logger
20+
api api.MAASServer
21+
}
22+
23+
// NewUpstreamDNSRulesService creates a new UpstreamDNSRulesService
24+
func NewUpstreamDNSRulesService(log logr.Logger, api api.MAASServer) *UpstreamDNSRulesService {
25+
return &UpstreamDNSRulesService{
26+
log: log,
27+
api: api,
28+
}
29+
}
30+
31+
// ReconcileMaasInstanceUpstreamDNSRules reconciles a MAAS instance upstream DNS rule
32+
func (s *UpstreamDNSRulesService) ReconcileMaasInstanceUpstreamDNSRules(rule v1alpha1.UpstreamDNSRule) (*types.ValidationRuleResult, error) {
33+
34+
vr := utils.BuildValidationResult(rule.Name, constants.ValidationTypeUDNS)
35+
36+
details, errs := s.findDNSServers(rule.NumDNSServers)
37+
38+
utils.UpdateResult(vr, errs, constants.ErrUDNSNotConfigured, details...)
39+
40+
if len(errs) > 0 {
41+
return vr, errs[0]
42+
}
43+
44+
return vr, nil
45+
}
46+
47+
func (s *UpstreamDNSRulesService) findDNSServers(expected int) ([]string, []error) {
48+
details := make([]string, 0)
49+
errs := make([]error, 0)
50+
51+
ns, err := s.api.Get("upstream_dns")
52+
if err != nil {
53+
return nil, []error{err}
54+
}
55+
nameservers := strings.Split(string(ns), " ")
56+
numServers := len(nameservers)
57+
58+
if nameservers[0] == "" {
59+
numServers = 0
60+
}
61+
62+
if numServers < expected {
63+
errs = append(errs, fmt.Errorf("expected %d DNS server(s), got %d", expected, numServers))
64+
} else {
65+
details = append(details, fmt.Sprintf("Found %d DNS server(s)", len(nameservers)))
66+
}
67+
return details, errs
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package dns
2+
3+
import (
4+
"testing"
5+
6+
"github.com/canonical/gomaasclient/api"
7+
"github.com/go-logr/logr"
8+
"github.com/stretchr/testify/assert"
9+
10+
"github.com/validator-labs/validator-plugin-maas/api/v1alpha1"
11+
)
12+
13+
type DummyMAASServer struct {
14+
api.MAASServer
15+
upstreamDNS string
16+
}
17+
18+
func (d *DummyMAASServer) Get(string) ([]byte, error) {
19+
return []byte(d.upstreamDNS), nil
20+
}
21+
22+
func TestReconcileMaasInstanceImageRule(t *testing.T) {
23+
24+
testCases := []struct {
25+
Name string
26+
ruleService *UpstreamDNSRulesService
27+
upstreamDNSRules []v1alpha1.UpstreamDNSRule
28+
errors []string
29+
details []string
30+
}{
31+
{
32+
Name: "Enough DNS servers are found in MAAS",
33+
ruleService: NewUpstreamDNSRulesService(
34+
logr.Logger{},
35+
&DummyMAASServer{
36+
upstreamDNS: "8.8.8.8",
37+
},
38+
),
39+
upstreamDNSRules: []v1alpha1.UpstreamDNSRule{
40+
{Name: "Upstream DNS rule 1", NumDNSServers: 1},
41+
},
42+
errors: nil,
43+
details: []string{"Found 1 DNS server(s)"},
44+
},
45+
{
46+
Name: "Not enough DNS servers are found in MAAS",
47+
ruleService: NewUpstreamDNSRulesService(
48+
logr.Logger{},
49+
&DummyMAASServer{
50+
upstreamDNS: "8.8.8.8",
51+
}),
52+
upstreamDNSRules: []v1alpha1.UpstreamDNSRule{
53+
{Name: "Upstream DNS rule 2", NumDNSServers: 2},
54+
},
55+
errors: []string{"expected 2 DNS server(s), got 1"},
56+
details: nil,
57+
},
58+
{
59+
Name: "No DNS servers are found in MAAS",
60+
ruleService: NewUpstreamDNSRulesService(
61+
logr.Logger{},
62+
&DummyMAASServer{
63+
upstreamDNS: "",
64+
}),
65+
upstreamDNSRules: []v1alpha1.UpstreamDNSRule{
66+
{Name: "Upstream DNS rule 3", NumDNSServers: 1},
67+
},
68+
errors: []string{"expected 1 DNS server(s), got 0"},
69+
details: nil,
70+
},
71+
}
72+
73+
for _, tc := range testCases {
74+
t.Run(tc.Name, func(t *testing.T) {
75+
var details []string
76+
var errors []string
77+
78+
for _, rule := range tc.upstreamDNSRules {
79+
vr, _ := tc.ruleService.ReconcileMaasInstanceUpstreamDNSRules(rule)
80+
details = append(details, vr.Condition.Details...)
81+
errors = append(errors, vr.Condition.Failures...)
82+
}
83+
84+
assert.Equal(t, len(tc.errors), len(errors), "Number of errors should match")
85+
for _, expectedError := range tc.errors {
86+
assert.Contains(t, errors, expectedError, "Expected error should be present")
87+
}
88+
assert.Equal(t, len(tc.details), len(details), "Number of details should match")
89+
for _, expectedDetail := range tc.details {
90+
assert.Contains(t, details, expectedDetail, "Expected detail should be present")
91+
}
92+
})
93+
}
94+
}

internal/validators/os/os_validator.go

+3-29
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import (
1111

1212
"github.com/validator-labs/validator-plugin-maas/api/v1alpha1"
1313
"github.com/validator-labs/validator-plugin-maas/internal/constants"
14-
vapi "github.com/validator-labs/validator/api/v1alpha1"
15-
vapiconstants "github.com/validator-labs/validator/pkg/constants"
14+
"github.com/validator-labs/validator-plugin-maas/internal/utils"
1615
"github.com/validator-labs/validator/pkg/types"
17-
"github.com/validator-labs/validator/pkg/util"
1816
)
1917

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

37-
vr := buildValidationResult(rule)
35+
vr := utils.BuildValidationResult(rule.Name, constants.ValidationTypeImage)
3836

3937
errs, details := s.findBootResources(rule)
4038

41-
s.updateResult(vr, errs, constants.ErrImageNotFound, details...)
39+
utils.UpdateResult(vr, errs, constants.ErrImageNotFound, details...)
4240

4341
if len(errs) > 0 {
4442
return vr, errs[0]
4543
}
4644
return vr, nil
4745
}
4846

49-
// buildValidationResult builds a default ValidationResult for a given validation type
50-
func buildValidationResult(rule v1alpha1.ImageRule) *types.ValidationRuleResult {
51-
state := vapi.ValidationSucceeded
52-
latestCondition := vapi.DefaultValidationCondition()
53-
latestCondition.Details = make([]string, 0)
54-
latestCondition.Failures = make([]string, 0)
55-
latestCondition.Message = fmt.Sprintf("All %s checks passed", constants.ValidationTypeImage)
56-
latestCondition.ValidationRule = fmt.Sprintf("%s-%s", vapiconstants.ValidationRulePrefix, util.Sanitize(rule.Name))
57-
latestCondition.ValidationType = constants.ValidationTypeImage
58-
return &types.ValidationRuleResult{Condition: &latestCondition, State: &state}
59-
}
60-
61-
// updateResult updates a ValidationRuleResult with a list of errors and details
62-
func (s *ImageRulesService) updateResult(vr *types.ValidationRuleResult, errs []error, errMsg string, details ...string) {
63-
if len(errs) > 0 {
64-
vr.State = util.Ptr(vapi.ValidationFailed)
65-
vr.Condition.Message = errMsg
66-
for _, err := range errs {
67-
vr.Condition.Failures = append(vr.Condition.Failures, err.Error())
68-
}
69-
}
70-
vr.Condition.Details = append(vr.Condition.Details, details...)
71-
}
72-
7347
// convertBootResourceToOSImage formats a list of BootResources as a list of OSImages
7448
func convertBootResourceToOSImage(images []entity.BootResource) []v1alpha1.Image {
7549
converted := make([]v1alpha1.Image, len(images))

0 commit comments

Comments
 (0)