Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/installer/config_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ func (g *InstallConfig) ValidateInstallConfig() []string {
}
}

if ob := g.Config.Codesphere.OpenfgaBackups; ob != nil && ob.Enabled {
if ob.DestinationPath == "" {
errors = append(errors, "openfga backups destinationPath is required when openfgaBackups is enabled")
}
if ob.EndpointURL == "" {
errors = append(errors, "openfga backups endpointURL is required when openfgaBackups is enabled")
}
}

return errors
}

Expand Down
29 changes: 29 additions & 0 deletions internal/installer/config_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,35 @@ var _ = Describe("ConfigManager", func() {
})
})

Context("openfga backups validation", func() {
It("should require destinationPath and endpointURL when enabled", func() {
configManager.Config.Codesphere.OpenfgaBackups = &files.OpenfgaBackupsConfig{
Enabled: true,
}
errors := configManager.ValidateInstallConfig()
Expect(errors).To(ContainElement(ContainSubstring("openfga backups destinationPath is required")))
Expect(errors).To(ContainElement(ContainSubstring("openfga backups endpointURL is required")))
})

It("should pass when enabled with required fields set", func() {
configManager.Config.Codesphere.OpenfgaBackups = &files.OpenfgaBackupsConfig{
Enabled: true,
DestinationPath: "s3://backup-openfga-dev",
EndpointURL: "https://storage.googleapis.com",
}
errors := configManager.ValidateInstallConfig()
Expect(errors).ToNot(ContainElement(ContainSubstring("openfga backups")))
})

It("should not require fields when disabled", func() {
configManager.Config.Codesphere.OpenfgaBackups = &files.OpenfgaBackupsConfig{
Enabled: false,
}
errors := configManager.ValidateInstallConfig()
Expect(errors).ToNot(ContainElement(ContainSubstring("openfga backups")))
})
})

Context("ceph validation", func() {
It("should require at least one Ceph host", func() {
configManager.Config.Ceph.Hosts = []files.CephHost{}
Expand Down
81 changes: 81 additions & 0 deletions internal/installer/files/config_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ type CodesphereConfig struct {
OAuth *OAuthProvidersConfig `yaml:"oauth,omitempty"`
ManagedServices []ManagedServiceConfig `yaml:"managedServices,omitempty"`
OpenBao *OpenBaoConfig `yaml:"openBao,omitempty"`
OpenfgaBackups *OpenfgaBackupsConfig `yaml:"openfgaBackups,omitempty"`
Migration *MigrationConfig `yaml:"migration,omitempty"`
TelemetryExport *TelemetryExport `yaml:"telemetryExport,omitempty"`
Override ChartOverride `yaml:"override,omitempty"`
Expand All @@ -339,6 +340,21 @@ type OpenBaoConfig struct {
User string `yaml:"user,omitempty"`
}

// OpenfgaBackupsConfig is the friendly representation of the OpenFGA database
// backup settings. On marshal it is translated into the openfga subchart values
// under codesphere.override (see buildOpenfgaBackupOverride).
type OpenfgaBackupsConfig struct {
Comment thread
NJona marked this conversation as resolved.
Enabled bool `yaml:"enabled"`
// Schedule is an optional 6-field cron expression. When empty the chart default applies.
Schedule string `yaml:"schedule,omitempty"`
// DestinationPath is the S3-style backup target (e.g. s3://backup-openfga-dev). Required when enabled.
DestinationPath string `yaml:"destinationPath,omitempty"`
// EndpointURL is the S3-compatible endpoint (e.g. https://s3.de.io.cloud.ovh.net). Required when enabled.
EndpointURL string `yaml:"endpointURL,omitempty"`
// RetentionPolicy is optional (e.g. "7d"). When empty the chart default of "7d" applies.
RetentionPolicy string `yaml:"retentionPolicy,omitempty"`
}

type OAuthProvidersConfig struct {
Oidc *OidcOAuthProvider `yaml:"oidc,omitempty"`
}
Expand Down Expand Up @@ -625,6 +641,7 @@ type S3ManagedServiceConfig struct {
// Marshal serializes the RootConfig to YAML
func (c *RootConfig) Marshal() ([]byte, error) {
c.buildACMEOverride()
c.buildOpenfgaBackupValues()
return yaml.Marshal(c)
}

Expand Down Expand Up @@ -714,6 +731,70 @@ func (c *RootConfig) buildACMEOverride() {
c.Cluster.Certificates.Override["issuers"] = issuers
}

// buildOpenfgaBackupValues translates codesphere.openfgaBackups into the openfga
// application values under pcApps.applications.openfga.valuesObject.postgres.backup.
// OpenFGA is deployed via the pc-apps app-of-apps (ArgoCD), not the codesphere
// umbrella chart, so this is where its Helm values belong. Optional fields
// (schedule, retentionPolicy) are only emitted when set so the chart defaults
// apply otherwise. Existing pcApps entries (e.g. other applications) are preserved.
func (c *RootConfig) buildOpenfgaBackupValues() {
ob := c.Codesphere.OpenfgaBackups
if ob == nil {
return
}

backup := map[string]interface{}{
"enabled": ob.Enabled,
}
if ob.Schedule != "" {
backup["schedule"] = ob.Schedule
}
if ob.DestinationPath != "" {
backup["destinationPath"] = ob.DestinationPath
}
if ob.EndpointURL != "" {
backup["endpointURL"] = ob.EndpointURL
}
if ob.RetentionPolicy != "" {
backup["retentionPolicy"] = ob.RetentionPolicy
}

if c.PcApps == nil {
c.PcApps = ChartValues{}
}

applications, ok := c.PcApps["applications"].(map[string]interface{})
if !ok {
applications = map[string]interface{}{}
}

openfga, ok := applications["openfga"].(map[string]interface{})
if !ok {
openfga = map[string]interface{}{}
}
// Only enable the openfga application when backups are on; never emit
// enabled:false here, as that would disable the whole openfga application
// (a core component) rather than just its backups.
if ob.Enabled {
openfga["enabled"] = true
}

valuesObject, ok := openfga["valuesObject"].(map[string]interface{})
if !ok {
valuesObject = map[string]interface{}{}
}
postgres, ok := valuesObject["postgres"].(map[string]interface{})
if !ok {
postgres = map[string]interface{}{}
}

postgres["backup"] = backup
valuesObject["postgres"] = postgres
openfga["valuesObject"] = valuesObject
applications["openfga"] = openfga
c.PcApps["applications"] = applications
}

// extractACMESolverFromOverride populates the ACMEConfig.Solver from
// cluster.certificates.override.issuers.acme.dnsSolver after unmarshaling.
func (c *RootConfig) extractACMESolverFromOverride() {
Expand Down
89 changes: 89 additions & 0 deletions internal/installer/files/config_yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,93 @@ cluster:
})

})

Describe("OpenFGA backup config structure", func() {
It("should marshal openfgaBackups into the pc-apps openfga application values", func() {
rootConfig.Codesphere.OpenfgaBackups = &files.OpenfgaBackupsConfig{
Enabled: true,
Schedule: "0 */30 * * * *",
DestinationPath: "s3://backup-openfga-dev",
EndpointURL: "https://s3.de.io.cloud.ovh.net",
}

data, err := rootConfig.Marshal()
Expect(err).NotTo(HaveOccurred())

var raw map[string]interface{}
Expect(yaml.Unmarshal(data, &raw)).NotTo(HaveOccurred())

pcApps := raw["pcApps"].(map[string]interface{})
applications := pcApps["applications"].(map[string]interface{})
Expect(applications["openfga"]).To(Equal(map[string]interface{}{
"enabled": true,
"valuesObject": map[string]interface{}{
"postgres": map[string]interface{}{
"backup": map[string]interface{}{
"enabled": true,
"schedule": "0 */30 * * * *",
"destinationPath": "s3://backup-openfga-dev",
"endpointURL": "https://s3.de.io.cloud.ovh.net",
},
},
},
}))
})

It("omits optional fields (schedule, retentionPolicy) when unset", func() {
rootConfig.Codesphere.OpenfgaBackups = &files.OpenfgaBackupsConfig{
Enabled: true,
DestinationPath: "s3://backup-openfga-dev",
EndpointURL: "https://s3.de.io.cloud.ovh.net",
}

data, err := rootConfig.Marshal()
Expect(err).NotTo(HaveOccurred())

var raw map[string]interface{}
Expect(yaml.Unmarshal(data, &raw)).NotTo(HaveOccurred())

backup := raw["pcApps"].(map[string]interface{})["applications"].(map[string]interface{})["openfga"].(map[string]interface{})["valuesObject"].(map[string]interface{})["postgres"].(map[string]interface{})["backup"].(map[string]interface{})
Expect(backup).To(HaveKey("destinationPath"))
Expect(backup).NotTo(HaveKey("schedule"))
Expect(backup).NotTo(HaveKey("retentionPolicy"))
})

It("preserves other pc-apps applications", func() {
rootConfig.PcApps = files.ChartValues{
"applications": map[string]interface{}{
"ssh-workspace-proxy": map[string]interface{}{"enabled": true},
},
}
rootConfig.Codesphere.OpenfgaBackups = &files.OpenfgaBackupsConfig{
Enabled: true,
DestinationPath: "s3://backup-openfga-dev",
EndpointURL: "https://s3.de.io.cloud.ovh.net",
}

data, err := rootConfig.Marshal()
Expect(err).NotTo(HaveOccurred())

var raw map[string]interface{}
Expect(yaml.Unmarshal(data, &raw)).NotTo(HaveOccurred())

applications := raw["pcApps"].(map[string]interface{})["applications"].(map[string]interface{})
Expect(applications).To(HaveKey("ssh-workspace-proxy"))
Expect(applications).To(HaveKey("openfga"))
})

It("does not generate openfga application values when openfgaBackups is unset", func() {
data, err := rootConfig.Marshal()
Expect(err).NotTo(HaveOccurred())

var raw map[string]interface{}
Expect(yaml.Unmarshal(data, &raw)).NotTo(HaveOccurred())

if pcApps, ok := raw["pcApps"].(map[string]interface{}); ok {
if applications, ok := pcApps["applications"].(map[string]interface{}); ok {
Expect(applications).NotTo(HaveKey("openfga"))
}
}
})
})
})
Loading