diff --git a/neonvm/controllers/virtualmachine_controller.go b/neonvm/controllers/virtualmachine_controller.go index fae0ccd60..1b209402a 100644 --- a/neonvm/controllers/virtualmachine_controller.go +++ b/neonvm/controllers/virtualmachine_controller.go @@ -49,7 +49,7 @@ import ( "github.com/neondatabase/autoscaling/neonvm/pkg/ipam" "github.com/neondatabase/autoscaling/pkg/api" - "github.com/neondatabase/autoscaling/pkg/util" + patchutil "github.com/neondatabase/autoscaling/pkg/util/patch" ) const ( @@ -723,7 +723,7 @@ func (r *VirtualMachineReconciler) doReconcile(ctx context.Context, virtualmachi func updatePodMetadataIfNecessary(ctx context.Context, c client.Client, vm *vmv1.VirtualMachine, runnerPod *corev1.Pod) error { log := log.FromContext(ctx) - var patches []util.JSONPatch + var patches []patchutil.Operation metaSpecs := []struct { metaField string @@ -756,7 +756,7 @@ func updatePodMetadataIfNecessary(ctx context.Context, c client.Client, vm *vmv1 // Add/update the entries we're expecting to be there for k, e := range spec.expected { if a, ok := spec.actual[k]; !ok || e != a { - patches = append(patches, util.JSONPatch{ + patches = append(patches, patchutil.Operation{ // From RFC 6902 (JSON patch): // // > The "add" operation performs one of the following functions, depending upon @@ -770,8 +770,8 @@ func updatePodMetadataIfNecessary(ctx context.Context, c client.Client, vm *vmv1 // > member's value is replaced. // // So: if the value is missing we'll add it. And if it's different, we'll replace it. - Op: util.PatchAdd, - Path: fmt.Sprintf("/metadata/%s/%s", spec.metaField, util.PatchPathEscape(k)), + Op: patchutil.OpAdd, + Path: fmt.Sprintf("/metadata/%s/%s", spec.metaField, patchutil.PathEscape(k)), Value: e, }) } @@ -782,9 +782,9 @@ func updatePodMetadataIfNecessary(ctx context.Context, c client.Client, vm *vmv1 for k := range spec.actual { if _, expected := spec.expected[k]; !expected && !spec.ignoreExtra[k] { removed = append(removed, k) - patches = append(patches, util.JSONPatch{ - Op: util.PatchRemove, - Path: fmt.Sprintf("/metadata/%s/%s", spec.metaField, util.PatchPathEscape(k)), + patches = append(patches, patchutil.Operation{ + Op: patchutil.OpRemove, + Path: fmt.Sprintf("/metadata/%s/%s", spec.metaField, patchutil.PathEscape(k)), }) } } diff --git a/pkg/agent/runner.go b/pkg/agent/runner.go index 8dda7e5a1..2a431e116 100644 --- a/pkg/agent/runner.go +++ b/pkg/agent/runner.go @@ -65,6 +65,7 @@ import ( "github.com/neondatabase/autoscaling/pkg/agent/schedwatch" "github.com/neondatabase/autoscaling/pkg/api" "github.com/neondatabase/autoscaling/pkg/util" + patchutil "github.com/neondatabase/autoscaling/pkg/util/patch" ) // PluginProtocolVersion is the current version of the agent<->scheduler plugin in use by this @@ -1353,12 +1354,12 @@ func (r *Runner) doVMUpdate( r.recordResourceChange(current, target, r.global.metrics.neonvmRequestedChange) // Make the NeonVM request - patches := []util.JSONPatch{{ - Op: util.PatchReplace, + patches := []patchutil.Operation{{ + Op: patchutil.OpReplace, Path: "/spec/guest/cpus/use", Value: target.VCPU.ToResourceQuantity(), }, { - Op: util.PatchReplace, + Op: patchutil.OpReplace, Path: "/spec/guest/memorySlots/use", Value: target.Mem, }} diff --git a/pkg/util/patch.go b/pkg/util/patch.go deleted file mode 100644 index a5693cd79..000000000 --- a/pkg/util/patch.go +++ /dev/null @@ -1,31 +0,0 @@ -// Construction of JSON patch messages. See https://jsonpatch.com/ - -package util - -import ( - "strings" -) - -type JSONPatchOp string - -const ( - PatchAdd JSONPatchOp = "add" - PatchRemove JSONPatchOp = "remove" - PatchReplace JSONPatchOp = "replace" - PatchMove JSONPatchOp = "move" - PatchCopy JSONPatchOp = "copy" - PatchTest JSONPatchOp = "test" -) - -type JSONPatch struct { - Op JSONPatchOp `json:"op"` - From string `json:"from,omitempty"` - Path string `json:"path"` - Value any `json:"value,omitempty"` -} - -var pathEscaper = strings.NewReplacer("~", "~0", "/", "~1") - -func PatchPathEscape(path string) string { - return pathEscaper.Replace(path) -} diff --git a/pkg/util/patch/patch.go b/pkg/util/patch/patch.go new file mode 100644 index 000000000..4d5fd1748 --- /dev/null +++ b/pkg/util/patch/patch.go @@ -0,0 +1,52 @@ +// Construction of JSON patches. See https://jsonpatch.com/ + +package util + +import ( + "strings" +) + +// OpKind is the kind of operation being performed in a single step +type OpKind string + +const ( + OpAdd OpKind = "add" + OpRemove OpKind = "remove" + OpReplace OpKind = "replace" + OpMove OpKind = "move" + OpCopy OpKind = "copy" + OpTest OpKind = "test" +) + +type JSONPatch = []Operation + +// Operation is a single step in the overall JSON patch +type Operation struct { + // Op is the kind of operation being performed in this step. See [OpKind] for more. + Op OpKind `json:"op"` + // Path is a [JSON pointer] to the target location of the operation. + // + // In general, nesting is separated by '/'s, with special characters escaped by '~'. + // [PathEscape] is provided to handle escaping, because it can get a little gnarly. + // + // As an example, if you want to add a field "foo" to the first element of an array, + // you'd use the path `/0/foo`. The jsonpatch website has more details (and clearer examples), + // refer there for more information: https://jsonpatch.com/#json-pointer + // + // [JSON pointer]: https://datatracker.ietf.org/doc/html/rfc6901/ + Path string `json:"path"` + // From gives the source location for "copy" or "move" operations. + From string `json:"from,omitempty"` + // Value is the new value to use, for "add", "replace", or "test" operations. + Value any `json:"value,omitempty"` +} + +var pathEscaper = strings.NewReplacer("~", "~0", "/", "~1") + +// PathEscape escapes a string for use in a segement of the Path field of an Operation +// +// This is useful, for example, when using arbitrary strings as map keys (like Kubernetes labels or +// annotations). +func PathEscape(s string) string { + return pathEscaper.Replace(s) +}