mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore(cli): replace clibase with external coder/serpent (#12252)
This commit is contained in:
+56
-61
@@ -31,13 +31,13 @@ import (
|
||||
"github.com/coder/pretty"
|
||||
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/cli/clibase"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/cli/config"
|
||||
"github.com/coder/coder/v2/cli/gitauth"
|
||||
"github.com/coder/coder/v2/cli/telemetry"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -84,9 +84,9 @@ var (
|
||||
errUnauthenticatedURLSaved = xerrors.New(notLoggedInURLSavedMessage)
|
||||
)
|
||||
|
||||
func (r *RootCmd) Core() []*clibase.Cmd {
|
||||
func (r *RootCmd) Core() []*serpent.Cmd {
|
||||
// Please re-sort this list alphabetically if you change it!
|
||||
return []*clibase.Cmd{
|
||||
return []*serpent.Cmd{
|
||||
r.dotfiles(),
|
||||
r.externalAuth(),
|
||||
r.login(),
|
||||
@@ -132,13 +132,13 @@ func (r *RootCmd) Core() []*clibase.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootCmd) AGPL() []*clibase.Cmd {
|
||||
func (r *RootCmd) AGPL() []*serpent.Cmd {
|
||||
all := append(r.Core(), r.Server( /* Do not import coderd here. */ nil))
|
||||
return all
|
||||
}
|
||||
|
||||
// Main is the entrypoint for the Coder CLI.
|
||||
func (r *RootCmd) RunMain(subcommands []*clibase.Cmd) {
|
||||
func (r *RootCmd) RunMain(subcommands []*serpent.Cmd) {
|
||||
rand.Seed(time.Now().UnixMicro())
|
||||
|
||||
cmd, err := r.Command(subcommands)
|
||||
@@ -166,10 +166,10 @@ func (r *RootCmd) RunMain(subcommands []*clibase.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
func (r *RootCmd) Command(subcommands []*serpent.Cmd) (*serpent.Cmd, error) {
|
||||
fmtLong := `Coder %s — A tool for provisioning self-hosted development environments with Terraform.
|
||||
`
|
||||
cmd := &clibase.Cmd{
|
||||
cmd := &serpent.Cmd{
|
||||
Use: "coder [global-flags] <subcommand>",
|
||||
Long: fmt.Sprintf(fmtLong, buildinfo.Version()) + formatExamples(
|
||||
example{
|
||||
@@ -181,7 +181,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
Command: "coder templates init",
|
||||
},
|
||||
),
|
||||
Handler: func(i *clibase.Invocation) error {
|
||||
Handler: func(i *serpent.Invocation) error {
|
||||
if r.versionFlag {
|
||||
return r.version(defaultVersionInfo).Handler(i)
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
cmd.AddSubcommands(subcommands...)
|
||||
|
||||
// Set default help handler for all commands.
|
||||
cmd.Walk(func(c *clibase.Cmd) {
|
||||
cmd.Walk(func(c *serpent.Cmd) {
|
||||
if c.HelpHandler == nil {
|
||||
c.HelpHandler = helpFn()
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
|
||||
var merr error
|
||||
// Add [flags] to usage when appropriate.
|
||||
cmd.Walk(func(cmd *clibase.Cmd) {
|
||||
cmd.Walk(func(cmd *serpent.Cmd) {
|
||||
const flags = "[flags]"
|
||||
if strings.Contains(cmd.Use, flags) {
|
||||
merr = errors.Join(
|
||||
@@ -244,7 +244,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
})
|
||||
|
||||
// Add alises when appropriate.
|
||||
cmd.Walk(func(cmd *clibase.Cmd) {
|
||||
cmd.Walk(func(cmd *serpent.Cmd) {
|
||||
// TODO: we should really be consistent about naming.
|
||||
if cmd.Name() == "delete" || cmd.Name() == "remove" {
|
||||
if slices.Contains(cmd.Aliases, "rm") {
|
||||
@@ -259,7 +259,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
})
|
||||
|
||||
// Sanity-check command options.
|
||||
cmd.Walk(func(cmd *clibase.Cmd) {
|
||||
cmd.Walk(func(cmd *serpent.Cmd) {
|
||||
for _, opt := range cmd.Options {
|
||||
// Verify that every option is configurable.
|
||||
if opt.Flag == "" && opt.Env == "" {
|
||||
@@ -282,7 +282,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
var debugOptions bool
|
||||
|
||||
// Add a wrapper to every command to enable debugging options.
|
||||
cmd.Walk(func(cmd *clibase.Cmd) {
|
||||
cmd.Walk(func(cmd *serpent.Cmd) {
|
||||
h := cmd.Handler
|
||||
if h == nil {
|
||||
// We should never have a nil handler, but if we do, do not
|
||||
@@ -291,12 +291,12 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
// is required for a command such as command grouping (e.g. `users'
|
||||
// and 'groups'), then the handler should be set to the helper
|
||||
// function.
|
||||
// func(inv *clibase.Invocation) error {
|
||||
// func(inv *serpent.Invocation) error {
|
||||
// return inv.Command.HelpHandler(inv)
|
||||
// }
|
||||
return
|
||||
}
|
||||
cmd.Handler = func(i *clibase.Invocation) error {
|
||||
cmd.Handler = func(i *serpent.Invocation) error {
|
||||
if !debugOptions {
|
||||
return h(i)
|
||||
}
|
||||
@@ -318,36 +318,36 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
r.clientURL = new(url.URL)
|
||||
}
|
||||
|
||||
globalGroup := &clibase.Group{
|
||||
globalGroup := &serpent.Group{
|
||||
Name: "Global",
|
||||
Description: `Global options are applied to all commands. They can be set using environment variables or flags.`,
|
||||
}
|
||||
cmd.Options = clibase.OptionSet{
|
||||
cmd.Options = serpent.OptionSet{
|
||||
{
|
||||
Flag: varURL,
|
||||
Env: envURL,
|
||||
Description: "URL to a deployment.",
|
||||
Value: clibase.URLOf(r.clientURL),
|
||||
Value: serpent.URLOf(r.clientURL),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: "debug-options",
|
||||
Description: "Print all options, how they're set, then exit.",
|
||||
Value: clibase.BoolOf(&debugOptions),
|
||||
Value: serpent.BoolOf(&debugOptions),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: varToken,
|
||||
Env: envSessionToken,
|
||||
Description: fmt.Sprintf("Specify an authentication token. For security reasons setting %s is preferred.", envSessionToken),
|
||||
Value: clibase.StringOf(&r.token),
|
||||
Value: serpent.StringOf(&r.token),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: varAgentToken,
|
||||
Env: envAgentToken,
|
||||
Description: "An agent authentication token.",
|
||||
Value: clibase.StringOf(&r.agentToken),
|
||||
Value: serpent.StringOf(&r.agentToken),
|
||||
Hidden: true,
|
||||
Group: globalGroup,
|
||||
},
|
||||
@@ -355,7 +355,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
Flag: varAgentTokenFile,
|
||||
Env: envAgentTokenFile,
|
||||
Description: "A file containing an agent authentication token.",
|
||||
Value: clibase.StringOf(&r.agentTokenFile),
|
||||
Value: serpent.StringOf(&r.agentTokenFile),
|
||||
Hidden: true,
|
||||
Group: globalGroup,
|
||||
},
|
||||
@@ -363,7 +363,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
Flag: varAgentURL,
|
||||
Env: "CODER_AGENT_URL",
|
||||
Description: "URL for an agent to access your deployment.",
|
||||
Value: clibase.URLOf(r.agentURL),
|
||||
Value: serpent.URLOf(r.agentURL),
|
||||
Hidden: true,
|
||||
Group: globalGroup,
|
||||
},
|
||||
@@ -371,35 +371,35 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
Flag: varNoVersionCheck,
|
||||
Env: envNoVersionCheck,
|
||||
Description: "Suppress warning when client and server versions do not match.",
|
||||
Value: clibase.BoolOf(&r.noVersionCheck),
|
||||
Value: serpent.BoolOf(&r.noVersionCheck),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: varNoFeatureWarning,
|
||||
Env: envNoFeatureWarning,
|
||||
Description: "Suppress warnings about unlicensed features.",
|
||||
Value: clibase.BoolOf(&r.noFeatureWarning),
|
||||
Value: serpent.BoolOf(&r.noFeatureWarning),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: varHeader,
|
||||
Env: "CODER_HEADER",
|
||||
Description: "Additional HTTP headers added to all requests. Provide as " + `key=value` + ". Can be specified multiple times.",
|
||||
Value: clibase.StringArrayOf(&r.header),
|
||||
Value: serpent.StringArrayOf(&r.header),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: varHeaderCommand,
|
||||
Env: "CODER_HEADER_COMMAND",
|
||||
Description: "An external command that outputs additional HTTP headers added to all requests. The command must output each header as `key=value` on its own line.",
|
||||
Value: clibase.StringOf(&r.headerCommand),
|
||||
Value: serpent.StringOf(&r.headerCommand),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: varNoOpen,
|
||||
Env: "CODER_NO_OPEN",
|
||||
Description: "Suppress opening the browser after logging in.",
|
||||
Value: clibase.BoolOf(&r.noOpen),
|
||||
Value: serpent.BoolOf(&r.noOpen),
|
||||
Hidden: true,
|
||||
Group: globalGroup,
|
||||
},
|
||||
@@ -408,7 +408,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
Env: "CODER_FORCE_TTY",
|
||||
Hidden: true,
|
||||
Description: "Force the use of a TTY.",
|
||||
Value: clibase.BoolOf(&r.forceTTY),
|
||||
Value: serpent.BoolOf(&r.forceTTY),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
@@ -416,20 +416,20 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
FlagShorthand: "v",
|
||||
Env: "CODER_VERBOSE",
|
||||
Description: "Enable verbose output.",
|
||||
Value: clibase.BoolOf(&r.verbose),
|
||||
Value: serpent.BoolOf(&r.verbose),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: varDisableDirect,
|
||||
Env: "CODER_DISABLE_DIRECT_CONNECTIONS",
|
||||
Description: "Disable direct (P2P) connections to workspaces.",
|
||||
Value: clibase.BoolOf(&r.disableDirect),
|
||||
Value: serpent.BoolOf(&r.disableDirect),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
Flag: "debug-http",
|
||||
Description: "Debug codersdk HTTP requests.",
|
||||
Value: clibase.BoolOf(&r.debugHTTP),
|
||||
Value: serpent.BoolOf(&r.debugHTTP),
|
||||
Group: globalGroup,
|
||||
Hidden: true,
|
||||
},
|
||||
@@ -438,7 +438,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
Env: "CODER_CONFIG_DIR",
|
||||
Description: "Path to the global `coder` config directory.",
|
||||
Default: config.DefaultDir(),
|
||||
Value: clibase.StringOf(&r.globalConfig),
|
||||
Value: serpent.StringOf(&r.globalConfig),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
@@ -446,7 +446,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
FlagShorthand: "z",
|
||||
Env: "CODER_ORGANIZATION",
|
||||
Description: "Select which organization (uuid or name) to use This overrides what is present in the config file.",
|
||||
Value: clibase.StringOf(&r.organizationSelect),
|
||||
Value: serpent.StringOf(&r.organizationSelect),
|
||||
Group: globalGroup,
|
||||
},
|
||||
{
|
||||
@@ -455,16 +455,11 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
|
||||
// They have two Coder CLIs, and want to tell the difference by running
|
||||
// the same base command.
|
||||
Description: "Run the version command. Useful for v1 customers migrating to v2.",
|
||||
Value: clibase.BoolOf(&r.versionFlag),
|
||||
Value: serpent.BoolOf(&r.versionFlag),
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
err := cmd.PrepareAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
@@ -490,7 +485,7 @@ type RootCmd struct {
|
||||
noFeatureWarning bool
|
||||
}
|
||||
|
||||
func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) {
|
||||
func addTelemetryHeader(client *codersdk.Client, inv *serpent.Invocation) {
|
||||
transport, ok := client.HTTPClient.Transport.(*codersdk.HeaderTransport)
|
||||
if !ok {
|
||||
transport = &codersdk.HeaderTransport{
|
||||
@@ -502,7 +497,7 @@ func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) {
|
||||
|
||||
var topts []telemetry.Option
|
||||
for _, opt := range inv.Command.FullOptions() {
|
||||
if opt.ValueSource == clibase.ValueSourceNone || opt.ValueSource == clibase.ValueSourceDefault {
|
||||
if opt.ValueSource == serpent.ValueSourceNone || opt.ValueSource == serpent.ValueSourceDefault {
|
||||
continue
|
||||
}
|
||||
topts = append(topts, telemetry.Option{
|
||||
@@ -534,28 +529,28 @@ func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) {
|
||||
|
||||
// InitClient sets client to a new client.
|
||||
// It reads from global configuration files if flags are not set.
|
||||
func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
|
||||
return clibase.Chain(
|
||||
func (r *RootCmd) InitClient(client *codersdk.Client) serpent.MiddlewareFunc {
|
||||
return serpent.Chain(
|
||||
r.initClientInternal(client, false),
|
||||
// By default, we should print warnings in addition to initializing the client
|
||||
r.PrintWarnings(client),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *RootCmd) InitClientMissingTokenOK(client *codersdk.Client) clibase.MiddlewareFunc {
|
||||
func (r *RootCmd) InitClientMissingTokenOK(client *codersdk.Client) serpent.MiddlewareFunc {
|
||||
return r.initClientInternal(client, true)
|
||||
}
|
||||
|
||||
// nolint: revive
|
||||
func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing bool) clibase.MiddlewareFunc {
|
||||
func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing bool) serpent.MiddlewareFunc {
|
||||
if client == nil {
|
||||
panic("client is nil")
|
||||
}
|
||||
if r == nil {
|
||||
panic("root is nil")
|
||||
}
|
||||
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
|
||||
return func(inv *clibase.Invocation) error {
|
||||
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
|
||||
return func(inv *serpent.Invocation) error {
|
||||
conf := r.createConfig()
|
||||
var err error
|
||||
if r.clientURL == nil || r.clientURL.String() == "" {
|
||||
@@ -604,15 +599,15 @@ func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootCmd) PrintWarnings(client *codersdk.Client) clibase.MiddlewareFunc {
|
||||
func (r *RootCmd) PrintWarnings(client *codersdk.Client) serpent.MiddlewareFunc {
|
||||
if client == nil {
|
||||
panic("client is nil")
|
||||
}
|
||||
if r == nil {
|
||||
panic("root is nil")
|
||||
}
|
||||
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
|
||||
return func(inv *clibase.Invocation) error {
|
||||
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
|
||||
return func(inv *serpent.Invocation) error {
|
||||
// We send these requests in parallel to minimize latency.
|
||||
var (
|
||||
versionErr = make(chan error)
|
||||
@@ -715,7 +710,7 @@ func (r *RootCmd) createAgentClient() (*agentsdk.Client, error) {
|
||||
}
|
||||
|
||||
// CurrentOrganization returns the currently active organization for the authenticated user.
|
||||
func CurrentOrganization(r *RootCmd, inv *clibase.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
|
||||
func CurrentOrganization(r *RootCmd, inv *serpent.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
|
||||
conf := r.createConfig()
|
||||
selected := r.organizationSelect
|
||||
if selected == "" && conf.Organization().Exists() {
|
||||
@@ -795,7 +790,7 @@ func (r *RootCmd) createConfig() config.Root {
|
||||
}
|
||||
|
||||
// isTTY returns whether the passed reader is a TTY or not.
|
||||
func isTTY(inv *clibase.Invocation) bool {
|
||||
func isTTY(inv *serpent.Invocation) bool {
|
||||
// If the `--force-tty` command is available, and set,
|
||||
// assume we're in a tty. This is primarily for cases on Windows
|
||||
// where we may not be able to reliably detect this automatically (ie, tests)
|
||||
@@ -811,16 +806,16 @@ func isTTY(inv *clibase.Invocation) bool {
|
||||
}
|
||||
|
||||
// isTTYOut returns whether the passed reader is a TTY or not.
|
||||
func isTTYOut(inv *clibase.Invocation) bool {
|
||||
func isTTYOut(inv *serpent.Invocation) bool {
|
||||
return isTTYWriter(inv, inv.Stdout)
|
||||
}
|
||||
|
||||
// isTTYErr returns whether the passed reader is a TTY or not.
|
||||
func isTTYErr(inv *clibase.Invocation) bool {
|
||||
func isTTYErr(inv *serpent.Invocation) bool {
|
||||
return isTTYWriter(inv, inv.Stderr)
|
||||
}
|
||||
|
||||
func isTTYWriter(inv *clibase.Invocation, writer io.Writer) bool {
|
||||
func isTTYWriter(inv *serpent.Invocation, writer io.Writer) bool {
|
||||
// If the `--force-tty` command is available, and set,
|
||||
// assume we're in a tty. This is primarily for cases on Windows
|
||||
// where we may not be able to reliably detect this automatically (ie, tests)
|
||||
@@ -871,7 +866,7 @@ func formatExamples(examples ...example) string {
|
||||
// is detected. forceCheck is a test flag and should always be false in production.
|
||||
//
|
||||
//nolint:revive
|
||||
func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client, clientVersion string) error {
|
||||
func (r *RootCmd) checkVersions(i *serpent.Invocation, client *codersdk.Client, clientVersion string) error {
|
||||
if r.noVersionCheck {
|
||||
return nil
|
||||
}
|
||||
@@ -905,7 +900,7 @@ func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RootCmd) checkWarnings(i *clibase.Invocation, client *codersdk.Client) error {
|
||||
func (r *RootCmd) checkWarnings(i *serpent.Invocation, client *codersdk.Client) error {
|
||||
if r.noFeatureWarning {
|
||||
return nil
|
||||
}
|
||||
@@ -931,7 +926,7 @@ func (r *RootCmd) checkWarnings(i *clibase.Invocation, client *codersdk.Client)
|
||||
}
|
||||
|
||||
// Verbosef logs a message if verbose mode is enabled.
|
||||
func (r *RootCmd) Verbosef(inv *clibase.Invocation, fmtStr string, args ...interface{}) {
|
||||
func (r *RootCmd) Verbosef(inv *serpent.Invocation, fmtStr string, args ...interface{}) {
|
||||
if r.verbose {
|
||||
cliui.Infof(inv.Stdout, fmtStr, args...)
|
||||
}
|
||||
@@ -1127,7 +1122,7 @@ func cliHumanFormatError(from string, err error, opts *formatOpts) (string, bool
|
||||
return formatCoderSDKError(from, sdkError, opts), true
|
||||
}
|
||||
|
||||
if cmdErr, ok := err.(*clibase.RunCommandError); ok {
|
||||
if cmdErr, ok := err.(*serpent.RunCommandError); ok {
|
||||
// no need to pass the "from" context to this since it is always
|
||||
// top level. We care about what is below this.
|
||||
return formatRunCommandError(cmdErr, opts), true
|
||||
@@ -1199,7 +1194,7 @@ func formatMultiError(from string, multi []error, opts *formatOpts) string {
|
||||
// broad, as it contains all errors that occur when running a command.
|
||||
// If you know the error is something else, like a codersdk.Error, make a new
|
||||
// formatter and add it to cliHumanFormatError function.
|
||||
func formatRunCommandError(err *clibase.RunCommandError, opts *formatOpts) string {
|
||||
func formatRunCommandError(err *serpent.RunCommandError, opts *formatOpts) string {
|
||||
var str strings.Builder
|
||||
_, _ = str.WriteString(pretty.Sprint(headLineStyle(), fmt.Sprintf("Encountered an error running %q", err.Cmd.FullName())))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user