From 9f6aa3594eb368bd44c1d752495844ac7a378af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rinaldo=20Pitzer=20J=C3=BAnior?= <16694899+rinaldodev@users.noreply.github.com> Date: Wed, 16 Nov 2022 13:20:50 -0300 Subject: [PATCH] add snapshot commands --- rhoc/pkg/cmd/deployments/deployments.go | 6 +- .../snapshot/deploymentSnapshot.go | 10 + rhoc/pkg/cmd/deployments/snapshot/snapshot.go | 176 ++++++++++++++ .../validateSnapshot/validateSnapshot.go | 217 ++++++++++++++++++ rhoc/pkg/service/service_func.go | 3 +- rhoc/pkg/util/cmdutil/cmdutil.go | 42 +++- 6 files changed, 450 insertions(+), 4 deletions(-) create mode 100644 rhoc/pkg/cmd/deployments/snapshot/deploymentSnapshot.go create mode 100644 rhoc/pkg/cmd/deployments/snapshot/snapshot.go create mode 100644 rhoc/pkg/cmd/deployments/validateSnapshot/validateSnapshot.go diff --git a/rhoc/pkg/cmd/deployments/deployments.go b/rhoc/pkg/cmd/deployments/deployments.go index b8c0d9f..958b2ae 100644 --- a/rhoc/pkg/cmd/deployments/deployments.go +++ b/rhoc/pkg/cmd/deployments/deployments.go @@ -3,7 +3,9 @@ package deployments import ( "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/cmd/deployments/get" "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/cmd/deployments/list" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/cmd/deployments/snapshot" "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/cmd/deployments/updateChannel" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/cmd/deployments/validateSnapshot" "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/util/cmdutil" "github.com/redhat-developer/app-services-cli/pkg/shared/factory" "github.com/spf13/cobra" @@ -20,7 +22,9 @@ func NewDeploymentsCommand(f *factory.Factory) *cobra.Command { cmd, list.NewListCommand(f), get.NewGetCommand(f), - updateChannel.NewUpdateChannelCommand(f)) + updateChannel.NewUpdateChannelCommand(f), + snapshot.NewSnapshotCommand(f), + validateSnapshot.NewValidateSnapshotCommand(f)) return cmd } diff --git a/rhoc/pkg/cmd/deployments/snapshot/deploymentSnapshot.go b/rhoc/pkg/cmd/deployments/snapshot/deploymentSnapshot.go new file mode 100644 index 0000000..36279d0 --- /dev/null +++ b/rhoc/pkg/cmd/deployments/snapshot/deploymentSnapshot.go @@ -0,0 +1,10 @@ +package snapshot + +import "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/api/admin" + +type DeploymentSnapshot struct { + ClusterId string + Id string + Kind string + Status admin.ConnectorState +} diff --git a/rhoc/pkg/cmd/deployments/snapshot/snapshot.go b/rhoc/pkg/cmd/deployments/snapshot/snapshot.go new file mode 100644 index 0000000..bde5568 --- /dev/null +++ b/rhoc/pkg/cmd/deployments/snapshot/snapshot.go @@ -0,0 +1,176 @@ +package snapshot + +import ( + "errors" + "fmt" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/service" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/util/cmdutil" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/util/dumper" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/util/request" + "github.com/redhat-developer/app-services-cli/pkg/shared/factory" + "github.com/spf13/cobra" + "time" +) + +const ( + CommandName = "snapshot" + CommandAlias = "ss" +) + +type options struct { + clusterAmount int + outputFile string + + f *factory.Factory +} + +func NewSnapshotCommand(f *factory.Factory) *cobra.Command { + opts := options{ + f: f, + } + + cmd := &cobra.Command{ + Short: "Takes a snapshot of deployments in oldest and ready clusters.", + Use: CommandName, + Aliases: []string{CommandAlias}, + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { + if opts.clusterAmount < 1 || opts.clusterAmount > 30 { + return errors.New("clusterAmount must be between 1 and 30") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return run(&opts) + }, + } + + cmdutil.AddClusterAmount(cmd, &opts.clusterAmount) + cmdutil.AddOutputFile(cmd, &opts.outputFile) + + return cmd +} + +func run(opts *options) error { + fmt.Fprintln(opts.f.IOStreams.Out, "Starting snapshot creation") + + if opts.outputFile == "" { + defaultFilename := fmt.Sprintf("snapshot%s.csv", time.Now().Format("20060102150405")) + fmt.Fprintf(opts.f.IOStreams.Out, "outputFile wasn't set, results will be written to %s\n", defaultFilename) + opts.outputFile = defaultFilename + } + + c, err := service.NewAdminClient(&service.Config{ + F: opts.f, + }) + if err != nil { + return err + } + + fmt.Fprintln(opts.f.IOStreams.Out, "Querying for clusters..") + + clusterListOpts := request.ListOptions{ + Page: 0, + Limit: opts.clusterAmount, + AllPages: false, + OrderBy: "created_at", + Search: "state=ready", + } + clusters, err := service.ListClusters(c, clusterListOpts) + if err != nil { + return err + } + + fmt.Fprintf(opts.f.IOStreams.Out, "Got %d clusters\n", clusters.Size) + + var snapshots []DeploymentSnapshot + for _, cluster := range clusters.Items { + fmt.Fprintf(opts.f.IOStreams.Out, "Querying for deployments in cluster %s..\n", cluster.Id) + + deployListOpts := request.ListDeploymentsOptions{ + ListOptions: request.ListOptions{ + AllPages: false, + Limit: 30, + Page: 0, + }, + ChannelUpdate: false, + DanglingDeployments: false, + } + + deployments, err := service.ListDeploymentsForCluster(c, deployListOpts, cluster.Id) + if err != nil { + return err + } + + fmt.Fprintf(opts.f.IOStreams.Out, "Got %d deployments in cluster %s\n", deployments.Size, cluster.Id) + + for _, deploy := range deployments.Items { + snapshots = append(snapshots, DeploymentSnapshot{ + ClusterId: cluster.Id, + Id: deploy.Id, + Kind: deploy.Spec.ConnectorTypeId, + Status: deploy.Status.Phase, + }) + } + } + + outputWriter, err := cmdutil.NewOutputFileWriter(opts.outputFile) + if err != nil { + return err + } + defer outputWriter.Close() + + fmt.Fprintf(opts.f.IOStreams.Out, "Creating snapshot with %d connector deployments\n", len(snapshots)) + err = dumper.DumpTable( + dumper.TableConfig[DeploymentSnapshot]{ + Style: dumper.TableStyleCSV, + Wide: false, + Columns: []dumper.Column[DeploymentSnapshot]{ + { + Name: "ClusterID", + Wide: false, + Getter: func(in *DeploymentSnapshot) dumper.Row { + return dumper.Row{ + Value: in.ClusterId, + } + }, + }, + { + Name: "ID", + Wide: false, + Getter: func(in *DeploymentSnapshot) dumper.Row { + return dumper.Row{ + Value: in.Id, + } + }, + }, + { + Name: "ConnectorTypeID", + Wide: false, + Getter: func(in *DeploymentSnapshot) dumper.Row { + return dumper.Row{ + Value: in.Kind, + } + }, + }, + { + Name: "Status", + Wide: false, + Getter: func(in *DeploymentSnapshot) dumper.Row { + return dumper.Row{ + Value: string(in.Status), + } + }, + }, + }, + }, + outputWriter, + snapshots, + ) + if err != nil { + return err + } + fmt.Fprintf(opts.f.IOStreams.Out, "Snapshot created with %d connector deployments\n", len(snapshots)) + + return nil +} diff --git a/rhoc/pkg/cmd/deployments/validateSnapshot/validateSnapshot.go b/rhoc/pkg/cmd/deployments/validateSnapshot/validateSnapshot.go new file mode 100644 index 0000000..49dc725 --- /dev/null +++ b/rhoc/pkg/cmd/deployments/validateSnapshot/validateSnapshot.go @@ -0,0 +1,217 @@ +package validateSnapshot + +import ( + "bytes" + "encoding/csv" + "errors" + "fmt" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/cmd/deployments/snapshot" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/service" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/util/cmdutil" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/util/dumper" + "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/pkg/util/request" + "github.com/redhat-developer/app-services-cli/pkg/shared/factory" + "github.com/spf13/cobra" + "io" + "os" +) + +const ( + CommandName = "validateSnapshot" + CommandAlias = "vs" +) + +type options struct { + clusterAmount int + inputFile string + + f *factory.Factory +} + +func NewValidateSnapshotCommand(f *factory.Factory) *cobra.Command { + opts := options{ + f: f, + } + + cmd := &cobra.Command{ + Short: "Validates a previous snapshot against the current state.", + Use: CommandName, + Aliases: []string{CommandAlias}, + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { + if opts.clusterAmount < 1 || opts.clusterAmount > 30 { + return errors.New("clusterAmount must be between 1 and 30") + } + if opts.inputFile == "" { + return errors.New("inputFile must be set") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return run(&opts) + }, + } + + cmdutil.AddClusterAmount(cmd, &opts.clusterAmount) + cmdutil.AddInputFile(cmd, &opts.inputFile) + + return cmd +} + +func run(opts *options) error { + fmt.Fprintln(opts.f.IOStreams.Out, "Starting snapshot validation") + + c, err := service.NewAdminClient(&service.Config{ + F: opts.f, + }) + if err != nil { + return err + } + + fmt.Fprintln(opts.f.IOStreams.Out, "Querying for clusters..") + + clusterListOpts := request.ListOptions{ + Page: 0, + Limit: opts.clusterAmount, + AllPages: false, + OrderBy: "created_at", + Search: "state=ready", + } + clusters, err := service.ListClusters(c, clusterListOpts) + if err != nil { + return err + } + + fmt.Fprintf(opts.f.IOStreams.Out, "Got %d clusters\n", clusters.Size) + + var snapshots []snapshot.DeploymentSnapshot + for _, cluster := range clusters.Items { + fmt.Fprintf(opts.f.IOStreams.Out, "Querying for deployments in cluster %s..\n", cluster.Id) + + deployListOpts := request.ListDeploymentsOptions{ + ListOptions: request.ListOptions{ + AllPages: false, + Limit: 30, + Page: 0, + }, + ChannelUpdate: false, + DanglingDeployments: false, + } + + deployments, err := service.ListDeploymentsForCluster(c, deployListOpts, cluster.Id) + if err != nil { + return err + } + + fmt.Fprintf(opts.f.IOStreams.Out, "Got %d deployments in cluster %s\n", deployments.Size, cluster.Id) + + for _, deploy := range deployments.Items { + snapshots = append(snapshots, snapshot.DeploymentSnapshot{ + ClusterId: cluster.Id, + Id: deploy.Id, + Kind: deploy.Spec.ConnectorTypeId, + Status: deploy.Status.Phase, + }) + } + } + + buffer := &bytes.Buffer{} + + fmt.Fprintf(opts.f.IOStreams.Out, "Creating snapshot with %d connector deployments\n", len(snapshots)) + err = dumper.DumpTable( + dumper.TableConfig[snapshot.DeploymentSnapshot]{ + Style: dumper.TableStyleCSV, + Wide: false, + Columns: []dumper.Column[snapshot.DeploymentSnapshot]{ + { + Name: "ClusterID", + Wide: false, + Getter: func(in *snapshot.DeploymentSnapshot) dumper.Row { + return dumper.Row{ + Value: in.ClusterId, + } + }, + }, + { + Name: "ID", + Wide: false, + Getter: func(in *snapshot.DeploymentSnapshot) dumper.Row { + return dumper.Row{ + Value: in.Id, + } + }, + }, + { + Name: "ConnectorTypeID", + Wide: false, + Getter: func(in *snapshot.DeploymentSnapshot) dumper.Row { + return dumper.Row{ + Value: in.Kind, + } + }, + }, + { + Name: "Status", + Wide: false, + Getter: func(in *snapshot.DeploymentSnapshot) dumper.Row { + return dumper.Row{ + Value: string(in.Status), + } + }, + }, + }, + }, + buffer, + snapshots, + ) + if err != nil { + return err + } + + return runComparison(opts.f.IOStreams.Out, opts.inputFile, buffer) +} + +func runComparison(out io.Writer, inputFile string, newSnapshot io.Reader) error { + fmt.Fprintln(out, "Comparing old and new snapshots..") + fmt.Fprintln(out, "--------") + + file, err := os.Open(inputFile) + if err != nil { + return err + } + + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if err != nil { + return err + } + + reader2 := csv.NewReader(newSnapshot) + records2, err := reader2.ReadAll() + if err != nil { + return err + } + + anyDiff := false + // print unequal lines + for i := range records { + diff := false + for j := range records[i] { + if records[i][j] != records2[i][j] { + diff = true + break + } + } + if diff { + anyDiff = true + fmt.Fprintf(out, "Difference located in line %d: \nwas: %v, \nnow: %v\n", i+1, records[i], records2[i]) + fmt.Fprintln(out, "--------") + } + } + + if !anyDiff { + fmt.Fprintln(out, "No diff found!") + } + + return nil +} diff --git a/rhoc/pkg/service/service_func.go b/rhoc/pkg/service/service_func.go index cf40173..406f62c 100644 --- a/rhoc/pkg/service/service_func.go +++ b/rhoc/pkg/service/service_func.go @@ -277,7 +277,8 @@ func ListDeploymentsForCluster(c *AdminAPI, opts request.ListDeploymentsOptions, return items, response.Error(err, httpRes) } - if len(result.Items) == 0 { + + if result.Size == 0 || result.Items == nil || len(result.Items) == 0 { break } diff --git a/rhoc/pkg/util/cmdutil/cmdutil.go b/rhoc/pkg/util/cmdutil/cmdutil.go index 4e479e3..363a7fa 100644 --- a/rhoc/pkg/util/cmdutil/cmdutil.go +++ b/rhoc/pkg/util/cmdutil/cmdutil.go @@ -2,11 +2,10 @@ package cmdutil import ( "fmt" - "strings" - "github.com/bf2fc6cc711aee1a0c2a/cos-tools/rhoc/internal/build" "github.com/redhat-developer/app-services-cli/pkg/core/cmdutil/flagutil" "github.com/redhat-developer/app-services-cli/pkg/core/ioutil/dump" + "strings" "github.com/AlecAivazis/survey/v2" p "github.com/gertd/go-pluralize" @@ -367,6 +366,45 @@ func AddRevision(cmd *cobra.Command, output *int64) *FlagOptions { return withFlagOptions(cmd, name) } +func AddClusterAmount(cmd *cobra.Command, output *int) *FlagOptions { + name := "clusterAmount" + + cmd.Flags().IntVar( + output, + name, + 3, + "Amount of clusters to query when taking the snapshot", + ) + + return withFlagOptions(cmd, name) +} + +func AddOutputFile(cmd *cobra.Command, output *string) *FlagOptions { + name := "outputFile" + + cmd.Flags().StringVar( + output, + name, + "", + "File to write the output to", + ) + + return withFlagOptions(cmd, name) +} + +func AddInputFile(cmd *cobra.Command, output *string) *FlagOptions { + name := "inputFile" + + cmd.Flags().StringVar( + output, + name, + "", + "File to read the input from", + ) + + return withFlagOptions(cmd, name) +} + func AddFile(cmd *cobra.Command, output *string) *FlagOptions { name := "file"