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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.5'
go-version-file: 'go.mod'

- name: Run golangci-lint
run: make lint
Expand All @@ -30,7 +30,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.5'
go-version-file: 'go.mod'

- name: Unit tests
run: make test
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.5'
go-version-file: 'go.mod'

- name: Run govulncheck
run: |
Expand Down
20 changes: 10 additions & 10 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,15 @@ License URL: https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE

----------
Module: golang.org/x/crypto
Version: v0.52.0
Version: v0.53.0
License: BSD-3-Clause
License URL: https://cs.opensource.google/go/x/crypto/+/v0.52.0:LICENSE
License URL: https://cs.opensource.google/go/x/crypto/+/v0.53.0:LICENSE

----------
Module: golang.org/x/net
Version: v0.55.0
Version: v0.56.0
License: BSD-3-Clause
License URL: https://cs.opensource.google/go/x/net/+/v0.55.0:LICENSE
License URL: https://cs.opensource.google/go/x/net/+/v0.56.0:LICENSE

----------
Module: golang.org/x/oauth2
Expand All @@ -239,21 +239,21 @@ License URL: https://cs.opensource.google/go/x/oauth2/+/v0.35.0:LICENSE

----------
Module: golang.org/x/sys
Version: v0.45.0
Version: v0.46.0
License: BSD-3-Clause
License URL: https://cs.opensource.google/go/x/sys/+/v0.45.0:LICENSE
License URL: https://cs.opensource.google/go/x/sys/+/v0.46.0:LICENSE

----------
Module: golang.org/x/term
Version: v0.43.0
Version: v0.44.0
License: BSD-3-Clause
License URL: https://cs.opensource.google/go/x/term/+/v0.43.0:LICENSE
License URL: https://cs.opensource.google/go/x/term/+/v0.44.0:LICENSE

----------
Module: golang.org/x/text
Version: v0.37.0
Version: v0.40.0
License: BSD-3-Clause
License URL: https://cs.opensource.google/go/x/text/+/v0.37.0:LICENSE
License URL: https://cs.opensource.google/go/x/text/+/v0.40.0:LICENSE

----------
Module: golang.org/x/time/rate
Expand Down
38 changes: 28 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,42 @@ Go 1.26+.
## Provider interface

```go
type Provider[CreateParams any, Status any, UpdateParams any] interface {
Create(ctx context.Context, params CreateParams) error
type Provider[PlanParams, Config, Secrets, Details, UpdateParams any] interface {
Create(ctx context.Context, id model.ServiceID, teamID int, customSubdomain *string,
plan PlanParams, config Config, secrets Secrets) error
List(ctx context.Context) ([]model.ServiceID, error)
GetStatus(ctx context.Context, ids []model.ServiceID) (map[model.ServiceID]Status, error)
Update(ctx context.Context, id model.ServiceID, args UpdateParams) error
GetStatus(ctx context.Context, ids []model.ServiceID) (map[model.ServiceID]ServiceStatus[PlanParams, Config, Details], error)
Update(ctx context.Context, id model.ServiceID, teamID int, customSubdomain *string,
args UpdateParams) error
Delete(ctx context.Context, id model.ServiceID) error
}
```

Embed `provider.Base` for the shared dependencies (Kubernetes client, logger) and helpers.
The split between the contract and your provider is visible in the signatures — you never declare
the contract's own fields or envelopes yourself:

Backups are an **opt-in capability**, generic over the provider's own request type:
| The contract defines | You define |
|---|---|
| `id`, `teamId`, `customSubdomain` — passed as arguments | `PlanParams` — contents of `plan.parameters` |
| the `plan: {parameters: …}` wrapper, unwrapped on the way in and re-wrapped on the way out | `Config` — contents of `config` |
| `msId` on backup requests | `Secrets` — contents of `secrets` |
| the `{plan, config, details}` status envelope (`ServiceStatus`) | `Details` — read-only status data (hostnames, ports, readiness) |
| HTTP status codes and error mapping | `UpdateParams` — your partial `PATCH` payload |

`PATCH` bodies are partial, so make `UpdateParams` fields pointers to tell "not sent" from "sent
empty". Build status values with `provider.NewServiceStatus(plan, config, details)`. Embed
`provider.Base` for the shared dependencies (Kubernetes client, logger) and helpers.

Backups are an **opt-in capability**, generic over the provider's own backup-store schemas:

```go
type Backups[BackupParams any] interface {
TakeBackup(ctx context.Context, backupID model.BackupId, params BackupParams) error
GetBackupStatus(ctx context.Context, backupID model.BackupId, params BackupParams) (BackupStatus, error)
DeleteBackup(ctx context.Context, backupID model.BackupId, params BackupParams) error
type Backups[BackupConfig, BackupSecrets any] interface {
TakeBackup(ctx context.Context, backupID model.BackupId, msID model.ServiceID,
config BackupConfig, secrets BackupSecrets) error
GetBackupStatus(ctx context.Context, backupID model.BackupId, msID model.ServiceID,
config BackupConfig, secrets BackupSecrets) (BackupStatus, error)
DeleteBackup(ctx context.Context, backupID model.BackupId, msID model.ServiceID,
config BackupConfig, secrets BackupSecrets) error
}
```

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/codesphere-cloud/managed-services-lib

go 1.26

toolchain go1.26.5
toolchain go1.26.6

require (
github.com/gin-gonic/gin v1.12.0
Expand Down
60 changes: 35 additions & 25 deletions model/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,53 @@

package model

import (
"encoding/json"
)

// ServiceID is a unique identifier for a managed service instance.
type ServiceID string

// BackupId is a unique identifier for a managed service backup.
type BackupId string

// PlanParameters defines the resource allocation for a managed service.
type PlanParameters struct {
// StorageMiB is the storage size in MiB.
StorageMiB int `json:"storage"`
// PlanSpec is the {"parameters": ...} envelope the contract wraps a plan in.
type PlanSpec[Params any] struct {
Parameters Params `json:"parameters"`
}

// CPUTenths is the CPU allocation in tenths of a core.
CPUTenths int `json:"cpu"`
// ServiceStatus is the per-service value of the status response. Build it with
// NewServiceStatus, which applies the contract's plan.parameters wrapper.
type ServiceStatus[PlanParams, Config, Details any] struct {
// Plan echoes the service's current plan parameters.
Plan PlanSpec[PlanParams] `json:"plan"`

// MemoryMiB is the memory allocation in MiB.
MemoryMiB int `json:"memory"`
}
// Config echoes the service's current configuration.
Config Config `json:"config"`

// Plan wraps the plan parameters.
type Plan struct {
Parameters PlanParameters `json:"parameters"`
}
// Details is read-only provider data (hostnames, ports, readiness, ...).
Details Details `json:"details"`

// ServiceConfig holds configuration for a managed service.
type ServiceConfig struct {
// Version is the version of the managed service.
Version string `json:"version"`
// Error carries a provider-detected problem with the service, if any; the
// caller sets this directly on the value NewServiceStatus returns.
Error string `json:"error,omitempty"`
}

// ServiceSecrets holds sensitive data for a managed service.
type ServiceSecrets struct {
// SuperuserPassword is the superuser/admin password.
SuperuserPassword string `json:"superuserPassword"`
// RecoverFrom, sent only on create, asks the provider to restore the new
// service from an existing backup instead of provisioning it empty. Config
// and Secrets are deferred as raw JSON since they are provider specific.
type RecoverFrom struct {
ID BackupId `json:"id"`
Config json.RawMessage `json:"config"`
Secrets json.RawMessage `json:"secrets"`
}

// ServiceDetails contains common status details for a managed service.
type ServiceDetails struct {
// Ready indicates if the service is ready to accept connections.
Ready bool `json:"ready"`
// BackupStatus is the backup-status response contract expected by Codesphere:
// whether the backup exists (was taken successfully) and, if it failed, why.
type BackupStatus struct {
// Exists is true once the backup has been taken successfully.
Exists bool `json:"exists"`

// Error contains the failure reason when the backup failed; empty otherwise.
Error string `json:"error,omitempty"`
}
4 changes: 2 additions & 2 deletions provider/backupjob.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ func DeleteBackupJobName(backupID model.BackupId) string {
}

// BackupStatusFromJob maps a Job snapshot to the backup status contract.
func BackupStatusFromJob(s client.JobState) BackupStatus {
status := BackupStatus{Exists: s.Phase == client.JobSucceeded}
func BackupStatusFromJob(s client.JobState) model.BackupStatus {
status := model.BackupStatus{Exists: s.Phase == client.JobSucceeded}
if s.Phase == client.JobFailed {
status.Error = s.Reason
}
Expand Down
69 changes: 45 additions & 24 deletions provider/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,68 @@ import (
// Each provider (e.g., Postgres, FerretDB) implements this interface
// to handle its specific lifecycle operations.
//
// Generic parameters:
// - CreateParams: the full managed-service payload accepted on create
// - Status: the per-service status payload returned to the marketplace
// - UpdateParams: the partial update payload accepted on PATCH
type Provider[CreateParams any, Status any, UpdateParams any] interface {
// Whatever the REST contract defines is an explicit parameter; the type
// parameters are the provider's own schemas. Providers therefore never declare
// the id/teamId/customSubdomain fields or the plan.parameters wrapper themselves —
// the library decodes them off the request and passes them in.
//
// Generic parameters, each the contents of one provider-defined section of the
// contract:
// - PlanParams: plan.parameters
// - Config: config
// - Secrets: secrets
// - Details: details, the read-only part of the status response
// - UpdateParams: the provider's partial PATCH payload
type Provider[PlanParams, Config, Secrets, Details, UpdateParams any] interface {
// Create creates a new managed service.
Create(ctx context.Context, params CreateParams) error
// recoverFrom is set when the request asks to restore the new service from a backup.
Create(ctx context.Context, id model.ServiceID, teamID int, customSubdomain *string,
plan PlanParams, config Config, secrets Secrets, recoverFrom *model.RecoverFrom) error

// List returns all service IDs managed by this provider.
List(ctx context.Context) ([]model.ServiceID, error)

// GetStatus returns the status of the specified services.
// Services that don't exist are simply omitted from the result map.
GetStatus(ctx context.Context, ids []model.ServiceID) (map[model.ServiceID]Status, error)
GetStatus(ctx context.Context, ids []model.ServiceID) (map[model.ServiceID]model.ServiceStatus[PlanParams, Config, Details], error)

// Update updates an existing managed service.
Update(ctx context.Context, id model.ServiceID, args UpdateParams) error
// Update updates an existing managed service. args holds whichever of the
// provider's own fields changed.
Update(ctx context.Context, id model.ServiceID, teamID int, customSubdomain *string,
args UpdateParams) error

// Delete deletes a managed service.
Delete(ctx context.Context, id model.ServiceID) error
}

// NewServiceStatus assembles a ServiceStatus, wrapping plan in the contract's
// plan.parameters envelope.
func NewServiceStatus[PlanParams, Config, Details any](
plan PlanParams,
config Config,
details Details,
) model.ServiceStatus[PlanParams, Config, Details] {
return model.ServiceStatus[PlanParams, Config, Details]{
Plan: model.PlanSpec[PlanParams]{Parameters: plan},
Config: config,
Details: details,
}
}

// Backups is the optional backup capability, kept separate from Provider so a
// provider opts in by implementing it.
type Backups[BackupParams any] interface {
// provider opts in by implementing it. The type parameters are the provider's own
// backup-store schemas. retentionDays is nil when the request left retention
// unmanaged.
type Backups[BackupConfig, BackupSecrets any] interface {
// TakeBackup initiates a backup of the managed service.
TakeBackup(ctx context.Context, backupID model.BackupId, params BackupParams) error
TakeBackup(ctx context.Context, backupID model.BackupId, msID model.ServiceID, teamID int,
config BackupConfig, secrets BackupSecrets, retentionDays *int) error

// GetBackupStatus returns the status of a backup.
GetBackupStatus(ctx context.Context, backupID model.BackupId, params BackupParams) (BackupStatus, error)
GetBackupStatus(ctx context.Context, backupID model.BackupId, msID model.ServiceID, teamID int,
config BackupConfig, secrets BackupSecrets, retentionDays *int) (model.BackupStatus, error)

// DeleteBackup deletes a backup.
DeleteBackup(ctx context.Context, backupID model.BackupId, params BackupParams) error
}

// BackupStatus is the backup-status response contract expected by Codesphere:
// whether the backup exists (was taken successfully) and, if it failed, why.
type BackupStatus struct {
// Exists is true once the backup has been taken successfully.
Exists bool `json:"exists"`

// Error contains the failure reason when the backup failed; empty otherwise.
Error string `json:"error,omitempty"`
DeleteBackup(ctx context.Context, backupID model.BackupId, msID model.ServiceID, teamID int,
config BackupConfig, secrets BackupSecrets, retentionDays *int) error
}
Loading
Loading