aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--client_options.go2
-rw-r--r--internal/internal.go11
-rw-r--r--lilin.go9
-rw-r--r--lilin_test.go48
-rw-r--r--scan_report.go18
-rw-r--r--service.go92
-rw-r--r--service_options.go89
-rw-r--r--service_test.go41
-rw-r--r--testdata/etc/lilin/service.d/http.cfg1
-rw-r--r--testdata/etc/lilin/service.d/tcp.cfg3
-rw-r--r--testdata/etc/lilin/service.d/udp.cfg3
-rw-r--r--worker.go17
-rw-r--r--worker_test.go47
13 files changed, 336 insertions, 45 deletions
diff --git a/client_options.go b/client_options.go
index 477bcb7..3203478 100644
--- a/client_options.go
+++ b/client_options.go
@@ -9,8 +9,6 @@ import (
"time"
)
-const defTimeout = 5 * time.Second
-
// ClientOptions options for client.
type ClientOptions struct {
serverURL *url.URL
diff --git a/internal/internal.go b/internal/internal.go
new file mode 100644
index 0000000..ce6de02
--- /dev/null
+++ b/internal/internal.go
@@ -0,0 +1,11 @@
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-only
+
+package internal
+
+import "time"
+
+// Now return the current time.
+var Now = func() time.Time {
+ return time.Now()
+}
diff --git a/lilin.go b/lilin.go
new file mode 100644
index 0000000..1fa04c1
--- /dev/null
+++ b/lilin.go
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-only
+
+package lilin
+
+import "time"
+
+// defTimeout define default timeout for service and client connection.
+const defTimeout = 5 * time.Second
diff --git a/lilin_test.go b/lilin_test.go
index ba1c069..fe951a0 100644
--- a/lilin_test.go
+++ b/lilin_test.go
@@ -12,17 +12,27 @@ import (
"time"
"git.sr.ht/~shulhan/lilin"
+ "git.sr.ht/~shulhan/lilin/internal"
"git.sr.ht/~shulhan/pakakeh.go/lib/net"
"git.sr.ht/~shulhan/pakakeh.go/lib/test"
)
var client *lilin.Client
+const (
+ dummyHTTPAddress = `127.0.0.1:6330`
+)
+
func TestMain(m *testing.M) {
+ internal.Now = func() time.Time {
+ return time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+ }
+
var server *lilin.Server
server = startServer()
client = createClient(server)
+ go dummyHTTPService()
m.Run()
@@ -75,6 +85,26 @@ func createClient(server *lilin.Server) (client *lilin.Client) {
return client
}
+func dummyHTTPService() {
+ var mux = http.NewServeMux()
+
+ mux.HandleFunc(`GET /health`, func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ var httpd = http.Server{
+ Addr: dummyHTTPAddress,
+ Handler: mux,
+ ReadTimeout: 5 * time.Second,
+ WriteTimeout: 5 * time.Second,
+ }
+
+ var err = httpd.ListenAndServe()
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
func TestServer_handleServicesSummary(t *testing.T) {
var gotSummary []lilin.Service
var err error
@@ -84,22 +114,6 @@ func TestServer_handleServicesSummary(t *testing.T) {
t.Fatal(err)
}
- var expSummary = []lilin.Service{{
- Name: `example http`,
- Type: `http`,
- Method: `GET`,
- Address: `http://127.0.0.1:6121/health`,
- Timeout: `5s`,
- }, {
- Name: `example tcp`,
- Type: `tcp`,
- Address: `127.0.0.1:6122`,
- Timeout: `5s`,
- }, {
- Name: `example udp`,
- Type: `udp`,
- Address: `127.0.0.1:6123`,
- Timeout: `5s`,
- }}
+ var expSummary = []lilin.Service{{}, {}, {}}
test.Assert(t, `ServicesSummary`, expSummary, gotSummary)
}
diff --git a/scan_report.go b/scan_report.go
new file mode 100644
index 0000000..16034de
--- /dev/null
+++ b/scan_report.go
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-only
+
+package lilin
+
+import "time"
+
+// ScanReport contains the result of scanning service.
+type ScanReport struct {
+ // The time when the scan started.
+ At time.Time
+
+ // The error message when scanned failed.
+ Error string
+
+ // Success is true if service available.
+ Success bool
+}
diff --git a/service.go b/service.go
index 29593a1..974c648 100644
--- a/service.go
+++ b/service.go
@@ -3,10 +3,92 @@
package lilin
+import (
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+
+ "git.sr.ht/~shulhan/lilin/internal"
+)
+
type Service struct {
- Name string
- Type string `ini:"::type"`
- Method string `ini:"::method"`
- Address string `ini:"::address"`
- Timeout string `ini:"::timeout"`
+ httpConn *http.Client
+ dialer *net.Dialer
+ opts ServiceOptions
+ isReady bool
+}
+
+// NewService create and initialize the connection to service.
+func NewService(opts ServiceOptions) (svc *Service, err error) {
+ svc = &Service{
+ opts: opts,
+ }
+ err = svc.opts.init()
+ if err != nil {
+ return nil, fmt.Errorf(`NewService: %w`, err)
+ }
+ return svc, nil
+}
+
+// Scan the service for availability.
+func (svc *Service) Scan() (report ScanReport) {
+ var err error
+
+ report.At = internal.Now()
+ if !svc.isReady {
+ err = svc.connect()
+ if err != nil {
+ report.Error = err.Error()
+ return report
+ }
+ }
+
+ switch svc.opts.scanURL.Scheme {
+ case serviceKindHTTP, serviceKindHTTPS:
+ var req = &http.Request{
+ Method: svc.opts.HTTPMethod,
+ URL: svc.opts.scanURL,
+ }
+ var httpResp *http.Response
+ httpResp, err = svc.httpConn.Do(req)
+ if err != nil {
+ report.Error = err.Error()
+ return report
+ }
+ if httpResp.StatusCode != 200 {
+ report.Error = httpResp.Status
+ return report
+ }
+
+ case serviceKindTCP, serviceKindUDP:
+ var conn net.Conn
+ conn, err = svc.dialer.Dial(`udp`, svc.opts.scanURL.Host)
+ if err != nil {
+ report.Error = err.Error()
+ return report
+ }
+ err = conn.Close()
+ if err != nil {
+ log.Printf(`Scan: Close: %s`, err)
+ }
+ }
+ report.Success = true
+ return report
+}
+
+func (svc *Service) connect() (err error) {
+ switch svc.opts.scanURL.Scheme {
+ case serviceKindHTTP, serviceKindHTTPS:
+ svc.httpConn = &http.Client{
+ Timeout: svc.opts.timeout,
+ }
+
+ case serviceKindTCP, serviceKindUDP:
+ svc.dialer = &net.Dialer{
+ Timeout: svc.opts.timeout,
+ }
+ }
+ svc.isReady = true
+ return nil
}
diff --git a/service_options.go b/service_options.go
new file mode 100644
index 0000000..957987a
--- /dev/null
+++ b/service_options.go
@@ -0,0 +1,89 @@
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-only
+
+package lilin
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+const (
+ serviceKindHTTP = `http`
+ serviceKindHTTPS = `https`
+ serviceKindTCP = `tcp`
+ serviceKindUDP = `udp`
+)
+
+type ServiceOptions struct {
+ scanURL *url.URL
+
+ Name string
+
+ // The Address of service, using scheme based, for example
+ // - http://example.com/health for HTTP service,
+ // - tcp://127.0.0.1:22 for TCP service,
+ // - udp://127.0.0.1:53 for UDP service.
+ Address string `ini:"::address"`
+
+ // HTTPMethod define HTTP method to be used to scan the HTTP service.
+ // Valid value is either DELETE, GET, HEAD, PATCH, POST, or PUT.
+ HTTPMethod string `ini:"::method"`
+
+ // Timeout for connecting and reading response from service.
+ Timeout string `ini:"::timeout"`
+
+ timeout time.Duration
+}
+
+func (opts *ServiceOptions) init() (err error) {
+ opts.scanURL, err = url.Parse(opts.Address)
+ if err != nil {
+ return fmt.Errorf(`invalid address %s`, opts.Address)
+ }
+
+ var scheme = strings.ToLower(opts.scanURL.Scheme)
+ switch scheme {
+ case `http`, `https`:
+ var httpMethod = strings.ToUpper(opts.HTTPMethod)
+ switch httpMethod {
+ case ``:
+ opts.HTTPMethod = http.MethodGet
+ case http.MethodDelete, http.MethodGet, http.MethodHead,
+ http.MethodOptions, http.MethodPatch, http.MethodPost,
+ http.MethodPut:
+ opts.HTTPMethod = httpMethod
+ default:
+ return fmt.Errorf(`invalid HTTP method %s`, opts.HTTPMethod)
+ }
+
+ case serviceKindTCP:
+ _, err = net.ResolveTCPAddr(scheme, opts.scanURL.Host)
+ if err != nil {
+ return fmt.Errorf(`invalid TCP address %s`, opts.Address)
+ }
+
+ case serviceKindUDP:
+ _, err = net.ResolveUDPAddr(scheme, opts.scanURL.Host)
+ if err != nil {
+ return fmt.Errorf(`invalid UDP address %s`, opts.Address)
+ }
+
+ default:
+ return fmt.Errorf(`unknown scheme in address %s`, scheme)
+ }
+
+ if len(opts.Timeout) == 0 {
+ opts.timeout = defTimeout
+ } else {
+ opts.timeout, err = time.ParseDuration(opts.Timeout)
+ if err != nil {
+ return fmt.Errorf(`invalid Timeout %s`, opts.Timeout)
+ }
+ }
+ return nil
+}
diff --git a/service_test.go b/service_test.go
new file mode 100644
index 0000000..02285cb
--- /dev/null
+++ b/service_test.go
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-only
+
+package lilin_test
+
+import (
+ "testing"
+
+ "git.sr.ht/~shulhan/lilin"
+ "git.sr.ht/~shulhan/lilin/internal"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/test"
+)
+
+func TestServiceScan_HTTP(t *testing.T) {
+ type testCase struct {
+ opts lilin.ServiceOptions
+ expReport lilin.ScanReport
+ }
+
+ var listCase = []testCase{{
+ opts: lilin.ServiceOptions{
+ Name: `http_service`,
+ Address: `http://` + dummyHTTPAddress + `/health`,
+ },
+ expReport: lilin.ScanReport{
+ At: internal.Now(),
+ Success: true,
+ },
+ }}
+
+ for _, tcase := range listCase {
+ svc, err := lilin.NewService(tcase.opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var gotReport = svc.Scan()
+
+ test.Assert(t, `Scan`, tcase.expReport, gotReport)
+ }
+}
diff --git a/testdata/etc/lilin/service.d/http.cfg b/testdata/etc/lilin/service.d/http.cfg
index 70e6d58..95a3554 100644
--- a/testdata/etc/lilin/service.d/http.cfg
+++ b/testdata/etc/lilin/service.d/http.cfg
@@ -1,5 +1,4 @@
[service "example http"]
-type = http
method = GET
address = http://127.0.0.1:6121/health
timeout = 5s
diff --git a/testdata/etc/lilin/service.d/tcp.cfg b/testdata/etc/lilin/service.d/tcp.cfg
index dce3c7d..0bf8cb5 100644
--- a/testdata/etc/lilin/service.d/tcp.cfg
+++ b/testdata/etc/lilin/service.d/tcp.cfg
@@ -1,4 +1,3 @@
[service "example tcp"]
-type = tcp
-address = 127.0.0.1:6122
+address = tcp://127.0.0.1:6122
timeout = 5s
diff --git a/testdata/etc/lilin/service.d/udp.cfg b/testdata/etc/lilin/service.d/udp.cfg
index b553c17..ac650b0 100644
--- a/testdata/etc/lilin/service.d/udp.cfg
+++ b/testdata/etc/lilin/service.d/udp.cfg
@@ -1,4 +1,3 @@
[service "example udp"]
-type = udp
-address = 127.0.0.1:6123
+address = udp://127.0.0.1:6123
timeout = 5s
diff --git a/worker.go b/worker.go
index bb061e9..d86852d 100644
--- a/worker.go
+++ b/worker.go
@@ -32,6 +32,10 @@ func newWorker(configDir string) (wrk *worker, err error) {
return wrk, nil
}
+type serviceConfigs struct {
+ Options map[string]ServiceOptions `ini:"service"`
+}
+
// loadServiceDir Load all the service configurations.
func (wrk *worker) loadServiceDir(configDir string) (err error) {
var serviceDir = filepath.Join(configDir, `service.d`)
@@ -42,6 +46,7 @@ func (wrk *worker) loadServiceDir(configDir string) (err error) {
return err
}
+ var svcConfigs serviceConfigs
var de os.DirEntry
for _, de = range listde {
if de.IsDir() {
@@ -64,13 +69,19 @@ func (wrk *worker) loadServiceDir(configDir string) (err error) {
return err
}
- err = ini.Unmarshal(rawcfg, wrk)
+ err = ini.Unmarshal(rawcfg, &svcConfigs)
if err != nil {
return err
}
}
- for name, service := range wrk.Services {
- service.Name = name
+ var svc *Service
+ for name, svcOpts := range svcConfigs.Options {
+ svcOpts.Name = name
+ svc, err = NewService(svcOpts)
+ if err != nil {
+ return err
+ }
+ wrk.Services[name] = svc
}
return nil
}
diff --git a/worker_test.go b/worker_test.go
index b8c7494..83fcc69 100644
--- a/worker_test.go
+++ b/worker_test.go
@@ -4,7 +4,9 @@
package lilin
import (
+ "net/url"
"testing"
+ "time"
"git.sr.ht/~shulhan/pakakeh.go/lib/test"
)
@@ -26,23 +28,42 @@ func TestNewWorker(t *testing.T) {
configDir: `testdata/etc/lilin/`,
expServices: map[string]*Service{
`example http`: &Service{
- Name: `example http`,
- Type: `http`,
- Method: `GET`,
- Address: `http://127.0.0.1:6121/health`,
- Timeout: `5s`,
+ opts: ServiceOptions{
+ scanURL: &url.URL{
+ Scheme: `http`,
+ Host: `127.0.0.1:6121`,
+ Path: `/health`,
+ },
+ Name: `example http`,
+ HTTPMethod: `GET`,
+ Address: `http://127.0.0.1:6121/health`,
+ Timeout: `5s`,
+ timeout: 5 * time.Second,
+ },
},
`example tcp`: &Service{
- Name: `example tcp`,
- Type: `tcp`,
- Address: `127.0.0.1:6122`,
- Timeout: `5s`,
+ opts: ServiceOptions{
+ scanURL: &url.URL{
+ Scheme: `tcp`,
+ Host: `127.0.0.1:6122`,
+ },
+ Name: `example tcp`,
+ Address: `tcp://127.0.0.1:6122`,
+ Timeout: `5s`,
+ timeout: 5 * time.Second,
+ },
},
`example udp`: &Service{
- Name: `example udp`,
- Type: `udp`,
- Address: `127.0.0.1:6123`,
- Timeout: `5s`,
+ opts: ServiceOptions{
+ scanURL: &url.URL{
+ Scheme: `udp`,
+ Host: `127.0.0.1:6123`,
+ },
+ Name: `example udp`,
+ Address: `udp://127.0.0.1:6123`,
+ Timeout: `5s`,
+ timeout: 5 * time.Second,
+ },
},
},
}}