Skip to content

Commit d710200

Browse files
authored
feat: add Image and ResolvedImage fields to SwarmOpts (#392)
* feat: add Image and ResolvedImage fields to SwarmOpts * feat: add version manifest with all current image entries (#393) * feat: add ManifestLoader for version manifest
1 parent a60c48c commit d710200

14 files changed

Lines changed: 1525 additions & 8 deletions

server/internal/config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ type DockerSwarm struct {
8282
BridgeNetworksSubnetBits int `koanf:"bridge_networks_subnet_bits" json:"bridge_networks_subnet_bits,omitempty"`
8383
DatabaseNetworksCIDR string `koanf:"database_networks_cidr" json:"database_networks_cidr,omitempty"`
8484
DatabaseNetworksSubnetBits int `koanf:"database_networks_subnet_bits" json:"database_networks_subnet_bits,omitempty"`
85+
// ManifestURL is the URL from which the version manifest is fetched.
86+
// Defaults to the pgEdge CDN URL if not set.
87+
ManifestURL string `koanf:"manifest_url" json:"manifest_url,omitempty"`
88+
// ManifestPath points to a local manifest file that bypasses URL fetching
89+
// entirely. Useful for air-gapped environments or testing.
90+
ManifestPath string `koanf:"manifest_path" json:"manifest_path,omitempty"`
8591
}
8692

8793
func (d DockerSwarm) validate() []error {
@@ -102,6 +108,10 @@ func (d DockerSwarm) validate() []error {
102108
return errs
103109
}
104110

111+
// DefaultManifestURL is the pgEdge CDN URL used when no manifest_url is configured.
112+
// TODO(PLAT-598): Replace with the real URL once the hosting location is confirmed.
113+
const DefaultManifestURL = "https://download.pgedge.com/manifests/version-manifest.json"
114+
105115
var defaultDockerSwarm = DockerSwarm{
106116
ImageRepositoryHost: "ghcr.io/pgedge",
107117
// This combination gives us 256 subnets with 16 addresses each.
@@ -110,6 +120,7 @@ var defaultDockerSwarm = DockerSwarm{
110120
// This combination gives us 256 subnets with 64 addresses each.
111121
DatabaseNetworksCIDR: "10.128.128.0/18",
112122
DatabaseNetworksSubnetBits: 26,
123+
ManifestURL: DefaultManifestURL,
113124
}
114125

115126
type SystemD struct {

server/internal/database/orchestrator.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,9 @@ type Orchestrator interface {
181181
StartInstance(ctx context.Context, instanceID string) error
182182
NodeDSN(ctx context.Context, rc *resource.Context, nodeName string, fromInstanceID string, dbName string) (*postgres.DSN, error)
183183
InstancePaths(pgVersion *ds.Version, instanceID string) (InstancePaths, error)
184+
// ReconcileInstanceSpec is called during spec reconciliation to allow the
185+
// orchestrator to update computed fields (e.g. resolved image) on the new
186+
// spec before it is persisted. old is nil when the instance is being created
187+
// for the first time.
188+
ReconcileInstanceSpec(old, new *InstanceSpec) error
184189
}

server/internal/database/service.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,13 @@ func (s *Service) ReconcileInstanceSpec(ctx context.Context, spec *InstanceSpec)
669669
case err == nil:
670670
previous = stored.Spec
671671
spec.CopySettingsFrom(previous)
672+
if err := s.orchestrator.ReconcileInstanceSpec(previous, spec); err != nil {
673+
return nil, fmt.Errorf("failed to reconcile instance spec: %w", err)
674+
}
672675
case errors.Is(err, storage.ErrNotFound):
676+
if err := s.orchestrator.ReconcileInstanceSpec(nil, spec); err != nil {
677+
return nil, fmt.Errorf("failed to reconcile instance spec: %w", err)
678+
}
673679
stored = &StoredInstanceSpec{}
674680
default:
675681
return nil, fmt.Errorf("failed to get current spec for instance '%s': %w", spec.InstanceID, err)

server/internal/database/spec.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ type ExtraNetworkSpec struct {
3030
type SwarmOpts struct {
3131
ExtraVolumes []ExtraVolumesSpec `json:"extra_volumes,omitempty"`
3232
ExtraNetworks []ExtraNetworkSpec `json:"extra_networks,omitempty"`
33-
ExtraLabels map[string]string `json:"extra_labels,omitempty"` // optional, used for custom labels on the swarm service
33+
ExtraLabels map[string]string `json:"extra_labels,omitempty"`
34+
// Image is a user-specified override. Never written by the CP.
35+
Image string `json:"image,omitempty"`
36+
// ResolvedImage is the CP-managed image tag. Written at instance creation,
37+
// upgrade application, and lazy backfill. Never set simultaneously with Image.
38+
ResolvedImage string `json:"resolved_image,omitempty"`
3439
}
3540
type OrchestratorOpts struct {
3641
Swarm *SwarmOpts `json:"docker,omitempty"`
@@ -301,6 +306,8 @@ func (d *SwarmOpts) Clone() *SwarmOpts {
301306
ExtraVolumes: clonedVolumes,
302307
ExtraNetworks: clonedNetworks,
303308
ExtraLabels: maps.Clone(d.ExtraLabels),
309+
Image: d.Image,
310+
ResolvedImage: d.ResolvedImage,
304311
}
305312
}
306313

server/internal/database/spec_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,38 @@ func TestSpec(t *testing.T) {
510510
})
511511
}
512512

513+
func TestSwarmOptsClone(t *testing.T) {
514+
t.Run("copies Image and ResolvedImage", func(t *testing.T) {
515+
orig := &database.SwarmOpts{
516+
Image: "custom-registry/pgedge:dev",
517+
ResolvedImage: "registry/pgedge:17.9-spock5.0.6-standard-1",
518+
ExtraLabels: map[string]string{"k": "v"},
519+
}
520+
cloned := orig.Clone()
521+
522+
assert.Equal(t, orig.Image, cloned.Image)
523+
assert.Equal(t, orig.ResolvedImage, cloned.ResolvedImage)
524+
})
525+
526+
t.Run("clone is independent of original", func(t *testing.T) {
527+
orig := &database.SwarmOpts{
528+
Image: "original-image",
529+
ResolvedImage: "original-resolved",
530+
}
531+
cloned := orig.Clone()
532+
cloned.Image = "mutated-image"
533+
cloned.ResolvedImage = "mutated-resolved"
534+
535+
assert.Equal(t, "original-image", orig.Image)
536+
assert.Equal(t, "original-resolved", orig.ResolvedImage)
537+
})
538+
539+
t.Run("nil clone returns nil", func(t *testing.T) {
540+
var s *database.SwarmOpts
541+
assert.Nil(t, s.Clone())
542+
})
543+
}
544+
513545
func TestSpec_NodeInstances_DBOwner(t *testing.T) {
514546
minimalSpec := func(users []*database.User) *database.Spec {
515547
return &database.Spec{

server/internal/logging/factory.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
ComponentDatabaseService Component = "database_service"
2020
ComponentElectionCandidate Component = "election_candidate"
2121
ComponentEmbeddedEtcd Component = "embedded_etcd"
22+
ComponentManifestLoader Component = "manifest_loader"
2223
ComponentMigration Component = "migration"
2324
ComponentMigrationRunner Component = "migration_runner"
2425
ComponentPortsService Component = "ports_service"

server/internal/orchestrator/swarm/images.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ func NewVersions(cfg config.Config) *Versions {
8080
return versions
8181
}
8282

83-
func (v *Versions) Supported() []*ds.PgEdgeVersion {
83+
func (v Versions) Supported() []*ds.PgEdgeVersion {
8484
return v.supportedVersions
8585
}
8686

87-
func (v *Versions) Default() *ds.PgEdgeVersion {
87+
func (v Versions) Default() *ds.PgEdgeVersion {
8888
return v.defaultVersion
8989
}
9090

@@ -100,7 +100,7 @@ func (v *Versions) addImage(version *ds.PgEdgeVersion, images *Images) {
100100
v.supportedVersions = append(v.supportedVersions, version)
101101
}
102102

103-
func (v *Versions) GetImages(version *ds.PgEdgeVersion) (*Images, error) {
103+
func (v Versions) GetImages(version *ds.PgEdgeVersion) (*Images, error) {
104104
pgv := version.PostgresVersion.String()
105105
sv := version.SpockVersion.String()
106106

0 commit comments

Comments
 (0)