Repository: armadaproject/armada
Pull request #2647 (2023-07-06 16:37:50)

Make reports accessible when using armada-scheduler


┆Issue is synchronized with this [Jira Task](https://gr-oss.atlassian.net/browse/BATCH-434) by [Unito](https://www.unito.io)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/armadaproject/armada/pull/2647?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) Report
Patch coverage: **`100.00`**% and project coverage change: **`+0.74`** :tada:
> Comparison is base [(`f128481`)](https://app.codecov.io/gh/armadaproject/armada/commit/f1284816cc4a90bc1e03f5970485f6247f996053?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) 16.07% compared to head [(`8223064`)](https://app.codecov.io/gh/armadaproject/armada/pull/2647?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) 16.82%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2647      +/-   ##
==========================================
+ Coverage   16.07%   16.82%   +0.74%     
==========================================
  Files         159      160       +1     
  Lines       12490    12636     +146     
  Branches      470      509      +39     
==========================================
+ Hits         2008     2126     +118     
- Misses      10320    10342      +22     
- Partials      162      168       +6     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `16.82% <100.00%> (+0.74%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://app.codecov.io/gh/armadaproject/armada/pull/2647?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) | Coverage Δ | |
|---|---|---|
| [pkg/api/util.go](https://app.codecov.io/gh/armadaproject/armada/pull/2647?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-cGtnL2FwaS91dGlsLmdv) | `23.00% <100.00%> (ø)` | |

... and [6 files with indirect coverage changes](https://app.codecov.io/gh/armadaproject/armada/pull/2647/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/armadaproject/armada/pull/2647?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).


Patch:
diff --git a/config/armada/config.yaml b/config/armada/config.yaml
index fbd15ba5..d13f36e7 100644
--- a/config/armada/config.yaml
+++ b/config/armada/config.yaml
@@ -9,6 +9,8 @@ cancelJobsBatchSize: 1000
 pulsarSchedulerEnabled: false
 probabilityOfUsingPulsarScheduler: 0
 ignoreJobSubmitChecks: false
+schedulerApiConnection:
+  armadaUrl: "localhost:50052"
 grpc:
   keepaliveParams:
     maxConnectionIdle: 5m
diff --git a/config/scheduler/config.yaml b/config/scheduler/config.yaml
index 92b21b80..e77da47f 100644
--- a/config/scheduler/config.yaml
+++ b/config/scheduler/config.yaml
@@ -37,6 +37,8 @@ leader:
   renewDeadline: 10s
   retryPeriod: 2s
   podName: "" # This must be set so viper allows env vars to overwrite it
+  leaderConnection:
+    armadaUrl: "" # <name> will get replaced with the lease owners name
 http:
   port: 8080
 grpc:
diff --git a/deployment/scheduler/templates/scheduler-statefulset.yaml b/deployment/scheduler/templates/scheduler-statefulset.yaml
index b7d5bcc4..35201980 100644
--- a/deployment/scheduler/templates/scheduler-statefulset.yaml
+++ b/deployment/scheduler/templates/scheduler-statefulset.yaml
@@ -49,6 +49,10 @@ spec:
               valueFrom:
                 fieldRef:
                   fieldPath: metadata.namespace
+            {{- if  not (((.Values.scheduler.applicationConfig).leader).leaderConnection).armadaUrl }}
+            - name: ARMADA_LEADER_LEADERCONNECTION_ARMADAURL
+              value: "<name>.{{ include "armada-scheduler.name" . }}.{{ .Release.Namespace }}.svc.cluster.local"
+            {{- end }}
           {{- if .Values.scheduler.env }}
             {{- toYaml .Values.scheduler.env | nindent 12 -}}
           {{- end }}
diff --git a/internal/armada/configuration/types.go b/internal/armada/configuration/types.go
index 23fea24b..e9994f9f 100644
--- a/internal/armada/configuration/types.go
+++ b/internal/armada/configuration/types.go
@@ -12,6 +12,8 @@ import (
 	authconfig "github.com/armadaproject/armada/internal/common/auth/configuration"
 	grpcconfig "github.com/armadaproject/armada/internal/common/grpc/configuration"
 	armadaresource "github.com/armadaproject/armada/internal/common/resource"
+	"github.com/armadaproject/armada/internal/common/types"
+	"github.com/armadaproject/armada/pkg/client"
 )
 
 type ArmadaConfig struct {
@@ -27,6 +29,8 @@ type ArmadaConfig struct {
 
 	Grpc grpcconfig.GrpcConfig
 
+	SchedulerApiConnection client.ApiConnectionDetails
+
 	PriorityHalfTime                  time.Duration
 	CancelJobsBatchSize               int
 	Redis                             redis.UniversalOptions
@@ -243,7 +247,7 @@ type PreemptionConfig struct {
 	// Map from priority class names to priority classes.
 	// Must be consistent with Kubernetes priority classes.
 	// I.e., priority classes defined here must be defined in all executor clusters and should map to the same priority.
-	PriorityClasses map[string]PriorityClass
+	PriorityClasses map[string]types.PriorityClass
 	// Priority class assigned to pods that do not specify one.
 	// Must be an entry in PriorityClasses above.
 	DefaultPriorityClass string
@@ -251,23 +255,11 @@ type PreemptionConfig struct {
 	PriorityClassNameOverride *string
 }
 
-type PriorityClass struct {
-	Priority int32
-	// If true, Armada may preempt jobs of this class to improve fairness.
-	Preemptible bool
-	// Limits resources assigned to jobs of this priority class.
-	// Specifically, jobs of this priority class are only scheduled if doing so does not exceed this limit.
-	MaximumResourceFractionPerQueue map[string]float64
-	// Per-pool override of MaximumResourceFractionPerQueue.
-	// If missing for a particular pool, MaximumResourceFractionPerQueue is used instead for that pool.
-	MaximumResourceFractionPerQueueByPool map[string]map[string]float64
-}
-
 func (p PreemptionConfig) PriorityByPriorityClassName() map[string]int32 {
 	return PriorityByPriorityClassName(p.PriorityClasses)
 }
 
-func PriorityByPriorityClassName(priorityClasses map[string]PriorityClass) map[string]int32 {
+func PriorityByPriorityClassName(priorityClasses map[string]types.PriorityClass) map[string]int32 {
 	rv := make(map[string]int32, len(priorityClasses))
 	for name, pc := range priorityClasses {
 		rv[name] = pc.Priority
@@ -279,7 +271,7 @@ func (p PreemptionConfig) AllowedPriorities() []int32 {
 	return AllowedPriorities(p.PriorityClasses)
 }
 
-func AllowedPriorities(priorityClasses map[string]PriorityClass) []int32 {
+func AllowedPriorities(priorityClasses map[string]types.PriorityClass) []int32 {
 	rv := make([]int32, 0, len(priorityClasses))
 	for _, v := range priorityClasses {
 		rv = append(rv, v.Priority)
diff --git a/internal/armada/server.go b/internal/armada/server.go
index cb0daf34..a61f9402 100644
--- a/internal/armada/server.go
+++ b/internal/armada/server.go
@@ -14,6 +14,7 @@ import (
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/sync/errgroup"
+	"google.golang.org/grpc"
 
 	"github.com/armadaproject/armada/internal/armada/cache"
 	"github.com/armadaproject/armada/internal/armada/configuration"
@@ -35,6 +36,7 @@ import (
 	schedulerdb "github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/api"
+	"github.com/armadaproject/armada/pkg/client"
 )
 
 func Serve(ctx context.Context, config *configuration.ArmadaConfig, healthChecks *health.MultiChecker) error {
@@ -266,12 +268,23 @@ func Serve(ctx context.Context, config *configuration.ArmadaConfig, healthChecks
 		config.Pulsar.MaxAllowedMessageSize,
 		legacyExecutorRepo,
 	)
-	if schedulingContextRepository, err := scheduler.NewSchedulingContextRepository(
-		config.Scheduling.MaxJobSchedulingContextsPerExecutor,
-	); err != nil {
+
+	schedulingContextRepository, err := scheduler.NewSchedulingContextRepository(config.Scheduling.MaxJobSchedulingContextsPerExecutor)
+	if err != nil {
 		return err
+	}
+	aggregatedQueueServer.SchedulingContextRepository = schedulingContextRepository
+
+	var schedulingReportsServer schedulerobjects.SchedulerReportingServer
+	if config.PulsarSchedulerEnabled {
+		schedulerApiConnection, err := createApiConnection(config.SchedulerApiConnection)
+		if err != nil {
+			return errors.Wrapf(err, "error creating connection to scheduler api")
+		}
+		schedulerApiReportsClient := schedulerobjects.NewSchedulerReportingClient(schedulerApiConnection)
+		schedulingReportsServer = scheduler.NewProxyingSchedulingReportsServer(schedulerApiReportsClient)
 	} else {
-		aggregatedQueueServer.SchedulingContextRepository = schedulingContextRepository
+		schedulingReportsServer = schedulingContextRepository
 	}
 
 	eventServer := server.NewEventServer(
@@ -297,12 +310,7 @@ func Serve(ctx context.Context, config *configuration.ArmadaConfig, healthChecks
 	api.RegisterSubmitServer(grpcServer, submitServerToRegister)
 	api.RegisterUsageServer(grpcServer, usageServer)
 	api.RegisterEventServer(grpcServer, eventServer)
-	if aggregatedQueueServer.SchedulingContextRepository != nil {
-		schedulerobjects.RegisterSchedulerReportingServer(
-			grpcServer,
-			aggregatedQueueServer.SchedulingContextRepository,
-		)
-	}
+	schedulerobjects.RegisterSchedulerReportingServer(grpcServer, schedulingReportsServer)
 
 	api.RegisterAggregatedQueueServer(grpcServer, aggregatedQueueServer)
 	grpc_prometheus.Register(grpcServer)
@@ -351,3 +359,13 @@ func validatePreemptionConfig(config configuration.PreemptionConfig) error {
 
 	return nil
 }
+
+func createApiConnection(connectionDetails client.ApiConnectionDetails) (*grpc.ClientConn, error) {
+	grpc_prometheus.EnableClientHandlingTimeHistogram()
+	return client.CreateApiConnectionWithCallOptions(
+		&connectionDetails,
+		[]grpc.CallOption{},
+		grpc.WithChainUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor),
+		grpc.WithChainStreamInterceptor(grpc_prometheus.StreamClientInterceptor),
+	)
+}
diff --git a/internal/common/types/scheduling.go b/internal/common/types/scheduling.go
new file mode 100644
index 00000000..1805ca62
--- /dev/null
+++ b/internal/common/types/scheduling.go
@@ -0,0 +1,13 @@
+package types
+
+type PriorityClass struct {
+	Priority int32
+	// If true, Armada may preempt jobs of this class to improve fairness.
+	Preemptible bool
+	// Limits resources assigned to jobs of this priority class.
+	// Specifically, jobs of this priority class are only scheduled if doing so does not exceed this limit.
+	MaximumResourceFractionPerQueue map[string]float64
+	// Per-pool override of MaximumResourceFractionPerQueue.
+	// If missing for a particular pool, MaximumResourceFractionPerQueue is used instead for that pool.
+	MaximumResourceFractionPerQueueByPool map[string]map[string]float64
+}
diff --git a/internal/common/validation/podspec.go b/internal/common/validation/podspec.go
index 592cef38..d1c556a1 100644
--- a/internal/common/validation/podspec.go
+++ b/internal/common/validation/podspec.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/armadaerrors"
+	"github.com/armadaproject/armada/internal/common/types"
 )
 
 func ValidatePodSpec(spec *v1.PodSpec, schedulingConfig *configuration.SchedulingConfig) error {
@@ -160,7 +161,7 @@ func validatePorts(podSpec *v1.PodSpec) error {
 	return nil
 }
 
-func validatePodSpecPriorityClass(podSpec *v1.PodSpec, preemptionEnabled bool, allowedPriorityClasses map[string]configuration.PriorityClass) error {
+func validatePodSpecPriorityClass(podSpec *v1.PodSpec, preemptionEnabled bool, allowedPriorityClasses map[string]types.PriorityClass) error {
 	priorityClassName := podSpec.PriorityClassName
 	if priorityClassName != "" {
 		if !preemptionEnabled {
diff --git a/internal/scheduler/adapters/adapters.go b/internal/scheduler/adapters/adapters.go
index dd67ad3e..30d8bcf5 100644
--- a/internal/scheduler/adapters/adapters.go
+++ b/internal/scheduler/adapters/adapters.go
@@ -6,7 +6,8 @@ import (
 	"golang.org/x/exp/maps"
 	v1 "k8s.io/api/core/v1"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
+
 	"github.com/armadaproject/armada/internal/common/logging"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/api"
@@ -14,7 +15,7 @@ import (
 
 // PodRequirementsFromPod function creates the schedulerobjects and creates a value for the
 // annotation field by supplying it with a cloned value of pod.Annotations
-func PodRequirementsFromPod(pod *v1.Pod, priorityByPriorityClassName map[string]configuration.PriorityClass) *schedulerobjects.PodRequirements {
+func PodRequirementsFromPod(pod *v1.Pod, priorityByPriorityClassName map[string]types.PriorityClass) *schedulerobjects.PodRequirements {
 	rv := PodRequirementsFromPodSpec(&pod.Spec, priorityByPriorityClassName)
 	rv.Annotations = maps.Clone(pod.Annotations)
 	return rv
@@ -23,7 +24,7 @@ func PodRequirementsFromPod(pod *v1.Pod, priorityByPriorityClassName map[string]
 // PodRequirementsFromPodSpec function returns *schedulerobjects.PodRequirements for podSpec.
 // An error is logged if the podSpec uses an unknown priority class.
 // This function may mutate podSpec.
-func PodRequirementsFromPodSpec(podSpec *v1.PodSpec, priorityByPriorityClassName map[string]configuration.PriorityClass) *schedulerobjects.PodRequirements {
+func PodRequirementsFromPodSpec(podSpec *v1.PodSpec, priorityByPriorityClassName map[string]types.PriorityClass) *schedulerobjects.PodRequirements {
 	priority, ok := api.PriorityFromPodSpec(podSpec, priorityByPriorityClassName)
 	if priorityByPriorityClassName != nil && !ok {
 		// Ignore this error if priorityByPriorityClassName is explicitly set to nil.
diff --git a/internal/scheduler/configuration/configuration.go b/internal/scheduler/configuration/configuration.go
index d0faf931..2b8fc3f4 100644
--- a/internal/scheduler/configuration/configuration.go
+++ b/internal/scheduler/configuration/configuration.go
@@ -7,6 +7,7 @@ import (
 	authconfig "github.com/armadaproject/armada/internal/common/auth/configuration"
 	"github.com/armadaproject/armada/internal/common/config"
 	grpcconfig "github.com/armadaproject/armada/internal/common/grpc/configuration"
+	"github.com/armadaproject/armada/pkg/client"
 )
 
 const (
@@ -68,6 +69,8 @@ type LeaderConfig struct {
 	RenewDeadline time.Duration
 	// RetryPeriod is the duration the LeaderElector clients should waite between tries of actions.
 	RetryPeriod time.Duration
+	// Connection details to the leader
+	LeaderConnection client.ApiConnectionDetails
 }
 
 type HttpConfig struct {
diff --git a/internal/scheduler/context/context.go b/internal/scheduler/context/context.go
index 3fd61bfd..7f5899fd 100644
--- a/internal/scheduler/context/context.go
+++ b/internal/scheduler/context/context.go
@@ -16,6 +16,7 @@ import (
 	"github.com/armadaproject/armada/internal/common/armadaerrors"
 	armadamaps "github.com/armadaproject/armada/internal/common/maps"
 	armadaslices "github.com/armadaproject/armada/internal/common/slices"
+	"github.com/armadaproject/armada/internal/common/types"
 	schedulerconfig "github.com/armadaproject/armada/internal/scheduler/configuration"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
@@ -32,7 +33,7 @@ type SchedulingContext struct {
 	// Resource pool of this executor.
 	Pool string
 	// Allowed priority classes.
-	PriorityClasses map[string]configuration.PriorityClass
+	PriorityClasses map[string]types.PriorityClass
 	// Default priority class.
 	DefaultPriorityClass string
 	// Determines how fairness is computed.
@@ -75,7 +76,7 @@ type SchedulingContext struct {
 func NewSchedulingContext(
 	executorId string,
 	pool string,
-	priorityClasses map[string]configuration.PriorityClass,
+	priorityClasses map[string]types.PriorityClass,
 	defaultPriorityClass string,
 	resourceScarcity map[string]float64,
 	totalResources schedulerobjects.ResourceList,
@@ -644,7 +645,7 @@ func (jctx *JobSchedulingContext) IsSuccessful() bool {
 	return jctx.UnschedulableReason == ""
 }
 
-func JobSchedulingContextsFromJobs[J interfaces.LegacySchedulerJob](priorityClasses map[string]configuration.PriorityClass, jobs []J) []*JobSchedulingContext {
+func JobSchedulingContextsFromJobs[J interfaces.LegacySchedulerJob](priorityClasses map[string]types.PriorityClass, jobs []J) []*JobSchedulingContext {
 	jctxs := make([]*JobSchedulingContext, len(jobs))
 	timestamp := time.Now()
 	for i, job := range jobs {
diff --git a/internal/scheduler/interfaces/interfaces.go b/internal/scheduler/interfaces/interfaces.go
index d77432d4..2070f319 100644
--- a/internal/scheduler/interfaces/interfaces.go
+++ b/internal/scheduler/interfaces/interfaces.go
@@ -5,7 +5,7 @@ import (
 
 	v1 "k8s.io/api/core/v1"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 )
 
@@ -17,7 +17,7 @@ type LegacySchedulerJob interface {
 	GetPerQueuePriority() uint32
 	GetSubmitTime() time.Time
 	GetAnnotations() map[string]string
-	GetPodRequirements(priorityClasses map[string]configuration.PriorityClass) *schedulerobjects.PodRequirements
+	GetPodRequirements(priorityClasses map[string]types.PriorityClass) *schedulerobjects.PodRequirements
 	GetPriorityClassName() string
 	GetNodeSelector() map[string]string
 	GetAffinity() *v1.Affinity
diff --git a/internal/scheduler/jobdb/job.go b/internal/scheduler/jobdb/job.go
index b9ae76d2..5bac48da 100644
--- a/internal/scheduler/jobdb/job.go
+++ b/internal/scheduler/jobdb/job.go
@@ -7,7 +7,7 @@ import (
 	"golang.org/x/exp/maps"
 	v1 "k8s.io/api/core/v1"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 )
 
@@ -228,7 +228,7 @@ func (job *Job) PodRequirements() *schedulerobjects.PodRequirements {
 }
 
 // GetPodRequirements is needed for compatibility with interfaces.LegacySchedulerJob.
-func (job *Job) GetPodRequirements(_ map[string]configuration.PriorityClass) *schedulerobjects.PodRequirements {
+func (job *Job) GetPodRequirements(_ map[string]types.PriorityClass) *schedulerobjects.PodRequirements {
 	return job.PodRequirements()
 }
 
diff --git a/internal/scheduler/jobiteration.go b/internal/scheduler/jobiteration.go
index 202bc38b..910abe31 100644
--- a/internal/scheduler/jobiteration.go
+++ b/internal/scheduler/jobiteration.go
@@ -7,7 +7,7 @@ import (
 	"golang.org/x/exp/slices"
 	"golang.org/x/sync/errgroup"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 )
 
@@ -47,13 +47,13 @@ func (it *InMemoryJobIterator) Next() (interfaces.LegacySchedulerJob, error) {
 type InMemoryJobRepository struct {
 	jobsByQueue     map[string][]interfaces.LegacySchedulerJob
 	jobsById        map[string]interfaces.LegacySchedulerJob
-	priorityClasses map[string]configuration.PriorityClass
+	priorityClasses map[string]types.PriorityClass
 	// If true, jobs are sorted first by priority class priority.
 	// If false, priority class is ignored when ordering jobs.
 	sortByPriorityClass bool
 }
 
-func NewInMemoryJobRepository(priorityClasses map[string]configuration.PriorityClass) *InMemoryJobRepository {
+func NewInMemoryJobRepository(priorityClasses map[string]types.PriorityClass) *InMemoryJobRepository {
 	return &InMemoryJobRepository{
 		jobsByQueue:         make(map[string][]interfaces.LegacySchedulerJob),
 		jobsById:            make(map[string]interfaces.LegacySchedulerJob),
diff --git a/internal/scheduler/leader.go b/internal/scheduler/leader.go
index 9ccfae74..a0c8b8a8 100644
--- a/internal/scheduler/leader.go
+++ b/internal/scheduler/leader.go
@@ -2,6 +2,7 @@ package scheduler
 
 import (
 	"context"
+	"sync"
 	"sync/atomic"
 
 	"github.com/google/uuid"
@@ -23,6 +24,13 @@ type LeaderController interface {
 	ValidateToken(tok LeaderToken) bool
 	// Run starts the controller.  This is a blocking call which will return when the provided context is cancelled
 	Run(ctx context.Context) error
+	// GetLeaderReport returns a report about the current leader
+	GetLeaderReport() LeaderReport
+}
+
+type LeaderReport struct {
+	IsCurrentProcessLeader bool
+	LeaderName             string
 }
 
 // LeaderToken is a token handed out to schedulers which they can use to determine if they are leader
@@ -63,6 +71,13 @@ func (lc *StandaloneLeaderController) GetToken() LeaderToken {
 	return lc.token
 }
 
+func (lc *StandaloneLeaderController) GetLeaderReport() LeaderReport {
+	return LeaderReport{
+		LeaderName:             "standalone",
+		IsCurrentProcessLeader: true,
+	}
+}
+
 func (lc *StandaloneLeaderController) ValidateToken(tok LeaderToken) bool {
 	if tok.leader {
 		return lc.token.id == tok.id
@@ -87,24 +102,27 @@ type LeaseListener interface {
 //
 // TODO: Move into package in common.
 type KubernetesLeaderController struct {
-	client   coordinationv1client.LeasesGetter
-	token    atomic.Value
-	config   schedulerconfig.LeaderConfig // TODO: Move necessary config into this struct.
-	listener LeaseListener
+	client            coordinationv1client.LeasesGetter
+	token             atomic.Value
+	config            schedulerconfig.LeaderConfig // TODO: Move necessary config into this struct.
+	currentLeaderLock sync.Mutex
+	currentLeader     string
+	listeners         []LeaseListener
 }
 
 func NewKubernetesLeaderController(config schedulerconfig.LeaderConfig, client coordinationv1client.LeasesGetter) *KubernetesLeaderController {
 	controller := &KubernetesLeaderController{
-		client: client,
-		token:  atomic.Value{},
-		config: config,
+		client:            client,
+		token:             atomic.Value{},
+		currentLeaderLock: sync.Mutex{},
+		config:            config,
 	}
 	controller.token.Store(InvalidLeaderToken())
 	return controller
 }
 
 func (lc *KubernetesLeaderController) RegisterListener(listener LeaseListener) {
-	lc.listener = listener
+	lc.listeners = append(lc.listeners, listener)
 }
 
 func (lc *KubernetesLeaderController) GetToken() LeaderToken {
@@ -140,17 +158,22 @@ func (lc *KubernetesLeaderController) Run(ctx context.Context) error {
 					OnStartedLeading: func(c context.Context) {
 						log.Infof("I am now leader")
 						lc.token.Store(NewLeaderToken())
-						if lc.listener != nil {
-							lc.listener.onStartedLeading(ctx)
+						for _, listener := range lc.listeners {
+							listener.onStartedLeading(ctx)
 						}
 					},
 					OnStoppedLeading: func() {
 						log.Infof("I am no longer leader")
 						lc.token.Store(InvalidLeaderToken())
-						if lc.listener != nil {
-							lc.listener.onStoppedLeading()
+						for _, listener := range lc.listeners {
+							listener.onStoppedLeading()
 						}
 					},
+					OnNewLeader: func(identity string) {
+						lc.currentLeaderLock.Lock()
+						defer lc.currentLeaderLock.Unlock()
+						lc.currentLeader = identity
+					},
 				},
 			})
 			log.Infof("leader election round finished")
@@ -158,6 +181,15 @@ func (lc *KubernetesLeaderController) Run(ctx context.Context) error {
 	}
 }
 
+func (lc *KubernetesLeaderController) GetLeaderReport() LeaderReport {
+	lc.currentLeaderLock.Lock()
+	defer lc.currentLeaderLock.Unlock()
+	return LeaderReport{
+		LeaderName:             lc.currentLeader,
+		IsCurrentProcessLeader: lc.currentLeader == lc.config.PodName,
+	}
+}
+
 // getNewLock returns a resourcelock.LeaseLock which is the resource used for locking when attempting leader election
 func (lc *KubernetesLeaderController) getNewLock() *resourcelock.LeaseLock {
 	return &resourcelock.LeaseLock{
diff --git a/internal/scheduler/leader_client.go b/internal/scheduler/leader_client.go
new file mode 100644
index 00000000..b1929379
--- /dev/null
+++ b/internal/scheduler/leader_client.go
@@ -0,0 +1,80 @@
+package scheduler
+
+import (
+	"fmt"
+	"strings"
+	"sync"
+
+	grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
+	"github.com/pkg/errors"
+	"google.golang.org/grpc"
+
+	"github.com/armadaproject/armada/internal/scheduler/configuration"
+	"github.com/armadaproject/armada/pkg/client"
+)
+
+const leaseHolderNameToken = "<name>"
+
+type LeaderClientConnectionProvider interface {
+	GetCurrentLeaderClientConnection() (bool, *grpc.ClientConn, error)
+}
+
+type LeaderConnectionProvider struct {
+	leaderController LeaderController
+	leaderConfig     configuration.LeaderConfig
+	connectionLock   sync.Mutex
+	connectionByName map[string]*grpc.ClientConn
+}
+
+func NewLeaderConnectionProvider(leaderController LeaderController, leaderConfig configuration.LeaderConfig) *LeaderConnectionProvider {
+	return &LeaderConnectionProvider{
+		leaderController: leaderController,
+		leaderConfig:     leaderConfig,
+		connectionLock:   sync.Mutex{},
+		connectionByName: map[string]*grpc.ClientConn{},
+	}
+}
+
+func (l *LeaderConnectionProvider) GetCurrentLeaderClientConnection() (bool, *grpc.ClientConn, error) {
+	currentLeader := l.leaderController.GetLeaderReport()
+
+	if currentLeader.IsCurrentProcessLeader {
+		return true, nil, nil
+	}
+	if currentLeader.LeaderName == "" {
+		return false, nil, fmt.Errorf("no leader found to retrieve scheduling reports from")
+	}
+
+	leaderClient, err := l.getClientByName(currentLeader.LeaderName)
+	return false, leaderClient, err
+}
+
+func (l *LeaderConnectionProvider) getClientByName(currentLeaderName string) (*grpc.ClientConn, error) {
+	l.connectionLock.Lock()
+	defer l.connectionLock.Unlock()
+
+	if leaderClient, present := l.connectionByName[currentLeaderName]; present {
+		return leaderClient, nil
+	}
+
+	leaderConnectionDetails := l.leaderConfig.LeaderConnection
+	leaderConnectionDetails.ArmadaUrl = strings.ReplaceAll(leaderConnectionDetails.ArmadaUrl, leaseHolderNameToken, currentLeaderName)
+
+	apiConnection, err := createApiConnection(leaderConnectionDetails)
+	if err != nil {
+		return nil, errors.Wrapf(err, "error creating connection to leader")
+	}
+
+	l.connectionByName[currentLeaderName] = apiConnection
+	return apiConnection, nil
+}
+
+func createApiConnection(connectionDetails client.ApiConnectionDetails) (*grpc.ClientConn, error) {
+	grpc_prometheus.EnableClientHandlingTimeHistogram()
+	return client.CreateApiConnectionWithCallOptions(
+		&connectionDetails,
+		[]grpc.CallOption{},
+		grpc.WithChainUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor),
+		grpc.WithChainStreamInterceptor(grpc_prometheus.StreamClientInterceptor),
+	)
+}
diff --git a/internal/scheduler/leader_proxying_reports_server.go b/internal/scheduler/leader_proxying_reports_server.go
new file mode 100644
index 00000000..38c4761c
--- /dev/null
+++ b/internal/scheduler/leader_proxying_reports_server.go
@@ -0,0 +1,72 @@
+package scheduler
+
+import (
+	"context"
+
+	"google.golang.org/grpc"
+
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+type LeaderProxyingSchedulingReportsServer struct {
+	localReportsServer               schedulerobjects.SchedulerReportingServer
+	leaderClientProvider             LeaderClientConnectionProvider
+	schedulerReportingClientProvider reportingClientProvider
+}
+
+func NewLeaderProxyingSchedulingReportsServer(
+	schedulingReportsRepository schedulerobjects.SchedulerReportingServer,
+	leaderClientProvider LeaderClientConnectionProvider,
+) *LeaderProxyingSchedulingReportsServer {
+	return &LeaderProxyingSchedulingReportsServer{
+		leaderClientProvider:             leaderClientProvider,
+		localReportsServer:               schedulingReportsRepository,
+		schedulerReportingClientProvider: &schedulerReportingClientProvider{},
+	}
+}
+
+func (s *LeaderProxyingSchedulingReportsServer) GetSchedulingReport(ctx context.Context, request *schedulerobjects.SchedulingReportRequest) (*schedulerobjects.SchedulingReport, error) {
+	isCurrentProcessLeader, leaderConnection, err := s.leaderClientProvider.GetCurrentLeaderClientConnection()
+	if isCurrentProcessLeader {
+		return s.localReportsServer.GetSchedulingReport(ctx, request)
+	}
+	if err != nil {
+		return nil, err
+	}
+	leaderClient := s.schedulerReportingClientProvider.GetSchedulerReportingClient(leaderConnection)
+	return leaderClient.GetSchedulingReport(ctx, request)
+}
+
+func (s *LeaderProxyingSchedulingReportsServer) GetQueueReport(ctx context.Context, request *schedulerobjects.QueueReportRequest) (*schedulerobjects.QueueReport, error) {
+	isCurrentProcessLeader, leaderConnection, err := s.leaderClientProvider.GetCurrentLeaderClientConnection()
+	if isCurrentProcessLeader {
+		return s.localReportsServer.GetQueueReport(ctx, request)
+	}
+	if err != nil {
+		return nil, err
+	}
+	leaderClient := s.schedulerReportingClientProvider.GetSchedulerReportingClient(leaderConnection)
+	return leaderClient.GetQueueReport(ctx, request)
+}
+
+func (s *LeaderProxyingSchedulingReportsServer) GetJobReport(ctx context.Context, request *schedulerobjects.JobReportRequest) (*schedulerobjects.JobReport, error) {
+	isCurrentProcessLeader, leaderConnection, err := s.leaderClientProvider.GetCurrentLeaderClientConnection()
+	if isCurrentProcessLeader {
+		return s.localReportsServer.GetJobReport(ctx, request)
+	}
+	if err != nil {
+		return nil, err
+	}
+	leaderClient := s.schedulerReportingClientProvider.GetSchedulerReportingClient(leaderConnection)
+	return leaderClient.GetJobReport(ctx, request)
+}
+
+type reportingClientProvider interface {
+	GetSchedulerReportingClient(conn *grpc.ClientConn) schedulerobjects.SchedulerReportingClient
+}
+
+type schedulerReportingClientProvider struct{}
+
+func (s *schedulerReportingClientProvider) GetSchedulerReportingClient(conn *grpc.ClientConn) schedulerobjects.SchedulerReportingClient {
+	return schedulerobjects.NewSchedulerReportingClient(conn)
+}
diff --git a/internal/scheduler/nodedb/nodedb.go b/internal/scheduler/nodedb/nodedb.go
index 1dbbb71b..45ff4ead 100644
--- a/internal/scheduler/nodedb/nodedb.go
+++ b/internal/scheduler/nodedb/nodedb.go
@@ -18,6 +18,7 @@ import (
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/armadaerrors"
 	armadamaps "github.com/armadaproject/armada/internal/common/maps"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/common/util"
 	schedulerconfig "github.com/armadaproject/armada/internal/scheduler/configuration"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
@@ -210,7 +211,7 @@ type NodeDb struct {
 	// Allowed priority classes.
 	// Because the number of database indices scales linearly with the number of distinct priorities,
 	// the efficiency of the NodeDb relies on the number of distinct priorities being small.
-	priorityClasses map[string]configuration.PriorityClass
+	priorityClasses map[string]types.PriorityClass
 	// Priorities, in increasing order, to try to schedule pods at.
 	// In particular, if a pod has priority class priority p, try to schedule that pod at priority
 	// prioritiesToTryAssigningAt[0], ..., prioritiesToTryAssigningAt[i],
@@ -265,7 +266,7 @@ type NodeDb struct {
 }
 
 func NewNodeDb(
-	priorityClasses map[string]configuration.PriorityClass,
+	priorityClasses map[string]types.PriorityClass,
 	maxExtraNodesToConsider uint,
 	indexedResources []configuration.IndexedResource,
 	indexedTaints,
@@ -664,7 +665,7 @@ func (nodeDb *NodeDb) selectNodeForPodWithIt(
 }
 
 // bindJobToNode returns a copy of node with job bound to it.
-func bindJobToNode(priorityClasses map[string]configuration.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) (*Node, error) {
+func bindJobToNode(priorityClasses map[string]types.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) (*Node, error) {
 	node = node.UnsafeCopy()
 	if err := bindJobToNodeInPlace(priorityClasses, job, node); err != nil {
 		return nil, err
@@ -673,7 +674,7 @@ func bindJobToNode(priorityClasses map[string]configuration.PriorityClass, job i
 }
 
 // bindJobToNodeInPlace is like bindJobToNode, but doesn't make a copy of node.
-func bindJobToNodeInPlace(priorityClasses map[string]configuration.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) error {
+func bindJobToNodeInPlace(priorityClasses map[string]types.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) error {
 	jobId := job.GetId()
 	requests := job.GetResourceRequirements().Requests
 
@@ -720,7 +721,7 @@ func bindJobToNodeInPlace(priorityClasses map[string]configuration.PriorityClass
 //     the jobs' priorities to evictedPriority; they are not subtracted from AllocatedByJobId and
 //     AllocatedByQueue.
 func EvictJobsFromNode(
-	priorityClasses map[string]configuration.PriorityClass,
+	priorityClasses map[string]types.PriorityClass,
 	jobFilter func(interfaces.LegacySchedulerJob) bool,
 	jobs []interfaces.LegacySchedulerJob,
 	node *Node,
@@ -740,7 +741,7 @@ func EvictJobsFromNode(
 }
 
 // evictJobFromNodeInPlace is the in-place operation backing EvictJobsFromNode.
-func evictJobFromNodeInPlace(priorityClasses map[string]configuration.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) error {
+func evictJobFromNodeInPlace(priorityClasses map[string]types.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) error {
 	jobId := job.GetId()
 	if _, ok := node.AllocatedByJobId[jobId]; !ok {
 		return errors.Errorf("job %s has no resources allocated on node %s", jobId, node.Id)
@@ -769,7 +770,7 @@ func evictJobFromNodeInPlace(priorityClasses map[string]configuration.PriorityCl
 }
 
 // UnbindJobsFromNode returns a node with all elements of jobs unbound from it.
-func UnbindJobsFromNode(priorityClasses map[string]configuration.PriorityClass, jobs []interfaces.LegacySchedulerJob, node *Node) (*Node, error) {
+func UnbindJobsFromNode(priorityClasses map[string]types.PriorityClass, jobs []interfaces.LegacySchedulerJob, node *Node) (*Node, error) {
 	node = node.UnsafeCopy()
 	for _, job := range jobs {
 		if err := unbindJobFromNodeInPlace(priorityClasses, job, node); err != nil {
@@ -780,7 +781,7 @@ func UnbindJobsFromNode(priorityClasses map[string]configuration.PriorityClass,
 }
 
 // UnbindJobFromNode returns a copy of node with job unbound from it.
-func UnbindJobFromNode(priorityClasses map[string]configuration.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) (*Node, error) {
+func UnbindJobFromNode(priorityClasses map[string]types.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) (*Node, error) {
 	node = node.UnsafeCopy()
 	if err := unbindJobFromNodeInPlace(priorityClasses, job, node); err != nil {
 		return nil, err
@@ -789,7 +790,7 @@ func UnbindJobFromNode(priorityClasses map[string]configuration.PriorityClass, j
 }
 
 // unbindPodFromNodeInPlace is like UnbindJobFromNode, but doesn't make a copy of node.
-func unbindJobFromNodeInPlace(priorityClasses map[string]configuration.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) error {
+func unbindJobFromNodeInPlace(priorityClasses map[string]types.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) error {
 	jobId := job.GetId()
 	requests := job.GetResourceRequirements().Requests
 
diff --git a/internal/scheduler/pool_assigner.go b/internal/scheduler/pool_assigner.go
index 29d16e4d..66e2ff99 100644
--- a/internal/scheduler/pool_assigner.go
+++ b/internal/scheduler/pool_assigner.go
@@ -10,6 +10,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/clock"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
 	"github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
@@ -31,7 +32,7 @@ type executor struct {
 
 type DefaultPoolAssigner struct {
 	executorTimeout        time.Duration
-	priorityClasses        map[string]configuration.PriorityClass
+	priorityClasses        map[string]types.PriorityClass
 	priorities             []int32
 	indexedResources       []configuration.IndexedResource
 	indexedTaints          []string
diff --git a/internal/scheduler/preempting_queue_scheduler.go b/internal/scheduler/preempting_queue_scheduler.go
index 7b58d355..8e50409e 100644
--- a/internal/scheduler/preempting_queue_scheduler.go
+++ b/internal/scheduler/preempting_queue_scheduler.go
@@ -16,6 +16,7 @@ import (
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	armadamaps "github.com/armadaproject/armada/internal/common/maps"
 	armadaslices "github.com/armadaproject/armada/internal/common/slices"
+	"github.com/armadaproject/armada/internal/common/types"
 	schedulerconfig "github.com/armadaproject/armada/internal/scheduler/configuration"
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
@@ -652,7 +653,7 @@ func (sch *PreemptingQueueScheduler) assertions(
 
 type Evictor struct {
 	jobRepo         JobRepository
-	priorityClasses map[string]configuration.PriorityClass
+	priorityClasses map[string]types.PriorityClass
 	nodeFilter      func(context.Context, *nodedb.Node) bool
 	jobFilter       func(context.Context, interfaces.LegacySchedulerJob) bool
 	postEvictFunc   func(context.Context, interfaces.LegacySchedulerJob, *nodedb.Node)
@@ -669,7 +670,7 @@ type EvictorResult struct {
 
 func NewNodeEvictor(
 	jobRepo JobRepository,
-	priorityClasses map[string]configuration.PriorityClass,
+	priorityClasses map[string]types.PriorityClass,
 	perNodeEvictionProbability float64,
 	jobFilter func(context.Context, interfaces.LegacySchedulerJob) bool,
 	random *rand.Rand,
@@ -695,7 +696,7 @@ func NewNodeEvictor(
 // on nodes for which nodeIdsToEvict[nodeId] is true.
 func NewFilteredEvictor(
 	jobRepo JobRepository,
-	priorityClasses map[string]configuration.PriorityClass,
+	priorityClasses map[string]types.PriorityClass,
 	nodeIdsToEvict map[string]bool,
 	jobIdsToEvict map[string]bool,
 ) *Evictor {
@@ -722,7 +723,7 @@ func NewFilteredEvictor(
 // with probability perNodeEvictionProbability.
 func NewOversubscribedEvictor(
 	jobRepo JobRepository,
-	priorityClasses map[string]configuration.PriorityClass,
+	priorityClasses map[string]types.PriorityClass,
 	defaultPriorityClass string,
 	perNodeEvictionProbability float64,
 	random *rand.Rand,
diff --git a/internal/scheduler/proxying_reports_server.go b/internal/scheduler/proxying_reports_server.go
new file mode 100644
index 00000000..187a8baa
--- /dev/null
+++ b/internal/scheduler/proxying_reports_server.go
@@ -0,0 +1,47 @@
+package scheduler
+
+import (
+	"context"
+	"time"
+
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+type ProxyingSchedulingReportsServer struct {
+	client schedulerobjects.SchedulerReportingClient
+}
+
+func NewProxyingSchedulingReportsServer(client schedulerobjects.SchedulerReportingClient) *ProxyingSchedulingReportsServer {
+	return &ProxyingSchedulingReportsServer{
+		client: client,
+	}
+}
+
+func (s *ProxyingSchedulingReportsServer) GetSchedulingReport(ctx context.Context, request *schedulerobjects.SchedulingReportRequest) (*schedulerobjects.SchedulingReport, error) {
+	ctx, cancel := reduceTimeout(ctx)
+	defer cancel()
+	return s.client.GetSchedulingReport(ctx, request)
+}
+
+func (s *ProxyingSchedulingReportsServer) GetQueueReport(ctx context.Context, request *schedulerobjects.QueueReportRequest) (*schedulerobjects.QueueReport, error) {
+	ctx, cancel := reduceTimeout(ctx)
+	defer cancel()
+	return s.client.GetQueueReport(ctx, request)
+}
+
+func (s *ProxyingSchedulingReportsServer) GetJobReport(ctx context.Context, request *schedulerobjects.JobReportRequest) (*schedulerobjects.JobReport, error) {
+	ctx, cancel := reduceTimeout(ctx)
+	defer cancel()
+	return s.client.GetJobReport(ctx, request)
+}
+
+// We reduce the context deadline here, to prevent our call and the caller who called us from timing out at the same time
+// This should mean our caller gets the real error message rather than a generic timeout error from client side
+func reduceTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
+	deadline, ok := ctx.Deadline()
+	if !ok {
+		return context.WithCancel(ctx)
+	}
+	deadline = deadline.Add(-time.Second)
+	return context.WithDeadline(ctx, deadline)
+}
diff --git a/internal/scheduler/schedulerapp.go b/internal/scheduler/schedulerapp.go
index 090edc93..bcdd896f 100644
--- a/internal/scheduler/schedulerapp.go
+++ b/internal/scheduler/schedulerapp.go
@@ -29,6 +29,7 @@ import (
 	"github.com/armadaproject/armada/internal/common/stringinterner"
 	schedulerconfig "github.com/armadaproject/armada/internal/scheduler/configuration"
 	"github.com/armadaproject/armada/internal/scheduler/database"
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/executorapi"
 )
 
@@ -172,8 +173,23 @@ func Run(config schedulerconfig.Configuration) error {
 	if err != nil {
 		return errors.WithMessage(err, "error creating submit checker")
 	}
-	// TODO(reports): Pass in a non-nil SchedulingContextRepository.
-	schedulingAlgo, err := NewFairSchedulingAlgo(config.Scheduling, config.MaxSchedulingDuration, executorRepository, queueRepository, nil)
+
+	schedulingContextRepository, err := NewSchedulingContextRepository(config.Scheduling.MaxJobSchedulingContextsPerExecutor)
+	if err != nil {
+		return errors.WithMessage(err, "error creating scheduling context repository")
+	}
+
+	leaderClientConnectionProvider := NewLeaderConnectionProvider(leaderController, config.Leader)
+	schedulingReportServer := NewLeaderProxyingSchedulingReportsServer(schedulingContextRepository, leaderClientConnectionProvider)
+	schedulerobjects.RegisterSchedulerReportingServer(grpcServer, schedulingReportServer)
+
+	schedulingAlgo, err := NewFairSchedulingAlgo(
+		config.Scheduling,
+		config.MaxSchedulingDuration,
+		executorRepository,
+		queueRepository,
+		schedulingContextRepository,
+	)
 	if err != nil {
 		return errors.WithMessage(err, "error creating scheduling algo")
 	}
diff --git a/internal/scheduler/scheduling_algo.go b/internal/scheduler/scheduling_algo.go
index 0ad7039b..5a29d9bc 100644
--- a/internal/scheduler/scheduling_algo.go
+++ b/internal/scheduler/scheduling_algo.go
@@ -17,6 +17,7 @@ import (
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/logging"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/common/util"
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
@@ -41,7 +42,7 @@ type FairSchedulingAlgo struct {
 	executorRepository          database.ExecutorRepository
 	queueRepository             database.QueueRepository
 	schedulingContextRepository *SchedulingContextRepository
-	priorityClasses             map[string]configuration.PriorityClass
+	priorityClasses             map[string]types.PriorityClass
 	indexedResources            []configuration.IndexedResource
 	rand                        *rand.Rand // injected here for repeatable testing
 	previousScheduleClusterId   string
@@ -432,7 +433,7 @@ func (repo *schedulerJobRepositoryAdapter) GetExistingJobsByIds(ids []string) ([
 }
 
 // constructNodeDb constructs a node db with all jobs bound to it.
-func (l *FairSchedulingAlgo) constructNodeDb(priorityClasses map[string]configuration.PriorityClass, jobs []*jobdb.Job, nodes []*schedulerobjects.Node) (*nodedb.NodeDb, error) {
+func (l *FairSchedulingAlgo) constructNodeDb(priorityClasses map[string]types.PriorityClass, jobs []*jobdb.Job, nodes []*schedulerobjects.Node) (*nodedb.NodeDb, error) {
 	nodeDb, err := nodedb.NewNodeDb(
 		priorityClasses,
 		l.config.MaxExtraNodesToConsider,
diff --git a/internal/scheduler/submitcheck.go b/internal/scheduler/submitcheck.go
index 77acce6a..220a7103 100644
--- a/internal/scheduler/submitcheck.go
+++ b/internal/scheduler/submitcheck.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	armadaslices "github.com/armadaproject/armada/internal/common/slices"
+	"github.com/armadaproject/armada/internal/common/types"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
 	"github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
@@ -43,7 +44,7 @@ type SubmitScheduleChecker interface {
 
 type SubmitChecker struct {
 	executorTimeout           time.Duration
-	priorityClasses           map[string]configuration.PriorityClass
+	priorityClasses           map[string]types.PriorityClass
 	gangIdAnnotation          string
 	executorById              map[string]minimalExecutor
 	priorities                []int32
diff --git a/internal/scheduleringester/config.go b/internal/scheduleringester/config.go
index a8e05ce9..062910ac 100644
--- a/internal/scheduleringester/config.go
+++ b/internal/scheduleringester/config.go
@@ -4,6 +4,7 @@ import (
 	"time"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 )
 
 type Configuration struct {
@@ -14,7 +15,7 @@ type Configuration struct {
 	// General Pulsar configuration
 	Pulsar configuration.PulsarConfig
 	// Map of allowed priority classes by name
-	PriorityClasses map[string]configuration.PriorityClass
+	PriorityClasses map[string]types.PriorityClass
 	// Pulsar subscription name
 	SubscriptionName string
 	// Number of messages that will be batched together before being inserted into the database
diff --git a/internal/scheduleringester/instructions.go b/internal/scheduleringester/instructions.go
index fa0e70c0..e44d580c 100644
--- a/internal/scheduleringester/instructions.go
+++ b/internal/scheduleringester/instructions.go
@@ -9,11 +9,11 @@ import (
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/exp/slices"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/compress"
 	"github.com/armadaproject/armada/internal/common/ingest"
 	"github.com/armadaproject/armada/internal/common/ingest/metrics"
 	protoutil "github.com/armadaproject/armada/internal/common/proto"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/scheduler/adapters"
 	schedulerdb "github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
@@ -29,13 +29,13 @@ type eventSequenceCommon struct {
 
 type InstructionConverter struct {
 	metrics         *metrics.Metrics
-	priorityClasses map[string]configuration.PriorityClass
+	priorityClasses map[string]types.PriorityClass
 	compressor      compress.Compressor
 }
 
 func NewInstructionConverter(
 	metrics *metrics.Metrics,
-	priorityClasses map[string]configuration.PriorityClass,
+	priorityClasses map[string]types.PriorityClass,
 	compressor compress.Compressor,
 ) ingest.InstructionConverter[*DbOperationsWithMessageIds] {
 	return &InstructionConverter{
diff --git a/pkg/api/util.go b/pkg/api/util.go
index 96fc8485..16f4e9ab 100644
--- a/pkg/api/util.go
+++ b/pkg/api/util.go
@@ -11,10 +11,10 @@ import (
 	"golang.org/x/exp/maps"
 	v1 "k8s.io/api/core/v1"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/armadaerrors"
 	"github.com/armadaproject/armada/internal/common/logging"
 	armadaresource "github.com/armadaproject/armada/internal/common/resource"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 )
 
@@ -117,7 +117,7 @@ func (job *Job) GetSubmitTime() time.Time {
 	return job.Created
 }
 
-func (job *Job) GetPodRequirements(priorityClasses map[string]configuration.PriorityClass) *schedulerobjects.PodRequirements {
+func (job *Job) GetPodRequirements(priorityClasses map[string]types.PriorityClass) *schedulerobjects.PodRequirements {
 	podSpec := job.GetMainPodSpec()
 
 	priority, ok := PriorityFromPodSpec(podSpec, priorityClasses)
@@ -193,7 +193,7 @@ func SchedulingResourceRequirementsFromPodSpec(podSpec *v1.PodSpec) v1.ResourceR
 // In both cases the value along with true boolean is returned.
 // PriorityClassName in priorityByPriorityClassName map.
 // If no priority is set for the pod spec, 0 along with a false boolean would be returned
-func PriorityFromPodSpec(podSpec *v1.PodSpec, priorityClasses map[string]configuration.PriorityClass) (int32, bool) {
+func PriorityFromPodSpec(podSpec *v1.PodSpec, priorityClasses map[string]types.PriorityClass) (int32, bool) {
 	// If there's no podspec there's nothing we can do
 	if podSpec == nil {
 		return 0, false


Test patch:
diff --git a/internal/armada/server/submit_test.go b/internal/armada/server/submit_test.go
index 151181a5..71be6a8e 100644
--- a/internal/armada/server/submit_test.go
+++ b/internal/armada/server/submit_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/armadaproject/armada/internal/common/auth/authorization"
 	"github.com/armadaproject/armada/internal/common/auth/permission"
 	armadaresource "github.com/armadaproject/armada/internal/common/resource"
+	schedulertypes "github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/common/util"
 	"github.com/armadaproject/armada/pkg/api"
 	"github.com/armadaproject/armada/pkg/client/queue"
@@ -1637,7 +1638,7 @@ func withSubmitServerAndRepos(action func(s *SubmitServer, jobRepo repository.Jo
 		MaxPodSpecSizeBytes: 65535,
 		Preemption: configuration.PreemptionConfig{
 			DefaultPriorityClass: "high",
-			PriorityClasses:      map[string]configuration.PriorityClass{"high": {0, false, nil, nil}},
+			PriorityClasses:      map[string]schedulertypes.PriorityClass{"high": {0, false, nil, nil}},
 		},
 		MinTerminationGracePeriod: time.Duration(30 * time.Second),
 		MaxTerminationGracePeriod: time.Duration(300 * time.Second),
diff --git a/internal/common/ingest/testfixtures/event.go b/internal/common/ingest/testfixtures/event.go
index 561e2673..d4bda96d 100644
--- a/internal/common/ingest/testfixtures/event.go
+++ b/internal/common/ingest/testfixtures/event.go
@@ -8,8 +8,8 @@ import (
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/eventutil"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/internal/scheduler/testfixtures"
 	"github.com/armadaproject/armada/pkg/armadaevents"
@@ -31,7 +31,7 @@ var (
 	PartitionMarkerGroupIdUuid  = armadaevents.UuidFromProtoUuid(PartitionMarkerGroupIdProto)
 	PriorityClassName           = "test-priority"
 	PriorityClassValue          = int32(100)
-	PriorityClasses             = map[string]configuration.PriorityClass{PriorityClassName: {Priority: PriorityClassValue}}
+	PriorityClasses             = map[string]types.PriorityClass{PriorityClassName: {Priority: PriorityClassValue}}
 	Groups                      = []string{"group1", "group2"}
 	NodeSelector                = map[string]string{"foo": "bar"}
 	Affinity                    = &v1.Affinity{
diff --git a/internal/common/validation/podspec_test.go b/internal/common/validation/podspec_test.go
index d0bfc66b..bffa1f27 100644
--- a/internal/common/validation/podspec_test.go
+++ b/internal/common/validation/podspec_test.go
@@ -10,6 +10,7 @@ import (
 	"k8s.io/utils/pointer"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 )
 
 func Test_ValidatePodSpec_checkForMissingValues(t *testing.T) {
@@ -61,7 +62,7 @@ func Test_ValidatePodSpec_terminationGracePeriod(t *testing.T) {
 	schedulingConfig := &configuration.SchedulingConfig{
 		Preemption: configuration.PreemptionConfig{
 			DefaultPriorityClass: "high",
-			PriorityClasses:      map[string]configuration.PriorityClass{"high": {Priority: 0}},
+			PriorityClasses:      map[string]types.PriorityClass{"high": {Priority: 0}},
 		},
 		MinTerminationGracePeriod: time.Duration(30 * time.Second),
 		MaxTerminationGracePeriod: time.Duration(300 * time.Second),
@@ -286,7 +287,7 @@ func minimalValidPodSpec() *v1.PodSpec {
 
 func Test_ValidatePodSpecPriorityClass(t *testing.T) {
 	validPriorityClass := &v1.PodSpec{PriorityClassName: "some-priority-class"}
-	allowedPriorityClasses := map[string]configuration.PriorityClass{"some-priority-class": {Priority: 10}}
+	allowedPriorityClasses := map[string]types.PriorityClass{"some-priority-class": {Priority: 10}}
 	assert.NoError(
 		t,
 		validatePodSpecPriorityClass(validPriorityClass, true, allowedPriorityClasses),
diff --git a/internal/scheduler/adapters/adapters_test.go b/internal/scheduler/adapters/adapters_test.go
index 943e6680..93b57637 100644
--- a/internal/scheduler/adapters/adapters_test.go
+++ b/internal/scheduler/adapters/adapters_test.go
@@ -5,19 +5,19 @@ import (
 	"os"
 	"testing"
 
-	"github.com/stretchr/testify/require"
-
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 )
 
 var (
-	priorityByPriorityClassName = map[string]configuration.PriorityClass{
+	priorityByPriorityClassName = map[string]types.PriorityClass{
 		"priority-0": {0, true, nil, nil},
 		"priority-1": {1, true, nil, nil},
 		"priority-2": {2, true, nil, nil},
@@ -61,7 +61,7 @@ func TestPodRequirementsFromPodSpecPriorityByPriorityClassName(t *testing.T) {
 	tests := []struct {
 		name                        string
 		podspec                     v1.PodSpec
-		priorityByPriorityClassName map[string]configuration.PriorityClass
+		priorityByPriorityClassName map[string]types.PriorityClass
 		loggedError                 bool
 		priority                    int32
 	}{
diff --git a/internal/scheduler/common_test.go b/internal/scheduler/common_test.go
index 73aae7be..89c15bad 100644
--- a/internal/scheduler/common_test.go
+++ b/internal/scheduler/common_test.go
@@ -9,6 +9,7 @@ import (
 	"k8s.io/apimachinery/pkg/api/resource"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/common/util"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/api"
@@ -68,7 +69,7 @@ func TestGetPodRequirements(t *testing.T) {
 			configuration.GangCardinalityAnnotation: "1",
 		},
 	}
-	actual := j.GetPodRequirements(map[string]configuration.PriorityClass{"armada-default": {Priority: int32(1)}})
+	actual := j.GetPodRequirements(map[string]types.PriorityClass{"armada-default": {Priority: int32(1)}})
 	assert.Equal(t, expected, actual)
 }
 
diff --git a/internal/scheduler/leader_client_test.go b/internal/scheduler/leader_client_test.go
new file mode 100644
index 00000000..e8909356
--- /dev/null
+++ b/internal/scheduler/leader_client_test.go
@@ -0,0 +1,103 @@
+package scheduler
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/armadaproject/armada/internal/scheduler/configuration"
+	"github.com/armadaproject/armada/pkg/client"
+)
+
+const currentProcessPodName = "current-process-name"
+
+var defaultLeaderConfig = configuration.LeaderConfig{
+	PodName: currentProcessPodName,
+	LeaderConnection: client.ApiConnectionDetails{
+		ArmadaUrl: "localhost:50052",
+	},
+}
+
+var templatedLeader = configuration.LeaderConfig{
+	PodName: currentProcessPodName,
+	LeaderConnection: client.ApiConnectionDetails{
+		ArmadaUrl: "<name>.localhost:50052",
+	},
+}
+
+func TestGetCurrentLeaderClientConnection(t *testing.T) {
+	leaderController := &FakeLeaderController{}
+	clientProvider := NewLeaderConnectionProvider(leaderController, defaultLeaderConfig)
+	leaderController.LeaderName = "new-leader"
+
+	isCurrentProcessLeader, result, err := clientProvider.GetCurrentLeaderClientConnection()
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.False(t, isCurrentProcessLeader)
+	assert.Equal(t, result.Target(), defaultLeaderConfig.LeaderConnection.ArmadaUrl)
+}
+
+func TestGetCurrentLeaderClientConnection_WithTemplatedConnection(t *testing.T) {
+	leaderController := &FakeLeaderController{}
+	clientProvider := NewLeaderConnectionProvider(leaderController, templatedLeader)
+
+	leaderController.LeaderName = "new-leader"
+	isCurrentProcessLeader, result, err := clientProvider.GetCurrentLeaderClientConnection()
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.False(t, isCurrentProcessLeader)
+	assert.Equal(t, result.Target(), "new-leader.localhost:50052")
+
+	leaderController.LeaderName = "new-leader-2"
+	isCurrentProcessLeader, result, err = clientProvider.GetCurrentLeaderClientConnection()
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.False(t, isCurrentProcessLeader)
+	assert.Equal(t, result.Target(), "new-leader-2.localhost:50052")
+}
+
+func TestGetCurrentLeaderClientConnection_NoLeader(t *testing.T) {
+	leaderController := &FakeLeaderController{}
+	clientProvider := NewLeaderConnectionProvider(leaderController, defaultLeaderConfig)
+
+	isCurrentProcessLeader, result, err := clientProvider.GetCurrentLeaderClientConnection()
+	assert.Nil(t, result)
+	assert.Error(t, err)
+	assert.False(t, isCurrentProcessLeader)
+}
+
+func TestGetCurrentLeaderClientConnection_OnCurrentProcessIsLeader(t *testing.T) {
+	leaderController := &FakeLeaderController{}
+	clientProvider := NewLeaderConnectionProvider(leaderController, defaultLeaderConfig)
+	leaderController.IsCurrentlyLeader = true
+
+	isCurrentProcessLeader, result, err := clientProvider.GetCurrentLeaderClientConnection()
+	assert.Nil(t, result)
+	assert.NoError(t, err)
+	assert.True(t, isCurrentProcessLeader)
+}
+
+type FakeLeaderController struct {
+	IsCurrentlyLeader bool
+	LeaderName        string
+}
+
+func (f *FakeLeaderController) GetToken() LeaderToken {
+	return NewLeaderToken()
+}
+
+func (f *FakeLeaderController) ValidateToken(tok LeaderToken) bool {
+	return f.IsCurrentlyLeader
+}
+
+func (f *FakeLeaderController) Run(ctx context.Context) error {
+	return nil
+}
+
+func (f *FakeLeaderController) GetLeaderReport() LeaderReport {
+	return LeaderReport{
+		LeaderName:             f.LeaderName,
+		IsCurrentProcessLeader: f.IsCurrentlyLeader,
+	}
+}
diff --git a/internal/scheduler/leader_proxying_reports_server_test.go b/internal/scheduler/leader_proxying_reports_server_test.go
new file mode 100644
index 00000000..5fc1874d
--- /dev/null
+++ b/internal/scheduler/leader_proxying_reports_server_test.go
@@ -0,0 +1,332 @@
+package scheduler
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc"
+
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+func TestLeaderProxyingSchedulingReportsServer_GetJobReports(t *testing.T) {
+	tests := map[string]struct {
+		err                          error
+		isCurrentProcessLeader       bool
+		expectedNumReportServerCalls int
+		expectedNumReportClientCalls int
+	}{
+		// Should send all requests to local reports server when leader
+		"current process leader": {
+			err:                          nil,
+			isCurrentProcessLeader:       true,
+			expectedNumReportServerCalls: 1,
+			expectedNumReportClientCalls: 0,
+		},
+		"current process leader return error": {
+			err:                          fmt.Errorf("error"),
+			isCurrentProcessLeader:       true,
+			expectedNumReportServerCalls: 1,
+			expectedNumReportClientCalls: 0,
+		},
+		// Should send all requests to remote server when not leader
+		"remote process is leader": {
+			err:                          nil,
+			isCurrentProcessLeader:       false,
+			expectedNumReportServerCalls: 0,
+			expectedNumReportClientCalls: 1,
+		},
+		"remote process is leader return error": {
+			err:                          fmt.Errorf("error"),
+			isCurrentProcessLeader:       false,
+			expectedNumReportServerCalls: 0,
+			expectedNumReportClientCalls: 1,
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+
+			sut, clientProvider, jobReportsServer, jobReportsClient := setupLeaderProxyingSchedulerReportsServerTest(t)
+			clientProvider.IsCurrentProcessLeader = tc.isCurrentProcessLeader
+
+			request := &schedulerobjects.JobReportRequest{JobId: "job-1"}
+
+			expectedResult := &schedulerobjects.JobReport{Report: "report"}
+
+			if tc.err == nil {
+				expectedResult = nil
+			}
+
+			jobReportsServer.GetJobReportResponse = expectedResult
+			jobReportsServer.Err = tc.err
+			jobReportsClient.GetJobReportResponse = expectedResult
+			jobReportsClient.Err = tc.err
+
+			result, err := sut.GetJobReport(ctx, request)
+
+			assert.Equal(t, tc.err, err)
+			assert.Equal(t, expectedResult, result)
+			assert.Len(t, jobReportsServer.GetJobReportCalls, tc.expectedNumReportServerCalls)
+			assert.Len(t, jobReportsClient.GetJobReportCalls, tc.expectedNumReportClientCalls)
+		})
+	}
+}
+
+func TestLeaderProxyingSchedulingReportsServer_GetSchedulingReport(t *testing.T) {
+	tests := map[string]struct {
+		err                          error
+		isCurrentProcessLeader       bool
+		expectedNumReportServerCalls int
+		expectedNumReportClientCalls int
+	}{
+		// Should send all requests to local reports server when leader
+		"current process leader": {
+			err:                          nil,
+			isCurrentProcessLeader:       true,
+			expectedNumReportServerCalls: 1,
+			expectedNumReportClientCalls: 0,
+		},
+		"current process leader return error": {
+			err:                          fmt.Errorf("error"),
+			isCurrentProcessLeader:       true,
+			expectedNumReportServerCalls: 1,
+			expectedNumReportClientCalls: 0,
+		},
+		// Should send all requests to remote server when not leader
+		"remote process is leader": {
+			err:                          nil,
+			isCurrentProcessLeader:       false,
+			expectedNumReportServerCalls: 0,
+			expectedNumReportClientCalls: 1,
+		},
+		"remote process is leader return error": {
+			err:                          fmt.Errorf("error"),
+			isCurrentProcessLeader:       false,
+			expectedNumReportServerCalls: 0,
+			expectedNumReportClientCalls: 1,
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+
+			sut, clientProvider, jobReportsServer, jobReportsClient := setupLeaderProxyingSchedulerReportsServerTest(t)
+			clientProvider.IsCurrentProcessLeader = tc.isCurrentProcessLeader
+
+			request := &schedulerobjects.SchedulingReportRequest{Verbosity: int32(1)}
+
+			expectedResult := &schedulerobjects.SchedulingReport{Report: "report"}
+
+			if tc.err == nil {
+				expectedResult = nil
+			}
+
+			jobReportsServer.GetSchedulingReportResponse = expectedResult
+			jobReportsServer.Err = tc.err
+			jobReportsClient.GetSchedulingReportResponse = expectedResult
+			jobReportsClient.Err = tc.err
+
+			result, err := sut.GetSchedulingReport(ctx, request)
+
+			assert.Equal(t, tc.err, err)
+			assert.Equal(t, expectedResult, result)
+			assert.Len(t, jobReportsServer.GetSchedulingReportCalls, tc.expectedNumReportServerCalls)
+			assert.Len(t, jobReportsClient.GetSchedulingReportCalls, tc.expectedNumReportClientCalls)
+		})
+	}
+}
+
+func TestLeaderProxyingSchedulingReportsServer_GetQueueReport(t *testing.T) {
+	tests := map[string]struct {
+		err                          error
+		isCurrentProcessLeader       bool
+		expectedNumReportServerCalls int
+		expectedNumReportClientCalls int
+	}{
+		// Should send all requests to local reports server when leader
+		"current process leader": {
+			err:                          nil,
+			isCurrentProcessLeader:       true,
+			expectedNumReportServerCalls: 1,
+			expectedNumReportClientCalls: 0,
+		},
+		"current process leader return error": {
+			err:                          fmt.Errorf("error"),
+			isCurrentProcessLeader:       true,
+			expectedNumReportServerCalls: 1,
+			expectedNumReportClientCalls: 0,
+		},
+		// Should send all requests to remote server when not leader
+		"remote process is leader": {
+			err:                          nil,
+			isCurrentProcessLeader:       false,
+			expectedNumReportServerCalls: 0,
+			expectedNumReportClientCalls: 1,
+		},
+		"remote process is leader return error": {
+			err:                          fmt.Errorf("error"),
+			isCurrentProcessLeader:       false,
+			expectedNumReportServerCalls: 0,
+			expectedNumReportClientCalls: 1,
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+
+			sut, clientProvider, jobReportsServer, jobReportsClient := setupLeaderProxyingSchedulerReportsServerTest(t)
+			clientProvider.IsCurrentProcessLeader = tc.isCurrentProcessLeader
+
+			request := &schedulerobjects.QueueReportRequest{Verbosity: int32(1), QueueName: "queue-1"}
+
+			expectedResult := &schedulerobjects.QueueReport{Report: "report"}
+
+			if tc.err == nil {
+				expectedResult = nil
+			}
+
+			jobReportsServer.GetQueueReportResponse = expectedResult
+			jobReportsServer.Err = tc.err
+			jobReportsClient.GetQueueReportResponse = expectedResult
+			jobReportsClient.Err = tc.err
+
+			result, err := sut.GetQueueReport(ctx, request)
+
+			assert.Equal(t, tc.err, err)
+			assert.Equal(t, expectedResult, result)
+			assert.Len(t, jobReportsServer.GetQueueReportCalls, tc.expectedNumReportServerCalls)
+			assert.Len(t, jobReportsClient.GetQueueReportCalls, tc.expectedNumReportClientCalls)
+		})
+	}
+}
+
+func setupLeaderProxyingSchedulerReportsServerTest(t *testing.T) (*LeaderProxyingSchedulingReportsServer, *FakeClientProvider, *FakeSchedulerReportingServer, *FakeSchedulerReportingClient) {
+	jobReportsServer := NewFakeSchedulerReportingServer()
+	jobReportsClient := NewFakeSchedulerReportingClient()
+	clientProvider := NewFakeClientProvider()
+
+	reportsServer := NewLeaderProxyingSchedulingReportsServer(jobReportsServer, clientProvider)
+
+	schedulingReportsClientProvider := NewFakeSchedulerReportingClientProvider()
+	schedulingReportsClientProvider.Client = jobReportsClient
+	reportsServer.schedulerReportingClientProvider = schedulingReportsClientProvider
+
+	return reportsServer, clientProvider, jobReportsServer, jobReportsClient
+}
+
+type GetSchedulingReportCall struct {
+	Context context.Context
+	Request *schedulerobjects.SchedulingReportRequest
+}
+
+type GetQueueReportCall struct {
+	Context context.Context
+	Request *schedulerobjects.QueueReportRequest
+}
+
+type GetJobReportCall struct {
+	Context context.Context
+	Request *schedulerobjects.JobReportRequest
+}
+
+type FakeSchedulerReportingServer struct {
+	GetSchedulingReportCalls    []GetSchedulingReportCall
+	GetSchedulingReportResponse *schedulerobjects.SchedulingReport
+
+	GetQueueReportCalls    []GetQueueReportCall
+	GetQueueReportResponse *schedulerobjects.QueueReport
+
+	GetJobReportCalls    []GetJobReportCall
+	GetJobReportResponse *schedulerobjects.JobReport
+	Err                  error
+}
+
+func NewFakeSchedulerReportingServer() *FakeSchedulerReportingServer {
+	return &FakeSchedulerReportingServer{
+		GetSchedulingReportCalls: []GetSchedulingReportCall{},
+		GetQueueReportCalls:      []GetQueueReportCall{},
+		GetJobReportCalls:        []GetJobReportCall{},
+	}
+}
+
+func (f *FakeSchedulerReportingServer) GetSchedulingReport(ctx context.Context, request *schedulerobjects.SchedulingReportRequest) (*schedulerobjects.SchedulingReport, error) {
+	f.GetSchedulingReportCalls = append(f.GetSchedulingReportCalls, GetSchedulingReportCall{Context: ctx, Request: request})
+	return f.GetSchedulingReportResponse, f.Err
+}
+
+func (f *FakeSchedulerReportingServer) GetQueueReport(ctx context.Context, request *schedulerobjects.QueueReportRequest) (*schedulerobjects.QueueReport, error) {
+	f.GetQueueReportCalls = append(f.GetQueueReportCalls, GetQueueReportCall{Context: ctx, Request: request})
+	return f.GetQueueReportResponse, f.Err
+}
+
+func (f *FakeSchedulerReportingServer) GetJobReport(ctx context.Context, request *schedulerobjects.JobReportRequest) (*schedulerobjects.JobReport, error) {
+	f.GetJobReportCalls = append(f.GetJobReportCalls, GetJobReportCall{Context: ctx, Request: request})
+	return f.GetJobReportResponse, f.Err
+}
+
+type FakeSchedulerReportingClient struct {
+	GetSchedulingReportCalls    []GetSchedulingReportCall
+	GetSchedulingReportResponse *schedulerobjects.SchedulingReport
+
+	GetQueueReportCalls    []GetQueueReportCall
+	GetQueueReportResponse *schedulerobjects.QueueReport
+
+	GetJobReportCalls    []GetJobReportCall
+	GetJobReportResponse *schedulerobjects.JobReport
+	Err                  error
+}
+
+func NewFakeSchedulerReportingClient() *FakeSchedulerReportingClient {
+	return &FakeSchedulerReportingClient{
+		GetSchedulingReportCalls: []GetSchedulingReportCall{},
+		GetQueueReportCalls:      []GetQueueReportCall{},
+		GetJobReportCalls:        []GetJobReportCall{},
+	}
+}
+
+func (f *FakeSchedulerReportingClient) GetSchedulingReport(ctx context.Context, request *schedulerobjects.SchedulingReportRequest, opts ...grpc.CallOption) (*schedulerobjects.SchedulingReport, error) {
+	f.GetSchedulingReportCalls = append(f.GetSchedulingReportCalls, GetSchedulingReportCall{Context: ctx, Request: request})
+	return f.GetSchedulingReportResponse, f.Err
+}
+
+func (f *FakeSchedulerReportingClient) GetQueueReport(ctx context.Context, request *schedulerobjects.QueueReportRequest, opts ...grpc.CallOption) (*schedulerobjects.QueueReport, error) {
+	f.GetQueueReportCalls = append(f.GetQueueReportCalls, GetQueueReportCall{Context: ctx, Request: request})
+	return f.GetQueueReportResponse, f.Err
+}
+
+func (f *FakeSchedulerReportingClient) GetJobReport(ctx context.Context, request *schedulerobjects.JobReportRequest, opts ...grpc.CallOption) (*schedulerobjects.JobReport, error) {
+	f.GetJobReportCalls = append(f.GetJobReportCalls, GetJobReportCall{Context: ctx, Request: request})
+	return f.GetJobReportResponse, f.Err
+}
+
+type FakeClientProvider struct {
+	Error                  error
+	IsCurrentProcessLeader bool
+}
+
+func NewFakeClientProvider() *FakeClientProvider {
+	return &FakeClientProvider{}
+}
+
+func (f *FakeClientProvider) GetCurrentLeaderClientConnection() (bool, *grpc.ClientConn, error) {
+	return f.IsCurrentProcessLeader, &grpc.ClientConn{}, f.Error
+}
+
+type FakeSchedulerReportingClientProvider struct {
+	Client schedulerobjects.SchedulerReportingClient
+}
+
+func NewFakeSchedulerReportingClientProvider() *FakeSchedulerReportingClientProvider {
+	return &FakeSchedulerReportingClientProvider{}
+}
+
+func (f *FakeSchedulerReportingClientProvider) GetSchedulerReportingClient(conn *grpc.ClientConn) schedulerobjects.SchedulerReportingClient {
+	return f.Client
+}
diff --git a/internal/scheduler/leader_test.go b/internal/scheduler/leader_test.go
index 63ac0042..1790c951 100644
--- a/internal/scheduler/leader_test.go
+++ b/internal/scheduler/leader_test.go
@@ -107,7 +107,7 @@ func TestK8sLeaderController_BecomingLeader(t *testing.T) {
 			// Run the test
 			controller := NewKubernetesLeaderController(testLeaderConfig(), client)
 			testListener := NewTestLeaseListener(controller)
-			controller.listener = testListener
+			controller.RegisterListener(testListener)
 			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 			go func() {
 				err := controller.Run(ctx)
diff --git a/internal/scheduler/proxying_reports_server_test.go b/internal/scheduler/proxying_reports_server_test.go
new file mode 100644
index 00000000..0dc81b54
--- /dev/null
+++ b/internal/scheduler/proxying_reports_server_test.go
@@ -0,0 +1,132 @@
+package scheduler
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+func TestProxyingSchedulingReportsServer_GetJobReports(t *testing.T) {
+	tests := map[string]struct {
+		err error
+	}{
+		"no error": {
+			err: nil,
+		},
+		"on error": {
+			err: fmt.Errorf("error"),
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+
+			sut, jobReportsClient := setupProxyingSchedulerReportsServerTest(t)
+
+			request := &schedulerobjects.JobReportRequest{JobId: "job-1"}
+
+			expectedResult := &schedulerobjects.JobReport{Report: "report"}
+
+			if tc.err == nil {
+				expectedResult = nil
+			}
+
+			jobReportsClient.GetJobReportResponse = expectedResult
+			jobReportsClient.Err = tc.err
+
+			result, err := sut.GetJobReport(ctx, request)
+
+			assert.Equal(t, tc.err, err)
+			assert.Equal(t, expectedResult, result)
+			assert.Len(t, jobReportsClient.GetJobReportCalls, 1)
+		})
+	}
+}
+
+func TestProxyingSchedulingReportsServer_GetSchedulingReport(t *testing.T) {
+	tests := map[string]struct {
+		err error
+	}{
+		"no error": {
+			err: nil,
+		},
+		"on error": {
+			err: fmt.Errorf("error"),
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+
+			sut, jobReportsClient := setupProxyingSchedulerReportsServerTest(t)
+
+			request := &schedulerobjects.SchedulingReportRequest{Verbosity: int32(1)}
+
+			expectedResult := &schedulerobjects.SchedulingReport{Report: "report"}
+
+			if tc.err == nil {
+				expectedResult = nil
+			}
+
+			jobReportsClient.GetSchedulingReportResponse = expectedResult
+			jobReportsClient.Err = tc.err
+
+			result, err := sut.GetSchedulingReport(ctx, request)
+
+			assert.Equal(t, tc.err, err)
+			assert.Equal(t, expectedResult, result)
+			assert.Len(t, jobReportsClient.GetSchedulingReportCalls, 1)
+		})
+	}
+}
+
+func TestProxyingSchedulingReportsServer_GetQueueReport(t *testing.T) {
+	tests := map[string]struct {
+		err error
+	}{
+		"no error": {
+			err: nil,
+		},
+		"on error": {
+			err: fmt.Errorf("error"),
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+
+			sut, jobReportsClient := setupProxyingSchedulerReportsServerTest(t)
+
+			request := &schedulerobjects.QueueReportRequest{Verbosity: int32(1), QueueName: "queue-1"}
+
+			expectedResult := &schedulerobjects.QueueReport{Report: "report"}
+
+			if tc.err == nil {
+				expectedResult = nil
+			}
+
+			jobReportsClient.GetQueueReportResponse = expectedResult
+			jobReportsClient.Err = tc.err
+
+			result, err := sut.GetQueueReport(ctx, request)
+
+			assert.Equal(t, tc.err, err)
+			assert.Equal(t, expectedResult, result)
+			assert.Len(t, jobReportsClient.GetQueueReportCalls, 1)
+		})
+	}
+}
+
+func setupProxyingSchedulerReportsServerTest(t *testing.T) (*ProxyingSchedulingReportsServer, *FakeSchedulerReportingClient) {
+	schedulerReportsClient := NewFakeSchedulerReportingClient()
+	sut := NewProxyingSchedulingReportsServer(schedulerReportsClient)
+	return sut, schedulerReportsClient
+}
diff --git a/internal/scheduler/testfixtures/testfixtures.go b/internal/scheduler/testfixtures/testfixtures.go
index 6396103c..06cae2cc 100644
--- a/internal/scheduler/testfixtures/testfixtures.go
+++ b/internal/scheduler/testfixtures/testfixtures.go
@@ -17,6 +17,7 @@ import (
 	"k8s.io/apimachinery/pkg/api/resource"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/common/util"
 	"github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
@@ -37,7 +38,7 @@ const (
 
 var (
 	BaseTime, _         = time.Parse("2006-01-02T15:04:05.000Z", "2022-03-01T15:04:05.000Z")
-	TestPriorityClasses = map[string]configuration.PriorityClass{
+	TestPriorityClasses = map[string]types.PriorityClass{
 		PriorityClass0:               {Priority: 0, Preemptible: true},
 		PriorityClass1:               {Priority: 1, Preemptible: true},
 		PriorityClass2:               {Priority: 2, Preemptible: true},
@@ -144,7 +145,7 @@ func WithPerPriorityLimitsConfig(limits map[string]map[string]float64, config co
 			panic(fmt.Sprintf("no priority class with name %s", priorityClassName))
 		}
 		// We need to make a copy to avoid mutating the priorityClasses, which are used by other tests too.
-		config.Preemption.PriorityClasses[priorityClassName] = configuration.PriorityClass{
+		config.Preemption.PriorityClasses[priorityClassName] = types.PriorityClass{
 			Priority:                              priorityClass.Priority,
 			Preemptible:                           priorityClass.Preemptible,
 			MaximumResourceFractionPerQueue:       limit,
diff --git a/pkg/api/util_test.go b/pkg/api/util_test.go
index 6b49ee91..c23eaab5 100644
--- a/pkg/api/util_test.go
+++ b/pkg/api/util_test.go
@@ -7,7 +7,7 @@ import (
 	v1 "k8s.io/api/core/v1"
 	resource "k8s.io/apimachinery/pkg/api/resource"
 
-	"github.com/armadaproject/armada/internal/armada/configuration"
+	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 )
 
@@ -20,7 +20,7 @@ const (
 )
 
 var (
-	TestPriorityClasses = map[string]configuration.PriorityClass{
+	TestPriorityClasses = map[string]types.PriorityClass{
 		PriorityClassEmpty: {Priority: 0, Preemptible: true},
 		PriorityClass0:     {Priority: 0, Preemptible: true},
 		PriorityClass1:     {Priority: 1, Preemptible: true},