diff options
| author | Shulhan <ms@kilabit.info> | 2025-07-25 02:30:37 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2025-07-29 00:31:23 +0700 |
| commit | 0537c5e094c7b6e5ff376ccdf0dba80adf5c4342 (patch) | |
| tree | b8c2c3621c1e76c5182acdc2f09291b796792a9e | |
| parent | 3d4c7b48674cd553ce8f670933af9b609992ae77 (diff) | |
| download | lilin-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.go | 2 | ||||
| -rw-r--r-- | internal/internal.go | 11 | ||||
| -rw-r--r-- | lilin.go | 9 | ||||
| -rw-r--r-- | lilin_test.go | 48 | ||||
| -rw-r--r-- | scan_report.go | 18 | ||||
| -rw-r--r-- | service.go | 92 | ||||
| -rw-r--r-- | service_options.go | 89 | ||||
| -rw-r--r-- | service_test.go | 41 | ||||
| -rw-r--r-- | testdata/etc/lilin/service.d/http.cfg | 1 | ||||
| -rw-r--r-- | testdata/etc/lilin/service.d/tcp.cfg | 3 | ||||
| -rw-r--r-- | testdata/etc/lilin/service.d/udp.cfg | 3 | ||||
| -rw-r--r-- | worker.go | 17 | ||||
| -rw-r--r-- | worker_test.go | 47 |
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 +} @@ -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 @@ -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, + }, }, }, }} |
