diff options
| -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, + }, }, }, }} |
