[#1307] cli: Introduce object patch
command
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
This commit is contained in:
parent
e890f1b4b1
commit
5ed317e24c
4 changed files with 220 additions and 0 deletions
|
@ -2,10 +2,13 @@ package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -869,3 +872,65 @@ func SyncContainerSettings(ctx context.Context, prm SyncContainerPrm) (*SyncCont
|
||||||
|
|
||||||
return new(SyncContainerRes), nil
|
return new(SyncContainerRes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PatchObjectPrm groups parameters of PatchObject operation.
|
||||||
|
type PatchObjectPrm struct {
|
||||||
|
commonObjectPrm
|
||||||
|
objectAddressPrm
|
||||||
|
|
||||||
|
NewAttributes []objectSDK.Attribute
|
||||||
|
|
||||||
|
ReplaceAttribute bool
|
||||||
|
|
||||||
|
PayloadPatches []PayloadPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayloadPatch struct {
|
||||||
|
Range objectSDK.Range
|
||||||
|
|
||||||
|
PayloadPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PatchRes struct {
|
||||||
|
OID oid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func Patch(ctx context.Context, prm PatchObjectPrm) (*PatchRes, error) {
|
||||||
|
patchPrm := client.PrmObjectPatch{
|
||||||
|
XHeaders: prm.xHeaders,
|
||||||
|
BearerToken: prm.bearerToken,
|
||||||
|
Session: prm.sessionToken,
|
||||||
|
Address: prm.objAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(prm.PayloadPatches, func(a, b PayloadPatch) int {
|
||||||
|
return cmp.Compare(a.Range.GetOffset(), b.Range.GetOffset())
|
||||||
|
})
|
||||||
|
|
||||||
|
patcher, err := prm.cli.ObjectPatchInit(ctx, patchPrm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init payload reading: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if patcher.PatchAttributes(ctx, prm.NewAttributes, prm.ReplaceAttribute) {
|
||||||
|
for _, pp := range prm.PayloadPatches {
|
||||||
|
payloadFile, err := os.OpenFile(pp.PayloadPath, os.O_RDONLY, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
applied := patcher.PatchPayload(ctx, &pp.Range, payloadFile)
|
||||||
|
_ = payloadFile.Close()
|
||||||
|
if !applied {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := patcher.Close(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &PatchRes{
|
||||||
|
OID: res.ObjectID(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
151
cmd/frostfs-cli/modules/object/patch.go
Normal file
151
cmd/frostfs-cli/modules/object/patch.go
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||||
|
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
newAttrsFlagName = "new-attrs"
|
||||||
|
replaceAttrsFlagName = "replace-attrs"
|
||||||
|
rangeFlagName = "range"
|
||||||
|
payloadFlagName = "payload"
|
||||||
|
)
|
||||||
|
|
||||||
|
var objectPatchCmd = &cobra.Command{
|
||||||
|
Use: "patch",
|
||||||
|
Run: patch,
|
||||||
|
Short: "Patch FrostFS object",
|
||||||
|
Long: "Patch FrostFS object. Each range passed to the command requires to pass a corresponding patch payload.",
|
||||||
|
Example: `
|
||||||
|
frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --new-attrs 'key1=val1,key2=val2' --replace-attrs
|
||||||
|
frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --range offX:lnX --payload /path/to/payloadX --range offY:lnY --payload /path/to/payloadY
|
||||||
|
frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --new-attrs 'key1=val1,key2=val2' --replace-attrs --range offX:lnX --payload /path/to/payload
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func initObjectPatchCmd() {
|
||||||
|
commonflags.Init(objectPatchCmd)
|
||||||
|
initFlagSession(objectPatchCmd, "PATCH")
|
||||||
|
|
||||||
|
flags := objectPatchCmd.Flags()
|
||||||
|
|
||||||
|
flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||||
|
_ = objectRangeCmd.MarkFlagRequired(commonflags.CIDFlag)
|
||||||
|
|
||||||
|
flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage)
|
||||||
|
_ = objectRangeCmd.MarkFlagRequired(commonflags.OIDFlag)
|
||||||
|
|
||||||
|
flags.String(newAttrsFlagName, "", "New object attributes in form of Key1=Value1,Key2=Value2")
|
||||||
|
flags.Bool(replaceAttrsFlagName, false, "Replace object attributes by new ones.")
|
||||||
|
flags.StringSlice(rangeFlagName, []string{}, "Range to which patch payload is applied. Format: offset:length")
|
||||||
|
flags.StringSlice(payloadFlagName, []string{}, "Path to file with patch payload.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func patch(cmd *cobra.Command, _ []string) {
|
||||||
|
var cnr cid.ID
|
||||||
|
var obj oid.ID
|
||||||
|
|
||||||
|
objAddr := readObjectAddress(cmd, &cnr, &obj)
|
||||||
|
|
||||||
|
ranges, err := getRangeSlice(cmd)
|
||||||
|
commonCmd.ExitOnErr(cmd, "", err)
|
||||||
|
|
||||||
|
payloads := patchPayloadPaths(cmd)
|
||||||
|
|
||||||
|
if len(ranges) != len(payloads) {
|
||||||
|
commonCmd.ExitOnErr(cmd, "", fmt.Errorf("the number of ranges and payloads are not equal: ranges = %d, payloads = %d", len(ranges), len(payloads)))
|
||||||
|
}
|
||||||
|
|
||||||
|
newAttrs, err := parseNewObjectAttrs(cmd)
|
||||||
|
commonCmd.ExitOnErr(cmd, "can't parse new object attributes: %w", err)
|
||||||
|
replaceAttrs, _ := cmd.Flags().GetBool(replaceAttrsFlagName)
|
||||||
|
|
||||||
|
pk := key.GetOrGenerate(cmd)
|
||||||
|
|
||||||
|
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||||
|
|
||||||
|
var prm internalclient.PatchObjectPrm
|
||||||
|
prm.SetClient(cli)
|
||||||
|
Prepare(cmd, &prm)
|
||||||
|
ReadOrOpenSession(cmd, &prm, pk, cnr, nil)
|
||||||
|
|
||||||
|
prm.SetAddress(objAddr)
|
||||||
|
prm.NewAttributes = newAttrs
|
||||||
|
prm.ReplaceAttribute = replaceAttrs
|
||||||
|
|
||||||
|
for i := range ranges {
|
||||||
|
prm.PayloadPatches = append(prm.PayloadPatches, internalclient.PayloadPatch{
|
||||||
|
Range: ranges[i],
|
||||||
|
PayloadPath: payloads[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := internalclient.Patch(cmd.Context(), prm)
|
||||||
|
if err != nil {
|
||||||
|
commonCmd.ExitOnErr(cmd, "can't patch the object: %w", err)
|
||||||
|
}
|
||||||
|
cmd.Println("Patched object ID: ", res.OID.EncodeToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNewObjectAttrs(cmd *cobra.Command) ([]objectSDK.Attribute, error) {
|
||||||
|
var rawAttrs []string
|
||||||
|
|
||||||
|
raw := cmd.Flag(newAttrsFlagName).Value.String()
|
||||||
|
if len(raw) != 0 {
|
||||||
|
rawAttrs = strings.Split(raw, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := make([]objectSDK.Attribute, len(rawAttrs), len(rawAttrs)+2) // name + timestamp attributes
|
||||||
|
for i := range rawAttrs {
|
||||||
|
k, v, found := strings.Cut(rawAttrs[i], "=")
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("invalid attribute format: %s", rawAttrs[i])
|
||||||
|
}
|
||||||
|
attrs[i].SetKey(k)
|
||||||
|
attrs[i].SetValue(v)
|
||||||
|
}
|
||||||
|
return attrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRangeSlice(cmd *cobra.Command) ([]objectSDK.Range, error) {
|
||||||
|
v, _ := cmd.Flags().GetStringSlice(rangeFlagName)
|
||||||
|
if len(v) == 0 {
|
||||||
|
return []objectSDK.Range{}, nil
|
||||||
|
}
|
||||||
|
rs := make([]objectSDK.Range, len(v))
|
||||||
|
for i := range v {
|
||||||
|
before, after, found := strings.Cut(v[i], rangeSep)
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("invalid range specifier: %s", v[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, err := strconv.ParseUint(before, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid '%s' range offset specifier: %w", v[i], err)
|
||||||
|
}
|
||||||
|
length, err := strconv.ParseUint(after, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid '%s' range length specifier: %w", v[i], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rs[i].SetOffset(offset)
|
||||||
|
rs[i].SetLength(length)
|
||||||
|
}
|
||||||
|
return rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchPayloadPaths(cmd *cobra.Command) []string {
|
||||||
|
v, _ := cmd.Flags().GetStringSlice(payloadFlagName)
|
||||||
|
return v
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ func init() {
|
||||||
objectRangeCmd,
|
objectRangeCmd,
|
||||||
objectLockCmd,
|
objectLockCmd,
|
||||||
objectNodesCmd,
|
objectNodesCmd,
|
||||||
|
objectPatchCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
Cmd.AddCommand(objectChildCommands...)
|
Cmd.AddCommand(objectChildCommands...)
|
||||||
|
@ -39,6 +40,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
initObjectPutCmd()
|
initObjectPutCmd()
|
||||||
|
initObjectPatchCmd()
|
||||||
initObjectDeleteCmd()
|
initObjectDeleteCmd()
|
||||||
initObjectGetCmd()
|
initObjectGetCmd()
|
||||||
initObjectSearchCmd()
|
initObjectSearchCmd()
|
||||||
|
|
|
@ -306,6 +306,8 @@ func finalizeSession(cmd *cobra.Command, dst SessionPrm, tok *session.Object, ke
|
||||||
case *internal.PutObjectPrm:
|
case *internal.PutObjectPrm:
|
||||||
common.PrintVerbose(cmd, "Binding session to object PUT...")
|
common.PrintVerbose(cmd, "Binding session to object PUT...")
|
||||||
tok.ForVerb(session.VerbObjectPut)
|
tok.ForVerb(session.VerbObjectPut)
|
||||||
|
case *internal.PatchObjectPrm:
|
||||||
|
tok.ForVerb(session.VerbObjectPatch)
|
||||||
case *internal.DeleteObjectPrm:
|
case *internal.DeleteObjectPrm:
|
||||||
common.PrintVerbose(cmd, "Binding session to object DELETE...")
|
common.PrintVerbose(cmd, "Binding session to object DELETE...")
|
||||||
tok.ForVerb(session.VerbObjectDelete)
|
tok.ForVerb(session.VerbObjectDelete)
|
||||||
|
|
Loading…
Reference in a new issue