ctr: add commands package with shared utility functions
Signed-off-by: Jess Valarezo <valarezo.jessica@gmail.com>
This commit is contained in:
parent
04659d9405
commit
a19a20303a
84
cmd/ctr/commands/commands.go
Normal file
84
cmd/ctr/commands/commands.go
Normal file
@ -0,0 +1,84 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
// SnapshotterFlags are cli flags specifying snapshotter names
|
||||
SnapshotterFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "snapshotter",
|
||||
Usage: "snapshotter name. Empty value stands for the daemon default value.",
|
||||
Value: containerd.DefaultSnapshotter,
|
||||
},
|
||||
}
|
||||
|
||||
// LabelFlag is a cli flag specifying labels
|
||||
LabelFlag = cli.StringSliceFlag{
|
||||
Name: "label",
|
||||
Usage: "labels to attach to the image",
|
||||
}
|
||||
|
||||
// RegistryFlags are cli flags specifying registry options
|
||||
RegistryFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "skip-verify,k",
|
||||
Usage: "skip SSL certificate validation",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "plain-http",
|
||||
Usage: "allow connections using plain HTTP",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "user,u",
|
||||
Usage: "user[:password] Registry user and password",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "refresh",
|
||||
Usage: "refresh token for authorization server",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// ObjectWithLabelArgs returns the first arg and a LabelArgs object
|
||||
func ObjectWithLabelArgs(clicontext *cli.Context) (string, map[string]string) {
|
||||
var (
|
||||
first = clicontext.Args().First()
|
||||
labelStrings = clicontext.Args().Tail()
|
||||
)
|
||||
|
||||
return first, LabelArgs(labelStrings)
|
||||
}
|
||||
|
||||
// LabelArgs returns a map of label key,value pairs
|
||||
func LabelArgs(labelStrings []string) map[string]string {
|
||||
labels := make(map[string]string, len(labelStrings))
|
||||
for _, label := range labelStrings {
|
||||
parts := strings.SplitN(label, "=", 2)
|
||||
key := parts[0]
|
||||
value := "true"
|
||||
if len(parts) > 1 {
|
||||
value = parts[1]
|
||||
}
|
||||
|
||||
labels[key] = value
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
// PrintAsJSON prints input in JSON format
|
||||
func PrintAsJSON(x interface{}) {
|
||||
b, err := json.MarshalIndent(x, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "can't marshal %+v as a JSON string: %v\n", x, err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
@ -10,6 +10,7 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/log"
|
||||
@ -212,7 +213,7 @@ var (
|
||||
Description: `Labels blobs in the content store`,
|
||||
Flags: []cli.Flag{},
|
||||
Action: func(context *cli.Context) error {
|
||||
object, labels := objectWithLabelArgs(context)
|
||||
object, labels := commands.ObjectWithLabelArgs(context)
|
||||
client, ctx, cancel, err := newClient(context)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
@ -39,7 +40,7 @@ not use this implementation as a guide. The end goal should be having metadata,
|
||||
content and snapshots ready for a direct use via the 'ctr run'.
|
||||
|
||||
Most of this is experimental and there are few leaps to make this work.`,
|
||||
Flags: append(registryFlags, labelFlag),
|
||||
Flags: append(commands.RegistryFlags, commands.LabelFlag),
|
||||
Action: func(clicontext *cli.Context) error {
|
||||
var (
|
||||
ref = clicontext.Args().First()
|
||||
@ -82,7 +83,7 @@ func fetch(ref string, cliContext *cli.Context) (containerd.Image, error) {
|
||||
})
|
||||
|
||||
log.G(pctx).WithField("image", ref).Debug("fetching")
|
||||
labels := labelArgs(cliContext.StringSlice("label"))
|
||||
labels := commands.LabelArgs(cliContext.StringSlice("label"))
|
||||
img, err := client.Pull(pctx, ref,
|
||||
containerd.WithPullLabels(labels),
|
||||
containerd.WithResolver(resolver),
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -17,7 +18,7 @@ var fetchObjectCommand = cli.Command{
|
||||
Usage: "retrieve objects from a remote",
|
||||
ArgsUsage: "[flags] <remote> <object> [<hint>, ...]",
|
||||
Description: `Fetch objects by identifier from a remote.`,
|
||||
Flags: registryFlags,
|
||||
Flags: commands.RegistryFlags,
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
ref = context.Args().First()
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/log"
|
||||
@ -127,7 +128,7 @@ var imagesSetLabelsCommand = cli.Command{
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
replaceAll = context.Bool("replace-all")
|
||||
name, labels = objectWithLabelArgs(context)
|
||||
name, labels = commands.ObjectWithLabelArgs(context)
|
||||
)
|
||||
client, ctx, cancel, err := newClient(context)
|
||||
if err != nil {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -21,14 +22,14 @@ var imagesImportCommand = cli.Command{
|
||||
Value: "",
|
||||
Usage: "reference object e.g. tag@digest (default: use the object specified in ref)",
|
||||
},
|
||||
labelFlag,
|
||||
commands.LabelFlag,
|
||||
},
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
ref = context.Args().First()
|
||||
in = context.Args().Get(1)
|
||||
refObject = context.String("ref-object")
|
||||
labels = labelArgs(context.StringSlice("label"))
|
||||
labels = commands.LabelArgs(context.StringSlice("label"))
|
||||
)
|
||||
client, ctx, cancel, err := newClient(context)
|
||||
if err != nil {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -27,7 +28,7 @@ var containerInfoCommand = cli.Command{
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printAsJSON(info)
|
||||
commands.PrintAsJSON(info)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
@ -15,7 +16,7 @@ var containersSetLabelsCommand = cli.Command{
|
||||
Description: "Set and clear labels for a container.",
|
||||
Flags: []cli.Flag{},
|
||||
Action: func(context *cli.Context) error {
|
||||
containerID, labels := objectWithLabelArgs(context)
|
||||
containerID, labels := commands.ObjectWithLabelArgs(context)
|
||||
if containerID == "" {
|
||||
return errors.New("please specify a container")
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/pkg/errors"
|
||||
@ -30,7 +31,7 @@ var namespacesCreateCommand = cli.Command{
|
||||
ArgsUsage: "[flags] <name> [<key>=<value]",
|
||||
Description: "Create a new namespace. It must be unique.",
|
||||
Action: func(context *cli.Context) error {
|
||||
namespace, labels := objectWithLabelArgs(context)
|
||||
namespace, labels := commands.ObjectWithLabelArgs(context)
|
||||
if namespace == "" {
|
||||
return errors.New("please specify a namespace")
|
||||
}
|
||||
@ -51,7 +52,7 @@ var namespacesSetLabelsCommand = cli.Command{
|
||||
Description: "Set and clear labels for a namespace.",
|
||||
Flags: []cli.Flag{},
|
||||
Action: func(context *cli.Context) error {
|
||||
namespace, labels := objectWithLabelArgs(context)
|
||||
namespace, labels := commands.ObjectWithLabelArgs(context)
|
||||
if namespace == "" {
|
||||
return errors.New("please specify a namespace")
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -20,7 +21,7 @@ command. As part of this process, we do the following:
|
||||
2. Prepare the snapshot filesystem with the pulled resources.
|
||||
3. Register metadata for the image.
|
||||
`,
|
||||
Flags: append(registryFlags, append(snapshotterFlags, labelFlag)...),
|
||||
Flags: append(commands.RegistryFlags, append(commands.SnapshotterFlags, commands.LabelFlag)...),
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
ref = context.Args().First()
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/containerd/containerd/progress"
|
||||
@ -37,7 +38,7 @@ var pushCommand = cli.Command{
|
||||
creating the associated configuration, and creating the manifest
|
||||
which references those resources.
|
||||
`,
|
||||
Flags: append(registryFlags, cli.StringFlag{
|
||||
Flags: append(commands.RegistryFlags, cli.StringFlag{
|
||||
Name: "manifest",
|
||||
Usage: "digest of manifest",
|
||||
}, cli.StringFlag{
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/log"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
@ -15,7 +16,7 @@ var pushObjectCommand = cli.Command{
|
||||
Usage: "push an object to a remote",
|
||||
ArgsUsage: "[flags] <remote> <object> <type>",
|
||||
Description: `Push objects by identifier to a remote.`,
|
||||
Flags: registryFlags,
|
||||
Flags: commands.RegistryFlags,
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
ref = context.Args().Get(0)
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/log"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/urfave/cli"
|
||||
@ -21,7 +22,7 @@ var rootfsUnpackCommand = cli.Command{
|
||||
Name: "unpack",
|
||||
Usage: "unpack applies layers from a manifest to a snapshot",
|
||||
ArgsUsage: "[flags] <digest>",
|
||||
Flags: snapshotterFlags,
|
||||
Flags: commands.SnapshotterFlags,
|
||||
Action: func(context *cli.Context) error {
|
||||
dgst, err := digest.Parse(context.Args().First())
|
||||
if err != nil {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/containers"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
@ -100,7 +101,7 @@ var runCommand = cli.Command{
|
||||
Name: "detach,d",
|
||||
Usage: "detach from the task after it has started execution",
|
||||
},
|
||||
}, snapshotterFlags...),
|
||||
}, commands.SnapshotterFlags...),
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
err error
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
@ -76,7 +77,7 @@ func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli
|
||||
opts []containerd.SpecOpts
|
||||
cOpts []containerd.NewContainerOpts
|
||||
)
|
||||
cOpts = append(cOpts, containerd.WithContainerLabels(labelArgs(context.StringSlice("label"))))
|
||||
cOpts = append(cOpts, containerd.WithContainerLabels(commands.LabelArgs(context.StringSlice("label"))))
|
||||
if context.Bool("rootfs") {
|
||||
opts = append(opts, containerd.WithRootFSPath(ref))
|
||||
} else {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/containers"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/log"
|
||||
@ -92,7 +93,7 @@ func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli
|
||||
labelStrings = context.StringSlice("label")
|
||||
)
|
||||
|
||||
labels := labelArgs(labelStrings)
|
||||
labels := commands.LabelArgs(labelStrings)
|
||||
|
||||
// TODO(mlaventure): get base image once we have a snapshotter
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
shim "github.com/containerd/containerd/linux/shim/v1"
|
||||
"github.com/containerd/typeurl"
|
||||
protobuf "github.com/gogo/protobuf/types"
|
||||
@ -182,7 +183,7 @@ var shimStateCommand = cli.Command{
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printAsJSON(r)
|
||||
commands.PrintAsJSON(r)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/mount"
|
||||
"github.com/containerd/containerd/progress"
|
||||
"github.com/containerd/containerd/snapshot"
|
||||
@ -17,7 +18,7 @@ import (
|
||||
var snapshotCommand = cli.Command{
|
||||
Name: "snapshot",
|
||||
Usage: "snapshot management",
|
||||
Flags: snapshotterFlags,
|
||||
Flags: commands.SnapshotterFlags,
|
||||
Subcommands: cli.Commands{
|
||||
listSnapshotCommand,
|
||||
usageSnapshotCommand,
|
||||
@ -325,7 +326,7 @@ var infoSnapshotCommand = cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
printAsJSON(info)
|
||||
commands.PrintAsJSON(info)
|
||||
|
||||
return nil
|
||||
},
|
||||
@ -338,7 +339,7 @@ var labelSnapshotCommand = cli.Command{
|
||||
Description: `Labels snapshots in the snapshotter`,
|
||||
Flags: []cli.Flag{},
|
||||
Action: func(context *cli.Context) error {
|
||||
key, labels := objectWithLabelArgs(context)
|
||||
key, labels := commands.ObjectWithLabelArgs(context)
|
||||
client, ctx, cancel, err := newClient(context)
|
||||
if err != nil {
|
||||
return err
|
||||
|
106
cmd/ctr/utils.go
106
cmd/ctr/utils.go
@ -5,7 +5,6 @@ import (
|
||||
gocontext "context"
|
||||
"crypto/tls"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -18,54 +17,15 @@ import (
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/containerd/containerd/remotes"
|
||||
"github.com/containerd/containerd/remotes/docker"
|
||||
"github.com/containerd/containerd/rootfs"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
snapshotterFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "snapshotter",
|
||||
Usage: "snapshotter name. Empty value stands for the daemon default value.",
|
||||
Value: containerd.DefaultSnapshotter,
|
||||
},
|
||||
}
|
||||
|
||||
labelFlag = cli.StringSliceFlag{
|
||||
Name: "label",
|
||||
Usage: "labels to attach to the pulled image",
|
||||
}
|
||||
|
||||
registryFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "skip-verify,k",
|
||||
Usage: "skip SSL certificate validation",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "plain-http",
|
||||
Usage: "allow connections using plain HTTP",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "user,u",
|
||||
Usage: "user[:password] Registry user and password",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "refresh",
|
||||
Usage: "refresh token for authorization server",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// appContext returns the context for a command. Should only be called once per
|
||||
// command, near the start.
|
||||
//
|
||||
@ -114,39 +74,6 @@ func passwordPrompt() (string, error) {
|
||||
return string(line), nil
|
||||
}
|
||||
|
||||
func getImageLayers(ctx gocontext.Context, image images.Image, cs content.Store) ([]rootfs.Layer, error) {
|
||||
p, err := content.ReadBlob(ctx, cs, image.Target.Digest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read manifest blob")
|
||||
}
|
||||
|
||||
var manifest ocispec.Manifest
|
||||
if err := json.Unmarshal(p, &manifest); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal manifest")
|
||||
}
|
||||
|
||||
diffIDs, err := image.RootFS(ctx, cs, platforms.Default())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to resolve rootfs")
|
||||
}
|
||||
|
||||
if len(diffIDs) != len(manifest.Layers) {
|
||||
return nil, errors.Errorf("mismatched image rootfs and manifest layers")
|
||||
}
|
||||
|
||||
layers := make([]rootfs.Layer, len(diffIDs))
|
||||
for i := range diffIDs {
|
||||
layers[i].Diff = ocispec.Descriptor{
|
||||
// TODO: derive media type from compressed type
|
||||
MediaType: ocispec.MediaTypeImageLayer,
|
||||
Digest: diffIDs[i],
|
||||
}
|
||||
layers[i].Blob = manifest.Layers[i]
|
||||
}
|
||||
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
// getResolver prepares the resolver from the environment and options.
|
||||
func getResolver(ctx gocontext.Context, clicontext *cli.Context) (remotes.Resolver, error) {
|
||||
username := clicontext.String("user")
|
||||
@ -312,36 +239,3 @@ func replaceOrAppendEnvValues(defaults, overrides []string) []string {
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
func objectWithLabelArgs(clicontext *cli.Context) (string, map[string]string) {
|
||||
var (
|
||||
namespace = clicontext.Args().First()
|
||||
labelStrings = clicontext.Args().Tail()
|
||||
)
|
||||
|
||||
return namespace, labelArgs(labelStrings)
|
||||
}
|
||||
|
||||
func labelArgs(labelStrings []string) map[string]string {
|
||||
labels := make(map[string]string, len(labelStrings))
|
||||
for _, label := range labelStrings {
|
||||
parts := strings.SplitN(label, "=", 2)
|
||||
key := parts[0]
|
||||
value := "true"
|
||||
if len(parts) > 1 {
|
||||
value = parts[1]
|
||||
}
|
||||
|
||||
labels[key] = value
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func printAsJSON(x interface{}) {
|
||||
b, err := json.MarshalIndent(x, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "can't marshal %+v as a JSON string: %v\n", x, err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user