feat: initial commit

This commit is contained in:
2023-12-13 20:07:22 +01:00
commit 5d0311b731
79 changed files with 3143 additions and 0 deletions

View File

@ -0,0 +1,59 @@
package client
import (
"os"
"forge.cadoles.com/arcad/arcast/internal/command/client/flag"
"forge.cadoles.com/arcad/arcast/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func Cast() *cli.Command {
return &cli.Command{
Name: "cast",
ArgsUsage: "<url>",
Flags: flag.ComposeFlags(
playerFlags...,
),
Action: func(ctx *cli.Context) error {
baseFlags := flag.GetBaseFlags(ctx)
url := ctx.Args().First()
if url == "" {
return errors.New("you must specify an url to cast")
}
statuses := make([]wrappedStatus, 0)
err := forEachPlayer(ctx, func(cl *client.Client, playerAddr string) error {
status, err := cl.Cast(ctx.Context, playerAddr, url)
if err != nil {
return errors.Wrap(err, "could not cast")
}
statuses = append(statuses, wrappedStatus{
Status: status.Status,
URL: status.URL,
Title: status.Title,
ID: status.ID,
Address: playerAddr,
})
return nil
})
if err != nil {
return errors.WithStack(err)
}
hints := wrappedStatusHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, AsAnySlice(statuses...)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,92 @@
package flag
import (
"fmt"
"os"
"strings"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
_ "gitlab.com/wpetit/goweb/cli/format/json"
"gitlab.com/wpetit/goweb/cli/format/table"
)
func ComposeFlags(flags ...cli.Flag) []cli.Flag {
baseFlags := []cli.Flag{
&cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Usage: fmt.Sprintf("use `FORMAT` as output format (available: %s)", format.Available()),
Value: string(table.Format),
},
&cli.StringFlag{
Name: "mode",
Aliases: []string{"m"},
Usage: fmt.Sprintf("use `MODE` as output mode (available: %s)", []format.OutputMode{format.OutputModeCompact, format.OutputModeWide}),
Value: string(format.OutputModeCompact),
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
EnvVars: []string{`ARCAST_TOKEN`},
Usage: "use `TOKEN` as authentication token",
},
&cli.StringFlag{
Name: "token-file",
EnvVars: []string{`ARCAST_TOKEN_FILE`},
Usage: "use `TOKEN_FILE` as file containing the authentication token",
Value: ".bouncer-token",
TakesFile: true,
},
}
flags = append(flags, baseFlags...)
return flags
}
type BaseFlags struct {
ServerURL string
Format format.Format
OutputMode format.OutputMode
Token string
TokenFile string
}
func GetBaseFlags(ctx *cli.Context) *BaseFlags {
serverURL := ctx.String("server")
rawFormat := ctx.String("format")
rawOutputMode := ctx.String("mode")
tokenFile := ctx.String("token-file")
token := ctx.String("token")
return &BaseFlags{
ServerURL: serverURL,
Format: format.Format(rawFormat),
OutputMode: format.OutputMode(rawOutputMode),
Token: token,
TokenFile: tokenFile,
}
}
func GetToken(flags *BaseFlags) (string, error) {
if flags.Token != "" {
return flags.Token, nil
}
if flags.TokenFile == "" {
return "", nil
}
rawToken, err := os.ReadFile(flags.TokenFile)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", errors.WithStack(err)
}
if rawToken == nil {
return "", nil
}
return strings.TrimSpace(string(rawToken)), nil
}

View File

@ -0,0 +1,37 @@
package client
import (
"gitlab.com/wpetit/goweb/cli/format"
)
func playerHints(outputMode format.OutputMode) format.Hints {
return format.Hints{
OutputMode: outputMode,
Props: []format.Prop{
format.NewProp("ID", "ID"),
format.NewProp("IPs", "IPs"),
format.NewProp("Port", "Port"),
},
}
}
type wrappedStatus struct {
ID string `json:"id"`
Status string `json:"status"`
URL string `json:"url"`
Title string `json:"title"`
Address string `json:"address"`
}
func wrappedStatusHints(outputMode format.OutputMode) format.Hints {
return format.Hints{
OutputMode: outputMode,
Props: []format.Prop{
format.NewProp("ID", "ID"),
format.NewProp("Address", "Address"),
format.NewProp("Status", "Status"),
format.NewProp("Title", "Title"),
format.NewProp("URL", "URL"),
},
}
}

View File

@ -0,0 +1,53 @@
package client
import (
"os"
"forge.cadoles.com/arcad/arcast/internal/command/client/flag"
"forge.cadoles.com/arcad/arcast/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func Reset() *cli.Command {
return &cli.Command{
Name: "reset",
Flags: flag.ComposeFlags(
playerFlags...,
),
Action: func(ctx *cli.Context) error {
baseFlags := flag.GetBaseFlags(ctx)
statuses := make([]wrappedStatus, 0)
err := forEachPlayer(ctx, func(cl *client.Client, playerAddr string) error {
status, err := cl.Reset(ctx.Context, playerAddr)
if err != nil {
return errors.Wrap(err, "could not cast")
}
statuses = append(statuses, wrappedStatus{
Status: status.Status,
URL: status.URL,
Title: status.Title,
ID: status.ID,
Address: playerAddr,
})
return nil
})
if err != nil {
return errors.WithStack(err)
}
hints := wrappedStatusHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, AsAnySlice(statuses...)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,15 @@
package client
import "github.com/urfave/cli/v2"
func Root() *cli.Command {
return &cli.Command{
Name: "client",
Subcommands: []*cli.Command{
Scan(),
Cast(),
Reset(),
Status(),
},
}
}

View File

@ -0,0 +1,49 @@
package client
import (
"context"
"os"
"time"
"forge.cadoles.com/arcad/arcast/internal/command/client/flag"
"forge.cadoles.com/arcad/arcast/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func Scan() *cli.Command {
return &cli.Command{
Name: "scan",
Flags: flag.ComposeFlags(
&cli.DurationFlag{
Name: "timeout",
EnvVars: []string{"ARCAST_CLIENT_SCAN_TIMEOUT"},
Usage: "Use `TIMEOUT` as maximum scan duration",
Value: 5 * time.Second,
},
),
Action: func(ctx *cli.Context) error {
baseFlags := flag.GetBaseFlags(ctx)
timeout := ctx.Duration("timeout")
client := client.New()
scanCtx, cancel := context.WithTimeout(ctx.Context, timeout)
defer cancel()
players, err := client.Scan(scanCtx)
if err != nil && (!errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded)) {
return errors.Wrap(err, "could not scan for players")
}
hints := playerHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, AsAnySlice(players...)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,53 @@
package client
import (
"os"
"forge.cadoles.com/arcad/arcast/internal/command/client/flag"
"forge.cadoles.com/arcad/arcast/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func Status() *cli.Command {
return &cli.Command{
Name: "status",
Flags: flag.ComposeFlags(
playerFlags...,
),
Action: func(ctx *cli.Context) error {
baseFlags := flag.GetBaseFlags(ctx)
statuses := make([]wrappedStatus, 0)
err := forEachPlayer(ctx, func(cl *client.Client, playerAddr string) error {
status, err := cl.Status(ctx.Context, playerAddr)
if err != nil {
return errors.Wrap(err, "could not cast")
}
statuses = append(statuses, wrappedStatus{
Status: status.Status,
URL: status.URL,
Title: status.Title,
ID: status.ID,
Address: playerAddr,
})
return nil
})
if err != nil {
return errors.WithStack(err)
}
hints := wrappedStatusHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, AsAnySlice(statuses...)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,91 @@
package client
import (
"context"
"fmt"
"time"
"forge.cadoles.com/arcad/arcast/pkg/client"
"forge.cadoles.com/arcad/arcast/pkg/server"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
var (
playerScanTimeoutFlag = "scan-timeout"
flagPlayerAddr = "player-address"
flagPlayerID = "player-id"
playerFlags = []cli.Flag{
&cli.DurationFlag{
Name: playerScanTimeoutFlag,
EnvVars: []string{"ARCAST_CLIENT_CAST_TIMEOUT"},
Usage: "Use `TIMEOUT` as maximum cast duration",
Value: 5 * time.Second,
},
&cli.StringSliceFlag{
Name: flagPlayerID,
Aliases: []string{"i"},
EnvVars: []string{"ARCAST_CLIENT_PLAYER_ID"},
Usage: "Use `ID` as player id",
Value: cli.NewStringSlice(),
},
&cli.StringSliceFlag{
Name: flagPlayerAddr,
Aliases: []string{"a"},
EnvVars: []string{"ARCAST_CLIENT_PLAYER_ADDR"},
Usage: "Use `ADDR` as player address",
Value: cli.NewStringSlice(),
},
}
)
func forEachPlayer(ctx *cli.Context, fn func(cl *client.Client, playerAddr string) error) error {
playerAddrs := ctx.StringSlice(flagPlayerAddr)
cl := client.New()
if ctx.IsSet(flagPlayerID) || !ctx.IsSet(flagPlayerAddr) {
scanTimeout := ctx.Duration(playerScanTimeoutFlag)
scanCtx, cancel := context.WithTimeout(ctx.Context, scanTimeout)
defer cancel()
playerIDs := ctx.StringSlice(flagPlayerID)
players, err := cl.Scan(scanCtx, client.WithPlayerIDs(playerIDs...))
if err != nil && (!errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded)) {
return errors.Wrap(err, "could not scan for players")
}
for _, p := range players {
preferredIP, err := server.FindPreferredLocalAddress(p.IPs...)
if err != nil {
return errors.Errorf("could not retrieve player '%s' preferred address", p.ID)
}
playerAddr := fmt.Sprintf("%s:%d", preferredIP, p.Port)
playerAddrs = append(playerAddrs, playerAddr)
}
}
if len(playerAddrs) == 0 {
return errors.New("no players found")
}
for _, addr := range playerAddrs {
if err := fn(cl, addr); err != nil {
return errors.WithStack(err)
}
}
return nil
}
func AsAnySlice[T any](src ...T) []any {
dst := make([]any, len(src))
for i, s := range src {
dst[i] = s
}
return dst
}