diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 08665c8..7b20a9f 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: HyperFleet API - version: 1.0.0 + version: 1.0.1 contact: name: HyperFleet Team license: @@ -40,6 +40,8 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + security: + - BearerAuth: [] post: operationId: postCluster summary: Create cluster @@ -71,6 +73,8 @@ paths: application/json: schema: $ref: '#/components/schemas/ClusterCreateRequest' + security: + - BearerAuth: [] /api/hyperfleet/v1/clusters/{cluster_id}: get: operationId: getClusterById @@ -97,6 +101,8 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + security: + - BearerAuth: [] /api/hyperfleet/v1/clusters/{cluster_id}/nodepools: get: operationId: getNodePoolsByClusterId @@ -129,6 +135,8 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + security: + - BearerAuth: [] post: operationId: createNodePool summary: Create nodepool @@ -161,6 +169,8 @@ paths: application/json: schema: $ref: '#/components/schemas/NodePoolCreateRequest' + security: + - BearerAuth: [] /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}: get: operationId: getNodePoolById @@ -194,6 +204,8 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + security: + - BearerAuth: [] /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses: post: operationId: postNodePoolStatuses @@ -307,6 +319,8 @@ paths: application/json: schema: $ref: '#/components/schemas/AdapterStatusCreateRequest' + security: + - BearerAuth: [] get: operationId: getClusterStatuses summary: List all adapter statuses for cluster @@ -334,6 +348,8 @@ paths: description: The server could not understand the request due to invalid syntax. '404': description: The server cannot find the requested resource. + security: + - BearerAuth: [] /api/hyperfleet/v1/nodepools: get: operationId: getNodePools @@ -360,12 +376,9 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + security: + - BearerAuth: [] components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - parameters: QueryParams.order: name: order @@ -404,13 +417,12 @@ components: name: search in: query required: false + description: |- + Filter results using TSL (Tree Search Language) query syntax. + Examples: `status.phase='NotReady'`, `name in ('c1','c2')`, `labels.region='us-east'` schema: type: string explode: false - description: | - Filter results using TSL (Tree Search Language) query syntax. - - Examples: `status.phase='NotReady'`, `name in ('c1','c2')`, `labels.region='us-east'` schemas: APIResource: type: object @@ -913,6 +925,7 @@ components: - updated_time - created_by - updated_by + - generation - owner_references - status properties: @@ -928,6 +941,11 @@ components: updated_by: type: string format: email + generation: + type: integer + format: int32 + minimum: 1 + description: Generation field is updated on customer updates, reflecting the version of the "intent" of the customer owner_references: $ref: '#/components/schemas/ObjectReference' status: @@ -943,6 +961,7 @@ components: environment: production pooltype: worker spec: {} + generation: 1 created_time: '2021-01-01T00:00:00Z' updated_time: '2021-01-01T00:00:00Z' created_by: user-123@example.com @@ -995,12 +1014,15 @@ components: description: Resource URI name: type: string + minLength: 3 + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ description: NodePool name (unique in a cluster) spec: allOf: - $ref: '#/components/schemas/NodePoolSpec' description: |- - Cluster specification + NodePool specification CLM doesn't know how to unmarshall the spec, it only stores and forwards to adapters to do their job But CLM will validate the schema before accepting the request NodePoolCreateRequest: @@ -1025,12 +1047,15 @@ components: description: Resource URI name: type: string + minLength: 3 + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ description: NodePool name (unique in a cluster) spec: allOf: - $ref: '#/components/schemas/NodePoolSpec' description: |- - Cluster specification + NodePool specification CLM doesn't know how to unmarshall the spec, it only stores and forwards to adapters to do their job But CLM will validate the schema before accepting the request example: @@ -1046,6 +1071,7 @@ components: - updated_time - created_by - updated_by + - generation - owner_references - status - name @@ -1063,6 +1089,11 @@ components: updated_by: type: string format: email + generation: + type: integer + format: int32 + minimum: 1 + description: Generation field is updated on customer updates, reflecting the version of the "intent" of the customer owner_references: $ref: '#/components/schemas/ObjectReference' status: @@ -1083,12 +1114,15 @@ components: description: Resource URI name: type: string + minLength: 3 + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ description: NodePool name (unique in a cluster) spec: allOf: - $ref: '#/components/schemas/NodePoolSpec' description: |- - Cluster specification + NodePool specification CLM doesn't know how to unmarshall the spec, it only stores and forwards to adapters to do their job But CLM will validate the schema before accepting the request NodePoolList: @@ -1205,7 +1239,11 @@ components: - Ready - Failed description: Phase of a resource (Cluster or NodePool) + securitySchemes: + BearerAuth: + type: http + scheme: bearer servers: - - url: https://api.hyperfleet.redhat.com + - url: https://hyperfleet.redhat.com description: Production variables: {} diff --git a/pkg/api/cluster_types.go b/pkg/api/cluster_types.go index 72fb468..aa68980 100644 --- a/pkg/api/cluster_types.go +++ b/pkg/api/cluster_types.go @@ -68,8 +68,6 @@ func (c *Cluster) BeforeUpdate(tx *gorm.DB) error { } type ClusterPatchRequest struct { - Name *string `json:"name,omitempty"` - Spec *map[string]interface{} `json:"spec,omitempty"` - Generation *int32 `json:"generation,omitempty"` - Labels *map[string]string `json:"labels,omitempty"` + Spec *map[string]interface{} `json:"spec,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` } diff --git a/pkg/api/node_pool_types.go b/pkg/api/node_pool_types.go index b966052..1f04bea 100644 --- a/pkg/api/node_pool_types.go +++ b/pkg/api/node_pool_types.go @@ -19,6 +19,9 @@ type NodePool struct { Labels datatypes.JSON `json:"labels,omitempty" gorm:"type:jsonb"` Href string `json:"href,omitempty" gorm:"size:500"` + // Version control + Generation int32 `json:"generation" gorm:"default:1;not null"` + // Owner references (expanded) OwnerID string `json:"owner_id" gorm:"size:255;not null;index"` OwnerKind string `json:"owner_kind" gorm:"size:50;not null"` @@ -55,6 +58,9 @@ func (np *NodePool) BeforeCreate(tx *gorm.DB) error { np.ID = NewID() np.CreatedTime = now np.UpdatedTime = now + if np.Generation == 0 { + np.Generation = 1 + } if np.OwnerKind == "" { np.OwnerKind = "Cluster" } @@ -78,7 +84,6 @@ func (np *NodePool) BeforeUpdate(tx *gorm.DB) error { } type NodePoolPatchRequest struct { - Name *string `json:"name,omitempty"` Spec *map[string]interface{} `json:"spec,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } diff --git a/pkg/api/presenters/node_pool.go b/pkg/api/presenters/node_pool.go index f9f005f..dd954b8 100644 --- a/pkg/api/presenters/node_pool.go +++ b/pkg/api/presenters/node_pool.go @@ -87,12 +87,13 @@ func PresentNodePool(nodePool *api.NodePool) openapi.NodePool { kind := nodePool.Kind result := openapi.NodePool{ - Id: &nodePool.ID, - Kind: &kind, - Href: &href, - Name: nodePool.Name, - Spec: spec, - Labels: &labels, + Id: &nodePool.ID, + Kind: &kind, + Href: &href, + Name: nodePool.Name, + Spec: spec, + Labels: &labels, + Generation: nodePool.Generation, OwnerReferences: openapi.ObjectReference{ Id: &nodePool.OwnerID, Kind: &nodePool.OwnerKind, diff --git a/pkg/dao/node_pool.go b/pkg/dao/node_pool.go index e62ddd2..e48c03e 100644 --- a/pkg/dao/node_pool.go +++ b/pkg/dao/node_pool.go @@ -1,6 +1,7 @@ package dao import ( + "bytes" "context" "gorm.io/gorm/clause" @@ -48,6 +49,23 @@ func (d *sqlNodePoolDao) Create(ctx context.Context, nodePool *api.NodePool) (*a func (d *sqlNodePoolDao) Replace(ctx context.Context, nodePool *api.NodePool) (*api.NodePool, error) { g2 := (*d.sessionFactory).New(ctx) + + // Get the existing nodePool to compare spec + existing, err := d.Get(ctx, nodePool.ID) + if err != nil { + db.MarkForRollback(ctx, err) + return nil, err + } + + // Compare spec: if changed, increment generation + if !bytes.Equal(existing.Spec, nodePool.Spec) { + nodePool.Generation = existing.Generation + 1 + } else { + // Spec unchanged, preserve generation + nodePool.Generation = existing.Generation + } + + // Save the nodePool if err := g2.Omit(clause.Associations).Save(nodePool).Error; err != nil { db.MarkForRollback(ctx, err) return nil, err diff --git a/pkg/handlers/cluster.go b/pkg/handlers/cluster.go index 3668488..fbd846d 100644 --- a/pkg/handlers/cluster.go +++ b/pkg/handlers/cluster.go @@ -69,10 +69,6 @@ func (h clusterHandler) Patch(w http.ResponseWriter, r *http.Request) { return nil, err } - //patch a field - if patch.Name != nil { - found.Name = *patch.Name - } if patch.Spec != nil { specJSON, err := json.Marshal(*patch.Spec) if err != nil { @@ -80,9 +76,6 @@ func (h clusterHandler) Patch(w http.ResponseWriter, r *http.Request) { } found.Spec = specJSON } - if patch.Generation != nil { - found.Generation = *patch.Generation - } clusterModel, err := h.cluster.Replace(ctx, found) if err != nil { diff --git a/pkg/handlers/node_pool.go b/pkg/handlers/node_pool.go index 8fe51ce..a57d88a 100644 --- a/pkg/handlers/node_pool.go +++ b/pkg/handlers/node_pool.go @@ -68,10 +68,6 @@ func (h nodePoolHandler) Patch(w http.ResponseWriter, r *http.Request) { return nil, err } - //patch a field - if patch.Name != nil { - found.Name = *patch.Name - } if patch.Spec != nil { specJSON, err := json.Marshal(*patch.Spec) if err != nil { diff --git a/pkg/services/node_pool.go b/pkg/services/node_pool.go index 43bb74d..0b949c4 100644 --- a/pkg/services/node_pool.go +++ b/pkg/services/node_pool.go @@ -186,8 +186,7 @@ func (s *sqlNodePoolService) UpdateNodePoolStatusFromAdapters(ctx context.Contex } // Compute overall phase using required adapters - // NodePool doesn't have a generation field, so we pass 0 which means "don't check generation" - newPhase := ComputePhase(ctx, adapterStatuses, requiredNodePoolAdapters, 0) + newPhase := ComputePhase(ctx, adapterStatuses, requiredNodePoolAdapters, nodePool.Generation) // Calculate min(adapters[].last_report_time) for nodepool.status.last_updated_time // This uses the OLDEST adapter timestamp to ensure Sentinel can detect stale adapters