msp/runtime: split contract into JobContract and ServiceContract (#63494)

Splits the runtime contract into a JobContract and ServiceContract.
This lets better handle initialisation such as env vars which is
conditional depending on the contract type.
## Test plan

<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->
ci
This commit is contained in:
James Cotter 2024-06-26 20:46:10 +01:00 committed by GitHub
parent aae480a77b
commit ea9c45df8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 75 additions and 30 deletions

View File

@ -37,7 +37,7 @@ var _ runtime.Service[Config] = (*Service)(nil)
func (Service) Name() string { return "enterprise-portal" }
func (Service) Version() string { return version.Version() }
func (Service) Initialize(ctx context.Context, logger log.Logger, contract runtime.Contract, config Config) (background.Routine, error) {
func (Service) Initialize(ctx context.Context, logger log.Logger, contract runtime.ServiceContract, config Config) (background.Routine, error) {
// We use Sourcegraph tracing code, so explicitly configure a trace policy
policy.SetTracePolicy(policy.TraceAll)
@ -46,7 +46,7 @@ func (Service) Initialize(ctx context.Context, logger log.Logger, contract runti
return nil, errors.Wrap(err, "initialize Redis client")
}
dbHandle, err := database.NewHandle(ctx, logger, contract, redisClient, version.Version())
dbHandle, err := database.NewHandle(ctx, logger, contract.Contract, redisClient, version.Version())
if err != nil {
return nil, errors.Wrap(err, "initialize database handle")
}
@ -76,7 +76,7 @@ func (Service) Initialize(ctx context.Context, logger log.Logger, contract runti
return nil, errors.Wrap(err, "create Sourcegraph Accounts client")
}
iamClient, closeIAMClient, err := newIAMClient(ctx, logger, contract, redisClient)
iamClient, closeIAMClient, err := newIAMClient(ctx, logger, contract.Contract, redisClient)
if err != nil {
return nil, errors.Wrap(err, "initialize IAM client")
}

View File

@ -29,7 +29,7 @@ func (s Service) Version() string { return version.Version() }
func (s Service) Initialize(
ctx context.Context,
logger log.Logger,
contract runtime.Contract,
contract runtime.ServiceContract,
config Config,
) (background.Routine, error) {
logger.Info("starting service")
@ -42,7 +42,7 @@ func (s Service) Initialize(
if !config.StatelessMode {
var err error
if bq, err = bigquery.NewClient(ctx, contract); err != nil {
if bq, err = bigquery.NewClient(ctx, contract.Contract); err != nil {
return nil, errors.Wrap(err, "bigquery")
}
if err := bq.Write(ctx, "service.initialized"); err != nil {
@ -50,7 +50,7 @@ func (s Service) Initialize(
}
logger.Info("bigquery connection success")
if rd, err = redis.NewClient(ctx, contract); err != nil {
if rd, err = redis.NewClient(ctx, contract.Contract); err != nil {
return nil, errors.Wrap(err, "redis")
}
if err := rd.Ping(ctx); err != nil {
@ -58,18 +58,17 @@ func (s Service) Initialize(
}
logger.Info("redis connection success")
if pg, err = postgresql.NewClient(ctx, contract); err != nil {
if pg, err = postgresql.NewClient(ctx, contract.Contract); err != nil {
return nil, errors.Wrap(err, "postgresl")
}
if err := pg.Ping(ctx); err != nil {
return nil, errors.Wrap(err, "postgresql.Ping")
}
logger.Info("postgresql connection success")
}
h := http.NewServeMux()
if err := httpapi.Register(h, contract, config.HTTPAPI); err != nil {
if err := httpapi.Register(h, contract.Contract, config.HTTPAPI); err != nil {
return nil, errors.Wrap(err, "httpapi.Register")
}

View File

@ -29,7 +29,7 @@ var _ runtime.Service[Config] = (*Service)(nil)
func (Service) Name() string { return "pings" }
func (Service) Version() string { return version.Version() }
func (Service) Initialize(ctx context.Context, logger log.Logger, contract runtime.Contract, config Config) (background.Routine, error) {
func (Service) Initialize(ctx context.Context, logger log.Logger, contract runtime.ServiceContract, config Config) (background.Routine, error) {
pubsubClient, err := pubsub.NewTopicClient(config.PubSub.ProjectID, config.PubSub.TopicID)
if err != nil {
return nil, errors.Errorf("create Pub/Sub client: %v", err)

View File

@ -42,7 +42,7 @@ var _ runtime.Service[Config] = (*Service)(nil)
func (Service) Name() string { return "telemetry-gateway" }
func (Service) Version() string { return version.Version() }
func (Service) Initialize(ctx context.Context, logger log.Logger, contract runtime.Contract, config Config) (background.Routine, error) {
func (Service) Initialize(ctx context.Context, logger log.Logger, contract runtime.ServiceContract, config Config) (background.Routine, error) {
// We use Sourcegraph tracing code, so explicitly configure a trace policy
policy.SetTracePolicy(policy.TraceAll)

View File

@ -415,7 +415,7 @@ func main() {
type Service struct{}
// Initialize implements runtime.Service.
func (s Service) Initialize(ctx context.Context, logger log.Logger, contract runtime.Contract, config config.Config) (background.Routine, error) {
func (s Service) Initialize(ctx context.Context, logger log.Logger, contract runtime.ServiceContract, config config.Config) (background.Routine, error) {
logger.Info("config loaded from environment", log.Object("config", log.String("SlackChannel", config.SlackChannel), log.Bool("Production", config.Production)))
bqWriter, err := contract.BigQuery.GetTableWriter(context.Background(), "agent_status")

View File

@ -39,7 +39,7 @@ func (s Service) Version() string { return version.Version() }
func (s Service) Initialize(
ctx context.Context,
logger log.Logger,
contract runtime.Contract,
contract runtime.ServiceContract,
config Config,
) (background.Routine, error) {
logger.Info("starting service")

View File

@ -8,6 +8,14 @@ import (
// configuration.
type Contract = contract.Contract
// ServiceContract loads standardized MSP-provisioned (Managed Services Platform)
// configuration.
type ServiceContract = contract.ServiceContract
// JobContract loads standardized MSP-provisioned (Managed Services Platform)
// configuration.
type JobContract = contract.JobContract
// Env carries pre-parsed environment variables and variables requested and
// errors encountered.
type Env = contract.Env

View File

@ -65,10 +65,6 @@ type Contract struct {
// in. In local development, this should be 'unknown' if ENVIRONMENT_ID is
// not set.
EnvironmentID string
// Port is the port the service must listen on.
Port int
// ExternalDNSName is the DNS name the service uses, if one is configured.
ExternalDNSName *string
// RedisEndpoint is the full connection string of a MSP Redis instance if
// provisioned, including any prerequisite authentication.
@ -90,6 +86,23 @@ type Contract struct {
internal internalContract
}
// ServiceContract loads standardized MSP-provisioned (Managed Services Platform)
// configuration for a service.
type ServiceContract struct {
// Port is the port the service must listen on.
Port int
// ExternalDNSName is the DNS name the service uses, if one is configured.
ExternalDNSName *string
Contract
}
// JobContract loads standardized MSP-provisioned (Managed Services Platform)
// configuration for a job.
type JobContract struct {
Contract
}
type ServiceMetadataProvider interface {
// Name is the service name, typically the all-lowercase, dash-delimited,
// machine-friendly 'id' of the service in its corresponding MSP service
@ -112,9 +125,25 @@ type internalContract struct {
environmentID string
}
// New returns a new Contract instance from configuration parsed from the Env
// NewService returns a new Contract instance from configuration parsed from the Env
// instance. Values are expected per the 'MSP contract'.
func New(logger log.Logger, service ServiceMetadataProvider, env *Env) Contract {
func NewService(logger log.Logger, service ServiceMetadataProvider, env *Env) ServiceContract {
return ServiceContract{
Port: env.GetInt("PORT", "", "service port"),
ExternalDNSName: env.GetOptional("EXTERNAL_DNS_NAME", "external DNS name provisioned for the service"),
Contract: newBase(logger, service, env),
}
}
// NewJob returns a new Contract instance from configuration parsed from the Env
// instance. Values are expected per the 'MSP contract'.
func NewJob(logger log.Logger, service ServiceMetadataProvider, env *Env) JobContract {
return JobContract{
Contract: newBase(logger, service, env),
}
}
func newBase(logger log.Logger, service ServiceMetadataProvider, env *Env) Contract {
defaultGCPProjectID := pointers.Deref(env.GetOptional("GOOGLE_CLOUD_PROJECT", "GCP project ID"), "")
internal := internalContract{
logger: logger,
@ -124,11 +153,9 @@ func New(logger log.Logger, service ServiceMetadataProvider, env *Env) Contract
isMSP := env.GetBool("MSP", "false", "indicates if we are running in a MSP environment")
return Contract{
MSP: isMSP,
EnvironmentID: internal.environmentID,
Port: env.GetInt("PORT", "", "service port"),
ExternalDNSName: env.GetOptional("EXTERNAL_DNS_NAME", "external DNS name provisioned for the service"),
RedisEndpoint: env.GetOptional("REDIS_ENDPOINT", "full Redis address, including any prerequisite authentication"),
MSP: isMSP,
EnvironmentID: internal.environmentID,
RedisEndpoint: env.GetOptional("REDIS_ENDPOINT", "full Redis address, including any prerequisite authentication"),
PostgreSQL: loadPostgreSQLContract(env, isMSP),
BigQuery: loadBigQueryContract(env),

View File

@ -17,11 +17,11 @@ func (m mockServiceMetadata) Name() string { return "mock-name" }
func (m mockServiceMetadata) Version() string { return "mock-version" }
func TestNewContract(t *testing.T) {
t.Run("sanity check", func(t *testing.T) {
t.Run("service sanity check", func(t *testing.T) {
e, err := contract.ParseEnv([]string{"MSP=true"})
require.NoError(t, err)
c := contract.New(logtest.Scoped(t), mockServiceMetadata{}, e)
c := contract.NewService(logtest.Scoped(t), mockServiceMetadata{}, e)
assert.NotZero(t, c)
assert.True(t, c.MSP)
@ -30,5 +30,16 @@ func TestNewContract(t *testing.T) {
assert.Error(t, err)
})
t.Run("job sanity check", func(t *testing.T) {
e, err := contract.ParseEnv([]string{"MSP=true"})
require.NoError(t, err)
c := contract.NewJob(logtest.Scoped(t), mockServiceMetadata{}, e)
assert.NotZero(t, c)
assert.True(t, c.MSP)
// Not expected to error, as there are no required env vars.
err = e.Validate()
assert.NoError(t, err)
})
// TODO: Add more validation tests
}

View File

@ -19,7 +19,7 @@ type Job[ConfigT any] interface {
Execute(
ctx context.Context,
logger log.Logger,
contract Contract,
contract JobContract,
config ConfigT,
) error
}
@ -66,7 +66,7 @@ func ExecuteJob[
// Load configuration variables from environment
config.Load(env)
ctr := contract.New(log.Scoped("msp.contract"), job, env)
ctr := contract.NewJob(log.Scoped("msp.contract"), job, env)
// Fast-exit with configuration facts if requested
if *showHelp {

View File

@ -21,7 +21,7 @@ type Service[ConfigT any] interface {
Initialize(
ctx context.Context,
logger log.Logger,
contract Contract,
contract ServiceContract,
config ConfigT,
) (background.Routine, error)
}
@ -69,7 +69,7 @@ func Start[
// Load configuration variables from environment
config.Load(env)
ctr := contract.New(log.Scoped("msp.contract"), service, env)
ctr := contract.NewService(log.Scoped("msp.contract"), service, env)
// Fast-exit with configuration facts if requested
if *showHelp {