aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2025-07-25 02:30:37 +0700
committerShulhan <ms@kilabit.info>2025-07-29 00:31:23 +0700
commit0537c5e094c7b6e5ff376ccdf0dba80adf5c4342 (patch)
treeb8c2c3621c1e76c5182acdc2f09291b796792a9e
parent3d4c7b48674cd553ce8f670933af9b609992ae77 (diff)
downloadlilin-0537c5e094c7b6e5ff376ccdf0dba80adf5c4342.tar.xz
all: refactoring Service to create with ServiceOptions
Instead of defining the options in the Service, create it in the ServiceOptions and pass it to NewService function. In this way, we can check it, initialize it, and set default value. The Address option now use URL with scheme, so we can derive the service type based on the scheme, for example "http://" for HTTP based service, "tcp://xxx" for TCP based service, and so on.
-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,
+ },
},
},
}}