From 997c6e8d7e1c948fb64171ab0dba6dcc51064255 Mon Sep 17 00:00:00 2001 From: Doma Palvolgyi <218264240+dpalvolgyi-pw@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:33:47 +0100 Subject: [PATCH 1/2] feat: gamelift initial support --- cmd/claws/imports_custom.go | 8 + custom/gamelift/builds/actions.go | 62 +++++ custom/gamelift/builds/constants.go | 7 + custom/gamelift/builds/dao.go | 128 ++++++++++ custom/gamelift/builds/register.go | 20 ++ custom/gamelift/builds/render.go | 145 +++++++++++ custom/gamelift/fleets/actions.go | 62 +++++ custom/gamelift/fleets/constants.go | 7 + custom/gamelift/fleets/dao.go | 189 ++++++++++++++ custom/gamelift/fleets/register.go | 20 ++ custom/gamelift/fleets/render.go | 234 ++++++++++++++++++ .../gamelift/game-session-queues/actions.go | 62 +++++ .../gamelift/game-session-queues/constants.go | 7 + custom/gamelift/game-session-queues/dao.go | 136 ++++++++++ .../gamelift/game-session-queues/register.go | 20 ++ custom/gamelift/game-session-queues/render.go | 128 ++++++++++ custom/gamelift/game-sessions/constants.go | 7 + custom/gamelift/game-sessions/dao.go | 193 +++++++++++++++ custom/gamelift/game-sessions/register.go | 20 ++ custom/gamelift/game-sessions/render.go | 211 ++++++++++++++++ .../gamelift/matchmaking-configs/actions.go | 62 +++++ .../gamelift/matchmaking-configs/constants.go | 7 + custom/gamelift/matchmaking-configs/dao.go | 172 +++++++++++++ .../gamelift/matchmaking-configs/register.go | 20 ++ custom/gamelift/matchmaking-configs/render.go | 210 ++++++++++++++++ custom/gamelift/scripts/actions.go | 62 +++++ custom/gamelift/scripts/constants.go | 7 + custom/gamelift/scripts/dao.go | 134 ++++++++++ custom/gamelift/scripts/register.go | 20 ++ custom/gamelift/scripts/render.go | 126 ++++++++++ go.mod | 7 +- go.sum | 14 +- internal/registry/registry.go | 8 + 33 files changed, 2506 insertions(+), 9 deletions(-) create mode 100644 custom/gamelift/builds/actions.go create mode 100644 custom/gamelift/builds/constants.go create mode 100644 custom/gamelift/builds/dao.go create mode 100644 custom/gamelift/builds/register.go create mode 100644 custom/gamelift/builds/render.go create mode 100644 custom/gamelift/fleets/actions.go create mode 100644 custom/gamelift/fleets/constants.go create mode 100644 custom/gamelift/fleets/dao.go create mode 100644 custom/gamelift/fleets/register.go create mode 100644 custom/gamelift/fleets/render.go create mode 100644 custom/gamelift/game-session-queues/actions.go create mode 100644 custom/gamelift/game-session-queues/constants.go create mode 100644 custom/gamelift/game-session-queues/dao.go create mode 100644 custom/gamelift/game-session-queues/register.go create mode 100644 custom/gamelift/game-session-queues/render.go create mode 100644 custom/gamelift/game-sessions/constants.go create mode 100644 custom/gamelift/game-sessions/dao.go create mode 100644 custom/gamelift/game-sessions/register.go create mode 100644 custom/gamelift/game-sessions/render.go create mode 100644 custom/gamelift/matchmaking-configs/actions.go create mode 100644 custom/gamelift/matchmaking-configs/constants.go create mode 100644 custom/gamelift/matchmaking-configs/dao.go create mode 100644 custom/gamelift/matchmaking-configs/register.go create mode 100644 custom/gamelift/matchmaking-configs/render.go create mode 100644 custom/gamelift/scripts/actions.go create mode 100644 custom/gamelift/scripts/constants.go create mode 100644 custom/gamelift/scripts/dao.go create mode 100644 custom/gamelift/scripts/register.go create mode 100644 custom/gamelift/scripts/render.go diff --git a/cmd/claws/imports_custom.go b/cmd/claws/imports_custom.go index 7801152..26f565a 100644 --- a/cmd/claws/imports_custom.go +++ b/cmd/claws/imports_custom.go @@ -179,6 +179,14 @@ import ( // Firewall Manager _ "github.com/clawscli/claws/custom/fms/policies" + // GameLift + _ "github.com/clawscli/claws/custom/gamelift/builds" + _ "github.com/clawscli/claws/custom/gamelift/fleets" + _ "github.com/clawscli/claws/custom/gamelift/game-session-queues" + _ "github.com/clawscli/claws/custom/gamelift/game-sessions" + _ "github.com/clawscli/claws/custom/gamelift/matchmaking-configs" + _ "github.com/clawscli/claws/custom/gamelift/scripts" + // Glue _ "github.com/clawscli/claws/custom/glue/crawlers" _ "github.com/clawscli/claws/custom/glue/databases" diff --git a/custom/gamelift/builds/actions.go b/custom/gamelift/builds/actions.go new file mode 100644 index 0000000..b80aaa5 --- /dev/null +++ b/custom/gamelift/builds/actions.go @@ -0,0 +1,62 @@ +package builds + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + + "github.com/clawscli/claws/internal/action" + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +func init() { + action.Global.Register("gamelift", "builds", []action.Action{ + { + Name: "Delete", + Shortcut: "D", + Type: action.ActionTypeAPI, + Operation: "DeleteBuild", + Confirm: action.ConfirmDangerous, + }, + }) + + action.RegisterExecutor("gamelift", "builds", executeBuildAction) +} + +func executeBuildAction(ctx context.Context, act action.Action, resource dao.Resource) action.ActionResult { + switch act.Operation { + case "DeleteBuild": + return executeDeleteBuild(ctx, resource) + default: + return action.UnknownOperationResult(act.Operation) + } +} + +func executeDeleteBuild(ctx context.Context, resource dao.Resource) action.ActionResult { + build, ok := resource.(*BuildResource) + if !ok { + return action.InvalidResourceResult() + } + + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return action.ActionResult{Success: false, Error: apperrors.Wrap(err, "create gamelift client")} + } + client := gamelift.NewFromConfig(cfg) + + buildId := build.GetID() + _, err = client.DeleteBuild(ctx, &gamelift.DeleteBuildInput{ + BuildId: &buildId, + }) + if err != nil { + return action.ActionResult{Success: false, Error: fmt.Errorf("delete build: %w", err)} + } + + return action.ActionResult{ + Success: true, + Message: fmt.Sprintf("Deleted build %s", build.GetName()), + } +} diff --git a/custom/gamelift/builds/constants.go b/custom/gamelift/builds/constants.go new file mode 100644 index 0000000..d4ded52 --- /dev/null +++ b/custom/gamelift/builds/constants.go @@ -0,0 +1,7 @@ +// Code generated by go generate; DO NOT EDIT. +// To regenerate: task gen-imports + +package builds + +// ServiceResourcePath is the canonical path for this resource type. +const ServiceResourcePath = "gamelift/builds" diff --git a/custom/gamelift/builds/dao.go b/custom/gamelift/builds/dao.go new file mode 100644 index 0000000..9122e35 --- /dev/null +++ b/custom/gamelift/builds/dao.go @@ -0,0 +1,128 @@ +package builds + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + "github.com/aws/aws-sdk-go-v2/service/gamelift/types" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +// BuildDAO provides data access for GameLift builds. +type BuildDAO struct { + dao.BaseDAO + client *gamelift.Client +} + +// NewBuildDAO creates a new BuildDAO. +func NewBuildDAO(ctx context.Context) (dao.DAO, error) { + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return nil, apperrors.Wrap(err, "new "+ServiceResourcePath+" dao") + } + return &BuildDAO{ + BaseDAO: dao.NewBaseDAO("gamelift", "builds"), + client: gamelift.NewFromConfig(cfg), + }, nil +} + +// List returns all GameLift builds. +func (d *BuildDAO) List(ctx context.Context) ([]dao.Resource, error) { + builds, err := appaws.Paginate(ctx, func(token *string) ([]types.Build, *string, error) { + output, err := d.client.ListBuilds(ctx, &gamelift.ListBuildsInput{ + NextToken: token, + }) + if err != nil { + return nil, nil, apperrors.Wrap(err, "list gamelift builds") + } + return output.Builds, output.NextToken, nil + }) + if err != nil { + return nil, err + } + + resources := make([]dao.Resource, len(builds)) + for i, build := range builds { + resources[i] = NewBuildResource(build) + } + return resources, nil +} + +// Get returns a specific GameLift build by ID. +func (d *BuildDAO) Get(ctx context.Context, id string) (dao.Resource, error) { + output, err := d.client.DescribeBuild(ctx, &gamelift.DescribeBuildInput{ + BuildId: &id, + }) + if err != nil { + return nil, apperrors.Wrapf(err, "describe gamelift build %s", id) + } + if output.Build == nil { + return nil, fmt.Errorf("gamelift build %s not found", id) + } + return NewBuildResource(*output.Build), nil +} + +// Delete deletes a GameLift build by ID. +func (d *BuildDAO) Delete(ctx context.Context, id string) error { + _, err := d.client.DeleteBuild(ctx, &gamelift.DeleteBuildInput{ + BuildId: &id, + }) + if err != nil { + return apperrors.Wrapf(err, "delete gamelift build %s", id) + } + return nil +} + +// BuildResource wraps a GameLift build. +type BuildResource struct { + dao.BaseResource + Build types.Build +} + +// NewBuildResource creates a new BuildResource. +func NewBuildResource(build types.Build) *BuildResource { + return &BuildResource{ + BaseResource: dao.BaseResource{ + ID: appaws.Str(build.BuildId), + Name: appaws.Str(build.Name), + ARN: appaws.Str(build.BuildArn), + Data: build, + }, + Build: build, + } +} + +// Status returns the build status. +func (r *BuildResource) Status() string { + return string(r.Build.Status) +} + +// Version returns the build version. +func (r *BuildResource) Version() string { + return appaws.Str(r.Build.Version) +} + +// OperatingSystem returns the OS. +func (r *BuildResource) OperatingSystem() string { + return string(r.Build.OperatingSystem) +} + +// SizeOnDisk returns the size in bytes. +func (r *BuildResource) SizeOnDisk() int64 { + return appaws.Int64(r.Build.SizeOnDisk) +} + +// CreationTime returns when the build was created. +func (r *BuildResource) CreationTime() *time.Time { + return r.Build.CreationTime +} + +// ServerSdkVersion returns the server SDK version. +func (r *BuildResource) ServerSdkVersion() string { + return appaws.Str(r.Build.ServerSdkVersion) +} diff --git a/custom/gamelift/builds/register.go b/custom/gamelift/builds/register.go new file mode 100644 index 0000000..6c524a8 --- /dev/null +++ b/custom/gamelift/builds/register.go @@ -0,0 +1,20 @@ +package builds + +import ( + "context" + + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/registry" + "github.com/clawscli/claws/internal/render" +) + +func init() { + registry.Global.RegisterCustom("gamelift", "builds", registry.Entry{ + DAOFactory: func(ctx context.Context) (dao.DAO, error) { + return NewBuildDAO(ctx) + }, + RendererFactory: func() render.Renderer { + return NewBuildRenderer() + }, + }) +} diff --git a/custom/gamelift/builds/render.go b/custom/gamelift/builds/render.go new file mode 100644 index 0000000..eb5a738 --- /dev/null +++ b/custom/gamelift/builds/render.go @@ -0,0 +1,145 @@ +package builds + +import ( + "fmt" + + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/render" +) + +// BuildRenderer renders GameLift builds. +type BuildRenderer struct { + render.BaseRenderer +} + +// NewBuildRenderer creates a new BuildRenderer. +func NewBuildRenderer() render.Renderer { + return &BuildRenderer{ + BaseRenderer: render.BaseRenderer{ + Service: "gamelift", + Resource: "builds", + Cols: []render.Column{ + {Name: "NAME", Width: 30, Getter: func(r dao.Resource) string { return r.GetName() }}, + {Name: "BUILD ID", Width: 24, Getter: func(r dao.Resource) string { return r.GetID() }, Priority: 3}, + {Name: "STATUS", Width: 14, Getter: getBuildStatus}, + {Name: "VERSION", Width: 16, Getter: getBuildVersion}, + {Name: "OS", Width: 16, Getter: getBuildOS}, + {Name: "SIZE", Width: 12, Getter: getBuildSize, Priority: 2}, + {Name: "SDK VERSION", Width: 14, Getter: getBuildSdkVersion, Priority: 3}, + {Name: "CREATED", Width: 20, Getter: getBuildCreated, Priority: 2}, + }, + }, + } +} + +func getBuildStatus(r dao.Resource) string { + build, ok := r.(*BuildResource) + if !ok { + return "" + } + return build.Status() +} + +func getBuildVersion(r dao.Resource) string { + build, ok := r.(*BuildResource) + if !ok { + return "" + } + return build.Version() +} + +func getBuildOS(r dao.Resource) string { + build, ok := r.(*BuildResource) + if !ok { + return "" + } + return build.OperatingSystem() +} + +func getBuildSize(r dao.Resource) string { + build, ok := r.(*BuildResource) + if !ok { + return "" + } + size := build.SizeOnDisk() + if size == 0 { + return "-" + } + return render.FormatSize(size) +} + +func getBuildSdkVersion(r dao.Resource) string { + build, ok := r.(*BuildResource) + if !ok { + return "" + } + return build.ServerSdkVersion() +} + +func getBuildCreated(r dao.Resource) string { + build, ok := r.(*BuildResource) + if !ok { + return "" + } + if t := build.CreationTime(); t != nil { + return t.Format("2006-01-02 15:04") + } + return "" +} + +// RenderDetail renders the detail view for a GameLift build. +func (rr *BuildRenderer) RenderDetail(resource dao.Resource) string { + build, ok := resource.(*BuildResource) + if !ok { + return "" + } + + d := render.NewDetailBuilder() + + d.Title("GameLift Build", build.GetName()) + + d.Section("Basic Information") + d.Field("Name", build.GetName()) + d.Field("Build ID", build.GetID()) + d.Field("ARN", build.GetARN()) + d.Field("Status", build.Status()) + + d.Section("Configuration") + if v := build.Version(); v != "" { + d.Field("Version", v) + } + d.Field("Operating System", build.OperatingSystem()) + if v := build.ServerSdkVersion(); v != "" { + d.Field("Server SDK Version", v) + } + + d.Section("Storage") + size := build.SizeOnDisk() + if size > 0 { + d.Field("Size on Disk", render.FormatSize(size)) + } else { + d.Field("Size on Disk", fmt.Sprintf("%d bytes", size)) + } + + d.Section("Timestamps") + if t := build.CreationTime(); t != nil { + d.Field("Created", t.Format("2006-01-02 15:04:05")) + } + + return d.String() +} + +// RenderSummary renders summary fields for a GameLift build. +func (rr *BuildRenderer) RenderSummary(resource dao.Resource) []render.SummaryField { + build, ok := resource.(*BuildResource) + if !ok { + return rr.BaseRenderer.RenderSummary(resource) + } + + return []render.SummaryField{ + {Label: "Name", Value: build.GetName()}, + {Label: "Build ID", Value: build.GetID()}, + {Label: "Status", Value: build.Status()}, + {Label: "Version", Value: build.Version()}, + } +} diff --git a/custom/gamelift/fleets/actions.go b/custom/gamelift/fleets/actions.go new file mode 100644 index 0000000..3f14b3c --- /dev/null +++ b/custom/gamelift/fleets/actions.go @@ -0,0 +1,62 @@ +package fleets + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + + "github.com/clawscli/claws/internal/action" + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +func init() { + action.Global.Register("gamelift", "fleets", []action.Action{ + { + Name: "Delete", + Shortcut: "D", + Type: action.ActionTypeAPI, + Operation: "DeleteFleet", + Confirm: action.ConfirmDangerous, + }, + }) + + action.RegisterExecutor("gamelift", "fleets", executeFleetAction) +} + +func executeFleetAction(ctx context.Context, act action.Action, resource dao.Resource) action.ActionResult { + switch act.Operation { + case "DeleteFleet": + return executeDeleteFleet(ctx, resource) + default: + return action.UnknownOperationResult(act.Operation) + } +} + +func executeDeleteFleet(ctx context.Context, resource dao.Resource) action.ActionResult { + fleet, ok := resource.(*FleetResource) + if !ok { + return action.InvalidResourceResult() + } + + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return action.ActionResult{Success: false, Error: apperrors.Wrap(err, "create gamelift client")} + } + client := gamelift.NewFromConfig(cfg) + + fleetId := fleet.GetID() + _, err = client.DeleteFleet(ctx, &gamelift.DeleteFleetInput{ + FleetId: &fleetId, + }) + if err != nil { + return action.ActionResult{Success: false, Error: fmt.Errorf("delete fleet: %w", err)} + } + + return action.ActionResult{ + Success: true, + Message: fmt.Sprintf("Deleted fleet %s", fleet.GetName()), + } +} diff --git a/custom/gamelift/fleets/constants.go b/custom/gamelift/fleets/constants.go new file mode 100644 index 0000000..222617e --- /dev/null +++ b/custom/gamelift/fleets/constants.go @@ -0,0 +1,7 @@ +// Code generated by go generate; DO NOT EDIT. +// To regenerate: task gen-imports + +package fleets + +// ServiceResourcePath is the canonical path for this resource type. +const ServiceResourcePath = "gamelift/fleets" diff --git a/custom/gamelift/fleets/dao.go b/custom/gamelift/fleets/dao.go new file mode 100644 index 0000000..f2fb13e --- /dev/null +++ b/custom/gamelift/fleets/dao.go @@ -0,0 +1,189 @@ +package fleets + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + "github.com/aws/aws-sdk-go-v2/service/gamelift/types" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +// FleetDAO provides data access for GameLift fleets. +type FleetDAO struct { + dao.BaseDAO + client *gamelift.Client +} + +// NewFleetDAO creates a new FleetDAO. +func NewFleetDAO(ctx context.Context) (dao.DAO, error) { + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return nil, apperrors.Wrap(err, "new "+ServiceResourcePath+" dao") + } + return &FleetDAO{ + BaseDAO: dao.NewBaseDAO("gamelift", "fleets"), + client: gamelift.NewFromConfig(cfg), + }, nil +} + +// List returns all GameLift fleets. +func (d *FleetDAO) List(ctx context.Context) ([]dao.Resource, error) { + attrs, err := appaws.Paginate(ctx, func(token *string) ([]types.FleetAttributes, *string, error) { + output, err := d.client.DescribeFleetAttributes(ctx, &gamelift.DescribeFleetAttributesInput{ + NextToken: token, + }) + if err != nil { + return nil, nil, apperrors.Wrap(err, "describe gamelift fleet attributes") + } + return output.FleetAttributes, output.NextToken, nil + }) + if err != nil { + return nil, err + } + + resources := make([]dao.Resource, len(attrs)) + for i, attr := range attrs { + resources[i] = NewFleetResource(attr) + } + return resources, nil +} + +// Get returns a specific GameLift fleet by ID. +func (d *FleetDAO) Get(ctx context.Context, id string) (dao.Resource, error) { + output, err := d.client.DescribeFleetAttributes(ctx, &gamelift.DescribeFleetAttributesInput{ + FleetIds: []string{id}, + }) + if err != nil { + return nil, apperrors.Wrapf(err, "describe gamelift fleet %s", id) + } + if len(output.FleetAttributes) == 0 { + return nil, apperrors.Wrapf(err, "gamelift fleet %s not found", id) + } + return NewFleetResource(output.FleetAttributes[0]), nil +} + +// Delete deletes a GameLift fleet by ID. +func (d *FleetDAO) Delete(ctx context.Context, id string) error { + _, err := d.client.DeleteFleet(ctx, &gamelift.DeleteFleetInput{ + FleetId: &id, + }) + if err != nil { + return apperrors.Wrapf(err, "delete gamelift fleet %s", id) + } + return nil +} + +// FleetResource wraps a GameLift fleet. +type FleetResource struct { + dao.BaseResource + Fleet types.FleetAttributes +} + +// NewFleetResource creates a new FleetResource. +func NewFleetResource(fleet types.FleetAttributes) *FleetResource { + return &FleetResource{ + BaseResource: dao.BaseResource{ + ID: appaws.Str(fleet.FleetId), + Name: appaws.Str(fleet.Name), + ARN: appaws.Str(fleet.FleetArn), + Data: fleet, + }, + Fleet: fleet, + } +} + +// Status returns the fleet status. +func (r *FleetResource) Status() string { + return string(r.Fleet.Status) +} + +// FleetType returns the fleet type (ON_DEMAND or SPOT). +func (r *FleetResource) FleetType() string { + return string(r.Fleet.FleetType) +} + +// InstanceType returns the EC2 instance type. +func (r *FleetResource) InstanceType() string { + return string(r.Fleet.InstanceType) +} + +// ComputeType returns the compute type. +func (r *FleetResource) ComputeType() string { + return string(r.Fleet.ComputeType) +} + +// OperatingSystem returns the OS. +func (r *FleetResource) OperatingSystem() string { + return string(r.Fleet.OperatingSystem) +} + +// BuildId returns the build ID. +func (r *FleetResource) BuildId() string { + return appaws.Str(r.Fleet.BuildId) +} + +// BuildArn returns the build ARN. +func (r *FleetResource) BuildArn() string { + return appaws.Str(r.Fleet.BuildArn) +} + +// ScriptId returns the script ID. +func (r *FleetResource) ScriptId() string { + return appaws.Str(r.Fleet.ScriptId) +} + +// ScriptArn returns the script ARN. +func (r *FleetResource) ScriptArn() string { + return appaws.Str(r.Fleet.ScriptArn) +} + +// Description returns the fleet description. +func (r *FleetResource) Description() string { + return appaws.Str(r.Fleet.Description) +} + +// CreationTime returns when the fleet was created. +func (r *FleetResource) CreationTime() *time.Time { + return r.Fleet.CreationTime +} + +// TerminationTime returns when the fleet was terminated. +func (r *FleetResource) TerminationTime() *time.Time { + return r.Fleet.TerminationTime +} + +// InstanceRoleArn returns the instance role ARN. +func (r *FleetResource) InstanceRoleArn() string { + return appaws.Str(r.Fleet.InstanceRoleArn) +} + +// ProtectionPolicy returns the new game session protection policy. +func (r *FleetResource) ProtectionPolicy() string { + return string(r.Fleet.NewGameSessionProtectionPolicy) +} + +// MetricGroups returns the metric groups. +func (r *FleetResource) MetricGroups() []string { + return r.Fleet.MetricGroups +} + +// StoppedActions returns the stopped fleet actions. +func (r *FleetResource) StoppedActions() []string { + actions := make([]string, len(r.Fleet.StoppedActions)) + for i, a := range r.Fleet.StoppedActions { + actions[i] = string(a) + } + return actions +} + +// CertificateType returns the certificate type. +func (r *FleetResource) CertificateType() string { + if r.Fleet.CertificateConfiguration != nil { + return string(r.Fleet.CertificateConfiguration.CertificateType) + } + return "" +} diff --git a/custom/gamelift/fleets/register.go b/custom/gamelift/fleets/register.go new file mode 100644 index 0000000..13d1f7e --- /dev/null +++ b/custom/gamelift/fleets/register.go @@ -0,0 +1,20 @@ +package fleets + +import ( + "context" + + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/registry" + "github.com/clawscli/claws/internal/render" +) + +func init() { + registry.Global.RegisterCustom("gamelift", "fleets", registry.Entry{ + DAOFactory: func(ctx context.Context) (dao.DAO, error) { + return NewFleetDAO(ctx) + }, + RendererFactory: func() render.Renderer { + return NewFleetRenderer() + }, + }) +} diff --git a/custom/gamelift/fleets/render.go b/custom/gamelift/fleets/render.go new file mode 100644 index 0000000..1c7111f --- /dev/null +++ b/custom/gamelift/fleets/render.go @@ -0,0 +1,234 @@ +package fleets + +import ( + "fmt" + "strings" + + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/render" +) + +// Ensure FleetRenderer implements render.Navigator +var _ render.Navigator = (*FleetRenderer)(nil) + +// FleetRenderer renders GameLift fleets. +type FleetRenderer struct { + render.BaseRenderer +} + +// NewFleetRenderer creates a new FleetRenderer. +func NewFleetRenderer() render.Renderer { + return &FleetRenderer{ + BaseRenderer: render.BaseRenderer{ + Service: "gamelift", + Resource: "fleets", + Cols: []render.Column{ + {Name: "NAME", Width: 30, Getter: func(r dao.Resource) string { return r.GetName() }}, + {Name: "FLEET ID", Width: 24, Getter: func(r dao.Resource) string { return r.GetID() }, Priority: 3}, + {Name: "STATUS", Width: 14, Getter: getFleetStatus}, + {Name: "FLEET TYPE", Width: 12, Getter: getFleetType, Priority: 2}, + {Name: "INSTANCE TYPE", Width: 16, Getter: getInstanceType}, + {Name: "BUILD/SCRIPT", Width: 24, Getter: getBuildOrScript, Priority: 2}, + {Name: "OS", Width: 16, Getter: getOperatingSystem, Priority: 3}, + {Name: "CREATED", Width: 20, Getter: getFleetCreated, Priority: 2}, + }, + }, + } +} + +func getFleetStatus(r dao.Resource) string { + fleet, ok := r.(*FleetResource) + if !ok { + return "" + } + return fleet.Status() +} + +func getFleetType(r dao.Resource) string { + fleet, ok := r.(*FleetResource) + if !ok { + return "" + } + return fleet.FleetType() +} + +func getInstanceType(r dao.Resource) string { + fleet, ok := r.(*FleetResource) + if !ok { + return "" + } + return fleet.InstanceType() +} + +func getBuildOrScript(r dao.Resource) string { + fleet, ok := r.(*FleetResource) + if !ok { + return "" + } + if id := fleet.BuildId(); id != "" { + return "build:" + id + } + if id := fleet.ScriptId(); id != "" { + return "script:" + id + } + return "" +} + +func getOperatingSystem(r dao.Resource) string { + fleet, ok := r.(*FleetResource) + if !ok { + return "" + } + return fleet.OperatingSystem() +} + +func getFleetCreated(r dao.Resource) string { + fleet, ok := r.(*FleetResource) + if !ok { + return "" + } + if t := fleet.CreationTime(); t != nil { + return t.Format("2006-01-02 15:04") + } + return "" +} + +// RenderDetail renders the detail view for a GameLift fleet. +func (rr *FleetRenderer) RenderDetail(resource dao.Resource) string { + fleet, ok := resource.(*FleetResource) + if !ok { + return "" + } + + d := render.NewDetailBuilder() + + d.Title("GameLift Fleet", fleet.GetName()) + + // Basic Info + d.Section("Basic Information") + d.Field("Name", fleet.GetName()) + d.Field("Fleet ID", fleet.GetID()) + d.Field("ARN", fleet.GetARN()) + d.Field("Status", fleet.Status()) + if desc := fleet.Description(); desc != "" { + d.Field("Description", desc) + } + + // Compute Configuration + d.Section("Compute Configuration") + d.Field("Fleet Type", fleet.FleetType()) + d.Field("Compute Type", fleet.ComputeType()) + if it := fleet.InstanceType(); it != "" { + d.Field("Instance Type", it) + } + d.Field("Operating System", fleet.OperatingSystem()) + + // Build/Script + if buildId := fleet.BuildId(); buildId != "" { + d.Section("Build") + d.Field("Build ID", buildId) + if arn := fleet.BuildArn(); arn != "" { + d.Field("Build ARN", arn) + } + } + if scriptId := fleet.ScriptId(); scriptId != "" { + d.Section("Script") + d.Field("Script ID", scriptId) + if arn := fleet.ScriptArn(); arn != "" { + d.Field("Script ARN", arn) + } + } + + // Security + d.Section("Security") + d.Field("Protection Policy", fleet.ProtectionPolicy()) + if role := fleet.InstanceRoleArn(); role != "" { + d.Field("Instance Role ARN", role) + } + if cert := fleet.CertificateType(); cert != "" { + d.Field("Certificate Type", cert) + } + + // Metrics + if groups := fleet.MetricGroups(); len(groups) > 0 { + d.Section("Monitoring") + d.Field("Metric Groups", strings.Join(groups, ", ")) + } + + // Stopped Actions + if actions := fleet.StoppedActions(); len(actions) > 0 { + d.Section("Stopped Actions") + d.Field("Actions", strings.Join(actions, ", ")) + } + + // Timestamps + d.Section("Timestamps") + if t := fleet.CreationTime(); t != nil { + d.Field("Created", t.Format("2006-01-02 15:04:05")) + } + if t := fleet.TerminationTime(); t != nil { + d.Field("Terminated", t.Format("2006-01-02 15:04:05")) + } + + return d.String() +} + +// RenderSummary renders summary fields for a GameLift fleet. +func (rr *FleetRenderer) RenderSummary(resource dao.Resource) []render.SummaryField { + fleet, ok := resource.(*FleetResource) + if !ok { + return rr.BaseRenderer.RenderSummary(resource) + } + + fields := []render.SummaryField{ + {Label: "Name", Value: fleet.GetName()}, + {Label: "Fleet ID", Value: fleet.GetID()}, + {Label: "ARN", Value: fleet.GetARN()}, + {Label: "Status", Value: fleet.Status()}, + {Label: "Instance Type", Value: fleet.InstanceType()}, + } + return fields +} + +// Navigations returns available navigations from a GameLift fleet. +func (rr *FleetRenderer) Navigations(resource dao.Resource) []render.Navigation { + fleet, ok := resource.(*FleetResource) + if !ok { + return nil + } + + navs := []render.Navigation{ + { + Key: "s", + Label: "Game Sessions", + Service: "gamelift", + Resource: "game-sessions", + FilterField: "FleetId", + FilterValue: fleet.GetID(), + }, + } + + if buildId := fleet.BuildId(); buildId != "" { + navs = append(navs, render.Navigation{ + Key: "b", + Label: fmt.Sprintf("Build (%s)", buildId), + Service: "gamelift", + Resource: "builds", + FilterField: "BuildId", + FilterValue: buildId, + }) + } + + if scriptId := fleet.ScriptId(); scriptId != "" { + navs = append(navs, render.Navigation{ + Key: "c", + Label: fmt.Sprintf("Script (%s)", scriptId), + Service: "gamelift", + Resource: "scripts", + FilterField: "ScriptId", + FilterValue: scriptId, + }) + } + + return navs +} diff --git a/custom/gamelift/game-session-queues/actions.go b/custom/gamelift/game-session-queues/actions.go new file mode 100644 index 0000000..a0225ca --- /dev/null +++ b/custom/gamelift/game-session-queues/actions.go @@ -0,0 +1,62 @@ +package gamesessionqueues + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + + "github.com/clawscli/claws/internal/action" + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +func init() { + action.Global.Register("gamelift", "game-session-queues", []action.Action{ + { + Name: "Delete", + Shortcut: "D", + Type: action.ActionTypeAPI, + Operation: "DeleteGameSessionQueue", + Confirm: action.ConfirmDangerous, + }, + }) + + action.RegisterExecutor("gamelift", "game-session-queues", executeQueueAction) +} + +func executeQueueAction(ctx context.Context, act action.Action, resource dao.Resource) action.ActionResult { + switch act.Operation { + case "DeleteGameSessionQueue": + return executeDeleteQueue(ctx, resource) + default: + return action.UnknownOperationResult(act.Operation) + } +} + +func executeDeleteQueue(ctx context.Context, resource dao.Resource) action.ActionResult { + queue, ok := resource.(*QueueResource) + if !ok { + return action.InvalidResourceResult() + } + + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return action.ActionResult{Success: false, Error: apperrors.Wrap(err, "create gamelift client")} + } + client := gamelift.NewFromConfig(cfg) + + name := queue.GetName() + _, err = client.DeleteGameSessionQueue(ctx, &gamelift.DeleteGameSessionQueueInput{ + Name: &name, + }) + if err != nil { + return action.ActionResult{Success: false, Error: fmt.Errorf("delete game session queue: %w", err)} + } + + return action.ActionResult{ + Success: true, + Message: fmt.Sprintf("Deleted game session queue %s", name), + } +} diff --git a/custom/gamelift/game-session-queues/constants.go b/custom/gamelift/game-session-queues/constants.go new file mode 100644 index 0000000..8171cfe --- /dev/null +++ b/custom/gamelift/game-session-queues/constants.go @@ -0,0 +1,7 @@ +// Code generated by go generate; DO NOT EDIT. +// To regenerate: task gen-imports + +package gamesessionqueues + +// ServiceResourcePath is the canonical path for this resource type. +const ServiceResourcePath = "gamelift/game-session-queues" diff --git a/custom/gamelift/game-session-queues/dao.go b/custom/gamelift/game-session-queues/dao.go new file mode 100644 index 0000000..bdaec83 --- /dev/null +++ b/custom/gamelift/game-session-queues/dao.go @@ -0,0 +1,136 @@ +package gamesessionqueues + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + "github.com/aws/aws-sdk-go-v2/service/gamelift/types" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +// QueueDAO provides data access for GameLift game session queues. +type QueueDAO struct { + dao.BaseDAO + client *gamelift.Client +} + +// NewQueueDAO creates a new QueueDAO. +func NewQueueDAO(ctx context.Context) (dao.DAO, error) { + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return nil, apperrors.Wrap(err, "new "+ServiceResourcePath+" dao") + } + return &QueueDAO{ + BaseDAO: dao.NewBaseDAO("gamelift", "game-session-queues"), + client: gamelift.NewFromConfig(cfg), + }, nil +} + +// List returns all GameLift game session queues. +func (d *QueueDAO) List(ctx context.Context) ([]dao.Resource, error) { + queues, err := appaws.Paginate(ctx, func(token *string) ([]types.GameSessionQueue, *string, error) { + output, err := d.client.DescribeGameSessionQueues(ctx, &gamelift.DescribeGameSessionQueuesInput{ + NextToken: token, + }) + if err != nil { + return nil, nil, apperrors.Wrap(err, "describe gamelift game session queues") + } + return output.GameSessionQueues, output.NextToken, nil + }) + if err != nil { + return nil, err + } + + resources := make([]dao.Resource, len(queues)) + for i, queue := range queues { + resources[i] = NewQueueResource(queue) + } + return resources, nil +} + +// Get returns a specific GameLift game session queue by name. +func (d *QueueDAO) Get(ctx context.Context, id string) (dao.Resource, error) { + output, err := d.client.DescribeGameSessionQueues(ctx, &gamelift.DescribeGameSessionQueuesInput{ + Names: []string{id}, + }) + if err != nil { + return nil, apperrors.Wrapf(err, "describe gamelift game session queue %s", id) + } + if len(output.GameSessionQueues) == 0 { + return nil, apperrors.Wrapf(err, "gamelift game session queue %s not found", id) + } + return NewQueueResource(output.GameSessionQueues[0]), nil +} + +// Delete deletes a GameLift game session queue by name. +func (d *QueueDAO) Delete(ctx context.Context, id string) error { + _, err := d.client.DeleteGameSessionQueue(ctx, &gamelift.DeleteGameSessionQueueInput{ + Name: &id, + }) + if err != nil { + return apperrors.Wrapf(err, "delete gamelift game session queue %s", id) + } + return nil +} + +// QueueResource wraps a GameLift game session queue. +type QueueResource struct { + dao.BaseResource + Queue types.GameSessionQueue +} + +// NewQueueResource creates a new QueueResource. +func NewQueueResource(queue types.GameSessionQueue) *QueueResource { + return &QueueResource{ + BaseResource: dao.BaseResource{ + ID: appaws.Str(queue.Name), + Name: appaws.Str(queue.Name), + ARN: appaws.Str(queue.GameSessionQueueArn), + Data: queue, + }, + Queue: queue, + } +} + +// TimeoutInSeconds returns the queue timeout. +func (r *QueueResource) TimeoutInSeconds() int32 { + return appaws.Int32(r.Queue.TimeoutInSeconds) +} + +// Destinations returns the queue destinations. +func (r *QueueResource) Destinations() []types.GameSessionQueueDestination { + return r.Queue.Destinations +} + +// DestinationCount returns the number of destinations. +func (r *QueueResource) DestinationCount() int { + return len(r.Queue.Destinations) +} + +// NotificationTarget returns the SNS notification target. +func (r *QueueResource) NotificationTarget() string { + return appaws.Str(r.Queue.NotificationTarget) +} + +// CustomEventData returns the custom event data. +func (r *QueueResource) CustomEventData() string { + return appaws.Str(r.Queue.CustomEventData) +} + +// PlayerLatencyPolicies returns the player latency policies. +func (r *QueueResource) PlayerLatencyPolicies() []types.PlayerLatencyPolicy { + return r.Queue.PlayerLatencyPolicies +} + +// FilterConfiguration returns the filter configuration. +func (r *QueueResource) FilterConfiguration() *types.FilterConfiguration { + return r.Queue.FilterConfiguration +} + +// PriorityConfiguration returns the priority configuration. +func (r *QueueResource) PriorityConfiguration() *types.PriorityConfiguration { + return r.Queue.PriorityConfiguration +} diff --git a/custom/gamelift/game-session-queues/register.go b/custom/gamelift/game-session-queues/register.go new file mode 100644 index 0000000..4580719 --- /dev/null +++ b/custom/gamelift/game-session-queues/register.go @@ -0,0 +1,20 @@ +package gamesessionqueues + +import ( + "context" + + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/registry" + "github.com/clawscli/claws/internal/render" +) + +func init() { + registry.Global.RegisterCustom("gamelift", "game-session-queues", registry.Entry{ + DAOFactory: func(ctx context.Context) (dao.DAO, error) { + return NewQueueDAO(ctx) + }, + RendererFactory: func() render.Renderer { + return NewQueueRenderer() + }, + }) +} diff --git a/custom/gamelift/game-session-queues/render.go b/custom/gamelift/game-session-queues/render.go new file mode 100644 index 0000000..8b7754c --- /dev/null +++ b/custom/gamelift/game-session-queues/render.go @@ -0,0 +1,128 @@ +package gamesessionqueues + +import ( + "fmt" + "strings" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/render" +) + +// QueueRenderer renders GameLift game session queues. +type QueueRenderer struct { + render.BaseRenderer +} + +// NewQueueRenderer creates a new QueueRenderer. +func NewQueueRenderer() render.Renderer { + return &QueueRenderer{ + BaseRenderer: render.BaseRenderer{ + Service: "gamelift", + Resource: "game-session-queues", + Cols: []render.Column{ + {Name: "NAME", Width: 30, Getter: func(r dao.Resource) string { return r.GetName() }}, + {Name: "TIMEOUT (s)", Width: 12, Getter: getQueueTimeout}, + {Name: "DESTINATIONS", Width: 14, Getter: getQueueDestinationCount}, + {Name: "NOTIFICATION", Width: 40, Getter: getQueueNotification, Priority: 3}, + }, + }, + } +} + +func getQueueTimeout(r dao.Resource) string { + queue, ok := r.(*QueueResource) + if !ok { + return "" + } + timeout := queue.TimeoutInSeconds() + if timeout == 0 { + return "-" + } + return fmt.Sprintf("%d", timeout) +} + +func getQueueDestinationCount(r dao.Resource) string { + queue, ok := r.(*QueueResource) + if !ok { + return "" + } + return fmt.Sprintf("%d", queue.DestinationCount()) +} + +func getQueueNotification(r dao.Resource) string { + queue, ok := r.(*QueueResource) + if !ok { + return "" + } + return queue.NotificationTarget() +} + +// RenderDetail renders the detail view for a GameLift game session queue. +func (rr *QueueRenderer) RenderDetail(resource dao.Resource) string { + queue, ok := resource.(*QueueResource) + if !ok { + return "" + } + + d := render.NewDetailBuilder() + + d.Title("GameLift Game Session Queue", queue.GetName()) + + d.Section("Basic Information") + d.Field("Name", queue.GetName()) + d.Field("ARN", queue.GetARN()) + d.Field("Timeout", fmt.Sprintf("%d seconds", queue.TimeoutInSeconds())) + + // Destinations + if dests := queue.Destinations(); len(dests) > 0 { + d.Section("Destinations") + for i, dest := range dests { + d.Field(fmt.Sprintf("Destination %d", i+1), appaws.Str(dest.DestinationArn)) + } + } + + // Player Latency Policies + if policies := queue.PlayerLatencyPolicies(); len(policies) > 0 { + d.Section("Player Latency Policies") + for i, policy := range policies { + d.Field(fmt.Sprintf("Policy %d", i+1), + fmt.Sprintf("Max latency: %dms, eval period: %ds", + appaws.Int32(policy.MaximumIndividualPlayerLatencyMilliseconds), + appaws.Int32(policy.PolicyDurationSeconds))) + } + } + + // Filter Configuration + if fc := queue.FilterConfiguration(); fc != nil && len(fc.AllowedLocations) > 0 { + d.Section("Filter Configuration") + d.Field("Allowed Locations", strings.Join(fc.AllowedLocations, ", ")) + } + + // Notification + if target := queue.NotificationTarget(); target != "" { + d.Section("Notifications") + d.Field("SNS Target", target) + } + + if data := queue.CustomEventData(); data != "" { + d.Field("Custom Event Data", data) + } + + return d.String() +} + +// RenderSummary renders summary fields for a GameLift game session queue. +func (rr *QueueRenderer) RenderSummary(resource dao.Resource) []render.SummaryField { + queue, ok := resource.(*QueueResource) + if !ok { + return rr.BaseRenderer.RenderSummary(resource) + } + + return []render.SummaryField{ + {Label: "Name", Value: queue.GetName()}, + {Label: "ARN", Value: queue.GetARN()}, + {Label: "Timeout", Value: fmt.Sprintf("%d seconds", queue.TimeoutInSeconds())}, + {Label: "Destinations", Value: fmt.Sprintf("%d", queue.DestinationCount())}, + } +} diff --git a/custom/gamelift/game-sessions/constants.go b/custom/gamelift/game-sessions/constants.go new file mode 100644 index 0000000..921d89d --- /dev/null +++ b/custom/gamelift/game-sessions/constants.go @@ -0,0 +1,7 @@ +// Code generated by go generate; DO NOT EDIT. +// To regenerate: task gen-imports + +package gamesessions + +// ServiceResourcePath is the canonical path for this resource type. +const ServiceResourcePath = "gamelift/game-sessions" diff --git a/custom/gamelift/game-sessions/dao.go b/custom/gamelift/game-sessions/dao.go new file mode 100644 index 0000000..954e896 --- /dev/null +++ b/custom/gamelift/game-sessions/dao.go @@ -0,0 +1,193 @@ +package gamesessions + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + "github.com/aws/aws-sdk-go-v2/service/gamelift/types" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +// GameSessionDAO provides data access for GameLift game sessions. +type GameSessionDAO struct { + dao.BaseDAO + client *gamelift.Client +} + +// NewGameSessionDAO creates a new GameSessionDAO. +func NewGameSessionDAO(ctx context.Context) (dao.DAO, error) { + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return nil, apperrors.Wrap(err, "new "+ServiceResourcePath+" dao") + } + return &GameSessionDAO{ + BaseDAO: dao.NewBaseDAO("gamelift", "game-sessions"), + client: gamelift.NewFromConfig(cfg), + }, nil +} + +// List returns GameLift game sessions, filtered by FleetId from context. +func (d *GameSessionDAO) List(ctx context.Context) ([]dao.Resource, error) { + fleetId := dao.GetFilterFromContext(ctx, "FleetId") + if fleetId == "" { + return nil, fmt.Errorf("FleetId filter is required to list game sessions; navigate from a fleet") + } + + sessions, err := appaws.Paginate(ctx, func(token *string) ([]types.GameSession, *string, error) { + output, err := d.client.DescribeGameSessions(ctx, &gamelift.DescribeGameSessionsInput{ + FleetId: &fleetId, + NextToken: token, + }) + if err != nil { + return nil, nil, apperrors.Wrap(err, "describe gamelift game sessions") + } + return output.GameSessions, output.NextToken, nil + }) + if err != nil { + return nil, err + } + + resources := make([]dao.Resource, len(sessions)) + for i, session := range sessions { + resources[i] = NewGameSessionResource(session) + } + return resources, nil +} + +// Get returns a specific GameLift game session by ID. +func (d *GameSessionDAO) Get(ctx context.Context, id string) (dao.Resource, error) { + output, err := d.client.DescribeGameSessions(ctx, &gamelift.DescribeGameSessionsInput{ + GameSessionId: &id, + }) + if err != nil { + return nil, apperrors.Wrapf(err, "describe gamelift game session %s", id) + } + if len(output.GameSessions) == 0 { + return nil, fmt.Errorf("gamelift game session %s not found", id) + } + return NewGameSessionResource(output.GameSessions[0]), nil +} + +// Delete is not supported for game sessions. +func (d *GameSessionDAO) Delete(_ context.Context, _ string) error { + return fmt.Errorf("delete is not supported for game sessions") +} + +// Supports returns whether this DAO supports the given operation. +func (d *GameSessionDAO) Supports(op dao.Operation) bool { + switch op { + case dao.OpList, dao.OpGet: + return true + default: + return false + } +} + +// GameSessionResource wraps a GameLift game session. +type GameSessionResource struct { + dao.BaseResource + Session types.GameSession +} + +// NewGameSessionResource creates a new GameSessionResource. +func NewGameSessionResource(session types.GameSession) *GameSessionResource { + sessionId := appaws.Str(session.GameSessionId) + name := appaws.Str(session.Name) + if name == "" { + name = appaws.ExtractResourceName(sessionId) + } + return &GameSessionResource{ + BaseResource: dao.BaseResource{ + ID: sessionId, + Name: name, + ARN: sessionId, // GameSessionId is the ARN + Data: session, + }, + Session: session, + } +} + +// Status returns the game session status. +func (r *GameSessionResource) Status() string { + return string(r.Session.Status) +} + +// StatusReason returns the status reason. +func (r *GameSessionResource) StatusReason() string { + return string(r.Session.StatusReason) +} + +// FleetId returns the fleet ID. +func (r *GameSessionResource) FleetId() string { + return appaws.Str(r.Session.FleetId) +} + +// FleetArn returns the fleet ARN. +func (r *GameSessionResource) FleetArn() string { + return appaws.Str(r.Session.FleetArn) +} + +// IpAddress returns the IP address. +func (r *GameSessionResource) IpAddress() string { + return appaws.Str(r.Session.IpAddress) +} + +// DnsName returns the DNS name. +func (r *GameSessionResource) DnsName() string { + return appaws.Str(r.Session.DnsName) +} + +// Port returns the port number. +func (r *GameSessionResource) Port() int32 { + return appaws.Int32(r.Session.Port) +} + +// CurrentPlayerSessionCount returns the current player count. +func (r *GameSessionResource) CurrentPlayerSessionCount() int32 { + return appaws.Int32(r.Session.CurrentPlayerSessionCount) +} + +// MaximumPlayerSessionCount returns the max player count. +func (r *GameSessionResource) MaximumPlayerSessionCount() int32 { + return appaws.Int32(r.Session.MaximumPlayerSessionCount) +} + +// PlayerSessionCreationPolicy returns the player session creation policy. +func (r *GameSessionResource) PlayerSessionCreationPolicy() string { + return string(r.Session.PlayerSessionCreationPolicy) +} + +// CreatorId returns the creator ID. +func (r *GameSessionResource) CreatorId() string { + return appaws.Str(r.Session.CreatorId) +} + +// Location returns the fleet location. +func (r *GameSessionResource) Location() string { + return appaws.Str(r.Session.Location) +} + +// CreationTime returns when the session was created. +func (r *GameSessionResource) CreationTime() *time.Time { + return r.Session.CreationTime +} + +// TerminationTime returns when the session was terminated. +func (r *GameSessionResource) TerminationTime() *time.Time { + return r.Session.TerminationTime +} + +// GameProperties returns the game properties. +func (r *GameSessionResource) GameProperties() []types.GameProperty { + return r.Session.GameProperties +} + +// GameSessionData returns the game session data. +func (r *GameSessionResource) GameSessionData() string { + return appaws.Str(r.Session.GameSessionData) +} diff --git a/custom/gamelift/game-sessions/register.go b/custom/gamelift/game-sessions/register.go new file mode 100644 index 0000000..c5484d0 --- /dev/null +++ b/custom/gamelift/game-sessions/register.go @@ -0,0 +1,20 @@ +package gamesessions + +import ( + "context" + + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/registry" + "github.com/clawscli/claws/internal/render" +) + +func init() { + registry.Global.RegisterCustom("gamelift", "game-sessions", registry.Entry{ + DAOFactory: func(ctx context.Context) (dao.DAO, error) { + return NewGameSessionDAO(ctx) + }, + RendererFactory: func() render.Renderer { + return NewGameSessionRenderer() + }, + }) +} diff --git a/custom/gamelift/game-sessions/render.go b/custom/gamelift/game-sessions/render.go new file mode 100644 index 0000000..416f9f0 --- /dev/null +++ b/custom/gamelift/game-sessions/render.go @@ -0,0 +1,211 @@ +package gamesessions + +import ( + "fmt" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/render" +) + +// Ensure GameSessionRenderer implements render.Navigator +var _ render.Navigator = (*GameSessionRenderer)(nil) + +// GameSessionRenderer renders GameLift game sessions. +type GameSessionRenderer struct { + render.BaseRenderer +} + +// NewGameSessionRenderer creates a new GameSessionRenderer. +func NewGameSessionRenderer() render.Renderer { + return &GameSessionRenderer{ + BaseRenderer: render.BaseRenderer{ + Service: "gamelift", + Resource: "game-sessions", + Cols: []render.Column{ + {Name: "SESSION NAME", Width: 28, Getter: func(r dao.Resource) string { return r.GetName() }}, + {Name: "STATUS", Width: 14, Getter: getSessionStatus}, + {Name: "PLAYERS", Width: 10, Getter: getSessionPlayers}, + {Name: "IP:PORT", Width: 22, Getter: getSessionEndpoint}, + {Name: "LOCATION", Width: 16, Getter: getSessionLocation, Priority: 2}, + {Name: "POLICY", Width: 14, Getter: getSessionPolicy, Priority: 3}, + {Name: "CREATED", Width: 20, Getter: getSessionCreated, Priority: 2}, + }, + }, + } +} + +func getSessionStatus(r dao.Resource) string { + session, ok := r.(*GameSessionResource) + if !ok { + return "" + } + return session.Status() +} + +func getSessionPlayers(r dao.Resource) string { + session, ok := r.(*GameSessionResource) + if !ok { + return "" + } + return fmt.Sprintf("%d/%d", session.CurrentPlayerSessionCount(), session.MaximumPlayerSessionCount()) +} + +func getSessionEndpoint(r dao.Resource) string { + session, ok := r.(*GameSessionResource) + if !ok { + return "" + } + ip := session.IpAddress() + port := session.Port() + if ip == "" { + return "" + } + if port > 0 { + return fmt.Sprintf("%s:%d", ip, port) + } + return ip +} + +func getSessionLocation(r dao.Resource) string { + session, ok := r.(*GameSessionResource) + if !ok { + return "" + } + return session.Location() +} + +func getSessionPolicy(r dao.Resource) string { + session, ok := r.(*GameSessionResource) + if !ok { + return "" + } + return session.PlayerSessionCreationPolicy() +} + +func getSessionCreated(r dao.Resource) string { + session, ok := r.(*GameSessionResource) + if !ok { + return "" + } + if t := session.CreationTime(); t != nil { + return t.Format("2006-01-02 15:04") + } + return "" +} + +// RenderDetail renders the detail view for a GameLift game session. +func (rr *GameSessionRenderer) RenderDetail(resource dao.Resource) string { + session, ok := resource.(*GameSessionResource) + if !ok { + return "" + } + + d := render.NewDetailBuilder() + + d.Title("GameLift Game Session", session.GetName()) + + d.Section("Basic Information") + d.Field("Name", session.GetName()) + d.Field("Game Session ID", session.GetID()) + d.Field("Status", session.Status()) + if reason := session.StatusReason(); reason != "" { + d.Field("Status Reason", reason) + } + + d.Section("Fleet") + d.Field("Fleet ID", session.FleetId()) + if arn := session.FleetArn(); arn != "" { + d.Field("Fleet ARN", arn) + } + if loc := session.Location(); loc != "" { + d.Field("Location", loc) + } + + d.Section("Connection") + if ip := session.IpAddress(); ip != "" { + d.Field("IP Address", ip) + } + if dns := session.DnsName(); dns != "" { + d.Field("DNS Name", dns) + } + if port := session.Port(); port > 0 { + d.Field("Port", fmt.Sprintf("%d", port)) + } + + d.Section("Players") + d.Field("Current Players", fmt.Sprintf("%d", session.CurrentPlayerSessionCount())) + d.Field("Max Players", fmt.Sprintf("%d", session.MaximumPlayerSessionCount())) + d.Field("Creation Policy", session.PlayerSessionCreationPolicy()) + if creator := session.CreatorId(); creator != "" { + d.Field("Creator ID", creator) + } + + // Game Properties + if props := session.GameProperties(); len(props) > 0 { + d.Section("Game Properties") + for _, prop := range props { + d.Field(appaws.Str(prop.Key), appaws.Str(prop.Value)) + } + } + + if data := session.GameSessionData(); data != "" { + d.Section("Game Session Data") + d.Field("Data", data) + } + + d.Section("Timestamps") + if t := session.CreationTime(); t != nil { + d.Field("Created", t.Format("2006-01-02 15:04:05")) + } + if t := session.TerminationTime(); t != nil { + d.Field("Terminated", t.Format("2006-01-02 15:04:05")) + } + + return d.String() +} + +// RenderSummary renders summary fields for a GameLift game session. +func (rr *GameSessionRenderer) RenderSummary(resource dao.Resource) []render.SummaryField { + session, ok := resource.(*GameSessionResource) + if !ok { + return rr.BaseRenderer.RenderSummary(resource) + } + + fields := []render.SummaryField{ + {Label: "Name", Value: session.GetName()}, + {Label: "Status", Value: session.Status()}, + {Label: "Players", Value: fmt.Sprintf("%d/%d", session.CurrentPlayerSessionCount(), session.MaximumPlayerSessionCount())}, + {Label: "Fleet ID", Value: session.FleetId()}, + } + + if ip := session.IpAddress(); ip != "" { + port := session.Port() + if port > 0 { + fields = append(fields, render.SummaryField{Label: "Endpoint", Value: fmt.Sprintf("%s:%d", ip, port)}) + } + } + + return fields +} + +// Navigations returns available navigations from a GameLift game session. +func (rr *GameSessionRenderer) Navigations(resource dao.Resource) []render.Navigation { + session, ok := resource.(*GameSessionResource) + if !ok { + return nil + } + + navs := []render.Navigation{ + { + Key: "f", + Label: fmt.Sprintf("Fleet (%s)", session.FleetId()), + Service: "gamelift", + Resource: "fleets", + FilterField: "FleetId", + FilterValue: session.FleetId(), + }, + } + + return navs +} diff --git a/custom/gamelift/matchmaking-configs/actions.go b/custom/gamelift/matchmaking-configs/actions.go new file mode 100644 index 0000000..e767ad5 --- /dev/null +++ b/custom/gamelift/matchmaking-configs/actions.go @@ -0,0 +1,62 @@ +package matchmakingconfigs + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + + "github.com/clawscli/claws/internal/action" + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +func init() { + action.Global.Register("gamelift", "matchmaking-configs", []action.Action{ + { + Name: "Delete", + Shortcut: "D", + Type: action.ActionTypeAPI, + Operation: "DeleteMatchmakingConfiguration", + Confirm: action.ConfirmDangerous, + }, + }) + + action.RegisterExecutor("gamelift", "matchmaking-configs", executeMatchmakingConfigAction) +} + +func executeMatchmakingConfigAction(ctx context.Context, act action.Action, resource dao.Resource) action.ActionResult { + switch act.Operation { + case "DeleteMatchmakingConfiguration": + return executeDeleteMatchmakingConfig(ctx, resource) + default: + return action.UnknownOperationResult(act.Operation) + } +} + +func executeDeleteMatchmakingConfig(ctx context.Context, resource dao.Resource) action.ActionResult { + config, ok := resource.(*MatchmakingConfigResource) + if !ok { + return action.InvalidResourceResult() + } + + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return action.ActionResult{Success: false, Error: apperrors.Wrap(err, "create gamelift client")} + } + client := gamelift.NewFromConfig(cfg) + + name := config.GetName() + _, err = client.DeleteMatchmakingConfiguration(ctx, &gamelift.DeleteMatchmakingConfigurationInput{ + Name: &name, + }) + if err != nil { + return action.ActionResult{Success: false, Error: fmt.Errorf("delete matchmaking configuration: %w", err)} + } + + return action.ActionResult{ + Success: true, + Message: fmt.Sprintf("Deleted matchmaking configuration %s", name), + } +} diff --git a/custom/gamelift/matchmaking-configs/constants.go b/custom/gamelift/matchmaking-configs/constants.go new file mode 100644 index 0000000..f259308 --- /dev/null +++ b/custom/gamelift/matchmaking-configs/constants.go @@ -0,0 +1,7 @@ +// Code generated by go generate; DO NOT EDIT. +// To regenerate: task gen-imports + +package matchmakingconfigs + +// ServiceResourcePath is the canonical path for this resource type. +const ServiceResourcePath = "gamelift/matchmaking-configs" diff --git a/custom/gamelift/matchmaking-configs/dao.go b/custom/gamelift/matchmaking-configs/dao.go new file mode 100644 index 0000000..1545313 --- /dev/null +++ b/custom/gamelift/matchmaking-configs/dao.go @@ -0,0 +1,172 @@ +package matchmakingconfigs + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + "github.com/aws/aws-sdk-go-v2/service/gamelift/types" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +// MatchmakingConfigDAO provides data access for GameLift matchmaking configurations. +type MatchmakingConfigDAO struct { + dao.BaseDAO + client *gamelift.Client +} + +// NewMatchmakingConfigDAO creates a new MatchmakingConfigDAO. +func NewMatchmakingConfigDAO(ctx context.Context) (dao.DAO, error) { + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return nil, apperrors.Wrap(err, "new "+ServiceResourcePath+" dao") + } + return &MatchmakingConfigDAO{ + BaseDAO: dao.NewBaseDAO("gamelift", "matchmaking-configs"), + client: gamelift.NewFromConfig(cfg), + }, nil +} + +// List returns all GameLift matchmaking configurations. +func (d *MatchmakingConfigDAO) List(ctx context.Context) ([]dao.Resource, error) { + configs, err := appaws.Paginate(ctx, func(token *string) ([]types.MatchmakingConfiguration, *string, error) { + output, err := d.client.DescribeMatchmakingConfigurations(ctx, &gamelift.DescribeMatchmakingConfigurationsInput{ + NextToken: token, + }) + if err != nil { + return nil, nil, apperrors.Wrap(err, "describe gamelift matchmaking configurations") + } + return output.Configurations, output.NextToken, nil + }) + if err != nil { + return nil, err + } + + resources := make([]dao.Resource, len(configs)) + for i, config := range configs { + resources[i] = NewMatchmakingConfigResource(config) + } + return resources, nil +} + +// Get returns a specific GameLift matchmaking configuration by name. +func (d *MatchmakingConfigDAO) Get(ctx context.Context, id string) (dao.Resource, error) { + output, err := d.client.DescribeMatchmakingConfigurations(ctx, &gamelift.DescribeMatchmakingConfigurationsInput{ + Names: []string{id}, + }) + if err != nil { + return nil, apperrors.Wrapf(err, "describe gamelift matchmaking configuration %s", id) + } + if len(output.Configurations) == 0 { + return nil, apperrors.Wrapf(err, "gamelift matchmaking configuration %s not found", id) + } + return NewMatchmakingConfigResource(output.Configurations[0]), nil +} + +// Delete deletes a GameLift matchmaking configuration by name. +func (d *MatchmakingConfigDAO) Delete(ctx context.Context, id string) error { + _, err := d.client.DeleteMatchmakingConfiguration(ctx, &gamelift.DeleteMatchmakingConfigurationInput{ + Name: &id, + }) + if err != nil { + return apperrors.Wrapf(err, "delete gamelift matchmaking configuration %s", id) + } + return nil +} + +// MatchmakingConfigResource wraps a GameLift matchmaking configuration. +type MatchmakingConfigResource struct { + dao.BaseResource + Config types.MatchmakingConfiguration +} + +// NewMatchmakingConfigResource creates a new MatchmakingConfigResource. +func NewMatchmakingConfigResource(config types.MatchmakingConfiguration) *MatchmakingConfigResource { + return &MatchmakingConfigResource{ + BaseResource: dao.BaseResource{ + ID: appaws.Str(config.Name), + Name: appaws.Str(config.Name), + ARN: appaws.Str(config.ConfigurationArn), + Data: config, + }, + Config: config, + } +} + +// RuleSetName returns the rule set name. +func (r *MatchmakingConfigResource) RuleSetName() string { + return appaws.Str(r.Config.RuleSetName) +} + +// RuleSetArn returns the rule set ARN. +func (r *MatchmakingConfigResource) RuleSetArn() string { + return appaws.Str(r.Config.RuleSetArn) +} + +// FlexMatchMode returns the FlexMatch mode. +func (r *MatchmakingConfigResource) FlexMatchMode() string { + return string(r.Config.FlexMatchMode) +} + +// BackfillMode returns the backfill mode. +func (r *MatchmakingConfigResource) BackfillMode() string { + return string(r.Config.BackfillMode) +} + +// AcceptanceRequired returns whether acceptance is required. +func (r *MatchmakingConfigResource) AcceptanceRequired() bool { + return appaws.Bool(r.Config.AcceptanceRequired) +} + +// AcceptanceTimeoutSeconds returns the acceptance timeout. +func (r *MatchmakingConfigResource) AcceptanceTimeoutSeconds() int32 { + return appaws.Int32(r.Config.AcceptanceTimeoutSeconds) +} + +// RequestTimeoutSeconds returns the request timeout. +func (r *MatchmakingConfigResource) RequestTimeoutSeconds() int32 { + return appaws.Int32(r.Config.RequestTimeoutSeconds) +} + +// AdditionalPlayerCount returns the additional player count. +func (r *MatchmakingConfigResource) AdditionalPlayerCount() int32 { + return appaws.Int32(r.Config.AdditionalPlayerCount) +} + +// Description returns the description. +func (r *MatchmakingConfigResource) Description() string { + return appaws.Str(r.Config.Description) +} + +// GameSessionQueueArns returns the game session queue ARNs. +func (r *MatchmakingConfigResource) GameSessionQueueArns() []string { + return r.Config.GameSessionQueueArns +} + +// NotificationTarget returns the SNS notification target. +func (r *MatchmakingConfigResource) NotificationTarget() string { + return appaws.Str(r.Config.NotificationTarget) +} + +// CreationTime returns when the configuration was created. +func (r *MatchmakingConfigResource) CreationTime() *time.Time { + return r.Config.CreationTime +} + +// GameProperties returns the game properties. +func (r *MatchmakingConfigResource) GameProperties() []types.GameProperty { + return r.Config.GameProperties +} + +// GameSessionData returns the game session data. +func (r *MatchmakingConfigResource) GameSessionData() string { + return appaws.Str(r.Config.GameSessionData) +} + +// CustomEventData returns the custom event data. +func (r *MatchmakingConfigResource) CustomEventData() string { + return appaws.Str(r.Config.CustomEventData) +} diff --git a/custom/gamelift/matchmaking-configs/register.go b/custom/gamelift/matchmaking-configs/register.go new file mode 100644 index 0000000..de4a034 --- /dev/null +++ b/custom/gamelift/matchmaking-configs/register.go @@ -0,0 +1,20 @@ +package matchmakingconfigs + +import ( + "context" + + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/registry" + "github.com/clawscli/claws/internal/render" +) + +func init() { + registry.Global.RegisterCustom("gamelift", "matchmaking-configs", registry.Entry{ + DAOFactory: func(ctx context.Context) (dao.DAO, error) { + return NewMatchmakingConfigDAO(ctx) + }, + RendererFactory: func() render.Renderer { + return NewMatchmakingConfigRenderer() + }, + }) +} diff --git a/custom/gamelift/matchmaking-configs/render.go b/custom/gamelift/matchmaking-configs/render.go new file mode 100644 index 0000000..0a27d2e --- /dev/null +++ b/custom/gamelift/matchmaking-configs/render.go @@ -0,0 +1,210 @@ +package matchmakingconfigs + +import ( + "fmt" + "strings" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/render" +) + +// Ensure MatchmakingConfigRenderer implements render.Navigator +var _ render.Navigator = (*MatchmakingConfigRenderer)(nil) + +// MatchmakingConfigRenderer renders GameLift matchmaking configurations. +type MatchmakingConfigRenderer struct { + render.BaseRenderer +} + +// NewMatchmakingConfigRenderer creates a new MatchmakingConfigRenderer. +func NewMatchmakingConfigRenderer() render.Renderer { + return &MatchmakingConfigRenderer{ + BaseRenderer: render.BaseRenderer{ + Service: "gamelift", + Resource: "matchmaking-configs", + Cols: []render.Column{ + {Name: "NAME", Width: 30, Getter: func(r dao.Resource) string { return r.GetName() }}, + {Name: "MODE", Width: 14, Getter: getMatchmakingMode}, + {Name: "BACKFILL", Width: 12, Getter: getMatchmakingBackfill}, + {Name: "ACCEPTANCE", Width: 12, Getter: getMatchmakingAcceptance}, + {Name: "RULE SET", Width: 24, Getter: getMatchmakingRuleSet}, + {Name: "TIMEOUT (s)", Width: 12, Getter: getMatchmakingTimeout, Priority: 2}, + {Name: "CREATED", Width: 20, Getter: getMatchmakingCreated, Priority: 2}, + }, + }, + } +} + +func getMatchmakingMode(r dao.Resource) string { + config, ok := r.(*MatchmakingConfigResource) + if !ok { + return "" + } + return config.FlexMatchMode() +} + +func getMatchmakingBackfill(r dao.Resource) string { + config, ok := r.(*MatchmakingConfigResource) + if !ok { + return "" + } + return config.BackfillMode() +} + +func getMatchmakingAcceptance(r dao.Resource) string { + config, ok := r.(*MatchmakingConfigResource) + if !ok { + return "" + } + if config.AcceptanceRequired() { + return "Required" + } + return "Not required" +} + +func getMatchmakingRuleSet(r dao.Resource) string { + config, ok := r.(*MatchmakingConfigResource) + if !ok { + return "" + } + return config.RuleSetName() +} + +func getMatchmakingTimeout(r dao.Resource) string { + config, ok := r.(*MatchmakingConfigResource) + if !ok { + return "" + } + timeout := config.RequestTimeoutSeconds() + if timeout == 0 { + return "-" + } + return fmt.Sprintf("%d", timeout) +} + +func getMatchmakingCreated(r dao.Resource) string { + config, ok := r.(*MatchmakingConfigResource) + if !ok { + return "" + } + if t := config.CreationTime(); t != nil { + return t.Format("2006-01-02 15:04") + } + return "" +} + +// RenderDetail renders the detail view for a GameLift matchmaking configuration. +func (rr *MatchmakingConfigRenderer) RenderDetail(resource dao.Resource) string { + config, ok := resource.(*MatchmakingConfigResource) + if !ok { + return "" + } + + d := render.NewDetailBuilder() + + d.Title("GameLift Matchmaking Configuration", config.GetName()) + + d.Section("Basic Information") + d.Field("Name", config.GetName()) + d.Field("ARN", config.GetARN()) + if desc := config.Description(); desc != "" { + d.Field("Description", desc) + } + + d.Section("Configuration") + d.Field("FlexMatch Mode", config.FlexMatchMode()) + d.Field("Backfill Mode", config.BackfillMode()) + if config.AcceptanceRequired() { + d.Field("Acceptance Required", "Yes") + if timeout := config.AcceptanceTimeoutSeconds(); timeout > 0 { + d.Field("Acceptance Timeout", fmt.Sprintf("%d seconds", timeout)) + } + } else { + d.Field("Acceptance Required", "No") + } + if timeout := config.RequestTimeoutSeconds(); timeout > 0 { + d.Field("Request Timeout", fmt.Sprintf("%d seconds", timeout)) + } + if count := config.AdditionalPlayerCount(); count > 0 { + d.Field("Additional Player Count", fmt.Sprintf("%d", count)) + } + + d.Section("Rule Set") + d.Field("Rule Set Name", config.RuleSetName()) + if arn := config.RuleSetArn(); arn != "" { + d.Field("Rule Set ARN", arn) + } + + // Game Session Queues + if queues := config.GameSessionQueueArns(); len(queues) > 0 { + d.Section("Game Session Queues") + for i, queueArn := range queues { + d.Field(fmt.Sprintf("Queue %d", i+1), queueArn) + } + } + + // Game Properties + if props := config.GameProperties(); len(props) > 0 { + d.Section("Game Properties") + for _, prop := range props { + d.Field(appaws.Str(prop.Key), appaws.Str(prop.Value)) + } + } + + // Notifications + if target := config.NotificationTarget(); target != "" { + d.Section("Notifications") + d.Field("SNS Target", target) + } + if data := config.CustomEventData(); data != "" { + d.Field("Custom Event Data", data) + } + + d.Section("Timestamps") + if t := config.CreationTime(); t != nil { + d.Field("Created", t.Format("2006-01-02 15:04:05")) + } + + return d.String() +} + +// RenderSummary renders summary fields for a GameLift matchmaking configuration. +func (rr *MatchmakingConfigRenderer) RenderSummary(resource dao.Resource) []render.SummaryField { + config, ok := resource.(*MatchmakingConfigResource) + if !ok { + return rr.BaseRenderer.RenderSummary(resource) + } + + return []render.SummaryField{ + {Label: "Name", Value: config.GetName()}, + {Label: "ARN", Value: config.GetARN()}, + {Label: "Mode", Value: config.FlexMatchMode()}, + {Label: "Rule Set", Value: config.RuleSetName()}, + } +} + +// Navigations returns available navigations from a GameLift matchmaking configuration. +func (rr *MatchmakingConfigRenderer) Navigations(resource dao.Resource) []render.Navigation { + config, ok := resource.(*MatchmakingConfigResource) + if !ok { + return nil + } + + var navs []render.Navigation + + // Navigate to game session queues + if queues := config.GameSessionQueueArns(); len(queues) > 0 { + queueName := appaws.ExtractResourceName(queues[0]) + navs = append(navs, render.Navigation{ + Key: "q", + Label: fmt.Sprintf("Queue (%s)", queueName), + Service: "gamelift", + Resource: "game-session-queues", + FilterField: "QueueName", + FilterValue: strings.TrimPrefix(queueName, "gamesessionqueue/"), + }) + } + + return navs +} diff --git a/custom/gamelift/scripts/actions.go b/custom/gamelift/scripts/actions.go new file mode 100644 index 0000000..7b7b536 --- /dev/null +++ b/custom/gamelift/scripts/actions.go @@ -0,0 +1,62 @@ +package scripts + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + + "github.com/clawscli/claws/internal/action" + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +func init() { + action.Global.Register("gamelift", "scripts", []action.Action{ + { + Name: "Delete", + Shortcut: "D", + Type: action.ActionTypeAPI, + Operation: "DeleteScript", + Confirm: action.ConfirmDangerous, + }, + }) + + action.RegisterExecutor("gamelift", "scripts", executeScriptAction) +} + +func executeScriptAction(ctx context.Context, act action.Action, resource dao.Resource) action.ActionResult { + switch act.Operation { + case "DeleteScript": + return executeDeleteScript(ctx, resource) + default: + return action.UnknownOperationResult(act.Operation) + } +} + +func executeDeleteScript(ctx context.Context, resource dao.Resource) action.ActionResult { + script, ok := resource.(*ScriptResource) + if !ok { + return action.InvalidResourceResult() + } + + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return action.ActionResult{Success: false, Error: apperrors.Wrap(err, "create gamelift client")} + } + client := gamelift.NewFromConfig(cfg) + + scriptId := script.GetID() + _, err = client.DeleteScript(ctx, &gamelift.DeleteScriptInput{ + ScriptId: &scriptId, + }) + if err != nil { + return action.ActionResult{Success: false, Error: fmt.Errorf("delete script: %w", err)} + } + + return action.ActionResult{ + Success: true, + Message: fmt.Sprintf("Deleted script %s", script.GetName()), + } +} diff --git a/custom/gamelift/scripts/constants.go b/custom/gamelift/scripts/constants.go new file mode 100644 index 0000000..2517d28 --- /dev/null +++ b/custom/gamelift/scripts/constants.go @@ -0,0 +1,7 @@ +// Code generated by go generate; DO NOT EDIT. +// To regenerate: task gen-imports + +package scripts + +// ServiceResourcePath is the canonical path for this resource type. +const ServiceResourcePath = "gamelift/scripts" diff --git a/custom/gamelift/scripts/dao.go b/custom/gamelift/scripts/dao.go new file mode 100644 index 0000000..805891b --- /dev/null +++ b/custom/gamelift/scripts/dao.go @@ -0,0 +1,134 @@ +package scripts + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/gamelift" + "github.com/aws/aws-sdk-go-v2/service/gamelift/types" + + appaws "github.com/clawscli/claws/internal/aws" + "github.com/clawscli/claws/internal/dao" + apperrors "github.com/clawscli/claws/internal/errors" +) + +// ScriptDAO provides data access for GameLift scripts. +type ScriptDAO struct { + dao.BaseDAO + client *gamelift.Client +} + +// NewScriptDAO creates a new ScriptDAO. +func NewScriptDAO(ctx context.Context) (dao.DAO, error) { + cfg, err := appaws.NewConfig(ctx) + if err != nil { + return nil, apperrors.Wrap(err, "new "+ServiceResourcePath+" dao") + } + return &ScriptDAO{ + BaseDAO: dao.NewBaseDAO("gamelift", "scripts"), + client: gamelift.NewFromConfig(cfg), + }, nil +} + +// List returns all GameLift scripts. +func (d *ScriptDAO) List(ctx context.Context) ([]dao.Resource, error) { + scripts, err := appaws.Paginate(ctx, func(token *string) ([]types.Script, *string, error) { + output, err := d.client.ListScripts(ctx, &gamelift.ListScriptsInput{ + NextToken: token, + }) + if err != nil { + return nil, nil, apperrors.Wrap(err, "list gamelift scripts") + } + return output.Scripts, output.NextToken, nil + }) + if err != nil { + return nil, err + } + + resources := make([]dao.Resource, len(scripts)) + for i, script := range scripts { + resources[i] = NewScriptResource(script) + } + return resources, nil +} + +// Get returns a specific GameLift script by ID. +func (d *ScriptDAO) Get(ctx context.Context, id string) (dao.Resource, error) { + output, err := d.client.DescribeScript(ctx, &gamelift.DescribeScriptInput{ + ScriptId: &id, + }) + if err != nil { + return nil, apperrors.Wrapf(err, "describe gamelift script %s", id) + } + if output.Script == nil { + return nil, fmt.Errorf("gamelift script %s not found", id) + } + return NewScriptResource(*output.Script), nil +} + +// Delete deletes a GameLift script by ID. +func (d *ScriptDAO) Delete(ctx context.Context, id string) error { + _, err := d.client.DeleteScript(ctx, &gamelift.DeleteScriptInput{ + ScriptId: &id, + }) + if err != nil { + return apperrors.Wrapf(err, "delete gamelift script %s", id) + } + return nil +} + +// ScriptResource wraps a GameLift script. +type ScriptResource struct { + dao.BaseResource + Script types.Script +} + +// NewScriptResource creates a new ScriptResource. +func NewScriptResource(script types.Script) *ScriptResource { + return &ScriptResource{ + BaseResource: dao.BaseResource{ + ID: appaws.Str(script.ScriptId), + Name: appaws.Str(script.Name), + ARN: appaws.Str(script.ScriptArn), + Data: script, + }, + Script: script, + } +} + +// Version returns the script version. +func (r *ScriptResource) Version() string { + return appaws.Str(r.Script.Version) +} + +// SizeOnDisk returns the size in bytes. +func (r *ScriptResource) SizeOnDisk() int64 { + return appaws.Int64(r.Script.SizeOnDisk) +} + +// CreationTime returns when the script was created. +func (r *ScriptResource) CreationTime() *time.Time { + return r.Script.CreationTime +} + +// NodeJsVersion returns the Node.js version. +func (r *ScriptResource) NodeJsVersion() string { + return appaws.Str(r.Script.NodeJsVersion) +} + +// StorageLocationBucket returns the S3 bucket. +func (r *ScriptResource) StorageLocationBucket() string { + if r.Script.StorageLocation != nil { + return appaws.Str(r.Script.StorageLocation.Bucket) + } + return "" +} + +// StorageLocationKey returns the S3 key. +func (r *ScriptResource) StorageLocationKey() string { + if r.Script.StorageLocation != nil { + return appaws.Str(r.Script.StorageLocation.Key) + } + return "" +} diff --git a/custom/gamelift/scripts/register.go b/custom/gamelift/scripts/register.go new file mode 100644 index 0000000..6db69ed --- /dev/null +++ b/custom/gamelift/scripts/register.go @@ -0,0 +1,20 @@ +package scripts + +import ( + "context" + + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/registry" + "github.com/clawscli/claws/internal/render" +) + +func init() { + registry.Global.RegisterCustom("gamelift", "scripts", registry.Entry{ + DAOFactory: func(ctx context.Context) (dao.DAO, error) { + return NewScriptDAO(ctx) + }, + RendererFactory: func() render.Renderer { + return NewScriptRenderer() + }, + }) +} diff --git a/custom/gamelift/scripts/render.go b/custom/gamelift/scripts/render.go new file mode 100644 index 0000000..a7a4d4a --- /dev/null +++ b/custom/gamelift/scripts/render.go @@ -0,0 +1,126 @@ +package scripts + +import ( + "github.com/clawscli/claws/internal/dao" + "github.com/clawscli/claws/internal/render" +) + +// ScriptRenderer renders GameLift scripts. +type ScriptRenderer struct { + render.BaseRenderer +} + +// NewScriptRenderer creates a new ScriptRenderer. +func NewScriptRenderer() render.Renderer { + return &ScriptRenderer{ + BaseRenderer: render.BaseRenderer{ + Service: "gamelift", + Resource: "scripts", + Cols: []render.Column{ + {Name: "NAME", Width: 30, Getter: func(r dao.Resource) string { return r.GetName() }}, + {Name: "SCRIPT ID", Width: 24, Getter: func(r dao.Resource) string { return r.GetID() }, Priority: 3}, + {Name: "VERSION", Width: 16, Getter: getScriptVersion}, + {Name: "NODE.JS", Width: 10, Getter: getScriptNodeJsVersion, Priority: 2}, + {Name: "SIZE", Width: 12, Getter: getScriptSize, Priority: 2}, + {Name: "CREATED", Width: 20, Getter: getScriptCreated}, + }, + }, + } +} + +func getScriptVersion(r dao.Resource) string { + script, ok := r.(*ScriptResource) + if !ok { + return "" + } + return script.Version() +} + +func getScriptNodeJsVersion(r dao.Resource) string { + script, ok := r.(*ScriptResource) + if !ok { + return "" + } + return script.NodeJsVersion() +} + +func getScriptSize(r dao.Resource) string { + script, ok := r.(*ScriptResource) + if !ok { + return "" + } + size := script.SizeOnDisk() + if size == 0 { + return "-" + } + return render.FormatSize(size) +} + +func getScriptCreated(r dao.Resource) string { + script, ok := r.(*ScriptResource) + if !ok { + return "" + } + if t := script.CreationTime(); t != nil { + return t.Format("2006-01-02 15:04") + } + return "" +} + +// RenderDetail renders the detail view for a GameLift script. +func (rr *ScriptRenderer) RenderDetail(resource dao.Resource) string { + script, ok := resource.(*ScriptResource) + if !ok { + return "" + } + + d := render.NewDetailBuilder() + + d.Title("GameLift Script", script.GetName()) + + d.Section("Basic Information") + d.Field("Name", script.GetName()) + d.Field("Script ID", script.GetID()) + d.Field("ARN", script.GetARN()) + + d.Section("Configuration") + if v := script.Version(); v != "" { + d.Field("Version", v) + } + if v := script.NodeJsVersion(); v != "" { + d.Field("Node.js Version", v) + } + + d.Section("Storage") + size := script.SizeOnDisk() + if size > 0 { + d.Field("Size on Disk", render.FormatSize(size)) + } + if bucket := script.StorageLocationBucket(); bucket != "" { + d.Field("S3 Bucket", bucket) + if key := script.StorageLocationKey(); key != "" { + d.Field("S3 Key", key) + } + } + + d.Section("Timestamps") + if t := script.CreationTime(); t != nil { + d.Field("Created", t.Format("2006-01-02 15:04:05")) + } + + return d.String() +} + +// RenderSummary renders summary fields for a GameLift script. +func (rr *ScriptRenderer) RenderSummary(resource dao.Resource) []render.SummaryField { + script, ok := resource.(*ScriptResource) + if !ok { + return rr.BaseRenderer.RenderSummary(resource) + } + + return []render.SummaryField{ + {Label: "Name", Value: script.GetName()}, + {Label: "Script ID", Value: script.GetID()}, + {Label: "Version", Value: script.Version()}, + } +} diff --git a/go.mod b/go.mod index a63d49a..e04480f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( charm.land/bubbletea/v2 v2.0.0-rc.2 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 github.com/atotto/clipboard v0.1.4 - github.com/aws/aws-sdk-go-v2 v1.41.0 + github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2/config v1.32.5 github.com/aws/aws-sdk-go-v2/service/accessanalyzer v1.45.7 github.com/aws/aws-sdk-go-v2/service/acm v1.37.18 @@ -48,6 +48,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/emr v1.57.4 github.com/aws/aws-sdk-go-v2/service/eventbridge v1.45.17 github.com/aws/aws-sdk-go-v2/service/fms v1.44.16 + github.com/aws/aws-sdk-go-v2/service/gamelift v1.50.0 github.com/aws/aws-sdk-go-v2/service/glue v1.135.3 github.com/aws/aws-sdk-go-v2/service/guardduty v1.70.1 github.com/aws/aws-sdk-go-v2/service/health v1.35.5 @@ -97,8 +98,8 @@ require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect diff --git a/go.sum b/go.sum index a55513e..d38d8bc 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 h1:059k1h5vvZ charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= -github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8= @@ -16,10 +16,10 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hg github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= @@ -102,6 +102,8 @@ github.com/aws/aws-sdk-go-v2/service/eventbridge v1.45.17 h1:ltbEzdlO5qKYK1FuwTt github.com/aws/aws-sdk-go-v2/service/eventbridge v1.45.17/go.mod h1:KXFNdzl+mZpQlLYm378Ml18wBHybbMpyBwNXuYjbDT4= github.com/aws/aws-sdk-go-v2/service/fms v1.44.16 h1:IoO9da/CYmn+WlJdEimFLj+n1Cv5vKQSd9gwZlNY1PY= github.com/aws/aws-sdk-go-v2/service/fms v1.44.16/go.mod h1:ps2AgucjzvCIdeuAOoXBRZUeVAqWgJ1+fGChfWRq3FM= +github.com/aws/aws-sdk-go-v2/service/gamelift v1.50.0 h1:knUB4jZTiIYcMQpdK4J6nk6zNQbHyTqEZL3KKaPavZs= +github.com/aws/aws-sdk-go-v2/service/gamelift v1.50.0/go.mod h1:JPSMCIr4USXQl0z5PXj7m9JFbb74k+U1L/QHzovpMMY= github.com/aws/aws-sdk-go-v2/service/glue v1.135.3 h1:Y3AJG3faZeMLkERgg+vdqhLDtBIx+8uc14BvWlxFcCY= github.com/aws/aws-sdk-go-v2/service/glue v1.135.3/go.mod h1:t3GxMA7CEzEXN6zmI6Br0gSLy+9x4ndsXTk1prQuP7s= github.com/aws/aws-sdk-go-v2/service/guardduty v1.70.1 h1:i6rDonvayDvW/AGQV3AjcQAZeC/oKclwhh2ozGNRRj8= diff --git a/internal/registry/registry.go b/internal/registry/registry.go index ebdc2d0..4622f3c 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -132,6 +132,7 @@ func defaultAliases() map[string]string { "cognito": "cognito-idp", "config": "configservice", "macie": "macie2", + "gl": "gamelift", } } @@ -165,6 +166,7 @@ func defaultDisplayNames() map[string]string { "directconnect": "Direct Connect", "dynamodb": "DynamoDB", "fms": "Firewall Manager", + "gamelift": "GameLift", "glue": "Glue", "guardduty": "GuardDuty", "health": "Health", @@ -263,6 +265,10 @@ func defaultCategories() []ServiceCategory { Name: "Cost Management", Services: []string{"risp", "ce", "budgets"}, }, + { + Name: "Game Development", + Services: []string{"gamelift"}, + }, } } @@ -547,6 +553,7 @@ var defaultResources = map[string]string{ "ec2": "instances", "ecr": "repositories", "ecs": "clusters", + "gamelift": "fleets", "eks": "clusters", "elbv2": "load-balancers", "emr": "clusters", @@ -645,6 +652,7 @@ var subResourceSet = map[string]struct{}{ "datasync/task-executions": {}, "batch/jobs": {}, "emr/steps": {}, + "gamelift/game-sessions": {}, "organizations/ous": {}, "license-manager/grants": {}, "appsync/data-sources": {}, From 936f9df26a6b0eaa6d351a4e92631f338becaaec Mon Sep 17 00:00:00 2001 From: Doma Palvolgyi <218264240+dpalvolgyi-pw@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:34:40 +0100 Subject: [PATCH 2/2] fix: improve error handling for gamelift resource retrieval This commit adresses concerns form the comments on PR #162 --- custom/gamelift/fleets/dao.go | 3 ++- custom/gamelift/game-session-queues/dao.go | 3 ++- custom/gamelift/matchmaking-configs/dao.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/custom/gamelift/fleets/dao.go b/custom/gamelift/fleets/dao.go index f2fb13e..b2c030c 100644 --- a/custom/gamelift/fleets/dao.go +++ b/custom/gamelift/fleets/dao.go @@ -2,6 +2,7 @@ package fleets import ( "context" + "fmt" "time" "github.com/aws/aws-sdk-go-v2/service/gamelift" @@ -61,7 +62,7 @@ func (d *FleetDAO) Get(ctx context.Context, id string) (dao.Resource, error) { return nil, apperrors.Wrapf(err, "describe gamelift fleet %s", id) } if len(output.FleetAttributes) == 0 { - return nil, apperrors.Wrapf(err, "gamelift fleet %s not found", id) + return nil, fmt.Errorf("gamelift fleet %s not found", id) } return NewFleetResource(output.FleetAttributes[0]), nil } diff --git a/custom/gamelift/game-session-queues/dao.go b/custom/gamelift/game-session-queues/dao.go index bdaec83..c8aa013 100644 --- a/custom/gamelift/game-session-queues/dao.go +++ b/custom/gamelift/game-session-queues/dao.go @@ -2,6 +2,7 @@ package gamesessionqueues import ( "context" + "fmt" "github.com/aws/aws-sdk-go-v2/service/gamelift" "github.com/aws/aws-sdk-go-v2/service/gamelift/types" @@ -60,7 +61,7 @@ func (d *QueueDAO) Get(ctx context.Context, id string) (dao.Resource, error) { return nil, apperrors.Wrapf(err, "describe gamelift game session queue %s", id) } if len(output.GameSessionQueues) == 0 { - return nil, apperrors.Wrapf(err, "gamelift game session queue %s not found", id) + return nil, fmt.Errorf("gamelift game session queue %s not found", id) } return NewQueueResource(output.GameSessionQueues[0]), nil } diff --git a/custom/gamelift/matchmaking-configs/dao.go b/custom/gamelift/matchmaking-configs/dao.go index 1545313..18e4986 100644 --- a/custom/gamelift/matchmaking-configs/dao.go +++ b/custom/gamelift/matchmaking-configs/dao.go @@ -2,6 +2,7 @@ package matchmakingconfigs import ( "context" + "fmt" "time" "github.com/aws/aws-sdk-go-v2/service/gamelift" @@ -61,7 +62,7 @@ func (d *MatchmakingConfigDAO) Get(ctx context.Context, id string) (dao.Resource return nil, apperrors.Wrapf(err, "describe gamelift matchmaking configuration %s", id) } if len(output.Configurations) == 0 { - return nil, apperrors.Wrapf(err, "gamelift matchmaking configuration %s not found", id) + return nil, fmt.Errorf("gamelift matchmaking configuration %s not found", id) } return NewMatchmakingConfigResource(output.Configurations[0]), nil }