Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions cmd/acsfleetctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"

"github.com/spf13/cobra"
"github.com/stackrox/acs-fleet-manager/internal/central/pkg/cmd/admin"
"github.com/stackrox/acs-fleet-manager/internal/central/pkg/cmd/centrals"
gitopsCmd "github.com/stackrox/acs-fleet-manager/internal/central/pkg/gitops/cmd"
)
Expand All @@ -26,6 +25,5 @@ func main() {

func setupSubCommands(rootCmd *cobra.Command) {
rootCmd.AddCommand(centrals.NewCentralsCommand())
rootCmd.AddCommand(admin.NewAdminCommand())
rootCmd.AddCommand(gitopsCmd.NewGitOpsCommand())
}
40 changes: 20 additions & 20 deletions cmd/fleet-manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package main

import (
"flag"
"fmt"
"os"

"github.com/stackrox/acs-fleet-manager/internal/central/pkg/cmd/admin"
"github.com/stackrox/acs-fleet-manager/pkg/cmd/migrate"
"github.com/stackrox/acs-fleet-manager/pkg/cmd/serve"

Expand All @@ -19,8 +22,6 @@ func main() {
// parsed.
_ = flag.CommandLine.Parse([]string{})

// pflag.CommandLine.AddGoFlagSet(flag.CommandLine)

// Always log to stderr by default
if err := flag.Set("logtostderr", "true"); err != nil {
glog.Infof("Unable to set logtostderr to true")
Expand All @@ -30,32 +31,31 @@ func main() {
central.ConfigProviders(),
)
if err != nil {
glog.Fatalf("error initializing: %v", err)
_, _ = fmt.Fprintf(os.Stderr, "error initializing: %v\n", err)
os.Exit(1)
}
defer env.Cleanup()
rootCmd := rootCommand(env)

if err := env.AddFlags(rootCmd.PersistentFlags()); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "unable to add global flags: %v\n", err)
os.Exit(1)
}
err = rootCmd.Execute()
env.Cleanup()
if err != nil {
os.Exit(1)
}
}

func rootCommand(env *environments.Env) *cobra.Command {
rootCmd := &cobra.Command{
Use: "fleet-manager",
Long: "fleet-manager is a service that exposes a Rest API to manage ACS Central instances.",
}

err = env.AddFlags(rootCmd.PersistentFlags())
if err != nil {
glog.Fatalf("Unable to add global flags: %s", err.Error())
}

rootCmd.AddCommand(admin.NewAdminCommand())
rootCmd.AddCommand(migrate.NewMigrateCommand(env))
rootCmd.AddCommand(serve.NewServeCommand(env))
// Unsupported CLI commands. Eventually some of them can be removed.
// rootCmd.AddCommand(cluster.NewClusterCommand(env))
// rootCmd.AddCommand(cloudprovider.NewCloudProviderCommand(env))
// rootCmd.AddCommand(errors.NewErrorsCommand(env))

if err := rootCmd.Execute(); err != nil {
glog.Fatalf("error running command: %v", err)
}

if err != nil {
glog.Fatalf("Unable to initialize environment: %s", err.Error())
}
return rootCmd
}
2 changes: 2 additions & 0 deletions internal/central/pkg/api/admin/private/api/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 20 additions & 19 deletions internal/central/pkg/api/admin/private/model_central.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions internal/central/pkg/cmd/admin/centrals/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ const (
// NewAdminCentralsCommand creates a new admin central command.
func NewAdminCentralsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "centrals",
Aliases: []string{"central"},
Short: "Perform admin central API calls.",
Long: "Perform admin central API calls.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
Use: "centrals",
Aliases: []string{"central"},
Short: "Perform admin central API calls.",
Long: "Perform admin central API calls.",
}
cmd.AddCommand(
NewAdminCentralsListCommand(),
NewAdminReportCommand(),
)

return cmd
Expand Down
2 changes: 1 addition & 1 deletion internal/central/pkg/cmd/admin/centrals/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func NewAdminCentralsListCommand() *cobra.Command {
Short: "lists all managed central requests",
Long: "lists all managed central requests",
Run: func(cmd *cobra.Command, args []string) {
runList(fleetmanagerclient.AuthenticatedClientWithRHOASToken(cmd.Context()), cmd, args)
runList(fleetmanagerclient.ClientFromContext(cmd.Context()), cmd, args)
},
}
return cmd
Expand Down
230 changes: 230 additions & 0 deletions internal/central/pkg/cmd/admin/centrals/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package centrals

import (
"context"
"fmt"
"io"
"net/http"
"strings"
"text/tabwriter"
"time"

"os"

"github.com/antihax/optional"
"github.com/golang/glog"

"github.com/spf13/cobra"
"github.com/stackrox/acs-fleet-manager/internal/central/constants"
adminAPI "github.com/stackrox/acs-fleet-manager/internal/central/pkg/api/admin/private"
"github.com/stackrox/acs-fleet-manager/internal/central/pkg/cmd/fleetmanagerclient"
)

var excludedStatuses = []string{
constants.CentralRequestStatusFailed.String(),
}

// NewAdminReportCommand creates the admin report subcommand.
func NewAdminReportCommand() *cobra.Command {
return &cobra.Command{
Use: "report",
Short: "Print ACS instance report in Slack mrkdwn format",
Long: "Print ACS instance report in Slack mrkdwn format.",
Run: func(cmd *cobra.Command, args []string) {
client := fleetmanagerclient.ClientFromContext(cmd.Context())
if err := runReport(cmd.Context(), client.AdminAPI(), os.Stdout); err != nil {
glog.Errorf("instance report failed: %v", err)
os.Exit(1)
}
},
}
}

// reportAPI is the subset of the fleet manager admin API needed for the report.
type reportAPI interface {
GetCentrals(ctx context.Context, opts *adminAPI.GetCentralsOpts) (adminAPI.CentralList, *http.Response, error)
}

func fetchAllCentrals(ctx context.Context, api reportAPI, search string) ([]adminAPI.Central, error) {
const pageSize = "100"
var all []adminAPI.Central

for page := 1; ; page++ {
opts := &adminAPI.GetCentralsOpts{
Page: optional.NewString(fmt.Sprintf("%d", page)),
Size: optional.NewString(pageSize),
OrderBy: optional.NewString("created_at desc"),
}
if search != "" {
opts.Search = optional.NewString(search)
}

list, _, err := api.GetCentrals(ctx, opts)
if err != nil {
return nil, fmt.Errorf("fetching centrals (page %d): %w", page, err)
}

if len(list.Items) == 0 {
break
}

all = append(all, list.Items...)

if int32(len(all)) >= list.Total {
break
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
return all, nil
}

func buildStatusFilter() string {
var parts []string
for _, s := range excludedStatuses {
parts = append(parts, fmt.Sprintf("status <> %s", s))
}
return strings.Join(parts, " and ")
}

func runReport(ctx context.Context, api reportAPI, w io.Writer) error {
statusFilter := buildStatusFilter()

allCentrals, err := fetchAllCentrals(ctx, api, statusFilter)
if err != nil {
return fmt.Errorf("fetching centrals: %w", err)
}

sevenDaysAgo := time.Now().UTC().Add(-7 * 24 * time.Hour)

var naInstances, euInstances, evalInstances, expiredInstances []adminAPI.Central

for i := range allCentrals {
c := &allCentrals[i]

if strings.HasPrefix(c.Name, "probe-") {
continue
}

if strings.HasPrefix(c.Region, "us-") {
if !c.CreatedAt.Before(sevenDaysAgo) {
naInstances = append(naInstances, *c)
}
}

if strings.HasPrefix(c.Region, "eu-") {
if !c.CreatedAt.Before(sevenDaysAgo) {
euInstances = append(euInstances, *c)
}
}

if c.InstanceType == "eval" {
evalInstances = append(evalInstances, *c)
}

if c.ExpiredAt != nil {
expiredInstances = append(expiredInstances, *c)
}
}

allFailedCentrals, err := fetchAllCentrals(ctx, api, "status = failed")
if err != nil {
return fmt.Errorf("fetching failed centrals: %w", err)
}
var failedCentrals []adminAPI.Central
for i := range allFailedCentrals {
if !strings.HasPrefix(allFailedCentrals[i].Name, "probe-") {
failedCentrals = append(failedCentrals, allFailedCentrals[i])
}
}

today := time.Now().UTC().Format("2006-01-02")
fmt.Fprintf(w, "*Instance Report* (%s)\n", today)
fmt.Fprintln(w, "I live in <https://github.com/stackrox/acs-fleet-manager|stackrox/acs-fleet-manager>, you can get your own copy by running `./fleet-manager admin central report` there.")

regionHeaders := []string{"ID", "Name", "Org", "Owner", "Created", "Type/Quota", "Status"}
writeSection(w, "All *new* instances in North America in the past *7 days*:", naInstances, regionHeaders, regionRow)
writeSection(w, "All *new* instances in Europe in the past *7 days*:", euInstances, regionHeaders, regionRow)

evalHeaders := []string{"ID", "Name", "Org", "Owner", "Created", "Region", "Type/Quota", "Status"}
writeSection(w, "All *eval* instances (we want none):", evalInstances, evalHeaders, fullRow)

expiredHeaders := []string{"ID", "Name", "Org", "Owner", "Expires", "Region", "Status"}
writeSection(w, "Instances that have *expiration date set*, they are usually removed 2 weeks after:", expiredInstances, expiredHeaders, expiredRow)

failedHeaders := []string{"ID", "Name", "Org", "Owner", "Region", "Type/Quota", "Failed Reason"}
writeSection(w, "All *failed* instances:", failedCentrals, failedHeaders, failedRow)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return nil
}

type rowFunc func(c *adminAPI.Central) []string

func writeSection(w io.Writer, title string, instances []adminAPI.Central, headers []string, rowFn rowFunc) {
fmt.Fprintf(w, "\n%s [%d]\n", title, len(instances))
if len(instances) == 0 {
fmt.Fprintln(w, "(none)")
return
}
fmt.Fprintln(w, "```")
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
writeRow(tw, headers)
for i := range instances {
writeRow(tw, rowFn(&instances[i]))
}
tw.Flush()
fmt.Fprintln(w, "```")
}

func writeRow(w io.Writer, values []string) {
for i, v := range values {
if i > 0 {
fmt.Fprint(w, "\t| ")
}
fmt.Fprint(w, v)
}
fmt.Fprintln(w)
}

func orgColumn(c *adminAPI.Central) string {
name := c.OrganisationName
if name == "" {
name = "-"
}
if c.OrganisationId == "" {
return name
}
return fmt.Sprintf("%s (%s)", name, c.OrganisationId)
}

func typeQuota(c *adminAPI.Central) string {
if c.QuotaType == "" {
return c.InstanceType
}
return c.InstanceType + "/" + c.QuotaType
}

func regionRow(c *adminAPI.Central) []string {
return []string{c.Id, c.Name, orgColumn(c), c.Owner, c.CreatedAt.Format("2006-01-02"),
typeQuota(c), c.Status}
}

func fullRow(c *adminAPI.Central) []string {
return []string{c.Id, c.Name, orgColumn(c), c.Owner, c.CreatedAt.Format("2006-01-02"), c.Region,
typeQuota(c), c.Status}
}

func expiredRow(c *adminAPI.Central) []string {
expires := "-"
if c.ExpiredAt != nil {
expires = c.ExpiredAt.Format("2006-01-02 15:04")
}
return []string{c.Id, c.Name, orgColumn(c), c.Owner, expires, c.Region, c.Status}
}

func failedRow(c *adminAPI.Central) []string {
reason := c.FailedReason
if reason == "" {
reason = "-"
}
return []string{c.Id, c.Name, orgColumn(c), c.Owner, c.Region,
typeQuota(c), reason}
}
Loading
Loading