From 07b51f2b45aa014806c1f15538f6a5dde4a56252 Mon Sep 17 00:00:00 2001 From: Patrice Breton Date: Wed, 8 Jul 2026 18:05:18 -0700 Subject: [PATCH 1/4] fix(rest-api): treat OS imageUrl/imageSha as immutable on update The operating-system PATCH validation advertised imageUrl/imageSha as patchable and forced them to be sent together, but site sync forwards those to Core's UpdateOsImage, which rejects any change to source_url/digest as read-only ("os_image update read-only attributes changed"). Callers who only wanted to rotate an image auth token were also blocked, since imageAuthToken could not be sent without imageUrl. Align the API layer with the immutable-image design: reject changes to imageUrl/imageSha up front with a clear 400 (re-sending the current value stays a no-op), and allow imageAuthType/imageAuthToken to be updated on image-based OS without re-sending imageUrl/imageSha. Update the OpenAPI descriptions and the model/handler tests to match. Signed-off-by: Patrice Breton --- .../pkg/api/handler/operatingsystem_test.go | 33 ++++++++++--- rest-api/api/pkg/api/model/operatingsystem.go | 37 ++++++++++++-- .../api/pkg/api/model/operatingsystem_test.go | 49 ++++++++++--------- rest-api/openapi/spec.yaml | 10 ++-- 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/rest-api/api/pkg/api/handler/operatingsystem_test.go b/rest-api/api/pkg/api/handler/operatingsystem_test.go index 8267d58a3b..31e9c761f2 100644 --- a/rest-api/api/pkg/api/handler/operatingsystem_test.go +++ b/rest-api/api/pkg/api/handler/operatingsystem_test.go @@ -1476,10 +1476,20 @@ func TestOperatingSystemHandler_Update(t *testing.T) { errBodyImageUrlIpxe, err := json.Marshal(updReqImageUrl) assert.Nil(t, err) - updReqValidImageUrl := model.APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("test-os-updated-3"), Description: cutil.GetPtr("Updated Description"), ImageURL: cutil.GetPtr("http://newimagepath.iso"), ImageSHA: cutil.GetPtr("10886660c5b2746ff48224646c5094ebcf88c889"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e"), ImageDisk: cutil.GetPtr("/dev/nvme2n1")} + // imageUrl/imageSha are immutable, so a valid update re-sends the + // existing imageUrl (matching the os5/os9 fixtures) unchanged while + // updating mutable image attributes such as the root filesystem and + // image disk. + updReqValidImageUrl := model.APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("test-os-updated-3"), Description: cutil.GetPtr("Updated Description"), ImageURL: cutil.GetPtr("https://oldimagepath.iso"), ImageSHA: cutil.GetPtr("10886660c5b2746ff48224646c5094ebcf88c889"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e"), ImageDisk: cutil.GetPtr("/dev/nvme2n1")} okBodyImageUrl, err := json.Marshal(updReqValidImageUrl) assert.Nil(t, err) + // Changing imageUrl is rejected up front because the underlying image + // content is immutable after creation. + updReqChangeImageUrl := model.APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("test-os-updated-change-url"), ImageURL: cutil.GetPtr("http://newimagepath.iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980")} + errBodyChangeImageUrl, err := json.Marshal(updReqChangeImageUrl) + assert.Nil(t, err) + updReqDeactivate := model.APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("test-os-updated-deactivate"), Description: cutil.GetPtr("Updated Description for deactivation"), IsActive: cutil.GetPtr(false), DeactivationNote: cutil.GetPtr("Deactivated for a valid reason")} okBodyDeactivate, err := json.Marshal(updReqDeactivate) assert.Nil(t, err) @@ -1728,23 +1738,32 @@ func TestOperatingSystemHandler_Update(t *testing.T) { expectedDeactivationNote: nil, }, { - name: "success when updated with required valid imageURL attribute", + name: "success when image OS updated with unchanged imageURL and mutable attributes", reqOrgName: ipOrg1, user: user, reqBody: string(okBodyImageUrl), - reqUpdateModel: &updReqImageUrl, + reqUpdateModel: &updReqValidImageUrl, osID: os5.ID.String(), expectedErr: false, expectedStatus: http.StatusOK, - expectedImageURL: cutil.GetPtr("http://newimagepath.iso"), + expectedImageURL: cutil.GetPtr("https://oldimagepath.iso"), + }, + { + name: "error when image OS update tries to change imageURL", + reqOrgName: ipOrg1, + user: user, + reqBody: string(errBodyChangeImageUrl), + osID: os5.ID.String(), + expectedErr: true, + expectedStatus: http.StatusBadRequest, }, { - name: "error when updated with required valid imageURL attribute failed with context deadline error", + name: "error when image OS update workflow fails with context deadline error", reqOrgName: ipOrg2, user: user, reqBody: string(okBodyImageUrl), - reqUpdateModel: &updReqImageUrl, - osID: os9.ID.String(), + reqUpdateModel: &updReqValidImageUrl, + osID: os9.ID.String(), expectedErr: true, expectedStatus: http.StatusInternalServerError, }, diff --git a/rest-api/api/pkg/api/model/operatingsystem.go b/rest-api/api/pkg/api/model/operatingsystem.go index 04b14cd1a6..9decdf5c5a 100644 --- a/rest-api/api/pkg/api/model/operatingsystem.go +++ b/rest-api/api/pkg/api/model/operatingsystem.go @@ -356,11 +356,40 @@ func (osur *APIOperatingSystemUpdateRequest) Validate(existingOS *cdbm.Operating } } - if osur.ImageURL != nil { + // imageUrl and imageSha identify the underlying image content and are + // immutable after creation. The Site treats source_url/digest as + // read-only and rejects any change during sync with + // "os_image update read-only attributes changed" (see api-core + // update_os_image); rejecting the change here gives the caller a clear, + // actionable error up front instead of a cryptic site-sync failure. + // Re-sending the current value is accepted as a no-op so clients that + // echo back the full resource still succeed. + if osur.ImageURL != nil && !util.IsNilOrEmptyStrPtr(existingOS.ImageURL) && *osur.ImageURL != *existingOS.ImageURL { + return validation.Errors{ + "imageUrl": errors.New("imageUrl cannot be changed after creation; create a new Operating System to use a different image"), + } + } + if osur.ImageSHA != nil && !util.IsNilOrEmptyStrPtr(existingOS.ImageSHA) && *osur.ImageSHA != *existingOS.ImageSHA { + return validation.Errors{ + "imageSha": errors.New("imageSha cannot be changed after creation; create a new Operating System to use a different image"), + } + } + + if isImageBased { + // Image auth credentials can be updated on their own — the caller + // does not have to re-send the immutable imageUrl/imageSha to + // rotate a token. Those fields, when present, are validated for + // format only; the immutability guard above has already rejected + // any attempt to change them. + // + // TODO: rootFsId/rootFsLabel are also read-only on the Site (see + // api-core update_os_image), so changing them still fails at sync. + // Left as-is to keep this fix scoped to the reported + // imageUrl/imageSha/imageAuthToken behavior. err = validation.ValidateStruct(osur, - validation.Field(&osur.ImageURL, is.URL), + validation.Field(&osur.ImageURL, + validation.When(osur.ImageURL != nil, is.URL)), validation.Field(&osur.ImageSHA, - validation.Required.Error(validationErrorValueRequired), validation.When(osur.ImageSHA != nil, validation.Match(util.ShaHashRegex).Error(errMsgInvalidImageSHA))), validation.Field(&osur.ImageAuthType, validation.When(!(util.IsNilOrEmptyStrPtr(osur.ImageAuthType)) && util.IsNilOrEmptyStrPtr(osur.ImageAuthToken), validation.Required.Error("imageAuthType cannot be specified if imageAuthToken is not specified")), @@ -378,6 +407,8 @@ func (osur *APIOperatingSystemUpdateRequest) Validate(existingOS *cdbm.Operating ) } else { err = validation.ValidateStruct(osur, + validation.Field(&osur.ImageURL, + validation.Nil.Error("imageURL cannot be specified for iPXE based Operating Systems")), validation.Field(&osur.ImageSHA, validation.Nil.Error("imageSHA cannot be specified if imageURL is not specified")), validation.Field(&osur.ImageAuthType, diff --git a/rest-api/api/pkg/api/model/operatingsystem_test.go b/rest-api/api/pkg/api/model/operatingsystem_test.go index 0d2de66c3e..2441b6ed72 100644 --- a/rest-api/api/pkg/api/model/operatingsystem_test.go +++ b/rest-api/api/pkg/api/model/operatingsystem_test.go @@ -199,7 +199,7 @@ func TestAPIOperatingSystemUpdateRequest_Validate(t *testing.T) { ID: uuid.New(), Name: "ab", ImageURL: cutil.GetPtr("https://oldimagepath.iso"), - ImageSHA: cutil.GetPtr("tttt"), + ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("fsID"), Status: cdbm.OperatingSystemStatusPending, Type: cdbm.OperatingSystemTypeImage, @@ -219,7 +219,7 @@ func TestAPIOperatingSystemUpdateRequest_Validate(t *testing.T) { ID: uuid.New(), Name: "abc", ImageURL: cutil.GetPtr("https://oldimagepath.iso"), - ImageSHA: cutil.GetPtr("tttt"), + ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsLabel: cutil.GetPtr("10886660c5b2746ff48224646c5094ebcf88c889"), Status: cdbm.OperatingSystemStatusPending, Type: cdbm.OperatingSystemTypeImage, @@ -252,34 +252,35 @@ func TestAPIOperatingSystemUpdateRequest_Validate(t *testing.T) { expectErr: true, }, { - desc: "error when imageURL is not valid", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("imagenet")}, + desc: "error when imageUrl is changed", + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("https://newimagepath.iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980")}, expectErr: true, }, { - desc: "error when imageURL and imageAuthType are specified but no imageAuthToken", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://image.net/iso"), ImageAuthType: cutil.GetPtr("Bearer")}, + desc: "error when imageSha is changed", + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("https://oldimagepath.iso"), ImageSHA: cutil.GetPtr("b2efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7981")}, expectErr: true, }, { - desc: "error when imageURL and imageAuthType are specified but imageAuthToken is empty", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://image.net/iso"), ImageAuthType: cutil.GetPtr("Bearer"), ImageAuthToken: cutil.GetPtr("")}, - expectErr: true, + desc: "ok when imageUrl and imageSha are re-sent unchanged", + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("https://oldimagepath.iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980")}, + expectErr: false, }, { - desc: "error when imageURL and imageAuthToken are specified but no imageAuthType", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://image.net/iso"), ImageAuthToken: cutil.GetPtr("rsa")}, - expectErr: true, + desc: "ok when imageAuthType and imageAuthToken are updated without imageUrl", + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageAuthType: cutil.GetPtr("Bearer"), ImageAuthToken: cutil.GetPtr("rotated-token")}, + expectErr: false, }, { - desc: "error when imageURL and imageAuthToken are specified but imageAuthType is invalid", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://image.net/iso"), ImageAuthToken: cutil.GetPtr("rsa"), ImageAuthType: cutil.GetPtr("VAPID")}, + desc: "error when imageAuthType is invalid", + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageAuthType: cutil.GetPtr("VAPID"), ImageAuthToken: cutil.GetPtr("rsa")}, expectErr: true, }, { - desc: "error when imageURL and imageAuthToken are specified but imageAuthType is empty", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://image.net/iso"), ImageAuthToken: cutil.GetPtr("rsa"), ImageAuthType: cutil.GetPtr("")}, - expectErr: true, + desc: "error when imageAuthToken is updated on an iPXE based Operating System", + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageAuthType: cutil.GetPtr("Bearer"), ImageAuthToken: cutil.GetPtr("rsa")}, + existingOS: existingIpxeBasedOS, + expectErr: true, }, { desc: "error when imageURL and ipxeScript both specified", @@ -288,28 +289,28 @@ func TestAPIOperatingSystemUpdateRequest_Validate(t *testing.T) { }, { desc: "error when both RootFsID and RootFsLabel are populated", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://iso.net/iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e"), RootFsLabel: cutil.GetPtr("test-label")}, + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e"), RootFsLabel: cutil.GetPtr("test-label")}, expectErr: true, }, { desc: "error when os created with rootFsID but request to update rootFsLabel", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://iso.net/iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsLabel: cutil.GetPtr("test-label")}, + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), RootFsLabel: cutil.GetPtr("test-label")}, expectErr: true, }, { desc: "error when os created with rootFsID and try to clear it without specifying rootFsLabel", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://iso.net/iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("")}, + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), RootFsID: cutil.GetPtr("")}, expectErr: true, }, { desc: "error when os created with rootFsLabel but request to update rootFsID", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://iso.net/iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e")}, + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e")}, existingOS: existingImageBasedOSWithFSLabel, expectErr: true, }, { desc: "error when os created with rootFsLabel and try to clear it without specifying rootFsID", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), ImageURL: cutil.GetPtr("http://iso.net/iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsLabel: cutil.GetPtr("")}, + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("abc"), RootFsLabel: cutil.GetPtr("")}, existingOS: existingImageBasedOSWithFSLabel, expectErr: true, }, @@ -332,12 +333,12 @@ func TestAPIOperatingSystemUpdateRequest_Validate(t *testing.T) { }, { desc: "ok when all valid image fields provided", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("ab"), ImageURL: cutil.GetPtr("http://iso.net/iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e")}, + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("ab"), ImageURL: cutil.GetPtr("https://oldimagepath.iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e")}, expectErr: false, }, { desc: "ok when optional image fields are empty", - obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("ab"), ImageURL: cutil.GetPtr("http://iso.net/iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e"), ImageDisk: cutil.GetPtr(""), ImageAuthType: cutil.GetPtr(""), ImageAuthToken: cutil.GetPtr("")}, + obj: APIOperatingSystemUpdateRequest{Name: cutil.GetPtr("ab"), ImageURL: cutil.GetPtr("https://oldimagepath.iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e"), ImageDisk: cutil.GetPtr(""), ImageAuthType: cutil.GetPtr(""), ImageAuthToken: cutil.GetPtr("")}, expectErr: false, }, } diff --git a/rest-api/openapi/spec.yaml b/rest-api/openapi/spec.yaml index 6ef5d698f8..48e27a934e 100644 --- a/rest-api/openapi/spec.yaml +++ b/rest-api/openapi/spec.yaml @@ -16973,7 +16973,7 @@ components: allowOverride: false - isActive: false deactivationNote: 'iPXE script is referencing a URL that is unreachable, needs to be researched' - description: Request data to update an Operating System. Only image-related attributes can be updated for image-based OS, and only iPXE attributes can be updated for iPXE-based OS + description: 'Request data to update an Operating System. For image-based OS, mutable image attributes (image authentication, root filesystem, image disk) can be updated; imageUrl and imageSha identify the underlying image and are immutable after creation. Only iPXE attributes can be updated for iPXE-based OS' properties: name: type: @@ -16997,22 +16997,22 @@ components: - string - 'null' format: uri - description: 'Original URL from which the Operating System image can be retrieved; required for image-based OS' + description: 'Original URL from which the Operating System image can be retrieved. Immutable after creation: it may be re-sent unchanged, but changing it is rejected. Create a new Operating System to use a different image.' imageSha: type: - string - 'null' - description: 'SHA hash of the image file, required for image-based OS' + description: 'SHA hash of the image file. Immutable after creation: it may be re-sent unchanged, but changing it is rejected.' imageAuthType: type: - string - 'null' - description: 'Authentication type for image URL, if needed, e.g., basic/bearer/token; required if imageAuthToken is specified' + description: 'Authentication type for image URL, if needed, e.g., basic/bearer/token; required if imageAuthToken is specified. Can be updated independently without re-sending imageUrl/imageSha.' imageAuthToken: type: - string - 'null' - description: 'Auth token to retrieve the image from image URL, required if imageAuthType is specified' + description: 'Auth token to retrieve the image from image URL, required if imageAuthType is specified. Can be updated independently without re-sending imageUrl/imageSha.' imageDisk: type: - string From 86526d44317590379c4b9f0d9967c0493bb285a7 Mon Sep 17 00:00:00 2001 From: Patrice Breton Date: Wed, 8 Jul 2026 18:24:14 -0700 Subject: [PATCH 2/4] fix formatting Signed-off-by: Patrice Breton --- rest-api/api/pkg/api/handler/operatingsystem_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest-api/api/pkg/api/handler/operatingsystem_test.go b/rest-api/api/pkg/api/handler/operatingsystem_test.go index 31e9c761f2..87d8182edf 100644 --- a/rest-api/api/pkg/api/handler/operatingsystem_test.go +++ b/rest-api/api/pkg/api/handler/operatingsystem_test.go @@ -1250,6 +1250,7 @@ func TestOperatingSystemHandler_Update(t *testing.T) { TenantID: &tenant1.ID, OsType: cdbm.OperatingSystemTypeImage, ImageURL: cutil.GetPtr("https://oldimagepath.iso"), + ImageSHA: cutil.GetPtr("10886660c5b2746ff48224646c5094ebcf88c889"), AllowOverride: false, EnableBlockStorage: true, PhoneHomeEnabled: false, @@ -1345,6 +1346,7 @@ func TestOperatingSystemHandler_Update(t *testing.T) { TenantID: &tenant2.ID, OsType: cdbm.OperatingSystemTypeImage, ImageURL: cutil.GetPtr("https://oldimagepath.iso"), + ImageSHA: cutil.GetPtr("10886660c5b2746ff48224646c5094ebcf88c889"), AllowOverride: false, EnableBlockStorage: true, PhoneHomeEnabled: false, @@ -1763,7 +1765,7 @@ func TestOperatingSystemHandler_Update(t *testing.T) { user: user, reqBody: string(okBodyImageUrl), reqUpdateModel: &updReqValidImageUrl, - osID: os9.ID.String(), + osID: os9.ID.String(), expectedErr: true, expectedStatus: http.StatusInternalServerError, }, From 01025e43af9a108c96d9855a8ee01d9e471fcb4c Mon Sep 17 00:00:00 2001 From: Patrice Breton Date: Thu, 9 Jul 2026 15:02:25 -0700 Subject: [PATCH 3/4] refactor(rest-api): consolidate OS update imageURL guard and fix example Move the non-image imageURL rejection into the early OS-type guard so it covers both iPXE and templated-iPXE OS, and drop the duplicate imageURL Nil rule from the non-image ValidateStruct block. Update the second OperatingSystemUpdateRequest OpenAPI example to omit the immutable imageUrl/imageSha, showing a mutable-only image OS update. Signed-off-by: Patrice Breton --- rest-api/api/pkg/api/model/operatingsystem.go | 14 ++++++-------- rest-api/openapi/spec.yaml | 2 -- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/rest-api/api/pkg/api/model/operatingsystem.go b/rest-api/api/pkg/api/model/operatingsystem.go index 9decdf5c5a..be7c3b7c4d 100644 --- a/rest-api/api/pkg/api/model/operatingsystem.go +++ b/rest-api/api/pkg/api/model/operatingsystem.go @@ -325,19 +325,19 @@ func (osur *APIOperatingSystemUpdateRequest) Validate(existingOS *cdbm.Operating } } - // verify if os created with ipxe script, if yes reject the update if imageURL provided - if existingOS.Type == cdbm.OperatingSystemTypeIPXE && osur.ImageURL != nil { + isImageBased := existingOS.Type == cdbm.OperatingSystemTypeImage + + // verify if os was not created as image-based, reject the update if imageURL provided + if !isImageBased && osur.ImageURL != nil { return validation.Errors{ - "imageURL": errors.New("unable to set image URL for iPXE based Operating System"), + "imageURL": errors.New("unable to set image URL for non-image based Operating System"), } - } else if existingOS.Type == cdbm.OperatingSystemTypeImage && osur.IpxeScript != nil { + } else if isImageBased && osur.IpxeScript != nil { return validation.Errors{ "ipxeScript": errors.New("unable to set iPXE script for image based Operating System"), } } - isImageBased := existingOS.Type == cdbm.OperatingSystemTypeImage - if !util.IsNilOrEmptyStrPtr(osur.RootFsID) && osur.RootFsLabel == nil && !util.IsNilOrEmptyStrPtr(existingOS.RootFsLabel) { return validation.Errors{ "rootFsId": errors.New("unable to set root filesystem id for Operating System with root filesystem label specified"), @@ -407,8 +407,6 @@ func (osur *APIOperatingSystemUpdateRequest) Validate(existingOS *cdbm.Operating ) } else { err = validation.ValidateStruct(osur, - validation.Field(&osur.ImageURL, - validation.Nil.Error("imageURL cannot be specified for iPXE based Operating Systems")), validation.Field(&osur.ImageSHA, validation.Nil.Error("imageSHA cannot be specified if imageURL is not specified")), validation.Field(&osur.ImageAuthType, diff --git a/rest-api/openapi/spec.yaml b/rest-api/openapi/spec.yaml index 48e27a934e..171f2c0879 100644 --- a/rest-api/openapi/spec.yaml +++ b/rest-api/openapi/spec.yaml @@ -16963,8 +16963,6 @@ components: allowOverride: false - name: debian-12-amd64 description: Official Debian 12 for AMD/Intel - imageUrl: 'https://saimei.ftp.acc.umu.se/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2' - imageSha: 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae imageAuthType: Bearer imageAuthToken: acbd18db4cc2f85cedef654fccc4a4d8 imageDisk: /dev/sda From b1d5a074296216ceefe2c46fff09b0d0872cdd3e Mon Sep 17 00:00:00 2001 From: Patrice Breton Date: Thu, 9 Jul 2026 15:19:36 -0700 Subject: [PATCH 4/4] generated sdk and doc Signed-off-by: Patrice Breton --- rest-api/docs/index.html | 18 +++++++++--------- .../model_operating_system_update_request.go | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/rest-api/docs/index.html b/rest-api/docs/index.html index fdd08f7ea9..3332559e39 100644 --- a/rest-api/docs/index.html +++ b/rest-api/docs/index.html @@ -6884,14 +6884,14 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Optional description of the Operating System

ipxeScript
string or null

iPXE script or URL, only applicable for iPXE-based OS. Cannot be specified if imageUrl is specified

-
imageUrl
string or null <uri>

Original URL from which the Operating System image can be retrieved; required for image-based OS

-
imageSha
string or null

SHA hash of the image file, required for image-based OS

-
imageAuthType
string or null

Authentication type for image URL, if needed, e.g., basic/bearer/token; required if imageAuthToken is specified

-
imageAuthToken
string or null

Auth token to retrieve the image from image URL, required if imageAuthType is specified

+
imageUrl
string or null <uri>

Original URL from which the Operating System image can be retrieved. Immutable after creation: it may be re-sent unchanged, but changing it is rejected. Create a new Operating System to use a different image.

+
imageSha
string or null

SHA hash of the image file. Immutable after creation: it may be re-sent unchanged, but changing it is rejected.

+
imageAuthType
string or null

Authentication type for image URL, if needed, e.g., basic/bearer/token; required if imageAuthToken is specified. Can be updated independently without re-sending imageUrl/imageSha.

+
imageAuthToken
string or null

Auth token to retrieve the image from image URL, required if imageAuthType is specified. Can be updated independently without re-sending imageUrl/imageSha.

imageDisk
string or null

Disk path where the image should be mounted, optional

rootFsId
string or null
Typical API Call Flow for Tenant