From 2bd0ae99bd7ac19ad051ee34ce3e8f297f4454e3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 24 Apr 2026 13:00:26 +0700 Subject: [PATCH 1/8] Migrate to checker-sdk-go v1.3.0 with standalone build tag The SDK split the HTTP server scaffolding into the new checker-sdk-go/checker/server subpackage. Update main.go to import server and call server.New, and isolate the interactive form code behind the standalone build tag so plugin/builtin builds skip net/http entirely. --- Dockerfile | 2 +- Makefile | 7 +++++-- checker/interactive.go | 6 ++++-- go.mod | 2 +- go.sum | 4 ++-- main.go | 6 +++--- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 447bd68..b1c8d9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-matrix . +RUN CGO_ENABLED=0 go build -tags standalone -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-matrix . FROM scratch COPY --from=builder /checker-matrix /checker-matrix diff --git a/Makefile b/Makefile index faf351c..6a80145 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,12 @@ CHECKER_SOURCES := main.go $(wildcard checker/*.go) GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION) -.PHONY: all plugin docker clean +.PHONY: all plugin docker test clean all: $(CHECKER_NAME) $(CHECKER_NAME): $(CHECKER_SOURCES) - go build -ldflags "$(GO_LDFLAGS)" -o $@ . + go build -tags standalone -ldflags "$(GO_LDFLAGS)" -o $@ . plugin: $(CHECKER_NAME).so @@ -21,5 +21,8 @@ $(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go) docker: docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) . +test: + go test -tags standalone ./... + clean: rm -f $(CHECKER_NAME) $(CHECKER_NAME).so diff --git a/checker/interactive.go b/checker/interactive.go index 4c00bd5..4f0deb8 100644 --- a/checker/interactive.go +++ b/checker/interactive.go @@ -1,3 +1,5 @@ +//go:build standalone + package checker import ( @@ -8,7 +10,7 @@ import ( sdk "git.happydns.org/checker-sdk-go/checker" ) -// RenderForm implements sdk.CheckerInteractive. +// RenderForm implements server.Interactive. func (p *matrixProvider) RenderForm() []sdk.CheckerOptionField { return []sdk.CheckerOptionField{ { @@ -29,7 +31,7 @@ func (p *matrixProvider) RenderForm() []sdk.CheckerOptionField { } } -// ParseForm implements sdk.CheckerInteractive. +// ParseForm implements server.Interactive. func (p *matrixProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) { domain := strings.TrimSpace(r.FormValue("serviceDomain")) if domain == "" { diff --git a/go.mod b/go.mod index 8c5c78c..c7c45f9 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module git.happydns.org/checker-matrix go 1.25.0 -require git.happydns.org/checker-sdk-go v1.2.0 +require git.happydns.org/checker-sdk-go v1.3.0 diff --git a/go.sum b/go.sum index 272600a..fe4952c 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -git.happydns.org/checker-sdk-go v1.2.0 h1:v4MpKAz0W3PwP+bxx3pya8w893sVH5xTD1of1cc0TV8= -git.happydns.org/checker-sdk-go v1.2.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-sdk-go v1.3.0 h1:FG2kIhlJCzI0m35EhxSgn4UWc9M4ha6aZTeoChu4l7A= +git.happydns.org/checker-sdk-go v1.3.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= diff --git a/main.go b/main.go index 2d19c41..22aae3a 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ import ( "log" matrix "git.happydns.org/checker-matrix/checker" - sdk "git.happydns.org/checker-sdk-go/checker" + "git.happydns.org/checker-sdk-go/checker/server" ) // Version is the standalone binary's version. It defaults to "custom-build" @@ -23,8 +23,8 @@ func main() { // CheckerDefinition.Version. matrix.Version = Version - server := sdk.NewServer(matrix.Provider()) - if err := server.ListenAndServe(*listenAddr); err != nil { + srv := server.New(matrix.Provider()) + if err := srv.ListenAndServe(*listenAddr); err != nil { log.Fatalf("server error: %v", err) } } From e4b6481d3286537bf889ba0dd82c60200038a45c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 26 Apr 2026 00:40:59 +0700 Subject: [PATCH 2/8] checker: split monolithic rule into per-concern rules Replace the single matrix_federation rule with individual rules for federation status, well-known delegation, SRV records, connection reachability, TLS checks, and homeserver version, so the UI surfaces a clear checklist. Drop the incorrect well-known/server_name equality check: m.server points at the delegated federation endpoint, which is intentionally distinct from server_name. --- checker/definition.go | 4 +- checker/rule.go | 107 +++++++++++++++--------------------- checker/rules_connection.go | 40 ++++++++++++++ checker/rules_federation.go | 61 ++++++++++++++++++++ checker/rules_srv.go | 48 ++++++++++++++++ checker/rules_tls.go | 73 ++++++++++++++++++++++++ checker/rules_version.go | 40 ++++++++++++++ checker/rules_wellknown.go | 43 +++++++++++++++ 8 files changed, 351 insertions(+), 65 deletions(-) create mode 100644 checker/rules_connection.go create mode 100644 checker/rules_federation.go create mode 100644 checker/rules_srv.go create mode 100644 checker/rules_tls.go create mode 100644 checker/rules_version.go create mode 100644 checker/rules_wellknown.go diff --git a/checker/definition.go b/checker/definition.go index 4e5ec7c..ebf653e 100644 --- a/checker/definition.go +++ b/checker/definition.go @@ -50,9 +50,7 @@ func Definition() *sdk.CheckerDefinition { }, }, }, - Rules: []sdk.CheckRule{ - Rule(), - }, + Rules: Rules(), Interval: &sdk.CheckIntervalSpec{ Min: 5 * time.Minute, Max: 7 * 24 * time.Hour, diff --git a/checker/rule.go b/checker/rule.go index 8184137..94d39f6 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -3,79 +3,62 @@ package checker import ( "context" "fmt" - "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) -// Rule returns a new matrix federation check rule. +// Rules returns the full list of CheckRules exposed by the Matrix checker. +// Each rule covers a single concern so the UI can show a clear checklist +// rather than a single monolithic pass/fail line. +func Rules() []sdk.CheckRule { + return []sdk.CheckRule{ + &federationOKRule{}, + &wellKnownRule{}, + &srvRecordsRule{}, + &connectionReachableRule{}, + &tlsChecksRule{}, + &versionRule{}, + } +} + +// Rule returns the aggregate federation rule. +// +// Deprecated: prefer Rules() which exposes every concern individually. Kept +// for backward compatibility with callers that embed a single rule. func Rule() sdk.CheckRule { - return &matrixRule{} + return &federationOKRule{} } -type matrixRule struct{} - -func (r *matrixRule) Name() string { - return "matrix_federation" -} - -func (r *matrixRule) Description() string { - return "Checks whether Matrix federation is working correctly" -} - -func (r *matrixRule) ValidateOptions(opts sdk.CheckerOptions) error { - return nil -} - -func (r *matrixRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { +// loadMatrixData fetches the Matrix observation. On error returns a +// CheckState the caller should emit to short-circuit its rule. +func loadMatrixData(ctx context.Context, obs sdk.ObservationGetter) (*MatrixFederationData, *sdk.CheckState) { var data MatrixFederationData if err := obs.Get(ctx, ObservationKeyMatrix, &data); err != nil { - return []sdk.CheckState{{ + return nil, &sdk.CheckState{ Status: sdk.StatusError, Message: fmt.Sprintf("Failed to get Matrix federation data: %v", err), - Code: "matrix_federation_error", - }} - } - - domain, _ := opts["serviceDomain"].(string) - domain = strings.TrimSuffix(domain, ".") - - if data.FederationOK { - version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version) - return []sdk.CheckState{{ - Status: sdk.StatusOK, - Message: fmt.Sprintf("Running %s", version), - Code: "matrix_federation_ok", - Meta: map[string]any{ - "version": version, - }, - }} - } - - var statusLine string - - if data.DNSResult.SRVError != nil && data.WellKnownResult.Result != "" { - statusLine = fmt.Sprintf("%s OR %s", data.DNSResult.SRVError.Message, data.WellKnownResult.Result) - } else if len(data.ConnectionErrors) > 0 { - var msg strings.Builder - for srv, cerr := range data.ConnectionErrors { - if msg.Len() > 0 { - msg.WriteString("; ") - } - msg.WriteString(srv) - msg.WriteString(": ") - msg.WriteString(cerr.Message) + Code: "matrix.observation_error", } - statusLine = fmt.Sprintf("Connection errors: %s", msg.String()) - } else if data.WellKnownResult.Server != domain { - statusLine = fmt.Sprintf("Bad homeserver_name: got %s, expected %s", data.WellKnownResult.Server, domain) - } else { - statusLine = fmt.Sprintf("Federation broken. Check https://federationtester.matrix.org/#%s", domain) } - - return []sdk.CheckState{{ - Status: sdk.StatusCrit, - Message: statusLine, - Code: "matrix_federation_fail", - }} + return &data, nil +} + +func passState(code, message string) sdk.CheckState { + return sdk.CheckState{Status: sdk.StatusOK, Message: message, Code: code} +} + +func infoState(code, message string) sdk.CheckState { + return sdk.CheckState{Status: sdk.StatusInfo, Message: message, Code: code} +} + +func warnState(code, message string) sdk.CheckState { + return sdk.CheckState{Status: sdk.StatusWarn, Message: message, Code: code} +} + +func critState(code, message string) sdk.CheckState { + return sdk.CheckState{Status: sdk.StatusCrit, Message: message, Code: code} +} + +func unknownState(code, message string) sdk.CheckState { + return sdk.CheckState{Status: sdk.StatusUnknown, Message: message, Code: code} } diff --git a/checker/rules_connection.go b/checker/rules_connection.go new file mode 100644 index 0000000..4b6cf12 --- /dev/null +++ b/checker/rules_connection.go @@ -0,0 +1,40 @@ +package checker + +import ( + "context" + "fmt" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +// connectionReachableRule checks that every federation endpoint returned +// by DNS accepted the TLS connection the tester attempted. +type connectionReachableRule struct{} + +func (r *connectionReachableRule) Name() string { return "matrix.connection_reachable" } +func (r *connectionReachableRule) Description() string { + return "Checks that every discovered federation endpoint accepts an inbound connection." +} + +func (r *connectionReachableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { + data, errSt := loadMatrixData(ctx, obs) + if errSt != nil { + return []sdk.CheckState{*errSt} + } + + if len(data.ConnectionErrors) == 0 && len(data.ConnectionReports) == 0 { + return []sdk.CheckState{infoState("matrix.connection_reachable.unknown", "No endpoint was probed by the federation tester.")} + } + + if len(data.ConnectionErrors) == 0 { + return []sdk.CheckState{passState("matrix.connection_reachable.ok", fmt.Sprintf("All %d endpoint(s) accepted the connection.", len(data.ConnectionReports)))} + } + + out := make([]sdk.CheckState, 0, len(data.ConnectionErrors)) + for addr, cerr := range data.ConnectionErrors { + st := critState("matrix.connection_reachable.fail", cerr.Message) + st.Subject = addr + out = append(out, st) + } + return out +} diff --git a/checker/rules_federation.go b/checker/rules_federation.go new file mode 100644 index 0000000..6261e50 --- /dev/null +++ b/checker/rules_federation.go @@ -0,0 +1,61 @@ +package checker + +import ( + "context" + "fmt" + "strings" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +// federationOKRule reflects the overall FederationOK flag reported by the +// Matrix Federation Tester. Other rules isolate specific concerns; this +// rule is the global verdict so callers get a single-line answer to +// "does this homeserver federate?". +type federationOKRule struct{} + +func (r *federationOKRule) Name() string { return "matrix.federation_ok" } +func (r *federationOKRule) Description() string { + return "Reports the overall federation status returned by the Matrix Federation Tester." +} + +func (r *federationOKRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { + data, errSt := loadMatrixData(ctx, obs) + if errSt != nil { + return []sdk.CheckState{*errSt} + } + + domain, _ := opts["serviceDomain"].(string) + domain = strings.TrimSuffix(domain, ".") + + if data.FederationOK { + version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version) + st := passState("matrix.federation_ok.ok", "Matrix federation is working.") + if version != "" { + st.Message = fmt.Sprintf("Matrix federation is working (running %s).", version) + st.Meta = map[string]any{"version": version} + } + return []sdk.CheckState{st} + } + + var statusLine string + switch { + case data.DNSResult.SRVError != nil && data.WellKnownResult.Result != "": + statusLine = fmt.Sprintf("%s OR %s", data.DNSResult.SRVError.Message, data.WellKnownResult.Result) + case len(data.ConnectionErrors) > 0: + var msg strings.Builder + for srv, cerr := range data.ConnectionErrors { + if msg.Len() > 0 { + msg.WriteString("; ") + } + msg.WriteString(srv) + msg.WriteString(": ") + msg.WriteString(cerr.Message) + } + statusLine = fmt.Sprintf("Connection errors: %s", msg.String()) + default: + statusLine = fmt.Sprintf("Federation broken. Check https://federationtester.matrix.org/#%s", domain) + } + + return []sdk.CheckState{critState("matrix.federation_ok.fail", statusLine)} +} diff --git a/checker/rules_srv.go b/checker/rules_srv.go new file mode 100644 index 0000000..e42de47 --- /dev/null +++ b/checker/rules_srv.go @@ -0,0 +1,48 @@ +package checker + +import ( + "context" + "fmt" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +// srvRecordsRule checks _matrix-fed._tcp / _matrix._tcp SRV delegation: was +// the lookup successful, and does it yield at least one record (or was it +// legitimately skipped because of a CNAME/well-known path)? +type srvRecordsRule struct{} + +func (r *srvRecordsRule) Name() string { return "matrix.srv_records" } +func (r *srvRecordsRule) Description() string { + return "Checks that the Matrix SRV lookup (_matrix-fed._tcp / _matrix._tcp) succeeded or was legitimately skipped." +} + +func (r *srvRecordsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { + data, errSt := loadMatrixData(ctx, obs) + if errSt != nil { + return []sdk.CheckState{*errSt} + } + + dns := data.DNSResult + + if dns.SRVError != nil { + return []sdk.CheckState{critState("matrix.srv_records.error", fmt.Sprintf("SRV lookup error: %s", dns.SRVError.Message))} + } + + if dns.SRVSkipped { + msg := "SRV lookup skipped by the federation tester." + if dns.SRVCName != "" { + msg = fmt.Sprintf("SRV lookup skipped (CNAME: %s).", dns.SRVCName) + } + return []sdk.CheckState{unknownState("matrix.srv_records.skipped", msg)} + } + + if len(dns.SRVRecords) == 0 { + return []sdk.CheckState{infoState( + "matrix.srv_records.absent", + "No Matrix SRV records published (federation may still work via well-known).", + )} + } + + return []sdk.CheckState{passState("matrix.srv_records.ok", fmt.Sprintf("%d SRV record(s) published.", len(dns.SRVRecords)))} +} diff --git a/checker/rules_tls.go b/checker/rules_tls.go new file mode 100644 index 0000000..2561092 --- /dev/null +++ b/checker/rules_tls.go @@ -0,0 +1,73 @@ +package checker + +import ( + "context" + "fmt" + "strings" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +// tlsChecksRule reviews the TLS-level findings the federation tester +// reports for every endpoint it managed to reach: certificate validity, +// matching server name, future expiry, presence of an Ed25519 key, and so +// on. One CheckState is emitted per reachable endpoint so the UI can pin +// the outcome on the exact address. +type tlsChecksRule struct{} + +func (r *tlsChecksRule) Name() string { return "matrix.tls_checks" } +func (r *tlsChecksRule) Description() string { + return "Reviews the TLS posture on every reachable federation endpoint (certificate chain, hostname match, Ed25519 key, …)." +} + +func (r *tlsChecksRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { + data, errSt := loadMatrixData(ctx, obs) + if errSt != nil { + return []sdk.CheckState{*errSt} + } + + if len(data.ConnectionReports) == 0 { + return []sdk.CheckState{infoState("matrix.tls_checks.skipped", "No endpoint reached: TLS posture could not be assessed.")} + } + + out := make([]sdk.CheckState, 0, len(data.ConnectionReports)) + for addr, cr := range data.ConnectionReports { + var problems []string + if !cr.Checks.MatchingServerName { + problems = append(problems, "server name does not match certificate") + } + if !cr.Checks.FutureValidUntilTS { + problems = append(problems, "certificate expired or near expiry") + } + if !cr.Checks.ValidCertificates { + problems = append(problems, "certificate chain is invalid") + } + if !cr.Checks.HasEd25519Key { + problems = append(problems, "no Ed25519 signing key advertised") + } + if !cr.Checks.AllEd25519ChecksOK { + problems = append(problems, "Ed25519 key verification failed") + } + for _, e := range cr.Errors { + if e != "" { + problems = append(problems, e) + } + } + + if len(problems) == 0 && cr.Checks.AllChecksOK { + st := passState("matrix.tls_checks.ok", "All TLS checks passed.") + st.Subject = addr + out = append(out, st) + continue + } + + msg := "TLS checks failed." + if len(problems) > 0 { + msg = fmt.Sprintf("TLS checks failed: %s.", strings.Join(problems, "; ")) + } + st := critState("matrix.tls_checks.fail", msg) + st.Subject = addr + out = append(out, st) + } + return out +} diff --git a/checker/rules_version.go b/checker/rules_version.go new file mode 100644 index 0000000..552dd68 --- /dev/null +++ b/checker/rules_version.go @@ -0,0 +1,40 @@ +package checker + +import ( + "context" + "fmt" + "strings" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +// versionRule reports whether the federation tester could fetch the +// homeserver version string. The test probe reaches /_matrix/federation/v1/version, +// so a failure here hints at a federation-path problem even when the rest +// of the federation handshake looks healthy. +type versionRule struct{} + +func (r *versionRule) Name() string { return "matrix.version" } +func (r *versionRule) Description() string { + return "Checks that the homeserver responds to /_matrix/federation/v1/version and reports its name and version." +} + +func (r *versionRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { + data, errSt := loadMatrixData(ctx, obs) + if errSt != nil { + return []sdk.CheckState{*errSt} + } + + if data.Version.Error != "" { + return []sdk.CheckState{warnState("matrix.version.error", fmt.Sprintf("Homeserver /version probe failed: %s", data.Version.Error))} + } + + version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version) + if version == "" { + return []sdk.CheckState{infoState("matrix.version.unknown", "Homeserver did not return a version string.")} + } + + st := passState("matrix.version.ok", fmt.Sprintf("Homeserver running %s.", version)) + st.Meta = map[string]any{"version": version} + return []sdk.CheckState{st} +} diff --git a/checker/rules_wellknown.go b/checker/rules_wellknown.go new file mode 100644 index 0000000..163693d --- /dev/null +++ b/checker/rules_wellknown.go @@ -0,0 +1,43 @@ +package checker + +import ( + "context" + "fmt" + "strings" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +// wellKnownRule checks the /.well-known/matrix/server delegation: was a +// delegation published, did it resolve, and does it point back at the +// expected server_name? +type wellKnownRule struct{} + +func (r *wellKnownRule) Name() string { return "matrix.well_known" } +func (r *wellKnownRule) Description() string { + return "Checks that /.well-known/matrix/server (if published) is valid and points at the expected server_name." +} + +func (r *wellKnownRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { + data, errSt := loadMatrixData(ctx, obs) + if errSt != nil { + return []sdk.CheckState{*errSt} + } + + wk := data.WellKnownResult + + // Nothing published: the host may rely on SRV only. Mark informational. + if wk.Server == "" && wk.Result == "" { + return []sdk.CheckState{infoState("matrix.well_known.absent", "No /.well-known/matrix/server delegation published (federation may still work via SRV).")} + } + + // Published but the tester flagged an error string. + if wk.Server == "" && wk.Result != "" { + if strings.Contains(strings.ToLower(wk.Result), "no .well-known") { + return []sdk.CheckState{unknownState("matrix.well_known.absent", "No /.well-known/matrix/server delegation found (federation may still work via SRV).")} + } + return []sdk.CheckState{critState("matrix.well_known.error", fmt.Sprintf("Well-known delegation error: %s", wk.Result))} + } + + return []sdk.CheckState{passState("matrix.well_known.ok", fmt.Sprintf("Well-known delegation resolves to %s.", wk.Server))} +} From d19bda771db56acf4c3e268ce992b3abc502ecb6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 26 Apr 2026 01:10:32 +0700 Subject: [PATCH 3/8] Run container as non-root user Add USER 65534:65534 to the scratch runtime image so the checker process does not run as root. --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index b1c8d9e..b7a7260 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,5 +10,6 @@ RUN CGO_ENABLED=0 go build -tags standalone -ldflags "-X main.Version=${CHECKER_ FROM scratch COPY --from=builder /checker-matrix /checker-matrix +USER 65534:65534 EXPOSE 8080 ENTRYPOINT ["/checker-matrix"] From 0fee4942949b9323818ed1fb87b3ee3e258ab7b2 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 26 Apr 2026 01:24:25 +0700 Subject: [PATCH 4/8] checker: report skipped TLS rule as StatusUnknown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When no endpoint is reached, the TLS posture cannot be assessed — this is a non-evaluation, not an informational finding. --- checker/rules_tls.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/rules_tls.go b/checker/rules_tls.go index 2561092..9f7125d 100644 --- a/checker/rules_tls.go +++ b/checker/rules_tls.go @@ -27,7 +27,7 @@ func (r *tlsChecksRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, } if len(data.ConnectionReports) == 0 { - return []sdk.CheckState{infoState("matrix.tls_checks.skipped", "No endpoint reached: TLS posture could not be assessed.")} + return []sdk.CheckState{unknownState("matrix.tls_checks.skipped", "No endpoint reached: TLS posture could not be assessed.")} } out := make([]sdk.CheckState, 0, len(data.ConnectionReports)) From 2af16d3ab99cae04ee08414fbe37da86818b92a5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 26 Apr 2026 03:56:38 +0700 Subject: [PATCH 5/8] checker: harden HTTP collection and stabilize report ordering Validate the federation tester URI placeholder, escape the domain, set a client timeout, cap the response body, and ship CA certificates in the scratch image so HTTPS calls succeed. Sort hosts, connection reports, and errors when rendering so output is deterministic, and deduplicate TLS problems. Drop the deprecated aggregate Rule() and add tests for collection and rules. --- Dockerfile | 1 + checker/collect.go | 24 ++++- checker/collect_test.go | 91 ++++++++++++++++++ checker/definition.go | 2 +- checker/interactive.go | 2 +- checker/provider.go | 5 - checker/report.go | 26 +++++- checker/rule.go | 8 -- checker/rules_connection.go | 15 ++- checker/rules_federation.go | 12 ++- checker/rules_test.go | 178 ++++++++++++++++++++++++++++++++++++ checker/rules_tls.go | 37 ++++++-- plugin/plugin.go | 3 +- 13 files changed, 363 insertions(+), 41 deletions(-) create mode 100644 checker/collect_test.go create mode 100644 checker/rules_test.go diff --git a/Dockerfile b/Dockerfile index b7a7260..279b526 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ COPY . . RUN CGO_ENABLED=0 go build -tags standalone -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-matrix . FROM scratch +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /checker-matrix /checker-matrix USER 65534:65534 EXPOSE 8080 diff --git a/checker/collect.go b/checker/collect.go index 9bc6340..e3175c4 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -4,12 +4,23 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" + "net/url" "strings" + "time" sdk "git.happydns.org/checker-sdk-go/checker" ) +const ( + defaultTesterURI = "https://federationtester.matrix.org/api/report?server_name=%s" + collectHTTPTimeout = 30 * time.Second + maxResponseBodySize = 5 << 20 // 5 MiB +) + +var collectHTTPClient = &http.Client{Timeout: collectHTTPTimeout} + func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) { domain, _ := opts["serviceDomain"].(string) if domain == "" { @@ -19,15 +30,20 @@ func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) ( testerURI, _ := opts["federationTesterServer"].(string) if testerURI == "" { - testerURI = "https://federationtester.matrix.org/api/report?server_name=%s" + testerURI = defaultTesterURI + } + if !strings.Contains(testerURI, "%s") { + return nil, fmt.Errorf("federationTesterServer must contain a %%s placeholder for the domain") } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(testerURI, domain), nil) + reqURL := fmt.Sprintf(testerURI, url.QueryEscape(domain)) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, fmt.Errorf("unable to build the request: %w", err) } - resp, err := http.DefaultClient.Do(req) + resp, err := collectHTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("unable to perform the test: %w", err) } @@ -38,7 +54,7 @@ func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) ( } var data MatrixFederationData - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + if err := json.NewDecoder(io.LimitReader(resp.Body, maxResponseBodySize)).Decode(&data); err != nil { return nil, fmt.Errorf("failed to decode federation tester response: %w", err) } diff --git a/checker/collect_test.go b/checker/collect_test.go new file mode 100644 index 0000000..b5d4a4b --- /dev/null +++ b/checker/collect_test.go @@ -0,0 +1,91 @@ +package checker + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +func TestCollectMissingDomain(t *testing.T) { + p := &matrixProvider{} + if _, err := p.Collect(context.Background(), sdk.CheckerOptions{}); err == nil { + t.Fatal("expected error when serviceDomain is empty") + } +} + +func TestCollectSuccess(t *testing.T) { + const body = `{ + "WellKnownResult": {"m.server": "matrix.example.org:8448", "result": ""}, + "DNSResult": {"SRVSkipped": false, "SRVRecords": [{"Target": "matrix.example.org.", "Port": 8448, "Priority": 10, "Weight": 5}]}, + "ConnectionReports": {"1.2.3.4:8448": {"Checks": {"AllChecksOK": true, "MatchingServerName": true, "FutureValidUntilTS": true, "HasEd25519Key": true, "AllEd25519ChecksOK": true, "ValidCertificates": true}}}, + "ConnectionErrors": {}, + "Version": {"name": "Synapse", "version": "1.100.0"}, + "FederationOK": true + }` + + var gotURL string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotURL = r.URL.String() + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(body)) + })) + defer srv.Close() + + p := &matrixProvider{} + out, err := p.Collect(context.Background(), sdk.CheckerOptions{ + "serviceDomain": "example.org.", + "federationTesterServer": srv.URL + "/api/report?server_name=%s", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(gotURL, "server_name=example.org") { + t.Errorf("unexpected URL %q", gotURL) + } + data, ok := out.(*MatrixFederationData) + if !ok || data == nil { + t.Fatalf("expected *MatrixFederationData, got %T", out) + } + if !data.FederationOK { + t.Error("expected FederationOK=true") + } + if data.Version.Name != "Synapse" { + t.Errorf("unexpected version name %q", data.Version.Name) + } +} + +func TestCollectNon2xx(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadGateway) + })) + defer srv.Close() + + p := &matrixProvider{} + _, err := p.Collect(context.Background(), sdk.CheckerOptions{ + "serviceDomain": "example.org", + "federationTesterServer": srv.URL + "/?s=%s", + }) + if err == nil { + t.Fatal("expected error on 502 response") + } +} + +func TestCollectMalformedJSON(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("not json")) + })) + defer srv.Close() + + p := &matrixProvider{} + _, err := p.Collect(context.Background(), sdk.CheckerOptions{ + "serviceDomain": "example.org", + "federationTesterServer": srv.URL + "/?s=%s", + }) + if err == nil { + t.Fatal("expected decode error") + } +} diff --git a/checker/definition.go b/checker/definition.go index ebf653e..cc296fe 100644 --- a/checker/definition.go +++ b/checker/definition.go @@ -17,7 +17,7 @@ import ( var Version = "built-in" // Definition returns the CheckerDefinition for the matrix federation checker. -func Definition() *sdk.CheckerDefinition { +func (p *matrixProvider) Definition() *sdk.CheckerDefinition { return &sdk.CheckerDefinition{ ID: "matrixim", Name: "Matrix Federation Tester", diff --git a/checker/interactive.go b/checker/interactive.go index 4f0deb8..9b35b79 100644 --- a/checker/interactive.go +++ b/checker/interactive.go @@ -35,7 +35,7 @@ func (p *matrixProvider) RenderForm() []sdk.CheckerOptionField { func (p *matrixProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) { domain := strings.TrimSpace(r.FormValue("serviceDomain")) if domain == "" { - return nil, errors.New("Matrix domain is required") + return nil, errors.New("matrix domain is required") } opts := sdk.CheckerOptions{ diff --git a/checker/provider.go b/checker/provider.go index de7a43d..111b30c 100644 --- a/checker/provider.go +++ b/checker/provider.go @@ -14,8 +14,3 @@ type matrixProvider struct{} func (p *matrixProvider) Key() sdk.ObservationKey { return ObservationKeyMatrix } - -// Definition implements sdk.CheckerDefinitionProvider. -func (p *matrixProvider) Definition() *sdk.CheckerDefinition { - return Definition() -} diff --git a/checker/report.go b/checker/report.go index 7952ae0..ad88a39 100644 --- a/checker/report.go +++ b/checker/report.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "html/template" + "sort" "strings" sdk "git.happydns.org/checker-sdk-go/checker" @@ -326,7 +327,13 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { } // Hosts - for name, h := range r.DNSResult.Hosts { + hostNames := make([]string, 0, len(r.DNSResult.Hosts)) + for name := range r.DNSResult.Hosts { + hostNames = append(hostNames, name) + } + sort.Strings(hostNames) + for _, name := range hostNames { + h := r.DNSResult.Hosts[name] data.Hosts = append(data.Hosts, matrixHostData{ Name: name, CName: h.CName, @@ -335,7 +342,13 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { } // Successful connections - for addr, cr := range r.ConnectionReports { + connAddrs := make([]string, 0, len(r.ConnectionReports)) + for addr := range r.ConnectionReports { + connAddrs = append(connAddrs, addr) + } + sort.Strings(connAddrs) + for _, addr := range connAddrs { + cr := r.ConnectionReports[addr] conn := matrixConnectionData{ Address: addr, TLSVersion: cr.Cipher.Version, @@ -363,10 +376,15 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { } // Failed connections - for addr, ce := range r.ConnectionErrors { + errAddrs := make([]string, 0, len(r.ConnectionErrors)) + for addr := range r.ConnectionErrors { + errAddrs = append(errAddrs, addr) + } + sort.Strings(errAddrs) + for _, addr := range errAddrs { data.ConnectionErrors = append(data.ConnectionErrors, matrixConnErrData{ Address: addr, - Message: ce.Message, + Message: r.ConnectionErrors[addr].Message, }) } diff --git a/checker/rule.go b/checker/rule.go index 94d39f6..8506856 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -21,14 +21,6 @@ func Rules() []sdk.CheckRule { } } -// Rule returns the aggregate federation rule. -// -// Deprecated: prefer Rules() which exposes every concern individually. Kept -// for backward compatibility with callers that embed a single rule. -func Rule() sdk.CheckRule { - return &federationOKRule{} -} - // loadMatrixData fetches the Matrix observation. On error returns a // CheckState the caller should emit to short-circuit its rule. func loadMatrixData(ctx context.Context, obs sdk.ObservationGetter) (*MatrixFederationData, *sdk.CheckState) { diff --git a/checker/rules_connection.go b/checker/rules_connection.go index 4b6cf12..4cfa7ba 100644 --- a/checker/rules_connection.go +++ b/checker/rules_connection.go @@ -3,6 +3,7 @@ package checker import ( "context" "fmt" + "sort" sdk "git.happydns.org/checker-sdk-go/checker" ) @@ -23,16 +24,22 @@ func (r *connectionReachableRule) Evaluate(ctx context.Context, obs sdk.Observat } if len(data.ConnectionErrors) == 0 && len(data.ConnectionReports) == 0 { - return []sdk.CheckState{infoState("matrix.connection_reachable.unknown", "No endpoint was probed by the federation tester.")} + return []sdk.CheckState{unknownState("matrix.connection_reachable.unknown", "No endpoint was probed by the federation tester.")} } if len(data.ConnectionErrors) == 0 { return []sdk.CheckState{passState("matrix.connection_reachable.ok", fmt.Sprintf("All %d endpoint(s) accepted the connection.", len(data.ConnectionReports)))} } - out := make([]sdk.CheckState, 0, len(data.ConnectionErrors)) - for addr, cerr := range data.ConnectionErrors { - st := critState("matrix.connection_reachable.fail", cerr.Message) + addrs := make([]string, 0, len(data.ConnectionErrors)) + for addr := range data.ConnectionErrors { + addrs = append(addrs, addr) + } + sort.Strings(addrs) + + out := make([]sdk.CheckState, 0, len(addrs)) + for _, addr := range addrs { + st := critState("matrix.connection_reachable.fail", data.ConnectionErrors[addr].Message) st.Subject = addr out = append(out, st) } diff --git a/checker/rules_federation.go b/checker/rules_federation.go index 6261e50..60b8e34 100644 --- a/checker/rules_federation.go +++ b/checker/rules_federation.go @@ -3,6 +3,7 @@ package checker import ( "context" "fmt" + "sort" "strings" sdk "git.happydns.org/checker-sdk-go/checker" @@ -41,16 +42,21 @@ func (r *federationOKRule) Evaluate(ctx context.Context, obs sdk.ObservationGett var statusLine string switch { case data.DNSResult.SRVError != nil && data.WellKnownResult.Result != "": - statusLine = fmt.Sprintf("%s OR %s", data.DNSResult.SRVError.Message, data.WellKnownResult.Result) + statusLine = fmt.Sprintf("%s; %s", data.DNSResult.SRVError.Message, data.WellKnownResult.Result) case len(data.ConnectionErrors) > 0: + srvs := make([]string, 0, len(data.ConnectionErrors)) + for srv := range data.ConnectionErrors { + srvs = append(srvs, srv) + } + sort.Strings(srvs) var msg strings.Builder - for srv, cerr := range data.ConnectionErrors { + for _, srv := range srvs { if msg.Len() > 0 { msg.WriteString("; ") } msg.WriteString(srv) msg.WriteString(": ") - msg.WriteString(cerr.Message) + msg.WriteString(data.ConnectionErrors[srv].Message) } statusLine = fmt.Sprintf("Connection errors: %s", msg.String()) default: diff --git a/checker/rules_test.go b/checker/rules_test.go new file mode 100644 index 0000000..c865d58 --- /dev/null +++ b/checker/rules_test.go @@ -0,0 +1,178 @@ +package checker + +import ( + "context" + "encoding/json" + "strings" + "testing" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +type stubObs struct { + data *MatrixFederationData + err error +} + +func (s stubObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error { + if s.err != nil { + return s.err + } + b, err := json.Marshal(s.data) + if err != nil { + return err + } + return json.Unmarshal(b, dest) +} + +func (s stubObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) { + return nil, nil +} + +func eval(t *testing.T, rule sdk.CheckRule, data *MatrixFederationData, opts sdk.CheckerOptions) []sdk.CheckState { + t.Helper() + return rule.Evaluate(context.Background(), stubObs{data: data}, opts) +} + +func TestFederationOKRulePass(t *testing.T) { + data := &MatrixFederationData{FederationOK: true} + data.Version.Name = "Synapse" + data.Version.Version = "1.100.0" + got := eval(t, &federationOKRule{}, data, sdk.CheckerOptions{"serviceDomain": "example.org."}) + if len(got) != 1 || got[0].Status != sdk.StatusOK { + t.Fatalf("expected single OK state, got %+v", got) + } + if !strings.Contains(got[0].Message, "Synapse 1.100.0") { + t.Errorf("expected version in message, got %q", got[0].Message) + } +} + +func TestFederationOKRuleFailDeterministicOrder(t *testing.T) { + data := &MatrixFederationData{} + data.ConnectionErrors = map[string]struct { + Message string `json:"Message"` + }{ + "z.example:8448": {Message: "boom z"}, + "a.example:8448": {Message: "boom a"}, + "m.example:8448": {Message: "boom m"}, + } + first := eval(t, &federationOKRule{}, data, nil)[0].Message + for range 5 { + if eval(t, &federationOKRule{}, data, nil)[0].Message != first { + t.Fatal("federation_ok message not stable across runs") + } + } + idxA := strings.Index(first, "a.example") + idxM := strings.Index(first, "m.example") + idxZ := strings.Index(first, "z.example") + if !(idxA < idxM && idxM < idxZ) { + t.Errorf("expected sorted order a Date: Sun, 26 Apr 2026 10:53:12 +0700 Subject: [PATCH 6/8] docker: add HEALTHCHECK probing /health The binary doubles as its own healthcheck client via the SDK's -healthcheck flag, so the probe works in the scratch image (no shell, no curl, no wget). --- Dockerfile | 2 ++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 279b526..a56c3b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,6 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifi COPY --from=builder /checker-matrix /checker-matrix USER 65534:65534 EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD ["/checker-matrix", "-healthcheck"] ENTRYPOINT ["/checker-matrix"] diff --git a/go.mod b/go.mod index c7c45f9..53001e2 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module git.happydns.org/checker-matrix go 1.25.0 -require git.happydns.org/checker-sdk-go v1.3.0 +require git.happydns.org/checker-sdk-go v1.5.0 diff --git a/go.sum b/go.sum index fe4952c..c389c68 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -git.happydns.org/checker-sdk-go v1.3.0 h1:FG2kIhlJCzI0m35EhxSgn4UWc9M4ha6aZTeoChu4l7A= -git.happydns.org/checker-sdk-go v1.3.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY= +git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= From 4079a928684a429884d6e3abc6b8aca31be6810e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 30 Apr 2026 08:55:18 +0700 Subject: [PATCH 7/8] Include rules section --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 7be7c78..66e5041 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,17 @@ Set the `endpoint` admin option for the `matrixim` checker to the URL of the running checker-matrix server (e.g., `http://checker-matrix:8080`). happyDomain will delegate observation collection to this endpoint. +## Rules + +| Code | Description | Severity | +|------------------------------|---------------------------------------------------------------------------------------------------|---------------------| +| `matrix.connection_reachable`| Checks that every discovered federation endpoint accepts an inbound connection. | CRITICAL | +| `matrix.federation_ok` | Reports the overall federation status returned by the Matrix Federation Tester. | CRITICAL | +| `matrix.srv_records` | Checks that the Matrix SRV lookup (`_matrix-fed._tcp` / `_matrix._tcp`) succeeded or was skipped. | CRITICAL | +| `matrix.tls_checks` | Reviews the TLS posture on every reachable federation endpoint (chain, hostname, Ed25519 key). | CRITICAL | +| `matrix.version` | Checks that the homeserver responds to `/_matrix/federation/v1/version` with name and version. | WARNING | +| `matrix.well_known` | Checks that `/.well-known/matrix/server` (if published) is valid and points at the server_name. | CRITICAL | + ## Options | Scope | Id | Description | From 86b5207a8f46a345b9cb2d39683fb7db8d4a0378 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 10 May 2026 19:03:59 +0800 Subject: [PATCH 8/8] Add CI/CD pipeline --- .drone-manifest.yml | 22 ++++++ .drone.yml | 187 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 .drone-manifest.yml create mode 100644 .drone.yml diff --git a/.drone-manifest.yml b/.drone-manifest.yml new file mode 100644 index 0000000..1046193 --- /dev/null +++ b/.drone-manifest.yml @@ -0,0 +1,22 @@ +image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux + - image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 + platform: + architecture: arm64 + os: linux + variant: v8 + - image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm + platform: + architecture: arm + os: linux + variant: v7 diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..142d61e --- /dev/null +++ b/.drone.yml @@ -0,0 +1,187 @@ +--- +kind: pipeline +type: docker +name: build-amd64 + +platform: + os: linux + arch: amd64 + +steps: + - name: checker build + image: golang:1-alpine + commands: + - apk add --no-cache git make + - make + environment: + CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}" + CGO_ENABLED: 0 + when: + event: + exclude: + - tag + + - name: checker build tag + image: golang:1-alpine + commands: + - apk add --no-cache git make + - make + environment: + CHECKER_VERSION: "${DRONE_SEMVER}" + CGO_ENABLED: 0 + when: + event: + - tag + + - name: publish on Docker Hub + image: plugins/docker + settings: + repo: happydomain/checker-matrix + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile + build_args: + - CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT} + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + exclude: + - tag + + - name: publish on Docker Hub (tag) + image: plugins/docker + settings: + repo: happydomain/checker-matrix + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile + build_args: + - CHECKER_VERSION=${DRONE_SEMVER} + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + - tag + +trigger: + branch: + exclude: + - renovate/* + event: + - cron + - push + - tag + +--- +kind: pipeline +type: docker +name: build-arm64 + +platform: + os: linux + arch: arm64 + +steps: + - name: checker build + image: golang:1-alpine + commands: + - apk add --no-cache git make + - make + environment: + CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}" + CGO_ENABLED: 0 + when: + event: + exclude: + - tag + + - name: checker build tag + image: golang:1-alpine + commands: + - apk add --no-cache git make + - make + environment: + CHECKER_VERSION: "${DRONE_SEMVER}" + CGO_ENABLED: 0 + when: + event: + - tag + + - name: publish on Docker Hub + image: plugins/docker + settings: + repo: happydomain/checker-matrix + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile + build_args: + - CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT} + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + exclude: + - tag + + - name: publish on Docker Hub (tag) + image: plugins/docker + settings: + repo: happydomain/checker-matrix + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile + build_args: + - CHECKER_VERSION=${DRONE_SEMVER} + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + - tag + +trigger: + event: + - cron + - push + - tag + +--- +kind: pipeline +name: docker-manifest + +platform: + os: linux + arch: arm64 + +steps: + - name: publish on Docker Hub + image: plugins/manifest + settings: + auto_tag: true + ignore_missing: true + spec: .drone-manifest.yml + username: + from_secret: docker_username + password: + from_secret: docker_password + +trigger: + branch: + exclude: + - renovate/* + event: + - cron + - push + - tag + +depends_on: + - build-amd64 + - build-arm64