From eac61fe9b2a7ecaba55011c4b46eb0152b391ff3 Mon Sep 17 00:00:00 2001 From: Dmitrii Stepanov Date: Mon, 16 Oct 2023 18:15:04 +0300 Subject: [PATCH] [#733] frostfs-cli: Add `control ir remove-container` Signed-off-by: Dmitrii Stepanov --- cmd/frostfs-cli/modules/control/ir.go | 2 + .../modules/control/ir_remove_container.go | 94 ++++++++++++++++++ pkg/innerring/initialization.go | 3 +- pkg/innerring/innerring.go | 22 ++-- pkg/morph/client/container/delete.go | 2 +- pkg/morph/client/static.go | 5 + pkg/services/control/ir/rpc.go | 15 ++- pkg/services/control/ir/server/calls.go | 64 ++++++++++++ pkg/services/control/ir/server/server.go | 16 +-- pkg/services/control/ir/service.pb.go | Bin 33564 -> 44317 bytes pkg/services/control/ir/service.proto | 19 ++++ pkg/services/control/ir/service_frostfs.pb.go | Bin 15548 -> 20918 bytes pkg/services/control/ir/service_grpc.pb.go | Bin 7044 -> 9163 bytes pkg/services/control/ir/types.pb.go | Bin 7799 -> 7799 bytes 14 files changed, 220 insertions(+), 22 deletions(-) create mode 100644 cmd/frostfs-cli/modules/control/ir_remove_container.go diff --git a/cmd/frostfs-cli/modules/control/ir.go b/cmd/frostfs-cli/modules/control/ir.go index 396d5d0a5..ac1371db7 100644 --- a/cmd/frostfs-cli/modules/control/ir.go +++ b/cmd/frostfs-cli/modules/control/ir.go @@ -12,8 +12,10 @@ func initControlIRCmd() { irCmd.AddCommand(tickEpochCmd) irCmd.AddCommand(removeNodeCmd) irCmd.AddCommand(irHealthCheckCmd) + irCmd.AddCommand(removeContainerCmd) initControlIRTickEpochCmd() initControlIRRemoveNodeCmd() initControlIRHealthCheckCmd() + initControlIRRemoveContainerCmd() } diff --git a/cmd/frostfs-cli/modules/control/ir_remove_container.go b/cmd/frostfs-cli/modules/control/ir_remove_container.go new file mode 100644 index 000000000..43173bcaa --- /dev/null +++ b/cmd/frostfs-cli/modules/control/ir_remove_container.go @@ -0,0 +1,94 @@ +package control + +import ( + "errors" + + "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" + rawclient "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/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" + ircontrol "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir" + ircontrolsrv "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir/server" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "github.com/spf13/cobra" +) + +const ( + ownerFlag = "owner" +) + +var removeContainerCmd = &cobra.Command{ + Use: "remove-container", + Short: "Schedules a container removal", + Long: `Schedules a container removal via a notary request. +Container data will be deleted asynchronously by policer. +To check removal status "frostfs-cli container list" command can be used.`, + Run: removeContainer, +} + +func initControlIRRemoveContainerCmd() { + initControlFlags(removeContainerCmd) + + flags := removeContainerCmd.Flags() + flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage) + flags.String(ownerFlag, "", "Container owner's wallet address.") + removeContainerCmd.MarkFlagsMutuallyExclusive(commonflags.CIDFlag, ownerFlag) +} + +func removeContainer(cmd *cobra.Command, _ []string) { + req := prepareRemoveContainerRequest(cmd) + + pk := key.Get(cmd) + c := getClient(cmd, pk) + + commonCmd.ExitOnErr(cmd, "could not sign request: %w", ircontrolsrv.SignMessage(pk, req)) + + var resp *ircontrol.RemoveContainerResponse + err := c.ExecRaw(func(client *rawclient.Client) error { + var err error + resp, err = ircontrol.RemoveContainer(client, req) + return err + }) + commonCmd.ExitOnErr(cmd, "failed to execute request: %w", err) + + verifyResponse(cmd, resp.GetSignature(), resp.GetBody()) + + if len(req.GetBody().GetContainerId()) > 0 { + cmd.Println("Container scheduled to removal") + } else { + cmd.Println("User containers sheduled to removal") + } +} + +func prepareRemoveContainerRequest(cmd *cobra.Command) *ircontrol.RemoveContainerRequest { + req := &ircontrol.RemoveContainerRequest{ + Body: &ircontrol.RemoveContainerRequest_Body{}, + } + + cidStr, err := cmd.Flags().GetString(commonflags.CIDFlag) + commonCmd.ExitOnErr(cmd, "failed to get cid: ", err) + + ownerStr, err := cmd.Flags().GetString(ownerFlag) + commonCmd.ExitOnErr(cmd, "failed to get owner: ", err) + + if len(ownerStr) == 0 && len(cidStr) == 0 { + commonCmd.ExitOnErr(cmd, "invalid usage: %w", errors.New("neither owner's wallet address nor container ID are specified")) + } + + if len(ownerStr) > 0 { + var owner user.ID + commonCmd.ExitOnErr(cmd, "invalid owner ID: %w", owner.DecodeString(ownerStr)) + var ownerID refs.OwnerID + owner.WriteToV2(&ownerID) + req.Body.Owner = ownerID.StableMarshal(nil) + } + + if len(cidStr) > 0 { + var containerID cid.ID + commonCmd.ExitOnErr(cmd, "invalid container ID: %w", containerID.DecodeString(cidStr)) + req.Body.ContainerId = containerID[:] + } + return req +} diff --git a/pkg/innerring/initialization.go b/pkg/innerring/initialization.go index 84112d121..eb1c4b2d4 100644 --- a/pkg/innerring/initialization.go +++ b/pkg/innerring/initialization.go @@ -343,7 +343,7 @@ func (s *Server) initGRPCServer(cfg *viper.Viper) error { p.SetPrivateKey(*s.key) p.SetHealthChecker(s) - controlSvc := controlsrv.New(p, s.netmapClient, + controlSvc := controlsrv.New(p, s.netmapClient, s.containerClient, controlsrv.WithAllowedKeys(authKeys), ) @@ -389,6 +389,7 @@ func (s *Server) initClientsFromMorph() (*serverMorphClients, error) { if err != nil { return nil, err } + s.containerClient = result.CnrClient s.netmapClient, err = nmClient.NewFromMorph(s.morphClient, s.contracts.netmap, fee, nmClient.TryNotary(), nmClient.AsAlphabet()) if err != nil { diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go index 1567e40d3..35ee2041d 100644 --- a/pkg/innerring/innerring.go +++ b/pkg/innerring/innerring.go @@ -16,6 +16,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/metrics" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client" balanceClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/balance" + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container" nmClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/subscriber" @@ -46,16 +47,17 @@ type ( epochTimer *timer.BlockTimer // global state - morphClient *client.Client - mainnetClient *client.Client - epochCounter atomic.Uint64 - epochDuration atomic.Uint64 - statusIndex *innerRingIndexer - precision precision.Fixed8Converter - healthStatus atomic.Int32 - balanceClient *balanceClient.Client - netmapClient *nmClient.Client - persistate *state.PersistentStorage + morphClient *client.Client + mainnetClient *client.Client + epochCounter atomic.Uint64 + epochDuration atomic.Uint64 + statusIndex *innerRingIndexer + precision precision.Fixed8Converter + healthStatus atomic.Int32 + balanceClient *balanceClient.Client + netmapClient *nmClient.Client + persistate *state.PersistentStorage + containerClient *container.Client // metrics irMetrics *metrics.InnerRingServiceMetrics diff --git a/pkg/morph/client/container/delete.go b/pkg/morph/client/container/delete.go index 5bc8fc188..b520120d2 100644 --- a/pkg/morph/client/container/delete.go +++ b/pkg/morph/client/container/delete.go @@ -67,7 +67,7 @@ func (d *DeletePrm) SetKey(key []byte) { // // If TryNotary is provided, calls notary contract. func (c *Client) Delete(p DeletePrm) error { - if len(p.signature) == 0 { + if len(p.signature) == 0 && !p.IsControl() { return errNilArgument } diff --git a/pkg/morph/client/static.go b/pkg/morph/client/static.go index 7aa17a70f..0531eacdf 100644 --- a/pkg/morph/client/static.go +++ b/pkg/morph/client/static.go @@ -115,6 +115,11 @@ func (i *InvokePrmOptional) SetControlTX(b bool) { i.controlTX = b } +// IsControl gets whether a control transaction will be used. +func (i *InvokePrmOptional) IsControl() bool { + return i.controlTX +} + // Invoke calls Invoke method of Client with static internal script hash and fee. // Supported args types are the same as in Client. // diff --git a/pkg/services/control/ir/rpc.go b/pkg/services/control/ir/rpc.go index 1b635c149..0c9400f6c 100644 --- a/pkg/services/control/ir/rpc.go +++ b/pkg/services/control/ir/rpc.go @@ -9,9 +9,10 @@ import ( const serviceName = "ircontrol.ControlService" const ( - rpcHealthCheck = "HealthCheck" - rpcTickEpoch = "TickEpoch" - rpcRemoveNode = "RemoveNode" + rpcHealthCheck = "HealthCheck" + rpcTickEpoch = "TickEpoch" + rpcRemoveNode = "RemoveNode" + rpcRemoveContainer = "RemoveContainer" ) // HealthCheck executes ControlService.HealthCheck RPC. @@ -40,6 +41,14 @@ func RemoveNode( return sendUnary[RemoveNodeRequest, RemoveNodeResponse](cli, rpcRemoveNode, req, opts...) } +func RemoveContainer( + cli *client.Client, + req *RemoveContainerRequest, + opts ...client.CallOption, +) (*RemoveContainerResponse, error) { + return sendUnary[RemoveContainerRequest, RemoveContainerResponse](cli, rpcRemoveContainer, req, opts...) +} + func sendUnary[I, O grpc.Message](cli *client.Client, rpcName string, req *I, opts ...client.CallOption) (*O, error) { var resp O wResp := &responseWrapper[*O]{ diff --git a/pkg/services/control/ir/server/calls.go b/pkg/services/control/ir/server/calls.go index 680d1e606..537905840 100644 --- a/pkg/services/control/ir/server/calls.go +++ b/pkg/services/control/ir/server/calls.go @@ -5,8 +5,12 @@ import ( "context" "fmt" + "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap" control "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -99,3 +103,63 @@ func (s *Server) RemoveNode(_ context.Context, req *control.RemoveNodeRequest) ( return resp, nil } + +// RemoveContainer forces a container removal. +func (s *Server) RemoveContainer(_ context.Context, req *control.RemoveContainerRequest) (*control.RemoveContainerResponse, error) { + if err := s.isValidRequest(req); err != nil { + return nil, status.Error(codes.PermissionDenied, err.Error()) + } + + if len(req.Body.GetContainerId()) > 0 && len(req.Body.GetOwner()) > 0 { + return nil, status.Error(codes.InvalidArgument, "specify the owner and container at the same time is not allowed") + } + + if len(req.Body.GetContainerId()) > 0 { + var containerID cid.ID + if err := containerID.Decode(req.Body.GetContainerId()); err != nil { + return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("failed to parse container ID: %s", err.Error())) + } + if err := s.removeContainer(containerID); err != nil { + return nil, err + } + } else { + var ownerID refs.OwnerID + if err := ownerID.Unmarshal(req.GetBody().GetOwner()); err != nil { + return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("failed to parse ownerID: %s", err.Error())) + } + var owner user.ID + if err := owner.ReadFromV2(ownerID); err != nil { + return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("failed to read owner: %s", err.Error())) + } + + cids, err := s.containerClient.ContainersOf(&owner) + if err != nil { + return nil, fmt.Errorf("failed to get owner's containers: %w", err) + } + + for _, containerID := range cids { + if err := s.removeContainer(containerID); err != nil { + return nil, err + } + } + } + + resp := &control.RemoveContainerResponse{ + Body: &control.RemoveContainerResponse_Body{}, + } + if err := SignMessage(&s.prm.key.PrivateKey, resp); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return resp, nil +} + +func (s *Server) removeContainer(containerID cid.ID) error { + var prm container.DeletePrm + prm.SetCID(containerID[:]) + prm.SetControlTX(true) + + if err := s.containerClient.Delete(prm); err != nil { + return fmt.Errorf("forcing container removal: %w", err) + } + return nil +} diff --git a/pkg/services/control/ir/server/server.go b/pkg/services/control/ir/server/server.go index dc00809a6..c2a4f88a6 100644 --- a/pkg/services/control/ir/server/server.go +++ b/pkg/services/control/ir/server/server.go @@ -3,6 +3,7 @@ package control import ( "fmt" + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap" ) @@ -12,10 +13,10 @@ import ( // To gain access to the service, any request must be // signed with a key from the white list. type Server struct { - prm Prm - netmapClient *netmap.Client - - allowedKeys [][]byte + prm Prm + netmapClient *netmap.Client + containerClient *container.Client + allowedKeys [][]byte } func panicOnPrmValue(n string, v any) { @@ -32,7 +33,7 @@ func panicOnPrmValue(n string, v any) { // Forms white list from all keys specified via // WithAllowedKeys option and a public key of // the parameterized private key. -func New(prm Prm, netmapClient *netmap.Client, opts ...Option) *Server { +func New(prm Prm, netmapClient *netmap.Client, containerClient *container.Client, opts ...Option) *Server { // verify required parameters switch { case prm.healthChecker == nil: @@ -47,8 +48,9 @@ func New(prm Prm, netmapClient *netmap.Client, opts ...Option) *Server { } return &Server{ - prm: prm, - netmapClient: netmapClient, + prm: prm, + netmapClient: netmapClient, + containerClient: containerClient, allowedKeys: append(o.allowedKeys, prm.key.PublicKey().Bytes()), } diff --git a/pkg/services/control/ir/service.pb.go b/pkg/services/control/ir/service.pb.go index bec74a3beaecfdcc8792ed3dc368d640e5f72eac..56d52be4cea02be4088b63df72a42400232e06a4 100644 GIT binary patch delta 3738 zcmbVPYfM|$9hW_9{D6QPUIq;2ZBrZWy|%Bh0|AnziAjP(RJI_8H#Qjj;*};WKxn5< z+9PB6edwaKD(fE7rgUxO4^vd_5ouqdJ-TG8CRH0@QZr4PmIi4b_F-#w&bh|i*e)NI z4>vx)^Z)<;?{f}syrz8fBjx5?p0iDG-i{YF<4&bsdpsUn8V|>U3s_gainogNMdyRj z_@&@JK30{$kLt!{k{iWFyyaCFr)ACu=a++vOSsFA;7=_<{Jd5z6CY8X$s%1fJuj2E zWtziT>m%$m%Jq8kxh%b&+Jr*H?b;Gsrg5wJWN_(NeCmqDYHrPpfh+NWGt0@n=8(*R z|7yOJGxn~!g^bvX>di8}_jMsO*fRtErv7Y}<6Gs;S=a~V5xm}(8TQ{B-3D23i-~w_ zF^G+0VTGFpF0=k_Q+op4})o|s-q;ZODO+M8z7%)Vo9TwPKD|R_$m@zt z#yEvDNEn9RJR?4>m$&PNw{hP`{Lnk6aHi5Em-@Oekkw?{M>v`0pDhU`?@XmjuG-b= z%2K`EwVWrC9pj(AYHhf3}T`pQkU3`*=mAc#tNae0Oj|p>j}zC_G~|5K2Mj6*mdP zV6{EXvG3NNdFq5K#9Wk|b7JEQhw)=yap5G}6X=(lz^5lo!)YD@JsSlTa;6CAZ=R{~ zEOw41h-2}R)R0}bn|=uc^Jgy+KlL~C$!+FRplT_3N+Af0!Xr5;3!Hb;azF@_SEZfK z4j$Z)6QjdwjbnncapU-YUn91Bt>iTR#Xv3er>uyLH4>VV8m9f(wcv)1Cy)BK6o(wl zmr03GKwZGCqN*;`yiS3qDxE&biV_9*yI;5RE0y1jTDtw4YHgtEhi z>%m6c3A@Q@yc-_Cn{)Z(Idg5=jM-UtK52Gh#KG1pQF}he)=rk1ia8h2>7Vm|j^}OI zn!AvacC$2`O`2NeV3vrWgctUXiZJ#z=vV?HWs$c;eFi6p0u_%F@GvH7iT_km;8KYEowFG;)@Av_8b+rjM>o^siCzm%#9c~3 zU=**%yGTijV%3CRkN?E&TH*^6b>=X!6|Zd0GO;&K^y($P1q9wJxL}ws!&<8`QLIZ;c2G(98!nSOn zc7bkpw|rX?W2F>~CbkcnNxY;ctq*MiiK(VhANYc%4HV#GE7Uocc}VAyWQEyb$k zg6k~4@TuDUNC^XvNSP-QiyDz`_aJVvBe~3L?z$r^_WD%_jW*QPTe05Ugo$E25;Nhz zi!N+B9r$F0!)UAz`M?H*QeCTk)9@REp@^ypjTQzEqm*%KLQ{;zYX?-^u5dzZG2#2> z-|--}fuHvA+U+AfEN&g5BoCp@j;lxiMD<}xK7myG=i1+0!qYnx*(-=+0}@?D2~*=C zGl>F<*J{Dn<6ZFeb1Vr*69NuRM6i8~Lw%yhAW{DM$ucA-IT#aRbe!TaGGvA8L=4|f z@K_wXyvY*0GNsV;GD(k4Tk+*oSg=~eF~35G1^e`MfUco{9{arloYMzwR3!K75S*hf zRvtj^SUpeSpsoxluyj9z6}1}1^Cfh~+2gyl+Vf{vR`#GXEh2lvgdZ*huq*XEdQ;7Y z+FC&dlXc-<%7Z^rO{kk`!aFmSXqgo#8ANK? zZoiQvl8A;GHx{!dd^fj8N2K{&1=@2Q)-LYB=0yW;&e`#6)&@RZ$hSy7Hg88qy2%{$ z3Ib^(l8F`gDjj5HFM3ktP%g{nP=S*lxf#9`Hj$e|^vWY_^-_&FToC$ompe02Mn*^G z1v7c8R2$U&-pvFEyp`!>M6_kQihUxw`?ahOOBoK^*_X|Q{*u0lM6Sk6-V@#C|0dez z4>Agvd=);M_cLT>z6|4eD;$uSfS!q0IP^obkn6735(D|;tQ0Cr>sg7MC#j;k^}KAC z$RUs!-BK_rk&AleIleHoTT-G@kO1z|*GJ-KPWPOCJFZ$S(qpVb*j(HSIzx{dE4yDiuBJd&h?nH9372L+~LTAC<)56#m3>iHO! I*4fwr0MZC}od5s; delta 8 PcmdnCm~l_#hCMa_6Ndyi diff --git a/pkg/services/control/ir/service_grpc.pb.go b/pkg/services/control/ir/service_grpc.pb.go index 6ba214da09c0a82a003442b5b4097c03ebd6c8bc..004c82446cb205f0ebaf92d29cba085a9b3f56f7 100644 GIT binary patch delta 959 zcmZoMKkYstj?s8xybOzpo{`DM`n8Ol$@zK3B?=l`oRb5kq($6Hb8>uBOEU6P{1S6h z6+pmtvK^<)D=Es)2~I64%S=v<4@%9=FH7~y zPf3l(rWItqIxbxxqY^XoQj5^Ff^;eIz@?SAG$(K56y9vh#K}DQFBdP!TM&(tXR)|~ zT?95}@ZYkp@iS^G4UYS((3*8C$sfiI#7+5`NE!E#Un`Q<;bl zc%Vs>10-T5%Zo`PMSz{yTqNFou_h#*pO7Dk!3?p3O$8o`8p$OUItoRpg?gC5SRk>R VX>zrcFnU-_elMkgj?rjhybO!6o}tmk`n8Ok=P*k#Z*FIAXPmr`NqTYv6VK#)uH4B>IF|tV zs+;qbxfP<1=yK4XNsgS!}SQrmT%6Lcn?ymIvK?ICG`%WQ9$C4mkic<^^y7