Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f2401fd
feat: add server diff functionality to compare local config with runn…
a-spiker Jul 4, 2025
8091669
test: add comprehensive tests for server diff functionality and argum…
a-spiker Jul 4, 2025
3531294
chore: update go.sum to remove obsolete aerospike-management-lib entries
a-spiker Jul 4, 2025
85d89ea
Merge branch 'main' into TOOLS-3077
a-spiker Jul 15, 2025
8a1c39b
feat: Improve type-aware comparison in diff logic
a-spiker Jul 15, 2025
820290b
fix: Improve valuesEqual logic and add comprehensive tests
a-spiker Jul 15, 2025
2aeffc1
feat: make slice comparison order-agnostic and efficient
a-spiker Jul 21, 2025
48df97f
feat: Add beta warning and adjust log levels in diff command
a-spiker Jul 21, 2025
e7419ba
refactor: refactor diff command and add subcommand for server diff
a-spiker Jul 22, 2025
0693130
refactor: Remove redundant and low-level unit tests from diff_test.go
a-spiker Jul 22, 2025
5fc646b
docs: Update diff command descriptions for server comparison
a-spiker Jul 24, 2025
a26cce2
docs: TOOLS-3077 Update diff command descriptions for clarity
a-spiker Jul 24, 2025
ad345f6
fix: Refactor format flag handling in diff commands
a-spiker Jul 24, 2025
83f1dd5
feat: TOOLS-3077 Refactor diff command to use subcommands
a-spiker Jul 24, 2025
645adf9
fix: Update diff tests to use substring match
a-spiker Jul 24, 2025
3c71e70
docs: Update diff command help text and examples
a-spiker Jul 24, 2025
d6b3799
Update cmd/diff.go
a-spiker Jul 29, 2025
e7bc39d
Merge branch 'main' into TOOLS-3092-1
a-spiker Jul 30, 2025
ab1c405
feat: Add list-versions command to list available server versions
a-spiker Jul 30, 2025
623634a
refactor: TOOLS-3092 Rename table format flag to verbose in list-vers…
a-spiker Aug 1, 2025
cd8aeb0
feat: TOOLS-3092 Add version diff command to compare Aerospike server…
a-spiker Aug 1, 2025
c0230b1
Merge branch 'main' into TOOLS-3092-1
a-spiker Aug 19, 2025
3d007d8
chore: Update schema submodule to latest commit f99d3c1
a-spiker Aug 19, 2025
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
149 changes: 143 additions & 6 deletions cmd/diff.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"sort"
"strconv"
"strings"

asConf "github.com/aerospike/aerospike-management-lib/asconfig"
"github.com/aerospike/aerospike-management-lib/info"
"github.com/aerospike/asconfig/conf"
"github.com/aerospike/asconfig/schema"
"github.com/aerospike/tools-common-go/config"
"github.com/aerospike/tools-common-go/flags"

"github.com/spf13/cobra"
)

const (
diffArgMin = 2
diffArgMax = 2
diffServerArgMin = 1 // For server diff, we need only one local file
diffServerArgMax = 1
diffArgMin = 2
diffArgMax = 2
diffServerArgMin = 1 // For server diff, we need only one local file
diffServerArgMax = 1
diffVersionsArgMin = 2
diffVersionsArgMax = 2
)

var (
errDiffConfigsDiffer = errors.Join(fmt.Errorf("configuration files are not equal"), ErrSilent)
errDiffTooFewArgs = fmt.Errorf("diff requires atleast %d file paths as arguments", diffArgMin)
errDiffTooManyArgs = fmt.Errorf("diff requires no more than %d file paths as arguments", diffArgMax)
errDiffServerTooFewArgs = fmt.Errorf("diff with --server requires exactly %d file path as argument", diffServerArgMin)
errDiffServerTooManyArgs = fmt.Errorf("diff with --server requires no more than %d file path as argument", diffServerArgMax)
errDiffServerTooFewArgs = fmt.Errorf("diff server requires exactly %d file path as argument", diffServerArgMin)
errDiffServerTooManyArgs = fmt.Errorf("diff server requires no more than %d file path as argument", diffServerArgMax)
errSchemaDiffTooFewArgs = fmt.Errorf("diff versions requires at least %d version arguments", diffVersionsArgMin)
errSchemaDiffTooManyArgs = fmt.Errorf("diff versions requires no more than %d version arguments", diffVersionsArgMax)
errInvalidSchemaVersion = fmt.Errorf("invalid schema version")
errInvalidVersionOrder = fmt.Errorf("first argument version must be less than or equal to second argument version")
)

func init() {
Expand Down Expand Up @@ -64,6 +73,7 @@ func newDiffCmd() *cobra.Command {
// Add subcommands
res.AddCommand(newDiffFilesCmd())
res.AddCommand(newDiffServerCmd())
res.AddCommand(newDiffVersionsCmd())

return res
}
Expand Down Expand Up @@ -121,6 +131,33 @@ func newDiffServerCmd() *cobra.Command {
return cmd
}

func newDiffVersionsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "versions [flags] <version1> <version2>",
Short: "Diff Aerospike server versions.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Short: "Diff Aerospike server versions.",
Short: "Show configuration file difference between versions of the Aerospike server.",

suggestion: thinking about how to clearly describe this

Long: `Diff compares the configuration between two Aerospike server versions,
showing which configuration parameters are added, removed, or changed.
This helps understand what configuration options are going away or
staying when upgrading between Aerospike versions.`,
Example: `
# Compare two Aerospike server versions
asconfig diff versions 7.0.0 7.2.0

# List supported Aerospike server versions
asconfig list-versions --verbose
`,
RunE: func(cmd *cobra.Command, args []string) error {
logger.Debug("Running versions diff command")
return runVersionsDiff(cmd, args)
},
}

cmd.Flags().BoolP("verbose", "v", false, "Show detailed information about property changes (type, default values, etc.)")
cmd.Flags().StringP("filter-path", "f", "", "Filter results to only show properties under the specified path (e.g., 'service', 'namespaces')")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it take nested path also? or just the main context?


return cmd
}

// runFileDiff handles the original file-to-file diff functionality
func runFileDiff(cmd *cobra.Command, args []string) error {
if len(args) < diffArgMin {
Expand Down Expand Up @@ -288,6 +325,106 @@ func runServerDiff(cmd *cobra.Command, args []string) error {
return nil
}

// runVersionsDiff compares the configuration between two Aerospike server versions.
func runVersionsDiff(cmd *cobra.Command, args []string) error {
logger.Debug("Running diff versions command")

if len(args) < diffVersionsArgMin {
return errSchemaDiffTooFewArgs
}

if len(args) > diffVersionsArgMax {
return errSchemaDiffTooManyArgs
}

version1 := args[0]
version2 := args[1]

// Validate version order
if !isVersionLessEqual(version1, version2) {
return errors.Join(errInvalidVersionOrder, fmt.Errorf("version %s is not less than or equal to %s", version1, version2))
}

logger.Debugf("Comparing schema from version %s to version %s", version1, version2)

// Load schemas
schemaMap, err := schema.NewSchemaMap()
if err != nil {
return fmt.Errorf("failed to load schema map: %w", err)
}

schema1, exists := schemaMap[version1]
if !exists {
return errors.Join(errInvalidSchemaVersion, fmt.Errorf("schema for version %s not found", version1))
}

schema2, exists := schemaMap[version2]
if !exists {
return errors.Join(errInvalidSchemaVersion, fmt.Errorf("schema for version %s not found", version2))
}

var parsedSchema1, parsedSchema2 map[string]interface{}
if err := json.Unmarshal([]byte(schema1), &parsedSchema1); err != nil {
return fmt.Errorf("failed to parse schema for version %s: %w", version1, err)
}

if err := json.Unmarshal([]byte(schema2), &parsedSchema2); err != nil {
return fmt.Errorf("failed to parse schema for version %s: %w", version2, err)
}

// Get flags
verbose, _ := cmd.Flags().GetBool("verbose")
filterPath, _ := cmd.Flags().GetString("filter-path")

filterSections := make(map[string]struct{})
if filterPath != "" {
sections := strings.Split(filterPath, ",")
for _, s := range sections {
filterSections[strings.TrimSpace(s)] = struct{}{}
}
}

// Compare the two JSON files
summary, err := compareSchemas(parsedSchema1, parsedSchema2, version1, version2)
if err != nil {
return fmt.Errorf("failed to compare schemas: %w", err)
}

// Output the results
printChangeSummary(summary, DiffOptions{
Verbose: verbose,
FilterSections: filterSections,
})

return nil
}

// isVersionLessEqual compares two version strings (X.X.X) and returns true if v1 <= v2
func isVersionLessEqual(v1, v2 string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alot of our tools have their own implementation of this. Could we use the one in the management liv here? https://github.com/aerospike/aerospike-management-lib/blob/504da4668525110dd75aa2bd5aa33d52a12e8233/utils.go#L72

If not, we should add this to tools common go so we can unify how our tools compare versions.

parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")

for i := 0; i < 3; i++ {
num1 := 0
if i < len(parts1) {
num1, _ = strconv.Atoi(parts1[i]) // Ignore error, Atoi returns 0 for invalid input
}

num2 := 0
if i < len(parts2) {
num2, _ = strconv.Atoi(parts2[i]) // Ignore error
}

if num1 < num2 {
return true
}
if num1 > num2 {
return false
}
}
return true // Versions are equal
}

// diffFlatMaps reports differences between flattened config maps
// this only works for maps 1 layer deep as produced by the management
// lib's flattenConf function
Expand Down
Loading
Loading