Skip to content
Closed
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
14 changes: 14 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,17 @@ form / flags ─┘ (hash) ↓
| `GET /api/nodes` | the GPU inventory: type, allocatable, in use, and what holds it |
| `GET /api/profile` | the site profile as parsed, after defaults |
| `GET /api/autoscaling`, `GET /api/releases/{ns}/{release}/autoscaling` | the autoscale loop's last pass per PD release with the form SLO on; in memory only, lost on restart |

### Complete user configuration

`userValues` on plan and dry-run compose requests represents one complete user document; see [the API design](design/user-values.md).

| Boundary | Behavior |
|---|---|
| Capability | `/api/cluster` advertises `capabilities.userValues` |
| User layers | Replace retained form/edit settings; preserve release identity and explicitly supplied request metadata |
| Clearing | `{}` removes previous user settings and inherits the target baseline |
| System layers | Upgrade composition carries autoscale and canary-system; their precedence remains authoritative |
| Autoscale opt-in | Read the explicit form/edit SLO choice together; catalog/site defaults alone do not opt a release in |
| Validation | Refuse ambiguous legacy inputs, service identity changes and a fixed replica count alongside an enabled scaler |
| Compatibility | Requests omitting `userValues` keep the legacy merge behavior |
35 changes: 35 additions & 0 deletions docs/design/user-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Complete user values for deployment composition

A configuration editor owns one complete document. Legacy upgrade requests merge retained form keys, which cannot represent removing an override while preserving later system layers.

## Request and composition

| Contract | Behavior |
|---|---|
| `GET /api/cluster` → `capabilities.userValues: true` | The server supports complete user documents |
| `POST /api/plans` or dry-run compose with `userValues: {...}` | Replace the previous user form/edit configuration with this complete mapping |
| `userValues: {}` | Remove prior user settings and inherit the target baseline |
| `userValues` omitted | Preserve legacy merge behavior |
| Mixed `userValues` and legacy `overrides`, `overridesYAML`, `editsYAML`, or `reuseSource` | Reject the ambiguous request |

```text
source / target / release metadata -> normal upgrade validation
complete user document -----------> edit layer
|
autoscale / system layers -> plan -> diff -> apply
```

- Store the complete document in the existing edit layer, with its existing validation/ownership semantics. Release identity and request metadata remain in the form/derived layers.
- `fromRelease` still validates model moves, resolves the running chart and carries system layers. It replaces retained user layers when `userValues` is present.
- The autoscale opt-in reads the explicit user form/edit layers together, so the same SLO choice has the same effect from structured controls or YAML. Catalog/site defaults alone do not opt a release in.
- Pin an upgrade's service identity. A conflicting `serviceId` inside the document is rejected.
- Preview uses the same composition path, including `fromRelease`; it never reconstructs the final layer order in the client.
- Authentication, deployment authorization and diff/apply revision checks remain the existing ones. Composition performs no deployment write.

## Compatibility

Clients require the advertised capability before submitting this request. Older clients retain their current behavior; older servers must be upgraded before the unified editor is enabled. No chart setting, CRD, database migration or permission change is required.

## Validation

Exercise installation, clearing an upgrade document, identity checks, preservation of unrelated user values, live autoscale carry/removal, ambiguous input rejection, and parity between planning and dry-run values. Record the results locally before publishing any change.
12 changes: 12 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,10 @@ components:
items: { $ref: "#/components/schemas/CatalogInfo" }
version: { type: string, description: "swissd's version." }
allowDeploy: { type: boolean }
capabilities:
type: object
properties:
userValues: { type: boolean, description: "Accepts complete user documents in PlanRequest.userValues." }
sites:
type: array
items: { $ref: "#/components/schemas/Site" }
Expand Down Expand Up @@ -1954,6 +1958,14 @@ components:
forceGpuProducts: { type: boolean, default: false, description: "Accept products the variant does not list, if this cluster has them." }
gpuResource: { type: string, description: "The resource to request, one the site profile's nodes.gpus lists for the variant's vendor. Empty picks it from the products' nodes, else the vendor's default. An upgrade sending no form keeps the resource the release runs on." }
overrides: { type: object, description: "Form-layer values. On an upgrade, merged over the release's form layer." }
userValues:
type: object
description: |
Complete user configuration, stored in the edit layer. On an upgrade,
replaces retained user form/edit values while keeping release identity
and later system layers. An empty mapping clears user settings.
Cannot be combined with overrides, overridesYAML, editsYAML or reuseSource.
Requires capabilities.userValues from GET /api/cluster.
overridesYAML: { type: string, description: "Form-layer values as YAML text." }
editsYAML: { type: string, description: "The edit layer, exempt from ownership. The autoscale layer is applied after it." }
createNamespace: { type: boolean, default: false, deprecated: true, description: "Use `applyFlags.createNamespace`." }
Expand Down
6 changes: 5 additions & 1 deletion internal/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ type Input struct {
Schema string

// Autoscale is the PD autoscaling layer
Autoscale values.Tree
Autoscale values.Tree
CanarySystem values.Tree
// GPUResource is the extended resource the variant's pods request on this
// cluster, resolved from inventory. Empty is the vendor's default in the
// profile's GPU table.
Expand Down Expand Up @@ -143,6 +144,9 @@ func Compose(in Input) (*plan.Plan, error) {
if len(in.Autoscale) > 0 {
layers[values.LayerAutoscale] = in.Autoscale
}
if len(in.CanarySystem) > 0 {
layers[values.LayerCanarySystem] = in.CanarySystem
}

p := &plan.Plan{
APIVersion: plan.APIVersion,
Expand Down
20 changes: 11 additions & 9 deletions internal/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ type clusterInfo struct {
CatalogFrom string `json:"catalogFrom,omitempty"`
CatalogRef string `json:"catalogRef,omitempty"`
// Catalogs is every catalog a page may select, in the profile's order.
Catalogs []catalogInfo `json:"catalogs,omitempty"`
Version string `json:"version"`
AllowDeploy bool `json:"allowDeploy"`
Sites []site.Site `json:"sites,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Catalogs []catalogInfo `json:"catalogs,omitempty"`
Version string `json:"version"`
AllowDeploy bool `json:"allowDeploy"`
Capabilities map[string]bool `json:"capabilities"`
Sites []site.Site `json:"sites,omitempty"`
Warnings []string `json:"warnings,omitempty"`
// LLMService is whether new installs can use the operator, and which
// backend they will use.
LLMService *llmClusterInfo `json:"llmservice,omitempty"`
Expand Down Expand Up @@ -134,10 +135,11 @@ func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {

repos, from := s.Catalogs(ctx)
info := clusterInfo{
Profile: s.cfg.Cluster.Profile.Ref(),
CatalogFrom: from,
Version: s.version,
AllowDeploy: s.cfg.Server.AllowDeploy,
Profile: s.cfg.Cluster.Profile.Ref(),
CatalogFrom: from,
Version: s.version,
AllowDeploy: s.cfg.Server.AllowDeploy,
Capabilities: map[string]bool{"userValues": true},
}
if p, err := s.Profile(ctx); err == nil {
// One instance, one profile. Its name is the cluster name.
Expand Down
11 changes: 7 additions & 4 deletions internal/server/autoscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ import (
)

// autoscaleOn is the whole gate. A release scales when its plan is
// disaggregated and the form layer turned the SLO requirement on.
// disaggregated and the user configuration turned the SLO requirement on.
func autoscaleOn(p *plan.Plan) bool {
return disaggregated(p) && formSLO(p)
return disaggregated(p) && userSLO(p)
}

func formSLO(p *plan.Plan) bool {
func userSLO(p *plan.Plan) bool {
if p == nil {
return false
}
on, ok := formBool(p.Layers[values.LayerForm], "sloRequirement.enabled")
user := values.Tree{}
values.Merge(user, p.Layers[values.LayerForm], values.LayerForm, nil)
values.Merge(user, p.Layers[values.LayerEdit], values.LayerEdit, nil)
on, ok := formBool(user, "sloRequirement.enabled")
return ok && on
}

Expand Down
Loading
Loading