forked from TrueCloudLab/frostfs-node
[#733] cli: Add control ir remove-container
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
This commit is contained in:
parent
f152f5d553
commit
188ab89bc9
15 changed files with 277 additions and 16 deletions
15
cmd/frostfs-cli/modules/control/ir.go
Normal file
15
cmd/frostfs-cli/modules/control/ir.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package control
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
var irCmd = &cobra.Command{
|
||||||
|
Use: "ir",
|
||||||
|
Short: "Operations with inner ring nodes",
|
||||||
|
Long: "Operations with inner ring nodes",
|
||||||
|
}
|
||||||
|
|
||||||
|
func initControlIRCmd() {
|
||||||
|
irCmd.AddCommand(removeContainerCmd)
|
||||||
|
|
||||||
|
initControlIRRemoveContainerCmd()
|
||||||
|
}
|
94
cmd/frostfs-cli/modules/control/ir_remove_container.go
Normal file
94
cmd/frostfs-cli/modules/control/ir_remove_container.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ func init() {
|
||||||
dropObjectsCmd,
|
dropObjectsCmd,
|
||||||
shardsCmd,
|
shardsCmd,
|
||||||
synchronizeTreeCmd,
|
synchronizeTreeCmd,
|
||||||
|
irCmd,
|
||||||
)
|
)
|
||||||
|
|
||||||
initControlHealthCheckCmd()
|
initControlHealthCheckCmd()
|
||||||
|
@ -40,4 +41,5 @@ func init() {
|
||||||
initControlDropObjectsCmd()
|
initControlDropObjectsCmd()
|
||||||
initControlShardsCmd()
|
initControlShardsCmd()
|
||||||
initControlSynchronizeTreeCmd()
|
initControlSynchronizeTreeCmd()
|
||||||
|
initControlIRCmd()
|
||||||
}
|
}
|
||||||
|
|
|
@ -520,7 +520,7 @@ func (s *Server) initGRPCServer(cfg *viper.Viper) error {
|
||||||
p.SetPrivateKey(*s.key)
|
p.SetPrivateKey(*s.key)
|
||||||
p.SetHealthChecker(s)
|
p.SetHealthChecker(s)
|
||||||
|
|
||||||
controlSvc := controlsrv.New(p,
|
controlSvc := controlsrv.New(p, s.containerClient,
|
||||||
controlsrv.WithAllowedKeys(authKeys),
|
controlsrv.WithAllowedKeys(authKeys),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -581,6 +581,7 @@ func (s *Server) initClientsFromMorph() (*serverMorphClients, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
s.containerClient = result.CnrClient
|
||||||
|
|
||||||
s.netmapClient, err = nmClient.NewFromMorph(s.morphClient, s.contracts.netmap, fee, nmClient.TryNotary(), nmClient.AsAlphabet())
|
s.netmapClient, err = nmClient.NewFromMorph(s.morphClient, s.contracts.netmap, fee, nmClient.TryNotary(), nmClient.AsAlphabet())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
||||||
auditClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/audit"
|
auditClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/audit"
|
||||||
balanceClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/balance"
|
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"
|
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/event"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/subscriber"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/subscriber"
|
||||||
|
@ -56,6 +57,7 @@ type (
|
||||||
balanceClient *balanceClient.Client
|
balanceClient *balanceClient.Client
|
||||||
netmapClient *nmClient.Client
|
netmapClient *nmClient.Client
|
||||||
persistate *state.PersistentStorage
|
persistate *state.PersistentStorage
|
||||||
|
containerClient *container.Client
|
||||||
|
|
||||||
// metrics
|
// metrics
|
||||||
metrics *metrics.InnerRingServiceMetrics
|
metrics *metrics.InnerRingServiceMetrics
|
||||||
|
|
|
@ -60,7 +60,7 @@ func (d *DeletePrm) SetToken(token []byte) {
|
||||||
//
|
//
|
||||||
// If TryNotary is provided, calls notary contract.
|
// If TryNotary is provided, calls notary contract.
|
||||||
func (c *Client) Delete(p DeletePrm) error {
|
func (c *Client) Delete(p DeletePrm) error {
|
||||||
if len(p.signature) == 0 {
|
if len(p.signature) == 0 && !p.IsControl() {
|
||||||
return errNilArgument
|
return errNilArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,12 @@ type InvokePrmOptional struct {
|
||||||
// `validUntilBlock` values by all notification
|
// `validUntilBlock` values by all notification
|
||||||
// receivers.
|
// receivers.
|
||||||
hash *util.Uint256
|
hash *util.Uint256
|
||||||
|
// controlTX controls whether the invoke method will use a rounded
|
||||||
|
// block height value, which is useful for control transactions which
|
||||||
|
// are required to be produced by all nodes with very high probability.
|
||||||
|
// It's only used by notary transactions and it affects only the
|
||||||
|
// computation of `validUntilBlock` values.
|
||||||
|
controlTX bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetHash sets optional hash of the transaction.
|
// SetHash sets optional hash of the transaction.
|
||||||
|
@ -104,6 +110,16 @@ func (i *InvokePrmOptional) SetHash(hash util.Uint256) {
|
||||||
i.hash = &hash
|
i.hash = &hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetControlTX sets whether a control transaction will be used.
|
||||||
|
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.
|
// Invoke calls Invoke method of Client with static internal script hash and fee.
|
||||||
// Supported args types are the same as in Client.
|
// Supported args types are the same as in Client.
|
||||||
//
|
//
|
||||||
|
|
|
@ -32,3 +32,22 @@ func (w *healthCheckResponseWrapper) FromGRPCMessage(m grpc.Message) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type removeContainerResponseWrapper struct {
|
||||||
|
m *RemoveContainerResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *removeContainerResponseWrapper) ToGRPCMessage() grpc.Message {
|
||||||
|
return w.m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *removeContainerResponseWrapper) FromGRPCMessage(m grpc.Message) error {
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
w.m, ok = m.(*RemoveContainerResponse)
|
||||||
|
if !ok {
|
||||||
|
return message.NewUnexpectedMessageType(m, w.m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ const serviceName = "ircontrol.ControlService"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rpcHealthCheck = "HealthCheck"
|
rpcHealthCheck = "HealthCheck"
|
||||||
|
rpcRemoveContainer = "RemoveContainer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealthCheck executes ControlService.HealthCheck RPC.
|
// HealthCheck executes ControlService.HealthCheck RPC.
|
||||||
|
@ -32,3 +33,24 @@ func HealthCheck(
|
||||||
|
|
||||||
return wResp.m, nil
|
return wResp.m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RemoveContainer(
|
||||||
|
cli *client.Client,
|
||||||
|
req *RemoveContainerRequest,
|
||||||
|
opts ...client.CallOption,
|
||||||
|
) (*RemoveContainerResponse, error) {
|
||||||
|
wResp := &removeContainerResponseWrapper{
|
||||||
|
m: new(RemoveContainerResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
wReq := &requestWrapper{
|
||||||
|
m: req,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.SendUnary(cli, common.CallMethodInfoUnary(serviceName, rpcRemoveContainer), wReq, wResp, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wResp.m, nil
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,13 @@ package control
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container"
|
||||||
control "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir"
|
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/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
@ -32,3 +37,63 @@ func (s *Server) HealthCheck(_ context.Context, req *control.HealthCheckRequest)
|
||||||
|
|
||||||
return resp, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package control
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is an entity that serves
|
// Server is an entity that serves
|
||||||
|
@ -13,6 +15,8 @@ type Server struct {
|
||||||
prm Prm
|
prm Prm
|
||||||
|
|
||||||
allowedKeys [][]byte
|
allowedKeys [][]byte
|
||||||
|
|
||||||
|
containerClient *container.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func panicOnPrmValue(n string, v any) {
|
func panicOnPrmValue(n string, v any) {
|
||||||
|
@ -29,7 +33,7 @@ func panicOnPrmValue(n string, v any) {
|
||||||
// Forms white list from all keys specified via
|
// Forms white list from all keys specified via
|
||||||
// WithAllowedKeys option and a public key of
|
// WithAllowedKeys option and a public key of
|
||||||
// the parameterized private key.
|
// the parameterized private key.
|
||||||
func New(prm Prm, opts ...Option) *Server {
|
func New(prm Prm, containerClient *container.Client, opts ...Option) *Server {
|
||||||
// verify required parameters
|
// verify required parameters
|
||||||
switch {
|
switch {
|
||||||
case prm.healthChecker == nil:
|
case prm.healthChecker == nil:
|
||||||
|
@ -47,5 +51,6 @@ func New(prm Prm, opts ...Option) *Server {
|
||||||
prm: prm,
|
prm: prm,
|
||||||
|
|
||||||
allowedKeys: append(o.allowedKeys, prm.key.PublicKey().Bytes()),
|
allowedKeys: append(o.allowedKeys, prm.key.PublicKey().Bytes()),
|
||||||
|
containerClient: containerClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
pkg/services/control/ir/service.pb.go
generated
BIN
pkg/services/control/ir/service.pb.go
generated
Binary file not shown.
|
@ -10,6 +10,8 @@ option go_package = "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/ir/
|
||||||
service ControlService {
|
service ControlService {
|
||||||
// Performs health check of the IR node.
|
// Performs health check of the IR node.
|
||||||
rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse);
|
rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse);
|
||||||
|
// Forces a container removal to be signaled by the IR node with high probability.
|
||||||
|
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health check request.
|
// Health check request.
|
||||||
|
@ -41,3 +43,21 @@ message HealthCheckResponse {
|
||||||
// Body signature.
|
// Body signature.
|
||||||
Signature signature = 2;
|
Signature signature = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message RemoveContainerRequest {
|
||||||
|
message Body{
|
||||||
|
bytes container_id = 1;
|
||||||
|
bytes owner = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Body body = 1;
|
||||||
|
Signature signature = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveContainerResponse {
|
||||||
|
message Body{}
|
||||||
|
|
||||||
|
Body body = 1;
|
||||||
|
Signature signature = 2;
|
||||||
|
}
|
BIN
pkg/services/control/ir/service_frostfs.pb.go
generated
BIN
pkg/services/control/ir/service_frostfs.pb.go
generated
Binary file not shown.
BIN
pkg/services/control/ir/service_grpc.pb.go
generated
BIN
pkg/services/control/ir/service_grpc.pb.go
generated
Binary file not shown.
Loading…
Reference in a new issue