frostfs-node/cmd/neofs-cli/modules/container.go
Leonard Lyubich bbc2b873ab [#950] cli: Refactor usage of NeoFS API client
The client needs of the CLI application are limited and change not often.
Interface changes of the client library should not affect the operation of
various application packages, if they do not change their requirements for
the provided functionality. To localize the use of the base client and
facilitate further support, an auxiliary package is implemented that will
only be used by the CLI application.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2021-11-03 18:26:36 +03:00

805 lines
20 KiB
Go

package cmd
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/nspcc-dev/neofs-api-go/pkg"
"github.com/nspcc-dev/neofs-api-go/pkg/acl"
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
cid "github.com/nspcc-dev/neofs-api-go/pkg/container/id"
"github.com/nspcc-dev/neofs-api-go/pkg/netmap"
"github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
"github.com/nspcc-dev/neofs-api-go/pkg/session"
internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client"
"github.com/nspcc-dev/neofs-node/pkg/core/version"
"github.com/nspcc-dev/neofs-sdk-go/pkg/policy"
"github.com/spf13/cobra"
)
const (
attributeDelimiter = "="
awaitTimeout = 120 // in seconds
)
// keywords of predefined basic ACL values
const (
basicACLPrivate = "private"
basicACLReadOnly = "public-read"
basicACLPublic = "public-read-write"
)
const sessionTokenFlag = "session"
// path to a file with encoded session token
var sessionTokenPath string
var (
containerOwner string
containerACL string
containerNonce string
containerPolicy string
containerAttributes []string
containerAwait bool
containerName string
containerNoTimestamp bool
containerID string
containerPathFrom string
containerPathTo string
containerJSON bool
eaclPathFrom string
)
var (
errDeleteTimeout = errors.New("timeout: container has not been removed from sidechain")
errCreateTimeout = errors.New("timeout: container has not been persisted on sidechain")
errSetEACLTimeout = errors.New("timeout: EACL has not been persisted on sidechain")
errUnsupportedEACLFormat = errors.New("unsupported eACL format")
)
// containerCmd represents the container command
var containerCmd = &cobra.Command{
Use: "container",
Short: "Operations with containers",
Long: "Operations with containers",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// bind exactly that cmd's flags to
// the viper before execution
bindCommonFlags(cmd)
bindAPIFlags(cmd)
},
}
var listContainersCmd = &cobra.Command{
Use: "list",
Short: "List all created containers",
Long: "List all created containers",
Run: func(cmd *cobra.Command, args []string) {
var oid *owner.ID
key, err := getKey()
exitOnErr(cmd, err)
if containerOwner == "" {
wallet, err := owner.NEO3WalletFromPublicKey(&key.PublicKey)
exitOnErr(cmd, err)
oid = owner.NewIDFromNeo3Wallet(wallet)
} else {
oid, err = ownerFromString(containerOwner)
exitOnErr(cmd, err)
}
var prm internalclient.ListContainersPrm
prepareAPIClientWithKey(cmd, key, &prm)
prm.SetOwner(oid)
res, err := internalclient.ListContainers(prm)
exitOnErr(cmd, errf("rpc error: %w", err))
// print to stdout
prettyPrintContainerList(cmd, res.IDList())
},
}
var createContainerCmd = &cobra.Command{
Use: "create",
Short: "Create new container",
Long: `Create new container and register it in the NeoFS.
It will be stored in sidechain when inner ring will accepts it.`,
Run: func(cmd *cobra.Command, args []string) {
placementPolicy, err := parseContainerPolicy(containerPolicy)
exitOnErr(cmd, err)
attributes, err := parseAttributes(containerAttributes)
exitOnErr(cmd, err)
basicACL, err := parseBasicACL(containerACL)
exitOnErr(cmd, err)
nonce, err := parseNonce(containerNonce)
exitOnErr(cmd, err)
tok, err := getSessionToken(sessionTokenPath)
exitOnErr(cmd, err)
cnr := container.New()
cnr.SetPlacementPolicy(placementPolicy)
cnr.SetBasicACL(basicACL)
cnr.SetAttributes(attributes)
cnr.SetNonceUUID(nonce)
cnr.SetSessionToken(tok)
cnr.SetOwnerID(tok.OwnerID())
var (
putPrm internalclient.PutContainerPrm
getPrm internalclient.GetContainerPrm
)
prepareAPIClient(cmd, &putPrm, &getPrm)
putPrm.SetContainer(cnr)
putPrm.SetSessionToken(tok)
res, err := internalclient.PutContainer(putPrm)
exitOnErr(cmd, errf("rpc error: %w", err))
id := res.ID()
cmd.Println("container ID:", id)
if containerAwait {
cmd.Println("awaiting...")
getPrm.SetContainerID(id)
for i := 0; i < awaitTimeout; i++ {
time.Sleep(1 * time.Second)
_, err := internalclient.GetContainer(getPrm)
if err == nil {
cmd.Println("container has been persisted on sidechain")
return
}
}
exitOnErr(cmd, errCreateTimeout)
}
},
}
var deleteContainerCmd = &cobra.Command{
Use: "delete",
Short: "Delete existing container",
Long: `Delete existing container.
Only owner of the container has a permission to remove container.`,
Run: func(cmd *cobra.Command, args []string) {
id, err := parseContainerID(containerID)
exitOnErr(cmd, err)
tok, err := getSessionToken(sessionTokenPath)
exitOnErr(cmd, err)
var (
delPrm internalclient.DeleteContainerPrm
getPrm internalclient.GetContainerPrm
)
prepareAPIClient(cmd, &delPrm, &getPrm)
delPrm.SetContainerID(id)
delPrm.SetSessionToken(tok)
_, err = internalclient.DeleteContainer(delPrm)
exitOnErr(cmd, errf("rpc error: %w", err))
cmd.Println("container delete method invoked")
if containerAwait {
cmd.Println("awaiting...")
getPrm.SetContainerID(id)
for i := 0; i < awaitTimeout; i++ {
time.Sleep(1 * time.Second)
_, err := internalclient.GetContainer(getPrm)
if err != nil {
cmd.Println("container has been removed:", containerID)
return
}
}
exitOnErr(cmd, errDeleteTimeout)
}
},
}
var listContainerObjectsCmd = &cobra.Command{
Use: "list-objects",
Short: "List existing objects in container",
Long: `List existing objects in container`,
Run: func(cmd *cobra.Command, args []string) {
id, err := parseContainerID(containerID)
exitOnErr(cmd, err)
filters := new(object.SearchFilters)
filters.AddRootFilter() // search only user created objects
var prm internalclient.SearchObjectsPrm
prepareSessionPrm(cmd, &prm)
prepareObjectPrm(cmd, &prm)
prm.SetContainerID(id)
prm.SetFilters(*filters)
res, err := internalclient.SearchObjects(prm)
exitOnErr(cmd, errf("rpc error: %w", err))
objectIDs := res.IDList()
for i := range objectIDs {
cmd.Println(objectIDs[i])
}
},
}
var getContainerInfoCmd = &cobra.Command{
Use: "get",
Short: "Get container field info",
Long: `Get container field info`,
Run: func(cmd *cobra.Command, args []string) {
var cnr *container.Container
if containerPathFrom != "" {
data, err := os.ReadFile(containerPathFrom)
exitOnErr(cmd, errf("can't read file: %w", err))
cnr = container.New()
err = cnr.Unmarshal(data)
exitOnErr(cmd, errf("can't unmarshal container: %w", err))
} else {
id, err := parseContainerID(containerID)
exitOnErr(cmd, err)
var prm internalclient.GetContainerPrm
prepareAPIClient(cmd, &prm)
prm.SetContainerID(id)
res, err := internalclient.GetContainer(prm)
exitOnErr(cmd, errf("rpc error: %w", err))
cnr = res.Container()
}
prettyPrintContainer(cmd, cnr, containerJSON)
if containerPathTo != "" {
var (
data []byte
err error
)
if containerJSON {
data, err = cnr.MarshalJSON()
exitOnErr(cmd, errf("can't JSON encode container: %w", err))
} else {
data, err = cnr.Marshal()
exitOnErr(cmd, errf("can't binary encode container: %w", err))
}
err = os.WriteFile(containerPathTo, data, 0644)
exitOnErr(cmd, errf("can't write container to file: %w", err))
}
},
}
var getExtendedACLCmd = &cobra.Command{
Use: "get-eacl",
Short: "Get extended ACL table of container",
Long: `Get extended ACL talbe of container`,
Run: func(cmd *cobra.Command, args []string) {
id, err := parseContainerID(containerID)
exitOnErr(cmd, err)
var eaclPrm internalclient.EACLPrm
prepareAPIClient(cmd, &eaclPrm)
eaclPrm.SetContainerID(id)
res, err := internalclient.EACL(eaclPrm)
exitOnErr(cmd, errf("rpc error: %w", err))
eaclTable := res.EACL()
sig := eaclTable.Signature()
if containerPathTo == "" {
cmd.Println("eACL: ")
prettyPrintEACL(cmd, eaclTable)
cmd.Println("Signature:")
printJSONMarshaler(cmd, sig, "signature")
return
}
var data []byte
if containerJSON {
data, err = eaclTable.MarshalJSON()
exitOnErr(cmd, errf("can't encode to JSON: %w", err))
} else {
data, err = eaclTable.Marshal()
exitOnErr(cmd, errf("can't encode to binary: %w", err))
}
cmd.Println("dumping data to file:", containerPathTo)
cmd.Println("Signature:")
printJSONMarshaler(cmd, sig, "signature")
err = os.WriteFile(containerPathTo, data, 0644)
exitOnErr(cmd, errf("could not write eACL to file: %w", err))
},
}
var setExtendedACLCmd = &cobra.Command{
Use: "set-eacl",
Short: "Set new extended ACL table for container",
Long: `Set new extended ACL table for container.
Container ID in EACL table will be substituted with ID from the CLI.`,
Run: func(cmd *cobra.Command, args []string) {
id, err := parseContainerID(containerID)
exitOnErr(cmd, err)
eaclTable, err := parseEACL(eaclPathFrom)
exitOnErr(cmd, err)
tok, err := getSessionToken(sessionTokenPath)
exitOnErr(cmd, err)
eaclTable.SetCID(id)
eaclTable.SetSessionToken(tok)
var (
setEACLPrm internalclient.SetEACLPrm
getEACLPrm internalclient.EACLPrm
)
prepareAPIClient(cmd, &setEACLPrm, &getEACLPrm)
setEACLPrm.SetSessionToken(tok)
setEACLPrm.SetEACLTable(eaclTable)
_, err = internalclient.SetEACL(setEACLPrm)
exitOnErr(cmd, errf("rpc error: %w", err))
if containerAwait {
exp, err := eaclTable.Marshal()
exitOnErr(cmd, errf("broken EACL table: %w", err))
cmd.Println("awaiting...")
getEACLPrm.SetContainerID(id)
for i := 0; i < awaitTimeout; i++ {
time.Sleep(1 * time.Second)
res, err := internalclient.EACL(getEACLPrm)
if err == nil {
// compare binary values because EACL could have been set already
got, err := res.EACL().Marshal()
if err != nil {
continue
}
if bytes.Equal(exp, got) {
cmd.Println("EACL has been persisted on sidechain")
return
}
}
}
exitOnErr(cmd, errSetEACLTimeout)
}
},
}
func initContainerListContainersCmd() {
initCommonFlags(listContainersCmd)
flags := listContainersCmd.Flags()
flags.StringVar(&containerOwner, "owner", "", "owner of containers (omit to use owner from private key)")
}
func initContainerCreateCmd() {
initCommonFlags(createContainerCmd)
flags := createContainerCmd.Flags()
flags.StringVar(&containerACL, "basic-acl", basicACLPrivate, fmt.Sprintf("hex encoded basic ACL value or keywords '%s', '%s', '%s'", basicACLPublic, basicACLPrivate, basicACLReadOnly))
flags.StringVarP(&containerPolicy, "policy", "p", "", "QL-encoded or JSON-encoded placement policy or path to file with it")
flags.StringSliceVarP(&containerAttributes, "attributes", "a", nil, "comma separated pairs of container attributes in form of Key1=Value1,Key2=Value2")
flags.StringVarP(&containerNonce, "nonce", "n", "", "UUIDv4 nonce value for container")
flags.BoolVar(&containerAwait, "await", false, "block execution until container is persisted")
flags.StringVar(&containerName, "name", "", "container name attribute")
flags.BoolVar(&containerNoTimestamp, "disable-timestamp", false, "disable timestamp container attribute")
}
func initContainerDeleteCmd() {
initCommonFlags(deleteContainerCmd)
flags := deleteContainerCmd.Flags()
flags.StringVar(&containerID, "cid", "", "container ID")
flags.BoolVar(&containerAwait, "await", false, "block execution until container is removed")
}
func initContainerListObjectsCmd() {
initCommonFlags(listContainerObjectsCmd)
flags := listContainerObjectsCmd.Flags()
flags.StringVar(&containerID, "cid", "", "container ID")
}
func initContainerInfoCmd() {
initCommonFlags(getContainerInfoCmd)
flags := getContainerInfoCmd.Flags()
flags.StringVar(&containerID, "cid", "", "container ID")
flags.StringVar(&containerPathTo, "to", "", "path to dump encoded container")
flags.StringVar(&containerPathFrom, "from", "", "path to file with encoded container")
flags.BoolVar(&containerJSON, "json", false, "print or dump container in JSON format")
}
func initContainerGetEACLCmd() {
initCommonFlags(getExtendedACLCmd)
flags := getExtendedACLCmd.Flags()
flags.StringVar(&containerID, "cid", "", "container ID")
flags.StringVar(&containerPathTo, "to", "", "path to dump encoded container (default: binary encoded)")
flags.BoolVar(&containerJSON, "json", false, "encode EACL table in json format")
}
func initContainerSetEACLCmd() {
initCommonFlags(setExtendedACLCmd)
flags := setExtendedACLCmd.Flags()
flags.StringVar(&containerID, "cid", "", "container ID")
flags.StringVar(&eaclPathFrom, "table", "", "path to file with JSON or binary encoded EACL table")
flags.BoolVar(&containerAwait, "await", false, "block execution until EACL is persisted")
}
func init() {
containerChildCommand := []*cobra.Command{
listContainersCmd,
createContainerCmd,
deleteContainerCmd,
listContainerObjectsCmd,
getContainerInfoCmd,
getExtendedACLCmd,
setExtendedACLCmd,
}
rootCmd.AddCommand(containerCmd)
containerCmd.AddCommand(containerChildCommand...)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// containerCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// containerCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
initContainerListContainersCmd()
initContainerCreateCmd()
initContainerDeleteCmd()
initContainerListObjectsCmd()
initContainerInfoCmd()
initContainerGetEACLCmd()
initContainerSetEACLCmd()
for _, containerCommand := range containerChildCommand {
flags := containerCommand.Flags()
flags.StringSliceVarP(&xHeaders, xHeadersKey, xHeadersShorthand, xHeadersDefault, xHeadersUsage)
flags.Uint32P(ttl, ttlShorthand, ttlDefault, ttlUsage)
}
for _, cmd := range []*cobra.Command{
createContainerCmd,
deleteContainerCmd,
setExtendedACLCmd,
} {
cmd.Flags().StringVar(
&sessionTokenPath,
sessionTokenFlag,
"",
"path to a JSON-encoded container session token",
)
}
}
// getSessionToken reads `<path>` as JSON file with session token and parses it.
func getSessionToken(path string) (*session.Token, error) {
// try to read session token from file
var tok *session.Token
if path != "" {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("could not open file with session token: %w", err)
}
tok = session.NewToken()
if err = tok.UnmarshalJSON(data); err != nil {
return nil, fmt.Errorf("could not ummarshal session token from file: %w", err)
}
}
return tok, nil
}
func prettyPrintContainerList(cmd *cobra.Command, list []*cid.ID) {
for i := range list {
cmd.Println(list[i])
}
}
func parseContainerPolicy(policyString string) (*netmap.PlacementPolicy, error) {
_, err := os.Stat(policyString) // check if `policyString` is a path to file with placement policy
if err == nil {
printVerbose("Reading placement policy from file: %s", policyString)
data, err := os.ReadFile(policyString)
if err != nil {
return nil, fmt.Errorf("can't read file with placement policy: %w", err)
}
policyString = string(data)
}
result, err := policy.Parse(policyString)
if err == nil {
printVerbose("Parsed QL encoded policy")
return result, nil
}
result = netmap.NewPlacementPolicy()
if err = result.UnmarshalJSON([]byte(policyString)); err == nil {
printVerbose("Parsed JSON encoded policy")
return result, nil
}
return nil, errors.New("can't parse placement policy")
}
func parseAttributes(attributes []string) ([]*container.Attribute, error) {
result := make([]*container.Attribute, 0, len(attributes)+2) // name + timestamp attributes
for i := range attributes {
kvPair := strings.Split(attributes[i], attributeDelimiter)
if len(kvPair) != 2 {
return nil, errors.New("invalid container attribute")
}
parsedAttribute := container.NewAttribute()
parsedAttribute.SetKey(kvPair[0])
parsedAttribute.SetValue(kvPair[1])
result = append(result, parsedAttribute)
}
if !containerNoTimestamp {
timestamp := container.NewAttribute()
timestamp.SetKey(container.AttributeTimestamp)
timestamp.SetValue(strconv.FormatInt(time.Now().Unix(), 10))
result = append(result, timestamp)
}
if containerName != "" {
cnrName := container.NewAttribute()
cnrName.SetKey(container.AttributeName)
cnrName.SetValue(containerName)
result = append(result, cnrName)
}
return result, nil
}
func parseBasicACL(basicACL string) (uint32, error) {
switch basicACL {
case basicACLPublic:
return acl.PublicBasicRule, nil
case basicACLPrivate:
return acl.PrivateBasicRule, nil
case basicACLReadOnly:
return acl.ReadOnlyBasicRule, nil
default:
basicACL = strings.Trim(strings.ToLower(basicACL), "0x")
value, err := strconv.ParseUint(basicACL, 16, 32)
if err != nil {
return 0, fmt.Errorf("can't parse basic ACL: %s", basicACL)
}
return uint32(value), nil
}
}
func parseNonce(nonce string) (uuid.UUID, error) {
if nonce == "" {
result := uuid.New()
printVerbose("Generating container nonce: %s", result)
return result, nil
}
uid, err := uuid.Parse(nonce)
if err != nil {
return uuid.UUID{}, fmt.Errorf("could not parse nonce: %w", err)
}
return uid, nil
}
func parseContainerID(idStr string) (*cid.ID, error) {
if idStr == "" {
return nil, errors.New("container ID is not set")
}
id := cid.New()
err := id.Parse(idStr)
if err != nil {
return nil, errors.New("can't decode container ID value")
}
return id, nil
}
func prettyPrintContainer(cmd *cobra.Command, cnr *container.Container, jsonEncoding bool) {
if cnr == nil {
return
}
if jsonEncoding {
data, err := cnr.MarshalJSON()
if err != nil {
printVerbose("Can't convert container to json: %w", err)
return
}
buf := new(bytes.Buffer)
if err := json.Indent(buf, data, "", " "); err != nil {
printVerbose("Can't pretty print json: %w", err)
}
cmd.Println(buf)
return
}
id := container.CalculateID(cnr)
cmd.Println("container ID:", id)
version := cnr.Version()
cmd.Printf("version: %d.%d\n", version.Major(), version.Minor())
cmd.Println("owner ID:", cnr.OwnerID())
basicACL := cnr.BasicACL()
cmd.Printf("basic ACL: %s", strconv.FormatUint(uint64(basicACL), 16))
switch basicACL {
case acl.PublicBasicRule:
cmd.Printf(" (%s)\n", basicACLPublic)
case acl.PrivateBasicRule:
cmd.Printf(" (%s)\n", basicACLPrivate)
case acl.ReadOnlyBasicRule:
cmd.Printf(" (%s)\n", basicACLReadOnly)
default:
cmd.Println()
}
for _, attribute := range cnr.Attributes() {
if attribute.Key() == container.AttributeTimestamp {
cmd.Printf("attribute: %s=%s (%s)\n",
attribute.Key(),
attribute.Value(),
prettyPrintUnixTime(attribute.Value()))
continue
}
cmd.Printf("attribute: %s=%s\n", attribute.Key(), attribute.Value())
}
nonce, err := cnr.NonceUUID()
if err == nil {
cmd.Println("nonce:", nonce)
} else {
cmd.Println("invalid nonce:", err)
}
cmd.Println("placement policy:")
cmd.Println(strings.Join(policy.Encode(cnr.PlacementPolicy()), "\n"))
}
func parseEACL(eaclPath string) (*eacl.Table, error) {
_, err := os.Stat(eaclPath) // check if `eaclPath` is an existing file
if err != nil {
return nil, errors.New("incorrect path to file with EACL")
}
printVerbose("Reading EACL from file: %s", eaclPath)
data, err := os.ReadFile(eaclPath)
if err != nil {
return nil, fmt.Errorf("can't read file with EACL: %w", err)
}
table := eacl.NewTable()
if err = table.UnmarshalJSON(data); err == nil {
validateAndFixEACLVersion(table)
printVerbose("Parsed JSON encoded EACL table")
return table, nil
}
if err = table.Unmarshal(data); err == nil {
validateAndFixEACLVersion(table)
printVerbose("Parsed binary encoded EACL table")
return table, nil
}
return nil, errUnsupportedEACLFormat
}
func validateAndFixEACLVersion(table *eacl.Table) {
v := table.Version()
if !version.IsValid(v) {
table.SetVersion(*pkg.SDKVersion())
}
}
func prettyPrintEACL(cmd *cobra.Command, table *eacl.Table) {
printJSONMarshaler(cmd, table, "eACL")
}
func printJSONMarshaler(cmd *cobra.Command, j json.Marshaler, entity string) {
data, err := j.MarshalJSON()
if err != nil {
printVerbose("Can't convert %s to json: %w", entity, err)
return
}
buf := new(bytes.Buffer)
if err := json.Indent(buf, data, "", " "); err != nil {
printVerbose("Can't pretty print json: %w", err)
return
}
cmd.Println(buf)
}