Simplify state reconciliation and support changing Softnet settings (#364)

* Simplify state reconciliation and support changing Softnet settings

* Remove unused "updateFunc" parameter from syncOnDiskVMs()

* Don't take an address of a loop variable

* ensure → ensures

* updateVMState(): don't forget to update VMState

* Introduce TestSpecUpdateSoftnet integration test

* Update OpenAPI specification to include generation/observedGeneration
This commit is contained in:
Nikolay Edigaryev
2025-11-06 20:56:31 +04:00
committed by GitHub
parent 00d27d1fa0
commit bafcf6fac2
16 changed files with 690 additions and 166 deletions
+18 -1
View File
@@ -142,7 +142,24 @@ func (controller *Controller) initAPI() *gin.Engine {
controller.createVM(c).Respond(c)
})
v1.PUT("/vms/:name", func(c *gin.Context) {
controller.updateVM(c).Respond(c)
if strings.HasPrefix(c.GetHeader("User-Agent"), "Orchard/0") {
// Backward compatibility for older Orchard Workers that still
// use the PUT /vms/{name} API endpoint to update a VM status
//
// Note that we include the "0" here to avoid targeting users
// of the github.com/cirruslabs/orchard/pkg/client package. For
// them, the UA string should normally be "Orchard/unknown-unknown".
//
// After some months/years we can remove this workaround and at
// the very worst the workers simply won't progress with the VMs
// assigned to them. An upgrade to a newer version will fix that.
controller.updateVMState(c).Respond(c)
} else {
controller.updateVMSpec(c).Respond(c)
}
})
v1.PUT("/vms/:name/state", func(c *gin.Context) {
controller.updateVMState(c).Respond(c)
})
v1.GET("/vms/:name", func(c *gin.Context) {
controller.getVM(c).Respond(c)
+3 -1
View File
@@ -1,11 +1,12 @@
package controller
import (
"net/http"
"github.com/cirruslabs/orchard/internal/responder"
"github.com/cirruslabs/orchard/internal/version"
v1pkg "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/gin-gonic/gin"
"net/http"
)
func (controller *Controller) controllerInfo(ctx *gin.Context) responder.Responder {
@@ -17,6 +18,7 @@ func (controller *Controller) controllerInfo(ctx *gin.Context) responder.Respond
capabilities := []v1pkg.ControllerCapability{
v1pkg.ControllerCapabilityRPCV1,
v1pkg.ControllerCapabilityVMStateEndpoint,
}
if controller.experimentalRPCV2 {
+55 -5
View File
@@ -12,6 +12,7 @@ import (
"github.com/cirruslabs/orchard/internal/simplename"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/gin-gonic/gin"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/samber/lo"
)
@@ -108,7 +109,7 @@ func (controller *Controller) createVM(ctx *gin.Context) responder.Responder {
return response
}
func (controller *Controller) updateVM(ctx *gin.Context) responder.Responder {
func (controller *Controller) updateVMSpec(ctx *gin.Context) responder.Responder {
if responder := controller.authorize(ctx, v1.ServiceAccountRoleComputeWrite); responder != nil {
return responder
}
@@ -119,12 +120,60 @@ func (controller *Controller) updateVM(ctx *gin.Context) responder.Responder {
return responder.JSON(http.StatusBadRequest, NewErrorResponse("invalid JSON was provided"))
}
if userVM.Name == "" {
return responder.JSON(http.StatusPreconditionFailed, NewErrorResponse("VM name is empty"))
}
name := ctx.Param("name")
return controller.storeUpdate(func(txn storepkg.Transaction) responder.Responder {
dbVM, err := txn.GetVM(userVM.Name)
dbVM, err := txn.GetVM(name)
if err != nil {
return responder.Error(err)
}
if dbVM.TerminalState() {
return responder.JSON(http.StatusPreconditionFailed,
NewErrorResponse("cannot update VM in a terminal state"))
}
// Softnet-specific logic: automatically enable Softnet when NetSoftnetAllow or NetSoftnetBlock are set
// and propagate deprecated and non-deprecated boolean fields into each other
if userVM.NetSoftnetDeprecated || userVM.NetSoftnet || len(userVM.NetSoftnetAllow) != 0 || len(userVM.NetSoftnetBlock) != 0 {
userVM.NetSoftnetDeprecated = true
userVM.NetSoftnet = true
}
if cmp.Equal(dbVM.VMSpec, userVM.VMSpec) {
// Nothing was changed
return responder.JSON(http.StatusOK, dbVM)
}
// VM specification was changed
dbVM.VMSpec = userVM.VMSpec
dbVM.Generation++
if err := txn.SetVM(*dbVM); err != nil {
controller.logger.Errorf("failed to update VM in the DB: %v", err)
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, dbVM)
})
}
func (controller *Controller) updateVMState(ctx *gin.Context) responder.Responder {
if responder := controller.authorize(ctx, v1.ServiceAccountRoleComputeWrite); responder != nil {
return responder
}
var userVM v1.VM
if err := ctx.ShouldBindJSON(&userVM); err != nil {
return responder.JSON(http.StatusBadRequest, NewErrorResponse("invalid JSON was provided"))
}
name := ctx.Param("name")
return controller.storeUpdate(func(txn storepkg.Transaction) responder.Responder {
dbVM, err := txn.GetVM(name)
if err != nil {
return responder.Error(err)
}
@@ -141,6 +190,7 @@ func (controller *Controller) updateVM(ctx *gin.Context) responder.Responder {
dbVM.Status = userVM.Status
dbVM.StatusMessage = userVM.StatusMessage
dbVM.ImageFQN = userVM.ImageFQN
dbVM.VMState = userVM.VMState
if err := txn.SetVM(*dbVM); err != nil {
controller.logger.Errorf("failed to update VM in the DB: %v", err)