github.com/letsencrypt/boulder@v0.20251208.0/cmd/email-exporter/main.go (about)

     1  package notmain
     2  
     3  import (
     4  	"context"
     5  	"flag"
     6  	"os"
     7  
     8  	"github.com/jmhodges/clock"
     9  
    10  	"github.com/letsencrypt/boulder/cmd"
    11  	"github.com/letsencrypt/boulder/email"
    12  	emailpb "github.com/letsencrypt/boulder/email/proto"
    13  	bgrpc "github.com/letsencrypt/boulder/grpc"
    14  )
    15  
    16  // Config holds the configuration for the email-exporter service.
    17  type Config struct {
    18  	EmailExporter struct {
    19  		cmd.ServiceConfig
    20  
    21  		// PerDayLimit enforces the daily request limit imposed by the Pardot
    22  		// API. The total daily limit, which varies based on the Salesforce
    23  		// Pardot subscription tier, must be distributed among all
    24  		// email-exporter instances. For more information, see:
    25  		// https://developer.salesforce.com/docs/marketing/pardot/guide/overview.html?q=rate+limits#daily-requests-limits
    26  		PerDayLimit float64 `validate:"required,min=1"`
    27  
    28  		// MaxConcurrentRequests enforces the concurrent request limit imposed
    29  		// by the Pardot API. This limit must be distributed among all
    30  		// email-exporter instances and be proportional to each instance's
    31  		// PerDayLimit. For example, if the total daily limit is 50,000 and one
    32  		// instance is assigned 40% (20,000 requests), it should also receive
    33  		// 40% of the max concurrent requests (2 out of 5). For more
    34  		// information, see:
    35  		// https://developer.salesforce.com/docs/marketing/pardot/guide/overview.html?q=rate+limits#concurrent-requests
    36  		MaxConcurrentRequests int `validate:"required,min=1,max=5"`
    37  
    38  		// PardotBusinessUnit is the Pardot business unit to use.
    39  		PardotBusinessUnit string `validate:"required"`
    40  
    41  		// ClientId is the OAuth API client ID provided by Salesforce.
    42  		ClientId cmd.PasswordConfig
    43  
    44  		// ClientSecret is the OAuth API client secret provided by Salesforce.
    45  		ClientSecret cmd.PasswordConfig
    46  
    47  		// SalesforceBaseURL is the base URL for the Salesforce API. (e.g.,
    48  		// "https://company.salesforce.com")
    49  		SalesforceBaseURL string `validate:"required"`
    50  
    51  		// PardotBaseURL is the base URL for the Pardot API. (e.g.,
    52  		// "https://pi.pardot.com")
    53  		PardotBaseURL string `validate:"required"`
    54  
    55  		// EmailCacheSize controls how many hashed email addresses are retained
    56  		// in memory to prevent duplicates from being sent to the Pardot API.
    57  		// Each entry consumes ~120 bytes, so 100,000 entries uses around 12 MB
    58  		// of memory. If left unset, no caching is performed.
    59  		EmailCacheSize int `validate:"omitempty,min=1"`
    60  	}
    61  	Syslog        cmd.SyslogConfig
    62  	OpenTelemetry cmd.OpenTelemetryConfig
    63  }
    64  
    65  func main() {
    66  	configFile := flag.String("config", "", "Path to configuration file")
    67  	grpcAddr := flag.String("addr", "", "gRPC listen address override")
    68  	debugAddr := flag.String("debug-addr", "", "Debug server address override")
    69  	flag.Parse()
    70  
    71  	if *configFile == "" {
    72  		flag.Usage()
    73  		os.Exit(1)
    74  	}
    75  
    76  	var c Config
    77  	err := cmd.ReadConfigFile(*configFile, &c)
    78  	cmd.FailOnError(err, "Reading JSON config file into config structure")
    79  
    80  	if *grpcAddr != "" {
    81  		c.EmailExporter.ServiceConfig.GRPC.Address = *grpcAddr
    82  	}
    83  	if *debugAddr != "" {
    84  		c.EmailExporter.ServiceConfig.DebugAddr = *debugAddr
    85  	}
    86  
    87  	scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.EmailExporter.ServiceConfig.DebugAddr)
    88  	defer oTelShutdown(context.Background())
    89  
    90  	logger.Info(cmd.VersionString())
    91  
    92  	clk := clock.New()
    93  	clientId, err := c.EmailExporter.ClientId.Pass()
    94  	cmd.FailOnError(err, "Loading clientId")
    95  	clientSecret, err := c.EmailExporter.ClientSecret.Pass()
    96  	cmd.FailOnError(err, "Loading clientSecret")
    97  
    98  	var cache *email.EmailCache
    99  	if c.EmailExporter.EmailCacheSize > 0 {
   100  		cache = email.NewHashedEmailCache(c.EmailExporter.EmailCacheSize, scope)
   101  	}
   102  
   103  	sfClient, err := email.NewSalesforceClientImpl(
   104  		clk,
   105  		c.EmailExporter.PardotBusinessUnit,
   106  		clientId,
   107  		clientSecret,
   108  		c.EmailExporter.SalesforceBaseURL,
   109  		c.EmailExporter.PardotBaseURL,
   110  	)
   111  	cmd.FailOnError(err, "Creating Pardot API client")
   112  	exporterServer := email.NewExporterImpl(sfClient, cache, c.EmailExporter.PerDayLimit, c.EmailExporter.MaxConcurrentRequests, scope, logger)
   113  
   114  	tlsConfig, err := c.EmailExporter.TLS.Load(scope)
   115  	cmd.FailOnError(err, "Loading email-exporter TLS config")
   116  
   117  	daemonCtx, shutdownExporterServer := context.WithCancel(context.Background())
   118  	go exporterServer.Start(daemonCtx)
   119  
   120  	start, err := bgrpc.NewServer(c.EmailExporter.GRPC, logger).Add(
   121  		&emailpb.Exporter_ServiceDesc, exporterServer).Build(tlsConfig, scope, clk)
   122  	cmd.FailOnError(err, "Configuring email-exporter gRPC server")
   123  
   124  	err = start()
   125  	shutdownExporterServer()
   126  	exporterServer.Drain()
   127  	cmd.FailOnError(err, "email-exporter gRPC service failed to start")
   128  }
   129  
   130  func init() {
   131  	cmd.RegisterCommand("email-exporter", main, &cmd.ConfigValidator{Config: &Config{}})
   132  }