Skip to content

Commit 9d73fbd

Browse files
Merge pull request #53 from Mischulee/HYPERFLEET-619
HYPERFLEET-619 - fix: Prevent duplicate nodepool names within a cluster
2 parents 27384cc + 4402b41 commit 9d73fbd

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

pkg/db/migrations/202511111055_add_node_pools.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ func addNodePools() *gormigrate.Migration {
6363
return err
6464
}
6565

66+
// Create unique index on (owner_id, name) to prevent duplicate nodepool names within a cluster
67+
createUniqueIdxSQL := "CREATE UNIQUE INDEX IF NOT EXISTS idx_node_pools_owner_name " +
68+
"ON node_pools(owner_id, name) WHERE deleted_at IS NULL;"
69+
if err := tx.Exec(createUniqueIdxSQL).Error; err != nil {
70+
return err
71+
}
72+
6673
// Add foreign key constraint to clusters
6774
addFKSQL := `
6875
ALTER TABLE node_pools
@@ -83,6 +90,9 @@ func addNodePools() *gormigrate.Migration {
8390
}
8491

8592
// Drop indexes
93+
if err := tx.Exec("DROP INDEX IF EXISTS idx_node_pools_owner_name;").Error; err != nil {
94+
return err
95+
}
8696
if err := tx.Exec("DROP INDEX IF EXISTS idx_node_pools_owner_id;").Error; err != nil {
8797
return err
8898
}

test/integration/node_pools_test.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import (
1313
"github.com/openshift-hyperfleet/hyperfleet-api/test"
1414
)
1515

16+
const (
17+
kindNodePool = "NodePool"
18+
)
19+
1620
// TestNodePoolGet is disabled because GET /nodepools/{id} is not in the OpenAPI spec
1721
// The API only supports:
1822
// - GET /api/hyperfleet/v1/nodepools (list all nodepools)
@@ -58,7 +62,7 @@ func TestNodePoolPost(t *testing.T) {
5862
Expect(err).NotTo(HaveOccurred())
5963

6064
// POST responses per openapi spec: 201, 409, 500
61-
kind := "NodePool"
65+
kind := kindNodePool
6266
nodePoolInput := openapi.NodePoolCreateRequest{
6367
Kind: &kind,
6468
Name: "test-name",
@@ -190,7 +194,7 @@ func TestGetNodePoolByClusterIdAndNodePoolId(t *testing.T) {
190194
Expect(err).NotTo(HaveOccurred())
191195

192196
// Create a nodepool for this cluster using the API
193-
kind := "NodePool"
197+
kind := kindNodePool
194198
nodePoolInput := openapi.NodePoolCreateRequest{
195199
Kind: &kind,
196200
Name: "test-np-get",
@@ -319,3 +323,48 @@ func TestNodePoolPost_WrongKind(t *testing.T) {
319323
Expect(ok).To(BeTrue())
320324
Expect(detail).To(ContainSubstring("kind must be 'NodePool'"))
321325
}
326+
327+
// TestNodePoolDuplicateNames tests that duplicate nodepool names within a cluster are rejected
328+
func TestNodePoolDuplicateNames(t *testing.T) {
329+
h, client := test.RegisterIntegration(t)
330+
331+
account := h.NewRandAccount()
332+
ctx := h.NewAuthenticatedContext(account)
333+
334+
// Create a cluster first
335+
cluster, err := h.Factories.NewClusters(h.NewID())
336+
Expect(err).NotTo(HaveOccurred())
337+
338+
// Create first nodepool with a specific name
339+
kind := kindNodePool
340+
nodePoolInput := openapi.NodePoolCreateRequest{
341+
Kind: &kind,
342+
Name: "test-duplicate",
343+
Spec: map[string]interface{}{"test": "spec"},
344+
}
345+
346+
resp, err := client.CreateNodePoolWithResponse(
347+
ctx, cluster.ID, openapi.CreateNodePoolJSONRequestBody(nodePoolInput), test.WithAuthToken(ctx),
348+
)
349+
Expect(err).NotTo(HaveOccurred())
350+
Expect(resp.StatusCode()).To(Equal(http.StatusCreated))
351+
352+
// Create second nodepool with the same name in the same cluster
353+
resp, err = client.CreateNodePoolWithResponse(
354+
ctx, cluster.ID, openapi.CreateNodePoolJSONRequestBody(nodePoolInput), test.WithAuthToken(ctx),
355+
)
356+
Expect(err).NotTo(HaveOccurred())
357+
Expect(resp.StatusCode()).
358+
To(Equal(http.StatusConflict), "Expected 409 Conflict for duplicate nodepool name in same cluster")
359+
360+
Expect(resp.ApplicationproblemJSONDefault).NotTo(BeNil(), "Expected response body to be present")
361+
problemDetail := resp.ApplicationproblemJSONDefault
362+
363+
Expect(*problemDetail.Code).To(Equal("HYPERFLEET-CNF-001"), "Expected conflict error code")
364+
Expect(problemDetail.Type).To(Equal("https://api.hyperfleet.io/errors/conflict"), "Expected conflict error type")
365+
Expect(problemDetail.Title).To(Equal("Resource Conflict"), "Expected conflict error title")
366+
367+
Expect(problemDetail.Detail).NotTo(BeNil(), "Expected error detail to be present")
368+
Expect(*problemDetail.Detail).To(ContainSubstring("already exists"),
369+
"Expected error detail to mention that resource already exists")
370+
}

0 commit comments

Comments
 (0)