diff --git a/docs/gossfile.md b/docs/gossfile.md index a463c2ce..0fe360be 100644 --- a/docs/gossfile.md +++ b/docs/gossfile.md @@ -193,6 +193,8 @@ dns: - ::1 server: 8.8.8.8 # Also supports server:port timeout: 500 # in milliseconds (Only used when server attribute is provided) + retry_count: 1 # Enables retry mechanism when greater than 0; number of additional attempts + retry_delay: 5 # Delay (in seconds) before each retry attempt ``` It is possible to validate the following types of DNS records, but requires the ```server``` attribute be set: @@ -217,6 +219,8 @@ dns: server: 208.67.222.222 addrs: - "a.dnstest.io." + retry_count: 2 + retry_delay: 5 # Validate a PTR record PTR:8.8.8.8: @@ -224,6 +228,8 @@ dns: server: 8.8.8.8 addrs: - "dns.google." + retry_count: 2 + retry_delay: 5 # Validate and SRV record SRV:_https._tcp.dnstest.io: @@ -232,6 +238,8 @@ dns: addrs: - "0 5 443 a.dnstest.io." - "10 10 443 b.dnstest.io." + retry_count: 2 + retry_delay: 5 ``` Please note that if you want `localhost` to **only** resolve `127.0.0.1` you'll need to use [Advanced Matchers](#advanced-matchers) @@ -486,6 +494,8 @@ package: versions: - 2.2.15 skip: false + retry_count: 1 # Enables retry mechanism when greater than 0; number of additional attempts + retry_delay: 180 # Delay (in seconds) before each retry attempt ``` !!! note diff --git a/resource/dns.go b/resource/dns.go index 6cd11400..260f8ca1 100644 --- a/resource/dns.go +++ b/resource/dns.go @@ -20,6 +20,8 @@ type DNS struct { Addrs matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"` Timeout int `json:"timeout" yaml:"timeout"` Server string `json:"server,omitempty" yaml:"server,omitempty"` + RetryCount int `json:"retry_count,omitempty" yaml:"retry_count,omitempty"` + RetryDelay int `json:"retry_delay,omitempty" yaml:"retry_delay,omitempty"` Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` } @@ -50,6 +52,8 @@ func (d *DNS) GetResolve() string { } return d.id } +func (d *DNS) GetRetryCount() int { return d.RetryCount } +func (d *DNS) GetRetryDelay() int { return d.RetryDelay } func (d *DNS) Validate(sys *system.System) []TestResult { ctx := context.WithValue(context.Background(), idKey{}, d.ID()) @@ -58,19 +62,34 @@ func (d *DNS) Validate(sys *system.System) []TestResult { d.Timeout = 500 } - sysDNS := sys.NewDNS(ctx, d.GetResolve(), sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server}) - var results []TestResult // Backwards compatibility hack for now if d.Resolvable == nil { d.Resolvable = d.Resolveable } - results = append(results, ValidateValue(d, "resolvable", d.Resolvable, sysDNS.Resolvable, skip)) + // Retry logic for resolvable + if d.RetryCount > 0 { + results = append(results, ValidateValueWithRetry(d, "resolvable", d.Resolvable, func() (any, error) { + sysDNS := sys.NewDNS(ctx, d.GetResolve(), sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server}) + return sysDNS.Resolvable() + }, skip, d.RetryCount, d.RetryDelay)) + } else { + sysDNS := sys.NewDNS(ctx, d.GetResolve(), sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server}) + results = append(results, ValidateValue(d, "resolvable", d.Resolvable, sysDNS.Resolvable, skip)) + } if shouldSkip(results) { skip = true } if d.Addrs != nil { - results = append(results, ValidateValue(d, "addrs", d.Addrs, sysDNS.Addrs, skip)) + if d.RetryCount > 0 { + results = append(results, ValidateValueWithRetry(d, "addrs", d.Addrs, func() (any, error) { + sysDNS := sys.NewDNS(ctx, d.GetResolve(), sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server}) + return sysDNS.Addrs() + }, skip, d.RetryCount, d.RetryDelay)) + } else { + sysDNS := sys.NewDNS(ctx, d.GetResolve(), sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server}) + results = append(results, ValidateValue(d, "addrs", d.Addrs, sysDNS.Addrs, skip)) + } } return results } diff --git a/resource/package.go b/resource/package.go index 6d393a1b..61a36001 100644 --- a/resource/package.go +++ b/resource/package.go @@ -9,13 +9,15 @@ import ( ) type Package struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - id string `json:"-" yaml:"-"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Installed matcher `json:"installed" yaml:"installed"` - Versions matcher `json:"versions,omitempty" yaml:"versions,omitempty"` - Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + id string `json:"-" yaml:"-"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Installed matcher `json:"installed" yaml:"installed"` + Versions matcher `json:"versions,omitempty" yaml:"versions,omitempty"` + RetryCount int `json:"retry_count,omitempty" yaml:"retry_count,omitempty"` + RetryDelay int `json:"retry_delay,omitempty" yaml:"retry_delay,omitempty"` + Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` } const ( @@ -45,20 +47,45 @@ func (p *Package) GetName() string { } return p.id } +func (p *Package) GetRetryCount() int { return p.RetryCount } +func (p *Package) GetRetryDelay() int { return p.RetryDelay } func (p *Package) Validate(sys *system.System) []TestResult { ctx := context.WithValue(context.Background(), idKey{}, p.ID()) skip := p.Skip - sysPkg := sys.NewPackage(ctx, p.GetName(), sys, util.Config{}) var results []TestResult - results = append(results, ValidateValue(p, "installed", p.Installed, sysPkg.Installed, skip)) + + // Handle retry logic for installed check + if p.RetryCount > 0 { + results = append(results, ValidateValueWithRetry(p, "installed", p.Installed, + func() (any, error) { + sysPkg := sys.NewPackage(ctx, p.GetName(), sys, util.Config{}) + return sysPkg.Installed() + }, skip, p.RetryCount, p.RetryDelay)) + } else { + sysPkg := sys.NewPackage(ctx, p.GetName(), sys, util.Config{}) + results = append(results, ValidateValue(p, "installed", p.Installed, sysPkg.Installed, skip)) + } + if shouldSkip(results) { skip = true } + + // Handle retry logic for versions check if p.Versions != nil { - results = append(results, ValidateValue(p, "version", p.Versions, sysPkg.Versions, skip)) + if p.RetryCount > 0 { + results = append(results, ValidateValueWithRetry(p, "version", p.Versions, + func() (any, error) { + sysPkg := sys.NewPackage(ctx, p.GetName(), sys, util.Config{}) + return sysPkg.Versions() + }, skip, p.RetryCount, p.RetryDelay)) + } else { + sysPkg := sys.NewPackage(ctx, p.GetName(), sys, util.Config{}) + results = append(results, ValidateValue(p, "version", p.Versions, sysPkg.Versions, skip)) + } } + return results } diff --git a/resource/resource.go b/resource/resource.go index 104972ad..67f1495d 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -39,6 +39,11 @@ type ResourceRead interface { GetMeta() meta } +type Retryable interface { + GetRetryCount() int + GetRetryDelay() int +} + type matcher any type meta map[string]any diff --git a/resource/validate.go b/resource/validate.go index 720c7b7d..bcafb137 100644 --- a/resource/validate.go +++ b/resource/validate.go @@ -126,6 +126,45 @@ func ValidateValue(res ResourceRead, property string, expectedValue any, actual return ValidateGomegaValue(res, property, expectedValue, actual, skip) } +func ValidateValueWithRetry(res ResourceRead, property string, expectedValue any, actualFunc func() (any, error), skip bool, retryCount int, retryDelay int) TestResult { + if skip { + // Return skip result immediately + skipFunc := func() (any, error) { return nil, nil } + return ValidateValue(res, property, expectedValue, skipFunc, skip) + } + + maxRetries := retryCount + 1 + if retryCount < 0 { + maxRetries = 1 + } + delay := time.Duration(retryDelay) * time.Second + if delay <= 0 { + delay = 1 * time.Second // Default delay if not specified or invalid + } + + var lastResult TestResult + + for attempt := 0; attempt < maxRetries; attempt++ { + actual, err := actualFunc() + + // Create a function that returns the current result + currentFunc := func() (any, error) { return actual, err } + result := ValidateValue(res, property, expectedValue, currentFunc, skip) + lastResult = result + + if result.Result == SUCCESS { + return result + } + + // If not the last attempt, wait before retrying + if attempt < maxRetries-1 { + time.Sleep(delay) + } + } + + return lastResult +} + func ValidateGomegaValue(res ResourceRead, property string, expectedValue any, actual any, skip bool) TestResult { id := res.ID() title := res.GetTitle()