From 3e2eb60f5c15d8a5a2b79f0a045522d75e1fc597 Mon Sep 17 00:00:00 2001 From: Naoki Kosaka Date: Thu, 21 Mar 2024 20:20:41 +0900 Subject: [PATCH] feat: implements RelayConfigV2 --- models/config_v2.go | 178 +++++++++++++++++++++++++++++++++++++++ models/config_v2_test.go | 143 +++++++++++++++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 models/config_v2.go create mode 100644 models/config_v2_test.go diff --git a/models/config_v2.go b/models/config_v2.go new file mode 100644 index 0000000..b04445a --- /dev/null +++ b/models/config_v2.go @@ -0,0 +1,178 @@ +package models + +import ( + "context" + "crypto/rsa" + "errors" + "fmt" + "net/url" + "time" + + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +type ServerConfig struct { + Domain *url.URL + Bind string + PrivateKey *rsa.PrivateKey +} + +type ServiceConfig struct { + Name string + Summary string + IconURL *url.URL + ImageURL *url.URL +} + +type RelayConfigV2 struct { + serverConfig *ServerConfig + serviceConfig *ServiceConfig + redisOptions *redis.Options + jobConcurrency int +} + +type RelayConfigV2BuilderOptions struct { + WithServerConfig bool + WithJobConcurrency bool +} + +func buildServerConfig() (*ServerConfig, error) { + domain, err := url.ParseRequestURI("https://" + viper.GetString("RELAY_DOMAIN")) + if err != nil { + return nil, errors.New("RELAY_DOMAIN: " + err.Error()) + } + + privateKey, err := readPrivateKeyRSA(viper.GetString("ACTOR_PEM")) + if err != nil { + return nil, errors.New("ACTOR_PEM: " + err.Error()) + } + + return &ServerConfig{ + Domain: domain, + Bind: viper.GetString("RELAY_BIND"), + PrivateKey: privateKey, + }, nil +} + +func buildServiceConfig() (*ServiceConfig, error) { + // Required fields + name := viper.GetString("RELAY_SERVICENAME") + summary := viper.GetString("RELAY_SUMMARY") + + // Optional fields + iconURL, err := url.ParseRequestURI(viper.GetString("RELAY_ICON")) + if err != nil { + logrus.Warn("RELAY_ICON: INVALID OR EMPTY. THIS COLUMN IS DISABLED.") + iconURL = nil + } + imageURL, err := url.ParseRequestURI(viper.GetString("RELAY_IMAGE")) + if err != nil { + logrus.Warn("RELAY_IMAGE: INVALID OR EMPTY. THIS COLUMN IS DISABLED.") + imageURL = nil + } + + return &ServiceConfig{ + Name: name, + Summary: summary, + IconURL: iconURL, + ImageURL: imageURL, + }, nil +} + +func NewRelayConfigV2(options RelayConfigV2BuilderOptions) (*RelayConfigV2, error) { + result := RelayConfigV2{} + + serviceOptions, err := buildServiceConfig() + if err != nil { + return nil, err + } + result.serviceConfig = serviceOptions + + redisOptions, err := redis.ParseURL(viper.GetString("REDIS_URL")) + if err != nil { + return nil, errors.New("REDIS_URL: " + err.Error()) + } + result.redisOptions = redisOptions + + // Works with API Server + if options.WithServerConfig { + serverOptions, err := buildServerConfig() + if err != nil { + return nil, err + } + result.serverConfig = serverOptions + } else { + result.serverConfig = nil + } + + // Works with Job Worker + if options.WithJobConcurrency { + viper.SetDefault("JOB_CONCURRENCY", 10) + jobConcurrency := viper.GetInt("JOB_CONCURRENCY") + if jobConcurrency < 1 { + return nil, errors.New("JOB_CONCURRENCY: Invalid Value") + } + result.jobConcurrency = jobConcurrency + } else { + result.jobConcurrency = 0 + } + + return &result, nil +} + +// ServerConfig is API Server options. +func (config *RelayConfigV2) ServerConfig() (*ServerConfig, error) { + if config.serverConfig != nil { + return config.serverConfig, nil + } + return nil, errors.New("this configuration does not have ServerConfig") +} + +// ServiceConfig is Relay Service options. +func (config *RelayConfigV2) ServiceConfig() *ServiceConfig { + return config.serviceConfig +} + +// RedisOptions is Redis options. +func (config *RelayConfigV2) RedisOptions() *redis.Options { + return config.redisOptions +} + +// JobConcurrency is Job concurrency. +func (config *RelayConfigV2) JobConcurrency() (int, error) { + if config.jobConcurrency == 0 { + return 0, errors.New("this configuration does not have JobConcurrency") + } + return config.jobConcurrency, nil +} + +func (config *RelayConfigV2) DumpWelcomeMessage(moduleName string, version string) string { + message := fmt.Sprintf(`Welcome to Activity-Relay %s - %s\n`, version, moduleName) + message += fmt.Sprintf(`- Configuration\n`) + message += fmt.Sprintf(`RELAY NAME : %s\n`, config.serviceConfig.Name) + message += fmt.Sprintf(`REDIS URL : %s\n`, config.redisOptions.Addr) + if config.serverConfig != nil { + message += fmt.Sprintf(`RELAY DOMAIN : %s\n`, config.serverConfig.Domain.Host) + message += fmt.Sprintf(`BIND ADDRESS : %s\n`, config.serverConfig.Bind) + } + if config.jobConcurrency != 0 { + message += fmt.Sprintf(`JOB_CONCURRENCY : %d\n`, config.jobConcurrency) + } + + return message +} + +func (config *RelayConfigV2) NewRedisClient(ctx context.Context) (*redis.Client, error) { + redisClient := redis.NewClient(config.redisOptions) + + pCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + _, err := redisClient.Ping(pCtx).Result() + if err != nil { + return nil, err + } + return redisClient, nil +} diff --git a/models/config_v2_test.go b/models/config_v2_test.go new file mode 100644 index 0000000..beb5b0e --- /dev/null +++ b/models/config_v2_test.go @@ -0,0 +1,143 @@ +package models + +import ( + "context" + "strings" + "testing" + + "github.com/spf13/viper" +) + +func TestNewRelayConfigV2(t *testing.T) { + t.Run("success generate valid options", func(t *testing.T) { + relayConfig, err := NewRelayConfigV2(RelayConfigV2BuilderOptions{ + WithServerConfig: true, + WithJobConcurrency: true, + }) + if err != nil { + t.Fatal(err) + } + + // ServerConfig + if relayConfig.serverConfig.Domain.Host != "relay.toot.yukimochi.jp" { + t.Error("fail - parse: RelayConfig.serverConfig.Domain") + } + if relayConfig.serverConfig.Bind != "0.0.0.0:8080" { + t.Error("fail - parse: RelayConfig.serverConfig.Bind") + } + if relayConfig.serverConfig.PrivateKey == nil { + t.Error("fail - parse: RelayConfig.serverConfig.PrivateKey") + } + + // ServiceConfig + if relayConfig.serviceConfig.Name != "YUKIMOCHI Toot Relay Service" { + t.Error("fail - parse: RelayConfig.serviceConfig.Name") + } + if relayConfig.serviceConfig.Summary != "YUKIMOCHI Toot Relay Service is Running by Activity-Relay" { + t.Error("fail - parse: RelayConfig.serviceConfig.Summary") + } + if relayConfig.serviceConfig.IconURL.String() != "https://example.com/example_icon.png" { + t.Error("fail - parse: RelayConfig.serviceConfig.IconURL") + } + if relayConfig.serviceConfig.ImageURL.String() != "https://example.com/example_image.png" { + t.Error("fail - parse: RelayConfig.serviceConfig.ImageURL") + } + + // RedisOptions + if relayConfig.redisOptions == nil { + t.Error("fail - parse: RelayConfig.redisOptions") + } + + // JobConcurrency + if relayConfig.jobConcurrency != 50 { + t.Error("fail - parse: RelayConfig.jobConcurrency") + } + }) + + t.Run("success generate valid options without serverConfig", func(t *testing.T) { + relayConfig, err := NewRelayConfigV2(RelayConfigV2BuilderOptions{ + WithServerConfig: false, + WithJobConcurrency: true, + }) + if err != nil { + t.Fatal(err) + } + + // ServerConfig + if relayConfig.serverConfig != nil { + t.Error("fail - parse: RelayConfig.serverConfig") + } + }) + + t.Run("success generate valid options without jobConcurrency", func(t *testing.T) { + relayConfig, err := NewRelayConfigV2(RelayConfigV2BuilderOptions{ + WithServerConfig: true, + WithJobConcurrency: false, + }) + if err != nil { + t.Fatal(err) + } + + // JobConcurrency + if relayConfig.jobConcurrency != 0 { + t.Error("fail - parse: RelayConfig.jobConcurrency") + } + }) + + t.Run("fail invalid options", func(t *testing.T) { + invalidExamples := map[string]string{ + "ACTOR_PEM@notFound": "../misc/test/notfound.pem", + "REDIS_URL@invalidURL": "", + } + + for key, invalidValue := range invalidExamples { + viperKey := strings.Split(key, "@")[0] + validValue := viper.GetString(viperKey) + + viper.Set(viperKey, invalidValue) + _, err := NewRelayConfig() + if err == nil { + t.Error("fail - invalid value should be raise error : " + key) + } + + viper.Set(viperKey, validValue) + } + }) +} + +func TestNewRedisClient(t *testing.T) { + t.Run("success create client for reachable redis serer", func(t *testing.T) { + relayConfig, err := NewRelayConfigV2(RelayConfigV2BuilderOptions{ + WithServerConfig: false, + WithJobConcurrency: false, + }) + if err != nil { + t.Fatal(err) + } + + _, err = relayConfig.NewRedisClient(context.Background()) + if err != nil { + t.Error("fail - create client for reachable redis serer") + } + }) + + t.Run("fail create client for unreachable redis serer", func(t *testing.T) { + validURL := viper.GetString("REDIS_URL") + viper.Set("REDIS_URL", "redis://localhost:6380") + + relayConfig, err := NewRelayConfigV2(RelayConfigV2BuilderOptions{ + WithServerConfig: false, + WithJobConcurrency: false, + }) + if err != nil { + t.Fatal(err) + } + + _, err = relayConfig.NewRedisClient(context.Background()) + if err == nil { + t.Error("fail - create client for unreachable redis serer") + } + + viper.Set("REDIS_URL", validURL) + }) +}