feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
46
internal/netaddr/cidr.go
Обычный файл
46
internal/netaddr/cidr.go
Обычный файл
@@ -0,0 +1,46 @@
|
||||
// Package netaddr provides network address utilities for RSMon.
|
||||
package netaddr
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Cidr is a wrapper for transferring CIDR values back and forth easily.
|
||||
type Cidr struct {
|
||||
Cidr net.IPNet
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (c *Cidr) Scan(value interface{}) error {
|
||||
c.Cidr.IP = nil
|
||||
c.Cidr.Mask = nil
|
||||
c.Valid = false
|
||||
if value == nil {
|
||||
c.Valid = false
|
||||
return nil
|
||||
}
|
||||
cidrAsBytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("could not convert scanned value to bytes")
|
||||
}
|
||||
_, parsedIPNet, parseErr := net.ParseCIDR(string(cidrAsBytes))
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
}
|
||||
c.Valid = true
|
||||
c.Cidr.IP = parsedIPNet.IP
|
||||
c.Cidr.Mask = parsedIPNet.Mask
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface. Note if c.Valid is false
|
||||
// or c.Cidr.IP is nil the database column value will be set to NULL.
|
||||
func (c Cidr) Value() (driver.Value, error) {
|
||||
if !c.Valid || c.Cidr.IP == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []byte(c.Cidr.String()), nil
|
||||
}
|
||||
116
internal/netaddr/cidr_test.go
Обычный файл
116
internal/netaddr/cidr_test.go
Обычный файл
@@ -0,0 +1,116 @@
|
||||
package netaddr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func TestCidr(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
cidr := Cidr{}
|
||||
|
||||
// Test scanning NULL values
|
||||
err := db.QueryRow("SELECT NULL::cidr").Scan(&cidr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cidr.Valid {
|
||||
t.Fatalf("expected null result")
|
||||
}
|
||||
|
||||
// Test setting NULL values
|
||||
err = db.QueryRow("SELECT $1::cidr", cidr).Scan(&cidr)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query null value failed: %s", err.Error())
|
||||
}
|
||||
if cidr.Valid {
|
||||
t.Fatalf("expected null result")
|
||||
}
|
||||
|
||||
// test encoding in query params, then decoding during Scan
|
||||
testBidirectional := func(c Cidr, label string) {
|
||||
err = db.QueryRow("SELECT $1::cidr", c).Scan(&cidr)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query %s cidr failed: %s", label, err.Error())
|
||||
}
|
||||
if !cidr.Valid {
|
||||
t.Fatalf("expected non-null value, got null for %s", label)
|
||||
}
|
||||
if !net.IP.Equal(c.Cidr.IP, cidr.Cidr.IP) {
|
||||
t.Fatalf("expected IP addresses to match, but did not for %s - %s %s", label, c.Cidr.IP.String(), cidr.Cidr.IP.String())
|
||||
}
|
||||
if !bytes.Equal(c.Cidr.Mask, cidr.Cidr.Mask) {
|
||||
t.Fatalf("expected net masks to match, but did not for %s", label)
|
||||
}
|
||||
}
|
||||
|
||||
// a few example CIDRs to test out
|
||||
_, exampleCidr, err := net.ParseCIDR("135.104.0.0/32")
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal error while building simple IP example - %s", err.Error())
|
||||
}
|
||||
simpleIP4 := Cidr{Cidr: *exampleCidr, Valid: true}
|
||||
testBidirectional(simpleIP4, "Simple IPv4")
|
||||
|
||||
_, exampleCidr, err = net.ParseCIDR("0.0.0.0/24")
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal error while building Zero IP example - %s", err.Error())
|
||||
}
|
||||
zeroIP4Subnet := Cidr{Cidr: *exampleCidr, Valid: true}
|
||||
testBidirectional(zeroIP4Subnet, "Zero IPv4 Subnet")
|
||||
|
||||
_, exampleCidr, err = net.ParseCIDR("135.104.0.0/24")
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal error while building simple IPv4 subnet example - %s", err.Error())
|
||||
}
|
||||
simpleIP4Subnet := Cidr{Cidr: *exampleCidr, Valid: true}
|
||||
testBidirectional(simpleIP4Subnet, "Simple IPv4 Subnet")
|
||||
|
||||
_, exampleCidr, err = net.ParseCIDR("::1/128")
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal error while building simple IPv6 loopback example - %s", err.Error())
|
||||
}
|
||||
ip6Loopback := Cidr{Cidr: *exampleCidr, Valid: true}
|
||||
testBidirectional(ip6Loopback, "IPv6 Loopback")
|
||||
|
||||
_, exampleCidr, err = net.ParseCIDR("abcd:2345::/65")
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal error while building simple IPv6 subnet example - %s", err.Error())
|
||||
}
|
||||
ip6Subnet := Cidr{Cidr: *exampleCidr, Valid: true}
|
||||
testBidirectional(ip6Subnet, "IPv6 Subnet #1")
|
||||
|
||||
_, exampleCidr, err = net.ParseCIDR("abcd:2300::/24")
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal error while building simple IPv6 subnet #2 example - %s", err.Error())
|
||||
}
|
||||
ip6Subnet2 := Cidr{Cidr: *exampleCidr, Valid: true}
|
||||
testBidirectional(ip6Subnet2, "IPv6 Subnet #2")
|
||||
|
||||
_, exampleCidr, err = net.ParseCIDR("2001:DB8::1/48")
|
||||
if err != nil {
|
||||
t.Fatalf("Fatal error while building simple IPv6 subnet #3 example - %s", err.Error())
|
||||
}
|
||||
ip6Subnet3 := Cidr{Cidr: *exampleCidr, Valid: true}
|
||||
testBidirectional(ip6Subnet3, "IPv6 Subnet #3")
|
||||
|
||||
// Error handling
|
||||
|
||||
// Bad argument
|
||||
cidr = Cidr{}
|
||||
err = cidr.Scan(456)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for non-byte[] argument to Scan")
|
||||
}
|
||||
|
||||
cidr = Cidr{}
|
||||
err = cidr.Scan([]byte(""))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for invalid CIDR")
|
||||
}
|
||||
}
|
||||
39
internal/netaddr/inet.go
Обычный файл
39
internal/netaddr/inet.go
Обычный файл
@@ -0,0 +1,39 @@
|
||||
package netaddr
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Inet is a wrapper for transferring Inet values back and forth easily.
|
||||
type Inet struct {
|
||||
Inet net.IP
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (i *Inet) Scan(value interface{}) error {
|
||||
i.Inet = nil
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
ipAsBytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("could not convert scanned value to bytes")
|
||||
}
|
||||
parsedIP := net.ParseIP(string(ipAsBytes))
|
||||
if parsedIP == nil {
|
||||
return nil
|
||||
}
|
||||
i.Inet = parsedIP
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface. Note if
|
||||
// i.IP is nil the database column value will be set to NULL.
|
||||
func (i Inet) Value() (driver.Value, error) {
|
||||
if i.Inet == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []byte(i.Inet.String()), nil
|
||||
}
|
||||
67
internal/netaddr/inet_test.go
Обычный файл
67
internal/netaddr/inet_test.go
Обычный файл
@@ -0,0 +1,67 @@
|
||||
package netaddr
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func TestInet(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
inet := Inet{}
|
||||
|
||||
// Test scanning NULL values
|
||||
err := db.QueryRow("SELECT NULL::inet").Scan(&inet)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if inet.Inet != nil {
|
||||
t.Fatalf("expected null result")
|
||||
}
|
||||
|
||||
// Test setting NULL values
|
||||
err = db.QueryRow("SELECT $1::inet", inet).Scan(&inet)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query null value failed: %s", err.Error())
|
||||
}
|
||||
if inet.Inet != nil {
|
||||
t.Fatalf("expected null result")
|
||||
}
|
||||
|
||||
// test encoding in query params, then decoding during Scan
|
||||
testBidirectional := func(i Inet, label string) {
|
||||
err = db.QueryRow("SELECT $1::inet", i).Scan(&inet)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query %s inet failed: %s", label, err.Error())
|
||||
}
|
||||
if inet.Inet == nil {
|
||||
t.Fatalf("expected non-null value, got null for %s", label)
|
||||
}
|
||||
if !net.IP.Equal(i.Inet, inet.Inet) {
|
||||
t.Fatalf("expected IP addresses to match, but did not for %s - %s %s", label, i.Inet.String(), inet.Inet.String())
|
||||
}
|
||||
}
|
||||
|
||||
testBidirectional(Inet{Inet: net.ParseIP("192.168.0.1")}, "Simple IPv4")
|
||||
testBidirectional(Inet{Inet: net.ParseIP("::1")}, "Loopback IPv6")
|
||||
testBidirectional(Inet{Inet: net.ParseIP("abcd:2345::")}, "Loopback IPv6")
|
||||
|
||||
// Bad argument
|
||||
inet = Inet{}
|
||||
err = inet.Scan(456)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for non-byte[] argument to Scan")
|
||||
}
|
||||
|
||||
inet = Inet{}
|
||||
err = inet.Scan([]byte(""))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error for empty string - %s", err.Error())
|
||||
}
|
||||
if inet.Inet != nil {
|
||||
t.Fatalf("Unexpected not null for empty/non-IP string string")
|
||||
}
|
||||
}
|
||||
43
internal/netaddr/macaddr.go
Обычный файл
43
internal/netaddr/macaddr.go
Обычный файл
@@ -0,0 +1,43 @@
|
||||
package netaddr
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Macaddr is a wrapper for transferring Macaddr values back and forth easily.
|
||||
type Macaddr struct {
|
||||
Macaddr net.HardwareAddr
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (m *Macaddr) Scan(value interface{}) error {
|
||||
m.Macaddr = nil
|
||||
m.Valid = false
|
||||
if value == nil {
|
||||
m.Valid = false
|
||||
return nil
|
||||
}
|
||||
macaddrAsBytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("could not convert scanned value to bytes")
|
||||
}
|
||||
parsedMacaddr, parseErr := net.ParseMAC(string(macaddrAsBytes))
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
}
|
||||
m.Valid = true
|
||||
m.Macaddr = parsedMacaddr
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface. Note if m.Valid is false
|
||||
// or m.Macaddr is nil the database column value will be set to NULL.
|
||||
func (m Macaddr) Value() (driver.Value, error) {
|
||||
if !m.Valid || m.Macaddr == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []byte(m.Macaddr.String()), nil
|
||||
}
|
||||
64
internal/netaddr/macaddr_test.go
Обычный файл
64
internal/netaddr/macaddr_test.go
Обычный файл
@@ -0,0 +1,64 @@
|
||||
package netaddr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func TestMacaddr(t *testing.T) {
|
||||
db := openTestConn(t)
|
||||
defer db.Close()
|
||||
|
||||
macaddr := Macaddr{}
|
||||
|
||||
// Test scanning NULL values
|
||||
err := db.QueryRow("SELECT NULL::macaddr").Scan(&macaddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if macaddr.Valid {
|
||||
t.Fatalf("expected null result")
|
||||
}
|
||||
|
||||
// Test setting NULL values
|
||||
err = db.QueryRow("SELECT $1::macaddr", macaddr).Scan(&macaddr)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query null value failed: %s", err.Error())
|
||||
}
|
||||
if macaddr.Valid {
|
||||
t.Fatalf("expected null result")
|
||||
}
|
||||
|
||||
// test encoding in query params, then decoding during Scan
|
||||
testBidirectional := func(m Macaddr, label string) {
|
||||
err = db.QueryRow("SELECT $1::macaddr", m).Scan(&macaddr)
|
||||
if err != nil {
|
||||
t.Fatalf("re-query %s macaddr failed: %s", label, err.Error())
|
||||
}
|
||||
if !macaddr.Valid {
|
||||
t.Fatalf("expected non-null value, got null for %s", label)
|
||||
}
|
||||
if !bytes.Equal(m.Macaddr, macaddr.Macaddr) {
|
||||
t.Fatalf("expected MAC addresses to match, but did not for %s", label)
|
||||
}
|
||||
}
|
||||
|
||||
simpleMac := Macaddr{Macaddr: net.HardwareAddr{1, 0x23, 0x45, 0x67, 0x89, 0xab}, Valid: true}
|
||||
testBidirectional(simpleMac, "Simple MAC Address")
|
||||
|
||||
// Bad argument
|
||||
macaddr = Macaddr{}
|
||||
err = macaddr.Scan(456)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for non-byte[] argument to Scan")
|
||||
}
|
||||
|
||||
macaddr = Macaddr{}
|
||||
err = macaddr.Scan([]byte(""))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for invalid Macaddr")
|
||||
}
|
||||
}
|
||||
35
internal/netaddr/main_test.go
Обычный файл
35
internal/netaddr/main_test.go
Обычный файл
@@ -0,0 +1,35 @@
|
||||
package netaddr
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// TestMain loads .env.test before any test runs so DATABASE_* env vars
|
||||
// are populated in this test binary. The netaddr package is intentionally
|
||||
// low-level and does not import config/env; this TestMain gives it the
|
||||
// same environment the rest of the test suite sees without pulling in
|
||||
// the app-wide env init.
|
||||
func TestMain(m *testing.M) {
|
||||
loadEnvIfPresent(".env.test")
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func loadEnvIfPresent(name string) {
|
||||
candidates := []string{name}
|
||||
if cwd := os.Getenv("CWD"); cwd != "" {
|
||||
candidates = append(candidates, filepath.Join(cwd, name))
|
||||
}
|
||||
if dir, err := os.Getwd(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(dir, name))
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
_ = godotenv.Load(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
39
internal/netaddr/testutil.go
Обычный файл
39
internal/netaddr/testutil.go
Обычный файл
@@ -0,0 +1,39 @@
|
||||
package netaddr
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
|
||||
_ "github.com/lib/pq" // postgres driver
|
||||
)
|
||||
|
||||
type Fatalistic interface {
|
||||
Fatal(args ...interface{})
|
||||
}
|
||||
|
||||
func openTestConn(t Fatalistic) *sql.DB {
|
||||
host := getEnv("DATABASE_HOST", getEnv("DB_HOST", "localhost"))
|
||||
if os.Getenv("CI") != "" && os.Getenv("DATABASE_HOST") == "" && os.Getenv("DB_HOST") == "" {
|
||||
host = "postgres"
|
||||
}
|
||||
port := getEnv("DATABASE_PORT", getEnv("DB_PORT", "35432"))
|
||||
user := getEnv("DATABASE_USER", getEnv("DB_USER", "rsmon"))
|
||||
password := getEnv("DATABASE_PASSWORD", getEnv("DB_PASSWORD", "rsmon"))
|
||||
dbname := getEnv("DATABASE_NAME", getEnv("DB_NAME", "rsmon_test"))
|
||||
conn, err := sql.Open(
|
||||
"postgres",
|
||||
"host="+host+" port="+port+" user="+user+" password="+password+" dbname="+dbname+" sslmode=disable",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Ссылка в новой задаче
Block a user