WIP for AWS gateway daemon

This commit is contained in:
Ben Duncan
2025-09-30 21:21:07 +08:00
parent a59988d614
commit 2a57cad67a
11 changed files with 657 additions and 10 deletions
+112
View File
@@ -55,6 +55,11 @@ var hiveCmd = &cobra.Command{
Short: "Manage the hive service",
}
var awsgwCmd = &cobra.Command{
Use: "awsgw",
Short: "Manage the awsgw (AWS gateway) service",
}
var predastoreStartCmd = &cobra.Command{
Use: "start",
Short: "Start the predastore service",
@@ -410,6 +415,88 @@ var hiveStatusCmd = &cobra.Command{
},
}
// AWS GW
var awsgwStartCmd = &cobra.Command{
Use: "start",
Short: "Start the awsgw service",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Starting awsgw service...")
cfgFile := viper.GetString("config")
if cfgFile == "" {
fmt.Println("Config file is not set")
return
}
// TODO: Support ENV vars, CLI, otherwise revert to config.LoadConfig()
appConfig, err := config.LoadConfig(cfgFile)
if err != nil {
fmt.Println("Error loading config file:", err)
return
}
// Overwrite defaults (CLI first, config second, env third)
awsgwHost := viper.GetString("host")
if awsgwHost != "" {
fmt.Println("Overwriting awsgw host to:", awsgwHost)
appConfig.AWSGW.Host = awsgwHost
}
awsgwTlsCert := viper.GetString("tls-cert")
if awsgwTlsCert != "" {
fmt.Println("Overwriting awsgw tls-cert to:", awsgwTlsCert)
appConfig.AWSGW.TLSCert = awsgwTlsCert
}
awsgwTlsKey := viper.GetString("tls-key")
if awsgwTlsKey != "" {
fmt.Println("Overwriting awsgw tls-key to:", awsgwTlsKey)
appConfig.AWSGW.TLSKey = awsgwTlsKey
}
service, err := service.New("awsgw", appConfig)
if err != nil {
fmt.Println("Error starting awsgw service:", err)
return
}
service.Start()
fmt.Println("AWSGW service started")
},
}
var awsgwStopCmd = &cobra.Command{
Use: "stop",
Short: "Stop the awsgw service",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Stopping awsgw service...")
service, err := service.New("awsgw", &config.Config{})
if err != nil {
fmt.Println("Error stopping awsgw service:", err)
return
}
service.Stop()
fmt.Println("AWSGW service stopped")
},
}
var awsgwStatusCmd = &cobra.Command{
Use: "status",
Short: "Get status of the awsgw service",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("AWSGW service status: ...")
},
}
func init() {
viper.SetEnvPrefix("HIVE") // Prefix for environment variables
@@ -521,4 +608,29 @@ func init() {
viper.BindEnv("wal-dir", "HIVE_WAL_DIR")
viper.BindPFlag("wal-dir", hiveCmd.PersistentFlags().Lookup("wal-dir"))
// AWS GW
serviceCmd.AddCommand(awsgwCmd)
awsgwCmd.PersistentFlags().String("host", "0.0.0.0:9999", "AWS Gateway server host")
viper.BindEnv("host", "HIVE_AWSGW_HOST")
viper.BindPFlag("host", awsgwCmd.PersistentFlags().Lookup("host"))
// AWS GW TLS Cert
awsgwCmd.PersistentFlags().String("tls-cert", "", "AWS Gateway TLS certificate")
viper.BindEnv("tls-cert", "HIVE_AWSGW_TLS_CERT")
viper.BindPFlag("tls-cert", awsgwCmd.PersistentFlags().Lookup("tls-cert"))
// AWS GW TLS Key
awsgwCmd.PersistentFlags().String("tls-key", "", "AWS Gateway TLS key")
viper.BindEnv("tls-key", "HIVE_AWSGW_TLS_KEY")
viper.BindPFlag("tls-key", awsgwCmd.PersistentFlags().Lookup("tls-key"))
awsgwCmd.PersistentFlags().Bool("debug", false, "AWS Gateway Debug")
viper.BindEnv("debug", "HIVE_AWSGW_DEBUG")
viper.BindPFlag("debug", awsgwCmd.PersistentFlags().Lookup("debug"))
awsgwCmd.AddCommand(awsgwStartCmd)
awsgwCmd.AddCommand(awsgwStopCmd)
awsgwCmd.AddCommand(awsgwStatusCmd)
}
+13
View File
@@ -0,0 +1,13 @@
# Test configuration file in TOML format.
version = "1.0"
region = "ap-southeast-2"
# Enable verbose debugging
debug = false
[[auth]]
access_key_id = "AKIAIOSFODNN7EXAMPLE"
secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
policy = [
{ bucket = "predastore", actions = ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListAllMyBuckets"] },
]
+8 -2
View File
@@ -7,8 +7,14 @@ secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
[daemon]
host = "0.0.0.0:4432"
sslkey = "config/server.key"
sslcert = "config/server.pem"
tlskey = "config/server.key"
tlscert = "config/server.pem"
[awsgw]
host = "0.0.0.0:9999"
tlskey = "config/server.key"
tlscert = "config/server.pem"
debug = true
[nats]
host = "0.0.0.0:4222"
+11 -2
View File
@@ -16,6 +16,7 @@ type Config struct {
Daemon DaemonConfig `mapstructure:"daemon"`
NATS NATSConfig `mapstructure:"nats"`
Predastore PredastoreConfig `mapstructure:"predastore"`
AWSGW AWSGWConfig `mapstructure:"awsgw"`
// Authentication
AccessKey string `mapstructure:"access_key"`
@@ -25,6 +26,14 @@ type Config struct {
WalDir string `mapstructure:"wal_dir"`
}
type AWSGWConfig struct {
Host string `mapstructure:"host"`
TLSKey string `mapstructure:"tlskey"`
TLSCert string `mapstructure:"tlscert"`
Debug bool `mapstructure:"debug"`
}
type PredastoreConfig struct {
Host string `mapstructure:"host"`
Bucket string `mapstructure:"bucket"`
@@ -37,8 +46,8 @@ type PredastoreConfig struct {
// DaemonConfig holds the daemon configuration
type DaemonConfig struct {
Host string `mapstructure:"host"`
SSLKey string `mapstructure:"sslkey"`
SSLCert string `mapstructure:"sslcert"`
TLSKey string `mapstructure:"tlskey"`
TLSCert string `mapstructure:"tlscert"`
}
// NATSConfig holds the NATS configuration
+8
View File
@@ -0,0 +1,8 @@
package gateway
import "github.com/gofiber/fiber/v2"
func (gw *GatewayConfig) Account_Request(ctx *fiber.Ctx) error {
return nil
}
+167
View File
@@ -0,0 +1,167 @@
package gateway
import (
"fmt"
"log/slog"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v2"
)
func (gw *GatewayConfig) EC2_Request(ctx *fiber.Ctx) error {
fmt.Println("HERE!")
fmt.Println("Body")
spew.Dump(ctx.Body())
fmt.Println("Request POST")
spew.Dump(ctx.Request())
// Headers
fmt.Println("Headers")
spew.Dump(ctx.GetReqHeaders())
queryArgs := parseAWSQueryArgs(string(ctx.Body()))
// Run the action
switch queryArgs["Action"] {
case "DescribeInstances":
return gw.EC2_DescribeInstances(ctx, queryArgs)
case "RunInstances":
return gw.EC2_RunInstances(ctx, queryArgs)
case "CreateKeyPair":
return gw.EC2_CreateKeyPair(ctx, queryArgs)
default:
slog.Warn("EC2 Unsupported Action", "action", queryArgs["Action"])
}
return fiber.NewError(fiber.StatusNotImplemented, "Action not implemented")
}
func (gw *GatewayConfig) EC2_DescribeInstances(ctx *fiber.Ctx, args map[string]string) error {
slog.Info("EC2 DescribeInstances called")
// Return a dummy response
response := `<?xml version="1.0" encoding="UTF-8"?>
<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>2b7ac2f1-9acd-4d73-b0a1-1b2e5607f9ab</requestId>
<reservationSet>
<item>
<reservationId>r-0f6b3c4d5e6789abc</reservationId>
<ownerId>123456789012</ownerId>
<groupSet/>
<instancesSet>
<item>
<instanceId>i-0123456789abcdef0</instanceId>
<imageId>ami-08d4ac5b634553e16</imageId>
<instanceState>
<code>16</code>
<name>running</name>
</instanceState>
<privateDnsName>ip-10-0-1-25.ap-southeast-2.compute.internal</privateDnsName>
<dnsName>ec2-3-26-14-112.ap-southeast-2.compute.amazonaws.com</dnsName>
<reason/>
<keyName>my-keypair</keyName>
<amiLaunchIndex>0</amiLaunchIndex>
<productCodes/>
<instanceType>t3.medium</instanceType>
<launchTime>2025-02-18T04:12:07.000Z</launchTime>
<placement>
<availabilityZone>ap-southeast-2a</availabilityZone>
<groupName/>
<tenancy>default</tenancy>
</placement>
<monitoring>
<state>disabled</state>
</monitoring>
<privateIpAddress>10.0.1.25</privateIpAddress>
<ipAddress>3.26.14.112</ipAddress>
<subnetId>subnet-0ab1c2d3e4f567890</subnetId>
<vpcId>vpc-0123abcd4567efgh8</vpcId>
<sourceDestCheck>true</sourceDestCheck>
<groupSet>
<item>
<groupId>sg-0abc1234def567890</groupId>
<groupName>default</groupName>
</item>
</groupSet>
<stateReason/>
<architecture>x86_64</architecture>
<rootDeviceType>ebs</rootDeviceType>
<rootDeviceName>/dev/xvda</rootDeviceName>
<blockDeviceMapping>
<item>
<deviceName>/dev/xvda</deviceName>
<ebs>
<volumeId>vol-06f5e4d3c2b1a0f98</volumeId>
<status>attached</status>
<attachTime>2025-02-18T04:12:09.000Z</attachTime>
<deleteOnTermination>true</deleteOnTermination>
</ebs>
</item>
</blockDeviceMapping>
<virtualizationType>hvm</virtualizationType>
<tagSet>
<item>
<key>Name</key>
<value>web-01</value>
</item>
</tagSet>
<hypervisor>xen</hypervisor>
<networkInterfaceSet>
<item>
<networkInterfaceId>eni-0a12b34c56d78ef90</networkInterfaceId>
<subnetId>subnet-0ab1c2d3e4f567890</subnetId>
<vpcId>vpc-0123abcd4567efgh8</vpcId>
<description/>
<ownerId>123456789012</ownerId>
<status>in-use</status>
<macAddress>0a:1b:2c:3d:4e:5f</macAddress>
<privateIpAddress>10.0.1.25</privateIpAddress>
<privateDnsName>ip-10-0-1-25.ap-southeast-2.compute.internal</privateDnsName>
<sourceDestCheck>true</sourceDestCheck>
<groupSet>
<item>
<groupId>sg-0abc1234def567890</groupId>
<groupName>default</groupName>
</item>
</groupSet>
<attachment>
<attachmentId>eni-attach-0abc123def4567890</attachmentId>
<deviceIndex>0</deviceIndex>
<status>attached</status>
<attachTime>2025-02-18T04:12:07.000Z</attachTime>
<deleteOnTermination>true</deleteOnTermination>
</attachment>
<privateIpAddressesSet>
<item>
<privateIpAddress>10.0.1.25</privateIpAddress>
<privateDnsName>ip-10-0-1-25.ap-southeast-2.compute.internal</privateDnsName>
<primary>true</primary>
</item>
</privateIpAddressesSet>
</item>
</networkInterfaceSet>
</item>
</instancesSet>
</item>
</reservationSet>
</DescribeInstancesResponse>
`
return ctx.SendString(response)
}
func (gw *GatewayConfig) EC2_RunInstances(ctx *fiber.Ctx, args map[string]string) error {
return nil
}
func (gw *GatewayConfig) EC2_CreateKeyPair(ctx *fiber.Ctx, args map[string]string) error {
slog.Info("EC2 CreateKeyPair called")
return nil
}
+228
View File
@@ -0,0 +1,228 @@
package gateway
import (
"errors"
"log/slog"
"os"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/mulgadc/predastore/s3"
"github.com/google/uuid"
)
type GatewayConfig struct {
Debug bool `json:"debug"`
DisableLogging bool `json:"disable_logging"`
}
var supportedServices = map[string]bool{
"ec2": true,
"iam": true,
"account": true,
}
func (gw *GatewayConfig) SetupRoutes() *fiber.App {
var logLevel slog.Level
if gw.Debug {
logLevel = slog.LevelDebug
} else if gw.DisableLogging {
logLevel = slog.LevelError
} else {
logLevel = slog.LevelInfo
}
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: logLevel,
})
// Create a new logger with the custom handler
slogger := slog.New(handler)
// Set it as the default logger
slog.SetDefault(slogger)
// Configure slog for logging
slog.New(slog.NewTextHandler(os.Stdout, nil))
app := fiber.New(fiber.Config{
// Disable the startup banner
DisableStartupMessage: gw.DisableLogging,
// Set the body limit for S3 specs to 5GiB
//BodyLimit: 5 * 1024 * 1024 * 1024,
// Override default error handler
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
return gw.ErrorHandler(ctx, err)
}},
)
if !gw.DisableLogging {
app.Use(logger.New())
}
s3 := s3.New(&s3.Config{})
// TODO: Support env var for config path, and external IAM
s3.ConfigPath = "config/awsgw/awsgw.toml"
err := s3.ReadConfig()
if err != nil {
slog.Error("Error reading config", "error", err)
return nil
}
// Add authentication middleware for all requests
app.Use(s3.SigV4AuthMiddleware)
// Define routes
app.Get("/*", func(c *fiber.Ctx) error {
return gw.Request(c)
})
app.Post("/*", func(c *fiber.Ctx) error {
return gw.Request(c)
})
return app
}
// Note, custom endpoints can be configured via ENV vars to the AWS SDK/CLI tool, with individual endpoints depending the service
// AWS_ENDPOINT_URL_EC2=https://localhost:9999/ aws --no-verify-ssl ec2 describe-instances
// aws --endpoint-url https://localhost:9999/ --no-verify-ssl eks list-clusters
// AWS_ENDPOINT_URL=https://localhost:9999/ aws --no-verify-ssl ec2 describe-instances
func (gw *GatewayConfig) Request(ctx *fiber.Ctx) error {
// Route the request to the appropriate endpoint (e.g EC2, IAM, etc)
// Determine the service from the Authorization header
// "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20250930/ap-southeast-2/account/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=adb8183545a9d6f5b626908582b4ca5f9a309137b1d4bcc802f0e79ede7af7bc"
authHeader := ctx.Get("Authorization")
parts := strings.Split(authHeader, ", ")
if len(parts) != 3 {
slog.Debug("Invalid Authorization header format")
return errors.New("AccessDenied")
}
// Parse credential
creds := strings.Split(strings.TrimPrefix(parts[0], "AWS4-HMAC-SHA256 Credential="), "/")
if len(creds) != 5 {
slog.Debug("Invalid credential scope")
return errors.New("AccessDenied")
}
svc := creds[3]
if !supportedServices[svc] {
slog.Debug("Unsupported service", "service", svc)
return errors.New("UnsupportedOperation")
}
slog.Info("Request", "service", svc, "method", ctx.Method(), "path", ctx.Path())
switch svc {
case "ec2":
return gw.EC2_Request(ctx)
case "account":
return gw.Account_Request(ctx)
case "iam":
return gw.IAM_Request(ctx)
default:
return errors.New("UnsupportedOperation")
}
}
func (gw *GatewayConfig) ErrorHandler(ctx *fiber.Ctx, err error) error {
// TODO: Support service type specific errors (e.g EC2, S3, etc)
// Status code defaults to 500
httpCode := fiber.StatusInternalServerError
var s3error s3.S3Error
var e *fiber.Error
// Check for specific error types
switch {
case strings.Contains(err.Error(), "NoSuchBucket") || strings.Contains(err.Error(), "Bucket not found"):
// File or bucket not found
httpCode = fiber.StatusNotFound
s3error.Code = "NoSuchBucket"
s3error.Message = "The specified bucket does not exist"
case strings.Contains(err.Error(), "AccessDenied") || strings.Contains(err.Error(), "Not enough permissions"):
// Permission error
httpCode = fiber.StatusForbidden
s3error.Code = "AccessDenied"
s3error.Message = "Access Denied"
case strings.Contains(err.Error(), "NoSuchObject") || strings.Contains(err.Error(), "not found") ||
errors.Is(err, os.ErrNotExist):
// File not found
httpCode = fiber.StatusNotFound
s3error.Code = "NoSuchKey"
s3error.Message = "The specified key does not exist"
case strings.Contains(err.Error(), "Invalid signature") || strings.Contains(err.Error(), "Invalid access key"):
// Authentication error
httpCode = fiber.StatusForbidden
s3error.Code = "SignatureDoesNotMatch"
s3error.Message = "The request signature does not match"
case strings.Contains(err.Error(), "Missing Authorization header"):
// Missing auth header
httpCode = fiber.StatusForbidden
s3error.Code = "AccessDenied"
s3error.Message = "Access Denied"
case strings.Contains(err.Error(), "UnsupportedOperation"):
// Unsupported operation
httpCode = fiber.StatusBadRequest
s3error.Code = "UnsupportedOperation"
s3error.Message = "The operation is not supported"
ctx.Set("X-Amzn-Errortype", "UnsupportedOperationException")
case errors.As(err, &e):
httpCode = e.Code
s3error.Message = e.Message
s3error.Code = "InternalError"
default:
s3error.Code = "InternalError"
s3error.Message = err.Error()
}
// Add request ID and host ID
s3error.RequestId = ctx.GetRespHeader("x-amz-request-id", uuid.NewString())
s3error.HostId = ctx.Hostname()
// Set standard S3 error response headers
ctx.Set("Content-Type", "application/xml")
return ctx.Status(httpCode).XML(s3error)
}
// Parse AWS query arguments (used by some services like EC2/S3)
func parseAWSQueryArgs(query string) map[string]string {
params := make(map[string]string)
pairs := strings.Split(query, "&")
for _, pair := range pairs {
kv := strings.SplitN(pair, "=", 2)
if len(kv) == 2 {
params[kv[0]] = kv[1]
} else if len(kv) == 1 {
params[kv[0]] = ""
}
}
return params
}
+8
View File
@@ -0,0 +1,8 @@
package gateway
import "github.com/gofiber/fiber/v2"
func (gw *GatewayConfig) IAM_Request(ctx *fiber.Ctx) error {
return nil
}
+8 -4
View File
@@ -3,6 +3,7 @@ package service
import (
"fmt"
"github.com/mulgadc/hive/hive/services/awsgw"
"github.com/mulgadc/hive/hive/services/hive"
"github.com/mulgadc/hive/hive/services/nats"
"github.com/mulgadc/hive/hive/services/predastore"
@@ -20,17 +21,20 @@ type Service interface {
func New(btype string, config interface{}) (Service, error) {
switch btype {
case "nats":
return nats.New(config)
case "predastore":
return predastore.New(config)
case "nats":
return nats.New(config)
case "viperblock":
return viperblockd.New(config)
case "hive":
return hive.New(config)
case "viperblock":
return viperblockd.New(config)
case "awsgw":
return awsgw.New(config)
}
+77
View File
@@ -0,0 +1,77 @@
package awsgw
import (
"log"
"log/slog"
"os"
"github.com/mulgadc/hive/hive/config"
"github.com/mulgadc/hive/hive/gateway"
"github.com/mulgadc/hive/hive/utils"
)
var serviceName = "awsgw"
type Service struct {
Config *config.Config
}
func New(cfg interface{}) (svc *Service, err error) {
svc = &Service{
Config: cfg.(*config.Config),
}
return svc, nil
}
func (svc *Service) Start() (int, error) {
utils.WritePidFile(serviceName, os.Getpid())
err := launchService(svc.Config)
if err != nil {
return 0, err
}
return os.Getpid(), nil
}
func (svc *Service) Stop() (err error) {
err = utils.StopProcess(serviceName)
return err
}
func (svc *Service) Status() (string, error) {
return "", nil
}
func (svc *Service) Shutdown() (err error) {
return svc.Stop()
}
func (svc *Service) Reload() (err error) {
return nil
}
func launchService(config *config.Config) (err error) {
/*
d := daemon.NewDaemon(config)
slog.Info("Starting AWS-GW daemon ...")
err = d.Start()
*/
gw := gateway.GatewayConfig{
Debug: config.AWSGW.Debug,
DisableLogging: false,
}
app := gw.SetupRoutes()
if err != nil {
slog.Warn("Failed to start AWS-GW daemon: %v", err)
return err
}
log.Fatal(app.ListenTLS(config.AWSGW.Host, config.AWSGW.TLSCert, config.AWSGW.TLSKey))
return nil
}
+17 -2
View File
@@ -196,13 +196,28 @@ export HIVE_WAL_DIR=$WAL_DIR
#HIVE_CMD="./bin/hive service hive start --config config/hive.toml --base-dir $HIVE_BASE_DIR --wal-dir $WAL_DIR"
HIVE_CMD="./bin/hive service hive start"
start_service "hive" "$HIVE_CMD"
# 5️⃣ Start AWS Gateway
echo ""
echo "4️5️⃣ Starting AWS Gateway..."
# Use the same base directory as Viperblock for consistency
export HIVE_CONFIG_PATH=$CONFIG_DIR/hive.toml
export HIVE_AWSGW_HOST="0.0.0.0:9999"
export HIVE_AWSGW_TLS_CERT=$CONFIG_DIR/server.pem
export HIVE_AWSGW_TLS_KEY=$CONFIG_DIR/server.key
#HIVE_CMD="air -c .air-hive.toml"
#HIVE_CMD="./bin/hive service hive start --config config/hive.toml --base-dir $HIVE_BASE_DIR --wal-dir $WAL_DIR"
HIVE_CMD="./bin/hive service awsgw start"
start_service "awsgw" "$HIVE_CMD"
echo ""
echo "🔗 Service endpoints will be:"
echo " - NATS: nats://localhost:4222"
echo " - Predastore: https://localhost:8443"
echo " - Hive Gateway: https://localhost:9999"
echo " - AWS Gateway: https://localhost:9999"
echo ""
echo "📊 Monitor background service logs:"
echo " tail -f $LOGS_DIR/*.log"