diff --git a/config/config.go b/config/config.go index c4e224d..4ae457b 100644 --- a/config/config.go +++ b/config/config.go @@ -11,4 +11,5 @@ type Config struct { IDTokenAudience string AlgoliaAppID string AlgoliaAPIKey string + CloudStorageBucketName string } diff --git a/docker-compose.yml b/docker-compose.yml index 439bc47..741c9e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,3 +21,4 @@ services: ALGOLIA_APP_ID: ALGOLIA_API_KEY: SECRET_SCANNER_URL: "" + CLOUD_STORAGE_BUCKET_NAME: "staging-comfy-registry" diff --git a/gateways/algolia/algolia-noop.go b/gateways/algolia/algolia-noop.go index cf08480..86cfce6 100644 --- a/gateways/algolia/algolia-noop.go +++ b/gateways/algolia/algolia-noop.go @@ -9,39 +9,34 @@ import ( var _ AlgoliaService = (*algolianoop)(nil) +// No-op implementation for AlgoliaService, logging calls instead of performing operations type algolianoop struct{} -// DeleteNode implements AlgoliaService. func (a *algolianoop) DeleteNode(ctx context.Context, node *ent.Node) error { log.Ctx(ctx).Info().Msgf("algolia noop: delete node: %s", node.ID) return nil } -// IndexNodes implements AlgoliaService. func (a *algolianoop) IndexNodes(ctx context.Context, nodes ...*ent.Node) error { log.Ctx(ctx).Info().Msgf("algolia noop: index nodes: %d number of nodes", len(nodes)) return nil } -// SearchNodes implements AlgoliaService. func (a *algolianoop) SearchNodes(ctx context.Context, query string, opts ...interface{}) ([]*ent.Node, error) { log.Ctx(ctx).Info().Msgf("algolia noop: search nodes: %s", query) return nil, nil } -// DeleteNodeVersion implements AlgoliaService. func (a *algolianoop) DeleteNodeVersions(ctx context.Context, nodes ...*ent.NodeVersion) error { - log.Ctx(ctx).Info().Msgf("algolia noop: delete node version: %d number of node versions", len(nodes)) + log.Ctx(ctx).Info().Msgf("algolia noop: delete node version: %d number of node versions", len(nodes)) return nil } -// IndexNodeVersions implements AlgoliaService. func (a *algolianoop) IndexNodeVersions(ctx context.Context, nodes ...*ent.NodeVersion) error { log.Ctx(ctx).Info().Msgf("algolia noop: index node versions: %d number of node versions", len(nodes)) return nil } -// SearchNodeVersions implements AlgoliaService. func (a *algolianoop) SearchNodeVersions(ctx context.Context, query string, opts ...interface{}) ([]*ent.NodeVersion, error) { log.Ctx(ctx).Info().Msgf("algolia noop: search node versions: %s", query) return nil, nil diff --git a/gateways/algolia/algolia.go b/gateways/algolia/algolia.go index e9ebe52..28597c7 100644 --- a/gateways/algolia/algolia.go +++ b/gateways/algolia/algolia.go @@ -3,10 +3,10 @@ package algolia import ( "context" "fmt" - "os" - "registry-backend/ent" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/rs/zerolog/log" + "registry-backend/config" // assuming a config package exists to hold config values + "registry-backend/ent" ) // AlgoliaService defines the interface for interacting with Algolia search. @@ -27,35 +27,21 @@ type algolia struct { client *search.Client } -// New creates a new Algolia service with the provided app ID and API key. -func New(appid, apikey string) (AlgoliaService, error) { - return &algolia{ - client: search.NewClient(appid, apikey), - }, nil -} - -// NewFromEnv creates a new Algolia service using environment variables for configuration. -func NewFromEnv() (AlgoliaService, error) { - appid, ok := os.LookupEnv("ALGOLIA_APP_ID") - if !ok { - return nil, fmt.Errorf("required env variable ALGOLIA_APP_ID is not set") - } - apikey, ok := os.LookupEnv("ALGOLIA_API_KEY") - if !ok { - return nil, fmt.Errorf("required env variable ALGOLIA_API_KEY is not set") - } - return New(appid, apikey) -} - -// NewFromEnvOrNoop creates a new Algolia service using environment variables or noop implementation if no environment found -func NewFromEnvOrNoop() (AlgoliaService, error) { - id := os.Getenv("ALGOLIA_APP_ID") - key := os.Getenv("ALGOLIA_API_KEY") - if id == "" && key == "" { +// NewAlgoliaService creates a new Algolia service using the provided config or returns a noop implementation if the config is missing. +func NewAlgoliaService(cfg *config.Config) (AlgoliaService, error) { + if cfg == nil || cfg.AlgoliaAppID == "" || cfg.AlgoliaAPIKey == "" { + // Return a noop implementation if config is nil or missing keys + log.Info().Msg("No Algolia configuration found, using noop implementation") return &algolianoop{}, nil } - return NewFromEnv() + // Fetch the Algolia app ID and API key from the provided config + appID := cfg.AlgoliaAppID + apiKey := cfg.AlgoliaAPIKey + + // Initialize the Algolia client + client := search.NewClient(appID, apiKey) + return &algolia{client: client}, nil } // IndexNodes indexes the provided nodes in Algolia. @@ -67,7 +53,7 @@ func (a *algolia) IndexNodes(ctx context.Context, nodes ...*ent.Node) error { o := map[string]interface{}{ "objectID": n.ID, "name": n.Name, - "publiser_id": n.PublisherID, + "publisher_id": n.PublisherID, "description": n.Description, "id": n.ID, "create_time": n.CreateTime, @@ -139,13 +125,13 @@ func (a *algolia) IndexNodeVersions(ctx context.Context, nodes ...*ent.NodeVersi res, err := index.SaveObjects(objects) if err != nil { - return fmt.Errorf("failed to index nodes: %w", err) + return fmt.Errorf("failed to index node versions: %w", err) } return res.Wait() } -// DeleteNodeVersion implements AlgoliaService. +// DeleteNodeVersions implements AlgoliaService. func (a *algolia) DeleteNodeVersions(ctx context.Context, nodes ...*ent.NodeVersion) error { index := a.client.InitIndex("node_versions_index") ids := []string{} @@ -154,7 +140,7 @@ func (a *algolia) DeleteNodeVersions(ctx context.Context, nodes ...*ent.NodeVers } res, err := index.DeleteObjects(ids) if err != nil { - return fmt.Errorf("failed to delete node: %w", err) + return fmt.Errorf("failed to delete node versions: %w", err) } return res.Wait() } @@ -164,7 +150,7 @@ func (a *algolia) SearchNodeVersions(ctx context.Context, query string, opts ... index := a.client.InitIndex("node_versions_index") res, err := index.Search(query, opts...) if err != nil { - return nil, fmt.Errorf("failed to search nodes: %w", err) + return nil, fmt.Errorf("failed to search node versions: %w", err) } var nodes []*ent.NodeVersion diff --git a/gateways/algolia/algolia_test.go b/gateways/algolia/algolia_test.go index b2c62f8..1c09723 100644 --- a/gateways/algolia/algolia_test.go +++ b/gateways/algolia/algolia_test.go @@ -3,6 +3,7 @@ package algolia import ( "context" "os" + "registry-backend/config" "registry-backend/ent" "registry-backend/ent/schema" "testing" @@ -23,7 +24,10 @@ func TestIndex(t *testing.T) { t.Skip("Required env variables `ALGOLIA_API_KEY` is not set") } - algolia, err := NewFromEnv() + algolia, err := NewAlgoliaService(&config.Config{ + AlgoliaAppID: os.Getenv("ALGOLIA_APP_ID"), + AlgoliaAPIKey: os.Getenv("ALGOLIA_API_KEY"), + }) require.NoError(t, err) t.Run("node", func(t *testing.T) { @@ -74,9 +78,7 @@ func TestIndex(t *testing.T) { } func TestNoop(t *testing.T) { - t.Setenv("ALGOLIA_APP_ID", "") - t.Setenv("ALGOLIA_API_KEY", "") - a, err := NewFromEnvOrNoop() + a, err := NewAlgoliaService(&config.Config{}) require.NoError(t, err) require.NoError(t, a.IndexNodes(context.Background(), &ent.Node{})) require.NoError(t, a.DeleteNode(context.Background(), &ent.Node{})) diff --git a/gateways/slack/slack.go b/gateways/slack/slack.go index a54cb39..9db55c7 100644 --- a/gateways/slack/slack.go +++ b/gateways/slack/slack.go @@ -1,45 +1,61 @@ -package gateway +package slack import ( "bytes" "encoding/json" "fmt" + "github.com/rs/zerolog/log" "net/http" "registry-backend/config" ) +// SlackService defines the interface for interacting with Slack notifications. type SlackService interface { SendRegistryMessageToSlack(msg string) error } -type DripSlackService struct { +// Ensure slackService struct implements SlackService interface +var _ SlackService = (*slackService)(nil) + +// slackService struct holds the configuration and webhook URL. +type slackService struct { registrySlackChannelWebhook string config *config.Config } -func NewSlackService(config *config.Config) *DripSlackService { - return &DripSlackService{ - config: config, - registrySlackChannelWebhook: config.SlackRegistryChannelWebhook, +// NewSlackService creates a new Slack service using the provided config or returns a noop implementation if the config is missing. +func NewSlackService(cfg *config.Config) SlackService { + if cfg == nil || cfg.SlackRegistryChannelWebhook == "" { + // Return a noop implementation if config is nil or missing keys + log.Info().Msg("No Slack configuration found, using noop implementation") + return &slackNoop{} } + return &slackService{ + config: cfg, + registrySlackChannelWebhook: cfg.SlackRegistryChannelWebhook, + } } type slackRequestBody struct { Text string `json:"text"` } -func (s *DripSlackService) SendRegistryMessageToSlack(msg string) error { - if s.config.DripEnv == "prod" { - return sendSlackNotification(msg, s.registrySlackChannelWebhook) +// SendRegistryMessageToSlack sends a message to the registry Slack channel. +func (s *slackService) SendRegistryMessageToSlack(msg string) error { + // Skip sending messages in non-production environments + if s.config.DripEnv != "prod" { + log.Info().Msg("Skipping Slack operations in non-prod environment") + return nil } - return nil + + return sendSlackNotification(msg, s.registrySlackChannelWebhook) } +// sendSlackNotification sends the message to the provided Slack webhook URL. func sendSlackNotification(msg string, slackWebhookURL string) error { if slackWebhookURL == "" { - println("No Slack webhook URL provided, skipping sending message to Slack") - return nil + return fmt.Errorf("no Slack webhook URL provided, skipping sending message to Slack") } body, err := json.Marshal(slackRequestBody{Text: msg}) @@ -69,3 +85,14 @@ func sendSlackNotification(msg string, slackWebhookURL string) error { return nil } + +// slackNoop is a noop implementation of the SlackService interface. +// It does nothing and is used when no valid config or the environment is non-production. +type slackNoop struct{} + +// Implement all SlackService methods for noop behavior. + +func (s *slackNoop) SendRegistryMessageToSlack(msg string) error { + // No-op, just return nil to avoid any side-effects + return nil +} diff --git a/gateways/storage/files.go b/gateways/storage/files.go index a652371..54fe01b 100644 --- a/gateways/storage/files.go +++ b/gateways/storage/files.go @@ -4,17 +4,16 @@ import ( "bytes" "context" "fmt" - "io" "os" "time" "cloud.google.com/go/storage" "github.com/rs/zerolog/log" + "registry-backend/config" ) -const BucketName = "comfy-workflow-json" - +// StorageService defines the interface for interacting with cloud storage. type StorageService interface { UploadFile(ctx context.Context, bucket, object, filePath string) (string, error) StreamFileUpload(w io.Writer, objectName, blob string) (string, string, error) @@ -22,31 +21,40 @@ type StorageService interface { GenerateSignedURL(bucketName, objectName string) (string, error) } -type GCPStorageService struct { +// Ensure storageService struct implements StorageService interface +var _ StorageService = (*storageService)(nil) + +// storageService struct holds the GCP storage client and configuration. +type storageService struct { client *storage.Client + config *config.Config } -func NewGCPStorageService(ctx context.Context) (*GCPStorageService, error) { - StorageClient, err := storage.NewClient(ctx) +// NewStorageService creates a new storage service using the provided config or returns a noop implementation if the config is missing. +func NewStorageService(cfg *config.Config) (StorageService, error) { + if cfg == nil { + // Return a noop implementation if config is nil or storage is not enabled + log.Info().Msg("No storage configuration found, using noop implementation") + return &storageNoop{}, nil + } + + // Initialize GCP storage client + client, err := storage.NewClient(context.Background()) if err != nil { return nil, fmt.Errorf("NewStorageClient: %v", err) } - return &GCPStorageService{ - client: StorageClient, + return &storageService{ + client: client, + config: cfg, }, nil } -// uploadFile uploads an object. -func (s *GCPStorageService) UploadFile(ctx context.Context, bucket, object string, filePath string) (string, error) { +// UploadFile uploads an object to GCP storage. +func (s *storageService) UploadFile(ctx context.Context, bucket, object, filePath string) (string, error) { log.Ctx(ctx).Info().Msgf("Uploading %v to %v/%v.\n", filePath, bucket, object) - client, err := storage.NewClient(ctx) - if err != nil { - return "", fmt.Errorf("storage.NewClient: %w", err) - } - defer client.Close() - // Open local file. + // Open local file f, err := os.Open(filePath) if err != nil { return "", fmt.Errorf("os.Open: %w", err) @@ -56,31 +64,23 @@ func (s *GCPStorageService) UploadFile(ctx context.Context, bucket, object strin ctx, cancel := context.WithTimeout(ctx, time.Second*50) defer cancel() - o := client.Bucket(bucket).Object(object) + o := s.client.Bucket(bucket).Object(object) - // Optional: set a generation-match precondition to avoid potential race - // conditions and data corruptions. The request to upload is aborted if the - // object's generation number does not match your precondition. - // For an object that does not yet exist, set the DoesNotExist precondition. + // Ensure that we don't overwrite an existing object. o = o.If(storage.Conditions{DoesNotExist: true}) - // If the live object already exists in your bucket, set instead a - // generation-match precondition using the live object's generation number. - // attrs, err := o.Attrs(ctx) - // if err != nil { - // return fmt.Errorf("object.Attrs: %w", err) - // } - // o = o.If(storage.Conditions{GenerationMatch: attrs.Generation}) - - // Upload an object with storage.Writer. + + // Upload the file to the cloud wc := o.NewWriter(ctx) - if _, err = io.Copy(wc, f); err != nil { + if _, err := io.Copy(wc, f); err != nil { return "", fmt.Errorf("io.Copy: %w", err) } if err := wc.Close(); err != nil { return "", fmt.Errorf("Writer.Close: %w", err) } + log.Ctx(ctx).Info().Msgf("Blob %v uploaded.\n", object) - // Make the file publicly accessible + + // Set the ACL to make the file publicly accessible if err := o.ACL().Set(ctx, storage.AllUsers, storage.RoleReader); err != nil { return "", fmt.Errorf("ACL().Set: %w", err) } @@ -91,14 +91,9 @@ func (s *GCPStorageService) UploadFile(ctx context.Context, bucket, object strin return publicURL, nil } -// StreamFileUpload uploads an object via a stream. -func (s *GCPStorageService) StreamFileUpload(w io.Writer, objectName string, blob string) (string, string, error) { +// StreamFileUpload uploads an object via a stream to GCP storage. +func (s *storageService) StreamFileUpload(w io.Writer, objectName, blob string) (string, string, error) { ctx := context.Background() - client, err := storage.NewClient(ctx) - if err != nil { - return "", "", fmt.Errorf("storage.NewClient: %w", err) - } - defer client.Close() b := []byte(blob) buf := bytes.NewBuffer(b) @@ -106,50 +101,40 @@ func (s *GCPStorageService) StreamFileUpload(w io.Writer, objectName string, blo ctx, cancel := context.WithTimeout(ctx, time.Second*50) defer cancel() - // Upload an object with storage.Writer. - wc := client.Bucket(BucketName).Object(objectName).NewWriter(ctx) - wc.ChunkSize = 0 // note retries are not supported for chunk size 0. + // Upload the object as a stream + wc := s.client.Bucket(s.config.CloudStorageBucketName).Object(objectName).NewWriter(ctx) + wc.ChunkSize = 0 // Note: retries are not supported for chunk size 0 - if _, err = io.Copy(wc, buf); err != nil { + if _, err := io.Copy(wc, buf); err != nil { return "", "", fmt.Errorf("io.Copy: %w", err) } - // Data can continue to be added to the file until the writer is closed. + if err := wc.Close(); err != nil { return "", "", fmt.Errorf("Writer.Close: %w", err) } - log.Ctx(ctx).Info().Msgf("%v uploaded to %v.\n", objectName, BucketName) - return BucketName, objectName, nil -} + log.Ctx(ctx).Info().Msgf("%v uploaded to %v.\n", objectName, s.config.CloudStorageBucketName) -func (s *GCPStorageService) GetFileUrl(ctx context.Context, bucketName, objectPath string) (string, error) { - // Get public url of a file in a bucket - client, err := storage.NewClient(ctx) - if err != nil { - return "", fmt.Errorf("storage.NewClient: %w", err) - } - defer client.Close() + return s.config.CloudStorageBucketName, objectName, nil +} - // Get Public URL - attrs, err := client.Bucket(bucketName).Object(objectPath).Attrs(ctx) +// GetFileUrl gets the public URL of a file from GCP storage. +func (s *storageService) GetFileUrl(ctx context.Context, bucketName, objectPath string) (string, error) { + // Get the public URL of a file in a bucket + attrs, err := s.client.Bucket(bucketName).Object(objectPath).Attrs(ctx) if err != nil { return "", fmt.Errorf("object.Attrs: %w", err) } + publicURL := attrs.MediaLink log.Ctx(ctx).Info().Msgf("Public URL: %v", publicURL) return publicURL, nil } -func (s *GCPStorageService) GenerateSignedURL(bucketName, objectName string) (string, error) { - ctx := context.Background() - client, err := storage.NewClient(ctx) - if err != nil { - return "", err - } - defer client.Close() - +// GenerateSignedURL generates a signed URL for uploading to GCP storage. +func (s *storageService) GenerateSignedURL(bucketName, objectName string) (string, error) { expires := time.Now().Add(15 * time.Minute) - url, err := client.Bucket(bucketName).SignedURL(objectName, &storage.SignedURLOptions{ + url, err := s.client.Bucket(bucketName).SignedURL(objectName, &storage.SignedURLOptions{ ContentType: "application/zip", Method: "PUT", Expires: expires, @@ -160,3 +145,28 @@ func (s *GCPStorageService) GenerateSignedURL(bucketName, objectName string) (st return url, nil } + +// storageNoop is a noop implementation of the StorageService interface. +type storageNoop struct{} + +// Implement all StorageService methods for noop behavior. + +func (s *storageNoop) UploadFile(ctx context.Context, bucket, object, filePath string) (string, error) { + // No-op, return nil to avoid side-effects + return "", nil +} + +func (s *storageNoop) StreamFileUpload(w io.Writer, objectName, blob string) (string, string, error) { + // No-op, return nil to avoid side-effects + return "", "", nil +} + +func (s *storageNoop) GetFileUrl(ctx context.Context, bucketName, objectPath string) (string, error) { + // No-op, return empty string and nil error + return "", nil +} + +func (s *storageNoop) GenerateSignedURL(bucketName, objectName string) (string, error) { + // No-op, return empty string and nil error + return "", nil +} diff --git a/main.go b/main.go index 3c39266..0eb9e7e 100644 --- a/main.go +++ b/main.go @@ -27,8 +27,11 @@ func validateEnvVars(env string) { "JWT_SECRET", } + // TODO: Add staging specific variables + // Additional variables mandatory for production and staging - prodStagingVars := []string{ + prodVars := []string{ + "CLOUD_STORAGE_BUCKET_NAME", "SLACK_REGISTRY_CHANNEL_WEBHOOK", "SECRET_SCANNER_URL", "SECURITY_COUNCIL_DISCORD_WEBHOOK", @@ -37,13 +40,13 @@ func validateEnvVars(env string) { "ID_TOKEN_AUDIENCE", } - // Add production and staging-specific variables - if env == "prod" || env == "staging" { - mandatoryVars = append(mandatoryVars, prodStagingVars...) + // Add production specific variables + if env == "prod" { + mandatoryVars = append(mandatoryVars, prodVars...) } // Validate that all mandatory environment variables are set - missingVars := []string{} + var missingVars []string for _, key := range mandatoryVars { if os.Getenv(key) == "" { missingVars = append(missingVars, key) @@ -74,16 +77,17 @@ func main() { // Build the application configuration appConfig := config.Config{ - ProjectID: os.Getenv("PROJECT_ID"), - DripEnv: dripEnv, - SlackRegistryChannelWebhook: os.Getenv("SLACK_REGISTRY_CHANNEL_WEBHOOK"), - JWTSecret: os.Getenv("JWT_SECRET"), - SecretScannerURL: os.Getenv("SECRET_SCANNER_URL"), - DiscordSecurityChannelWebhook: os.Getenv("SECURITY_COUNCIL_DISCORD_WEBHOOK"), + ProjectID: os.Getenv("PROJECT_ID"), + DripEnv: dripEnv, + SlackRegistryChannelWebhook: os.Getenv("SLACK_REGISTRY_CHANNEL_WEBHOOK"), + JWTSecret: os.Getenv("JWT_SECRET"), + SecretScannerURL: os.Getenv("SECRET_SCANNER_URL"), + DiscordSecurityChannelWebhook: os.Getenv("SECURITY_COUNCIL_DISCORD_WEBHOOK"), DiscordSecurityPrivateChannelWebhook: os.Getenv("SECURITY_COUNCIL_DISCORD_PRIVATE_WEBHOOK"), - AlgoliaAppID: os.Getenv("ALGOLIA_APP_ID"), - AlgoliaAPIKey: os.Getenv("ALGOLIA_API_KEY"), - IDTokenAudience: os.Getenv("ID_TOKEN_AUDIENCE"), + AlgoliaAppID: os.Getenv("ALGOLIA_APP_ID"), + AlgoliaAPIKey: os.Getenv("ALGOLIA_API_KEY"), + IDTokenAudience: os.Getenv("ID_TOKEN_AUDIENCE"), + CloudStorageBucketName: os.Getenv("CLOUD_STORAGE_BUCKET_NAME"), } // Construct the database connection string @@ -106,7 +110,8 @@ func main() { // Run database migrations in local development to keep the schema up to date if dripEnv == "localdev" { log.Info().Msg("Running migrations for local development.") - if err := client.Schema.Create(context.Background(), migrate.WithDropIndex(true), migrate.WithDropColumn(true)); err != nil { + if err := client.Schema.Create(context.Background(), + migrate.WithDropIndex(true), migrate.WithDropColumn(true)); err != nil { log.Fatal().Err(err).Msg("Failed to create schema resources during migration.") } } @@ -122,7 +127,11 @@ func main() { }() // Initialize and start the server - server := server.NewServer(client, &appConfig) + registryServer, err := server.NewServer(client, &appConfig) + if err != nil { + log.Fatal().Err(err).Msg("Failed to initialize the server.") + } + log.Info().Msg("Starting the server.") - log.Fatal().Err(server.Start()).Msg("Server has stopped unexpectedly.") + log.Fatal().Err(registryServer.Start()).Msg("Server has stopped unexpectedly.") } diff --git a/run-service-prod.yaml b/run-service-prod.yaml index 7f47d3d..1a8f9c7 100644 --- a/run-service-prod.yaml +++ b/run-service-prod.yaml @@ -65,6 +65,8 @@ spec: name: PROD_ALGOLIA_API_KEY - name: ID_TOKEN_AUDIENCE value: https://api.comfy.org + - name: CLOUD_STORAGE_BUCKET_NAME + value: comfy-registry resources: limits: cpu: 4000m diff --git a/run-service-staging.yaml b/run-service-staging.yaml index cf8bed3..4755b87 100644 --- a/run-service-staging.yaml +++ b/run-service-staging.yaml @@ -44,6 +44,11 @@ spec: secretKeyRef: key: 1 name: SECURITY_COUNCIL_DISCORD_WEBHOOK + - name: SECURITY_COUNCIL_DISCORD_PRIVATE_WEBHOOK + valueFrom: + secretKeyRef: + key: 1 + name: SECURITY_COUNCIL_DISCORD_PRIVATE_WEBHOOK - name: ALGOLIA_APP_ID valueFrom: secretKeyRef: @@ -55,4 +60,6 @@ spec: key: 2 name: STAGING_ALGOLIA_API_KEY - name: ID_TOKEN_AUDIENCE - value: https://stagingapi.comfy.org \ No newline at end of file + value: https://stagingapi.comfy.org + - name: CLOUD_STORAGE_BUCKET_NAME + value: staging-comfy-registry diff --git a/server/implementation/api.implementation.go b/server/implementation/api.implementation.go index a29e162..4728233 100644 --- a/server/implementation/api.implementation.go +++ b/server/implementation/api.implementation.go @@ -20,7 +20,13 @@ type DripStrictServerImplementation struct { MixpanelService *mixpanel.ApiClient } -func NewStrictServerImplementation(client *ent.Client, config *config.Config, storageService storage.StorageService, slackService gateway.SlackService, discordService discord.DiscordService, algolia algolia.AlgoliaService) *DripStrictServerImplementation { +func NewStrictServerImplementation( + client *ent.Client, + config *config.Config, + storageService storage.StorageService, + slackService gateway.SlackService, + discordService discord.DiscordService, + algolia algolia.AlgoliaService) *DripStrictServerImplementation { return &DripStrictServerImplementation{ Client: client, ComfyCIService: dripservices_comfyci.NewComfyCIService(config), diff --git a/server/server.go b/server/server.go index 3c5c885..83d7789 100644 --- a/server/server.go +++ b/server/server.go @@ -7,7 +7,7 @@ import ( "registry-backend/ent" "registry-backend/gateways/algolia" "registry-backend/gateways/discord" - gateway "registry-backend/gateways/slack" + "registry-backend/gateways/slack" "registry-backend/gateways/storage" handler "registry-backend/server/handlers" "registry-backend/server/implementation" @@ -25,24 +25,72 @@ import ( "github.com/labstack/echo/v4" ) +type ServerDependencies struct { + StorageService storage.StorageService + SlackService slack.SlackService + AlgoliaService algolia.AlgoliaService + DiscordService discord.DiscordService + MonitoringClient monitoring.MetricClient +} + type Server struct { - Client *ent.Client - Config *config.Config + Client *ent.Client + Config *config.Config + Dependencies *ServerDependencies } -func NewServer(client *ent.Client, config *config.Config) *Server { +func NewServer(client *ent.Client, config *config.Config) (*Server, error) { + deps, err := initializeDependencies(config) + if err != nil { + return nil, err + } return &Server{ - Client: client, - Config: config, + Client: client, + Config: config, + Dependencies: deps, + }, nil +} + +func initializeDependencies(config *config.Config) (*ServerDependencies, error) { + storageService, err := storage.NewStorageService(config) + if err != nil { + log.Error().Err(err).Msg("Failed to initialize storage service") + return nil, err + } + + slackService := slack.NewSlackService(config) + + algoliaService, err := algolia.NewAlgoliaService(config) + if err != nil { + log.Error().Err(err).Msg("Failed to initialize Algolia service") + return nil, err + } + + discordService := discord.NewDiscordService(config) + + mon, err := monitoring.NewMetricClient(context.Background()) + if err != nil { + log.Error().Err(err).Msg("Failed to initialize monitoring client") + return nil, err } + + return &ServerDependencies{ + StorageService: storageService, + SlackService: slackService, + AlgoliaService: algoliaService, + DiscordService: discordService, + MonitoringClient: *mon, + }, nil } func (s *Server) Start() error { e := echo.New() e.HideBanner = true + + // Apply middleware e.Use(drip_middleware.TracingMiddleware) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"*"}, // This allows all origins + AllowOrigins: []string{"*"}, AllowMethods: []string{"*"}, AllowHeaders: []string{"*"}, AllowOriginFunc: func(origin string) (bool, error) { @@ -54,7 +102,6 @@ func (s *Server) Start() error { LogURI: true, LogStatus: true, LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { - // Ignore when url is path /vm/{sessionId} if strings.HasPrefix(c.Request().URL.Path, "/vm/") { return nil } @@ -66,47 +113,40 @@ func (s *Server) Start() error { }, })) - storageService, err := storage.NewGCPStorageService(context.Background()) - if err != nil { - return err - } - - slackService := gateway.NewSlackService(s.Config) - algoliaService, err := algolia.NewFromEnvOrNoop() - if err != nil { - return err - } - discordService := discord.NewDiscordService(s.Config) + // Attach implementation of the generated OAPI strict server + impl := implementation.NewStrictServerImplementation( + s.Client, s.Config, s.Dependencies.StorageService, s.Dependencies.SlackService, + s.Dependencies.DiscordService, s.Dependencies.AlgoliaService) - mon, err := monitoring.NewMetricClient(context.Background()) - if err != nil { - return err - } - - // Attach implementation of generated oapi strict server. - impl := implementation.NewStrictServerImplementation(s.Client, s.Config, storageService, slackService, discordService, algoliaService) - - // Define middlewares in the order of operations + // Define middleware for authorization authorizationManager := drip_authorization.NewAuthorizationManager(s.Client, impl.RegistryService) middlewares := []generated.StrictMiddlewareFunc{ authorizationManager.AuthorizationMiddleware(), } + + // Create the strict handler with middlewares wrapped := generated.NewStrictHandler(impl, middlewares) + // Register routes generated.RegisterHandlers(e, wrapped) + // Define public routes e.GET("/openapi", handler.SwaggerHandler) - e.GET("/health", func(c echo.Context) error { - return c.String(200, "OK") - }) + e.GET("/health", s.HealthCheckHandler) - // Global Middlewares - e.Use(drip_metric.MetricsMiddleware(mon, s.Config)) + // Apply global middlewares + e.Use(drip_metric.MetricsMiddleware(&s.Dependencies.MonitoringClient, s.Config)) e.Use(drip_authentication.FirebaseAuthMiddleware(s.Client)) e.Use(drip_authentication.ServiceAccountAuthMiddleware()) e.Use(drip_authentication.JWTAdminAuthMiddleware(s.Client, s.Config.JWTSecret)) e.Use(drip_middleware.ErrorLoggingMiddleware()) - e.Logger.Fatal(e.Start(":8080")) - return nil + // Start the server + return e.Start(":8080") +} + +// HealthCheckHandler performs health checks on the critical dependencies +func (s *Server) HealthCheckHandler(c echo.Context) error { + // This could be extended to check storage, slack, and other dependencies + return c.String(200, "OK") }