Add a test plugin for Zonemaster
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
ced60c7b31
commit
226c80e607
3 changed files with 322 additions and 0 deletions
7
plugins/zonemaster/Makefile
Normal file
7
plugins/zonemaster/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
PLUGIN_NAME=zonemaster
|
||||
TARGET=../happydomain-plugin-test-$(PLUGIN_NAME).so
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): *.go
|
||||
go build -buildmode=plugin -o $@ git.happydns.org/happyDomain/plugins/$(PLUGIN_NAME)
|
||||
9
plugins/zonemaster/main.go
Normal file
9
plugins/zonemaster/main.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func NewTestPlugin() (happydns.TestPlugin, error) {
|
||||
return &ZonemasterTest{}, nil
|
||||
}
|
||||
306
plugins/zonemaster/test.go
Normal file
306
plugins/zonemaster/test.go
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
type ZonemasterTest struct {
|
||||
}
|
||||
|
||||
func (p *ZonemasterTest) PluginEnvName() []string {
|
||||
return []string{
|
||||
"zonemaster",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ZonemasterTest) Version() happydns.PluginVersionInfo {
|
||||
return happydns.PluginVersionInfo{
|
||||
Name: "Zonemaster",
|
||||
Version: "0.1",
|
||||
AvailableOn: happydns.PluginAvailability{
|
||||
ApplyToDomain: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ZonemasterTest) AvailableOptions() happydns.PluginOptionsDocumentation {
|
||||
return happydns.PluginOptionsDocumentation{
|
||||
RunOpts: []happydns.PluginOptionDocumentation{
|
||||
{
|
||||
Id: "domainName",
|
||||
Type: "string",
|
||||
Label: "Domain name to test",
|
||||
AutoFill: happydns.AutoFillDomainName,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Id: "profile",
|
||||
Type: "string",
|
||||
Label: "Profile",
|
||||
Placeholder: "default",
|
||||
Default: "default",
|
||||
},
|
||||
},
|
||||
UserOpts: []happydns.PluginOptionDocumentation{
|
||||
{
|
||||
Id: "language",
|
||||
Type: "select",
|
||||
Label: "Result language",
|
||||
Default: "en",
|
||||
Choices: []string{
|
||||
"en", // English
|
||||
"fr", // French
|
||||
"de", // German
|
||||
"es", // Spanish
|
||||
"sv", // Swedish
|
||||
"da", // Danish
|
||||
"fi", // Finnish
|
||||
"nb", // Norwegian Bokmål
|
||||
"nl", // Dutch
|
||||
"pt", // Portuguese
|
||||
},
|
||||
},
|
||||
},
|
||||
AdminOpts: []happydns.PluginOptionDocumentation{
|
||||
{
|
||||
Id: "zonemasterAPIURL",
|
||||
Type: "string",
|
||||
Label: "Zonemaster API URL",
|
||||
Placeholder: "https://zonemaster.net/api",
|
||||
Default: "https://zonemaster.net/api",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// JSON-RPC request/response structures
|
||||
type jsonRPCRequest struct {
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
Params any `json:"params"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
type jsonRPCResponse struct {
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
// Zonemaster API structures
|
||||
type startTestParams struct {
|
||||
Domain string `json:"domain"`
|
||||
Profile string `json:"profile,omitempty"`
|
||||
IPv4 bool `json:"ipv4,omitempty"`
|
||||
IPv6 bool `json:"ipv6,omitempty"`
|
||||
}
|
||||
|
||||
type testProgressParams struct {
|
||||
TestID string `json:"test_id"`
|
||||
}
|
||||
|
||||
type getResultsParams struct {
|
||||
ID string `json:"id"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
type testResult struct {
|
||||
Module string `json:"module"`
|
||||
Message string `json:"message"`
|
||||
Level string `json:"level"`
|
||||
Testcase string `json:"testcase,omitempty"`
|
||||
}
|
||||
|
||||
type zonemasterResults struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
HashID string `json:"hash_id"`
|
||||
Params map[string]any `json:"params"`
|
||||
Results []testResult `json:"results"`
|
||||
TestcaseDescriptions map[string]string `json:"testcase_descriptions,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ZonemasterTest) callJSONRPC(apiURL, method string, params any) (json.RawMessage, error) {
|
||||
reqBody := jsonRPCRequest{
|
||||
Jsonrpc: "2.0",
|
||||
Method: method,
|
||||
Params: params,
|
||||
ID: 1,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call API: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var rpcResp jsonRPCResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rpcResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if rpcResp.Error != nil {
|
||||
return nil, fmt.Errorf("API error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
||||
}
|
||||
|
||||
return rpcResp.Result, nil
|
||||
}
|
||||
|
||||
func (p *ZonemasterTest) RunTest(options happydns.PluginOptions, meta map[string]string) (*happydns.PluginResult, error) {
|
||||
// Extract options
|
||||
domainName, ok := options["domainName"].(string)
|
||||
if !ok || domainName == "" {
|
||||
return nil, fmt.Errorf("domainName is required")
|
||||
}
|
||||
domainName = strings.TrimSuffix(domainName, ".")
|
||||
|
||||
apiURL, ok := options["zonemasterAPIURL"].(string)
|
||||
if !ok || apiURL == "" {
|
||||
return nil, fmt.Errorf("zonemasterAPIURL is required")
|
||||
}
|
||||
apiURL = strings.TrimSuffix(apiURL, "/")
|
||||
|
||||
language := "en"
|
||||
if lang, ok := options["language"].(string); ok && lang != "" {
|
||||
language = lang
|
||||
}
|
||||
|
||||
profile := "default"
|
||||
if prof, ok := options["profile"].(string); ok && prof != "" {
|
||||
profile = prof
|
||||
}
|
||||
|
||||
// Step 1: Start the test
|
||||
startParams := startTestParams{
|
||||
Domain: domainName,
|
||||
Profile: profile,
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
}
|
||||
|
||||
result, err := p.callJSONRPC(apiURL, "start_domain_test", startParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start test: %w", err)
|
||||
}
|
||||
|
||||
var testID string
|
||||
if err := json.Unmarshal(result, &testID); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse test ID: %w", err)
|
||||
}
|
||||
|
||||
if testID == "" {
|
||||
return nil, fmt.Errorf("received empty test ID")
|
||||
}
|
||||
|
||||
// Step 2: Poll for test completion
|
||||
progressParams := testProgressParams{TestID: testID}
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
timeout := time.After(10 * time.Minute)
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return nil, fmt.Errorf("test timeout after 10 minutes (test ID: %s)", testID)
|
||||
|
||||
case <-ticker.C:
|
||||
result, err := p.callJSONRPC(apiURL, "test_progress", progressParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check progress: %w", err)
|
||||
}
|
||||
|
||||
var progress float64
|
||||
if err := json.Unmarshal(result, &progress); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse progress: %w", err)
|
||||
}
|
||||
|
||||
if progress >= 100 {
|
||||
goto testComplete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testComplete:
|
||||
// Step 3: Get test results
|
||||
resultsParams := getResultsParams{
|
||||
ID: testID,
|
||||
Language: language,
|
||||
}
|
||||
|
||||
result, err = p.callJSONRPC(apiURL, "get_test_results", resultsParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get results: %w", err)
|
||||
}
|
||||
|
||||
var results zonemasterResults
|
||||
if err := json.Unmarshal(result, &results); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse results: %w", err)
|
||||
}
|
||||
|
||||
// Analyze results to determine overall status
|
||||
var (
|
||||
errorCount int
|
||||
warningCount int
|
||||
infoCount int
|
||||
criticalMsgs []string
|
||||
)
|
||||
|
||||
for _, r := range results.Results {
|
||||
switch strings.ToUpper(r.Level) {
|
||||
case "CRITICAL", "ERROR":
|
||||
errorCount++
|
||||
if len(criticalMsgs) < 5 { // Keep first 5 critical messages
|
||||
criticalMsgs = append(criticalMsgs, r.Message)
|
||||
}
|
||||
case "WARNING":
|
||||
warningCount++
|
||||
case "INFO", "NOTICE":
|
||||
infoCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Determine status
|
||||
var status happydns.PluginResultStatus
|
||||
var statusLine string
|
||||
|
||||
if errorCount > 0 {
|
||||
status = happydns.PluginResultStatusKO
|
||||
statusLine = fmt.Sprintf("%d error(s), %d warning(s) found", errorCount, warningCount)
|
||||
if len(criticalMsgs) > 0 {
|
||||
statusLine += ": " + strings.Join(criticalMsgs[:min(2, len(criticalMsgs))], "; ")
|
||||
}
|
||||
} else if warningCount > 0 {
|
||||
status = happydns.PluginResultStatusWarn
|
||||
statusLine = fmt.Sprintf("%d warning(s) found", warningCount)
|
||||
} else {
|
||||
status = happydns.PluginResultStatusOK
|
||||
statusLine = fmt.Sprintf("All tests passed (%d checks)", len(results.Results))
|
||||
}
|
||||
|
||||
return &happydns.PluginResult{
|
||||
Status: status,
|
||||
StatusLine: statusLine,
|
||||
Report: results,
|
||||
}, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue