feat(client): tenant management commands

This commit is contained in:
2024-02-27 14:14:30 +01:00
parent 15a0bf6ecc
commit c851a1f51b
61 changed files with 1376 additions and 272 deletions

View File

@ -0,0 +1,51 @@
package agent
import (
"os"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func ClaimCommand() *cli.Command {
return &cli.Command{
Name: "claim",
Usage: "Claim agent",
Flags: clientFlag.ComposeFlags(
&cli.StringFlag{
Name: "agent-thumbprint",
Value: "",
Required: true,
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
agentThumbprint := ctx.String("agent-thumbprint")
client := client.New(baseFlags.ServerURL, client.WithToken(token))
agent, err := client.ClaimAgent(ctx.Context, agentThumbprint)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := agentHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,53 @@
package agent
import (
"os"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func CountCommand() *cli.Command {
return &cli.Command{
Name: "count",
Usage: "Count agents",
Flags: clientFlag.ComposeFlags(),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
_, total, err := client.QueryAgents(ctx.Context)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := format.Hints{
OutputMode: baseFlags.OutputMode,
}
results := []struct {
Total int `json:"total"`
}{
{
Total: total,
},
}
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(results)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,56 @@
package agent
import (
"os"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func DeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Usage: "Delete agent",
Flags: agentFlag.WithAgentFlags(),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
agentID, err = client.DeleteAgent(ctx.Context, agentID)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := format.Hints{
OutputMode: baseFlags.OutputMode,
}
if err := format.Write(baseFlags.Format, os.Stdout, hints, struct {
ID datastore.AgentID `json:"id"`
}{
ID: agentID,
}); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,34 @@
package flag
import (
"errors"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"github.com/urfave/cli/v2"
)
func WithAgentFlags(flags ...cli.Flag) []cli.Flag {
baseFlags := clientFlag.ComposeFlags(
&cli.Int64Flag{
Name: "agent-id",
Aliases: []string{"a"},
Usage: "use `AGENT_ID` as selected agent",
Value: -1,
},
)
flags = append(flags, baseFlags...)
return flags
}
func AssertAgentID(ctx *cli.Context) (datastore.AgentID, error) {
rawAgentID := ctx.Int64("agent-id")
if rawAgentID == -1 {
return -1, errors.New("flag 'agent-id' is required")
}
return datastore.AgentID(rawAgentID), nil
}

View File

@ -0,0 +1,49 @@
package agent
import (
"os"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func GetCommand() *cli.Command {
return &cli.Command{
Name: "get",
Usage: "Get agent",
Flags: agentFlag.WithAgentFlags(),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
agent, err := client.GetAgent(ctx.Context, agentID)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := agentHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,20 @@
package agent
import (
"gitlab.com/wpetit/goweb/cli/format"
"gitlab.com/wpetit/goweb/cli/format/table"
)
func agentHints(outputMode format.OutputMode) format.Hints {
return format.Hints{
OutputMode: outputMode,
Props: []format.Prop{
format.NewProp("ID", "ID"),
format.NewProp("Label", "Label"),
format.NewProp("Thumbprint", "Thumbprint"),
format.NewProp("Status", "Status"),
format.NewProp("ContactedAt", "ContactedAt", table.WithCompactModeMaxColumnWidth(20)),
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),
},
}
}

View File

@ -0,0 +1,89 @@
package agent
import (
"os"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func QueryCommand() *cli.Command {
return &cli.Command{
Name: "query",
Usage: "Query agents",
Flags: clientFlag.ComposeFlags(
&cli.StringSliceFlag{
Name: "thumbprints",
Usage: "use `THUMBPRINTS` as query filter",
Value: nil,
},
&cli.Int64SliceFlag{
Name: "statuses",
Usage: "use `STATUSES` as query filter",
},
&cli.Int64SliceFlag{
Name: "ids",
Usage: "use `IDS` as query filter",
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
options := make([]client.QueryAgentsOptionFunc, 0)
thumbprints := ctx.StringSlice("thumbprints")
if thumbprints != nil {
options = append(options, client.WithQueryAgentsThumbprints(thumbprints...))
}
rawIDs := ctx.Int64Slice("ids")
if rawIDs != nil {
agentIDs := func(ids []int64) []datastore.AgentID {
agentIDs := make([]datastore.AgentID, len(ids))
for i, id := range ids {
agentIDs[i] = datastore.AgentID(id)
}
return agentIDs
}(rawIDs)
options = append(options, client.WithQueryAgentsID(agentIDs...))
}
rawStatuses := ctx.Int64Slice("statuses")
if rawStatuses != nil {
statuses := func(rawStatuses []int64) []datastore.AgentStatus {
statuses := make([]datastore.AgentStatus, len(rawStatuses))
for i, status := range rawStatuses {
statuses[i] = datastore.AgentStatus(status)
}
return statuses
}(rawStatuses)
options = append(options, client.WithQueryAgentsStatus(statuses...))
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
agents, _, err := client.QueryAgents(ctx.Context, options...)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := agentHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(agents)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,22 @@
package agent
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/spec"
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "agent",
Usage: "Agents related commands",
Subcommands: []*cli.Command{
QueryCommand(),
CountCommand(),
UpdateCommand(),
GetCommand(),
DeleteCommand(),
ClaimCommand(),
spec.Root(),
},
}
}

View File

@ -0,0 +1,67 @@
package spec
import (
"os"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func DeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Usage: "Delete spec",
Flags: agentFlag.WithAgentFlags(
&cli.StringFlag{
Name: "spec-name",
Usage: "use `NAME` as specification's name",
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
specName, err := assertSpecName(ctx)
if err != nil {
return errors.WithStack(err)
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
specName, err = client.DeleteAgentSpec(ctx.Context, agentID, specName)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := format.Hints{
OutputMode: baseFlags.OutputMode,
}
if err := format.Write(baseFlags.Format, os.Stdout, hints, struct {
Name spec.Name `json:"name"`
}{
Name: specName,
}); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,50 @@
package spec
import (
"os"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func GetCommand() *cli.Command {
return &cli.Command{
Name: "get",
Usage: "Get agent specifications",
Flags: agentFlag.WithAgentFlags(),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
specs, err := client.GetAgentSpecs(ctx.Context, agentID)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := format.Hints{
OutputMode: baseFlags.OutputMode,
}
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(specs)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,17 @@
package spec
import (
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "spec",
Usage: "Specifications related commands",
Subcommands: []*cli.Command{
GetCommand(),
UpdateCommand(),
DeleteCommand(),
},
}
}

View File

@ -0,0 +1,185 @@
package spec
import (
"encoding/json"
"os"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func UpdateCommand() *cli.Command {
return &cli.Command{
Name: "update",
Usage: "Update agent specification",
Flags: agentFlag.WithAgentFlags(
&cli.StringFlag{
Name: "spec-name",
Usage: "use `NAME` as specification's name",
},
&cli.StringFlag{
Name: "spec-data",
Usage: "use `DATA` as specification's data, '-' to read from STDIN",
},
&cli.BoolFlag{
Name: "no-patch",
Usage: "Dont use spec-data as a patch to existing specification",
},
&cli.IntFlag{
Name: "revision",
Usage: "Use `REVISION` as specification revision number",
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
specName, err := assertSpecName(ctx)
if err != nil {
return errors.WithStack(err)
}
specData, err := assertSpecData(ctx)
if err != nil {
return errors.WithStack(err)
}
noPatch := ctx.Bool("no-patch")
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
specs, err := client.GetAgentSpecs(ctx.Context, agentID)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
var existingSpec spec.Spec
for _, s := range specs {
if s.SpecName() != specName {
continue
}
existingSpec = s
break
}
revision := 0
if existingSpec != nil {
originSpecData := existingSpec.SpecData()
if !noPatch {
specData, err = applyPatch(originSpecData, specData)
if err != nil {
return errors.WithStack(err)
}
}
revision = existingSpec.SpecRevision()
}
if specificRevision := ctx.Int("revision"); specificRevision != 0 {
revision = specificRevision
}
rawSpec := &spec.RawSpec{
Name: specName,
Revision: revision,
Data: specData,
}
if err := spec.Validate(ctx.Context, rawSpec); err != nil {
return errors.WithStack(apierr.Wrap(err))
}
spec, err := client.UpdateAgentSpec(ctx.Context, agentID, rawSpec)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := format.Hints{
OutputMode: baseFlags.OutputMode,
}
if err := format.Write(baseFlags.Format, os.Stdout, hints, spec); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}
func assertSpecName(ctx *cli.Context) (spec.Name, error) {
specName := ctx.String("spec-name")
if specName == "" {
return "", errors.New("flag 'spec-name' is required")
}
return spec.Name(specName), nil
}
func assertSpecData(ctx *cli.Context) (map[string]any, error) {
rawSpecData := ctx.String("spec-data")
if rawSpecData == "" {
return nil, errors.New("flag 'spec-data' is required")
}
var specData map[string]any
if rawSpecData == "-" {
decoder := json.NewDecoder(os.Stdin)
if err := decoder.Decode(&specData); err != nil {
return nil, errors.WithStack(err)
}
} else {
if err := json.Unmarshal([]byte(rawSpecData), &specData); err != nil {
return nil, errors.WithStack(err)
}
}
return specData, nil
}
func applyPatch(origin any, patch any) (map[string]any, error) {
originJSON, err := json.Marshal(origin)
if err != nil {
return nil, errors.WithStack(err)
}
patchJSON, err := json.Marshal(patch)
if err != nil {
return nil, errors.WithStack(err)
}
result, err := jsonpatch.MergePatch(originJSON, patchJSON)
if err != nil {
return nil, errors.WithStack(err)
}
var specData map[string]any
if err := json.Unmarshal(result, &specData); err != nil {
}
return specData, nil
}

View File

@ -0,0 +1,72 @@
package agent
import (
"os"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/pkg/client"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/cli/format"
)
func UpdateCommand() *cli.Command {
return &cli.Command{
Name: "update",
Usage: "Updata agent",
Flags: agentFlag.WithAgentFlags(
&cli.IntFlag{
Name: "status",
Usage: "Set `STATUS` to selected agent",
Value: -1,
},
&cli.StringFlag{
Name: "label",
Usage: "Set `LABEL` to selected agent",
Value: "",
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
token, err := clientFlag.GetToken(baseFlags)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
options := make([]client.UpdateAgentOptionFunc, 0)
status := ctx.Int("status")
if status != -1 {
options = append(options, client.WithAgentStatus(status))
}
label := ctx.String("label")
if label != "" {
options = append(options, client.WithAgentLabel(label))
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
agent, err := client.UpdateAgent(ctx.Context, agentID, options...)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := agentHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}