diff --git a/cli/cmd/login.go b/cli/cmd/login.go index c6909d96dd0620cdd7c4a083aaf170841f9d83cc..ac210be646fd12778915ccf1b3429dcae938daa0 100644 --- a/cli/cmd/login.go +++ b/cli/cmd/login.go @@ -59,10 +59,7 @@ var loginCmd = &cobra.Command{ // log out to remove active session in case an user is already logged in if userToken != "" { - _, err := api.Logout(createContextWithAuthorization(), viper.GetString("controllerAPIEndpoint"), nbUserName) - if err != nil { - pterm.Error.Println("error logging out active user", err) - } + _, _ = api.Logout(createContextWithAuthorization(), viper.GetString("controllerAPIEndpoint"), nbUserName) } // TODO: maybe add credentials in context instead of context.TODO() diff --git a/controller/cmd/root.go b/controller/cmd/root.go index 7276344020865de5d146d98f63f781005cbe905d..22f31befeb4ba4800556f11217506a4e4fda3e41 100644 --- a/controller/cmd/root.go +++ b/controller/cmd/root.go @@ -100,7 +100,7 @@ func initConfig() { } else { env := config.DetermineConfigEnvironment() - log.Infof("environment is %s\n", env) + log.Debugf("environment is %s\n", env) viper.AddConfigPath(configHome) viper.AddConfigPath("/usr/local/etc/gosdn/") diff --git a/controller/cmd/version.go b/controller/cmd/version.go new file mode 100644 index 0000000000000000000000000000000000000000..bc4902b31b1f7e61f78355b2446efe25243dafc6 --- /dev/null +++ b/controller/cmd/version.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "fmt" + + "code.fbi.h-da.de/danet/gosdn/controller/version" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// version represents the version command. +var versionCmd = &cobra.Command{ + Use: "version", + Short: "returns information about the controllers version", + Long: `Version allows the user to access version and build information + about the current binary.`, + + RunE: func(cmd *cobra.Command, args []string) error { + log.SetFormatter(&log.TextFormatter{ + DisableQuote: true, + }) + v, err := version.NewVersion() + if err != nil { + log.Error(err) + } + fmt.Println(v.String()) + return nil + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/controller/version/version.go b/controller/version/version.go new file mode 100644 index 0000000000000000000000000000000000000000..4b146dd9337b44a594226e0c1cc9d08afbe253ef --- /dev/null +++ b/controller/version/version.go @@ -0,0 +1,173 @@ +package version + +import ( + "fmt" + "runtime/debug" + "strconv" + "strings" + "time" +) + +var ( + // The versionString is provided through ldflags while building. This + // variable should only be set through ldflags! + // Defaults to `0.0.0`. + versionString = "0.0.0" +) + +// BuildInformation contains additional information about the build. The information +// is taken from `debug.ReadBuildInfo`. +type BuildInformation struct { + // goos describes the os the binary is built for. + goos string + // goarch describes the architecture the binary is built for. + goarch string + // revision is a reference to the last git commit the binary is based on. + revision string + // time is the timestamp of the last git commit the binary is based on. + time time.Time + // modified returns true if the build is modified and contains additional + // changes from the last commit - a so called dirty build. + modified bool +} + +// Version describes the current version of the goSDN controller and contains +// additional build information. +type Version struct { + major int + minor int + patch int + + BuildInformation +} + +// NewVersion creates a new Version from the versionString, provided via +// ldflags while running `go build`. If no version information is provided +// through out the build process a default value is used. +// +// Additional build information is gathered with the help of +// `debug.ReadBuildInfo` and is added to the Version. +func NewVersion() (*Version, error) { + buildInfo, ok := debug.ReadBuildInfo() + if !ok { + return nil, fmt.Errorf("Failed reading the binaries build information") + } + + parsedVersion, err := parseVersionString(versionString) + if err != nil { + return nil, fmt.Errorf("Could not parse version string: %w", err) + } + + version := &Version{ + major: parsedVersion[0], + minor: parsedVersion[1], + patch: parsedVersion[2], + } + + for _, bSetting := range buildInfo.Settings { + switch bSetting.Key { + case "GOOS": + version.goos = bSetting.Value + case "GOARCH": + version.goarch = bSetting.Value + case "vcs.revision": + version.revision = bSetting.Value + case "vcs.time": + parsedTime, _ := time.Parse(time.RFC3339, bSetting.Value) + version.time = parsedTime + case "vcs.modified": + version.modified = bSetting.Value == "true" + } + } + + return version, nil +} + +func (v *Version) String() string { + return fmt.Sprintf( + `Version: %d.%d.%d +Build Information: + - GOOS: %s + - GOARCH: %s + - Revision: %s + - Time: %s + - Modified (dirty): %t`, + v.major, v.minor, v.patch, v.goos, v.goarch, v.revision, v.time.String(), v.modified, + ) +} + +// Compare compares the provided version with the current version. Some of the +// build information is included in the comparison. This means that revision, +// time and the modified values are compared aswell. +// +// Returns 0, -1, or 1 based on the fact if the version is equal, smaller or +// larger than the incoming version. +// If one of the versions is modified then a comparison is not possible, since +// it cannot be said with certainty that the two versions are the same. In this +// case a -2 is returned. +func (v *Version) Compare(toCompare *Version) int { + if v.modified || toCompare.modified { + return -2 + } + + // compare the major segment. + if c := compareVersionSegment(v.major, toCompare.major); c != 0 { + return c + } + // compare the minor segment. + if c := compareVersionSegment(v.minor, toCompare.minor); c != 0 { + return c + } + // compare the patch segment. + if c := compareVersionSegment(v.patch, toCompare.patch); c != 0 { + return c + } + + // basic version is the same; now the commit information is compared. + if v.revision != toCompare.revision { + if v.time.Before(toCompare.time) { + return -1 + } + return 1 + } + + return 0 +} + +// compareVersionSegment is a helper function that allows to compare version +// segments like, e.g. major, minor and patch between two versions. +// +// Left describes the segment of the version that should be compared against +// and right describes the incoming version. +// +// The function returns 0, -1, or 1 based on the fact if the version segment +// to be compared against is equal, smaller or larger than the incoming version +// segment. +func compareVersionSegment(left, right int) int { + if left < right { + return -1 + } + + if left > right { + return 1 + } + + return 0 +} + +// parseVersionString is a helper function that parses the given string into +// three integers: major, minor and patch. The integers are returned as an +// array of integers in that respective order. +func parseVersionString(s string) ([]int, error) { + versionSegments := strings.SplitN(s, ".", 3) + + res := make([]int, 3) + for i, versionSegment := range versionSegments { + versionSegmentAsInt, err := strconv.ParseInt(versionSegment, 10, 64) + if err != nil { + return nil, err + } + res[i] = int(versionSegmentAsInt) + } + return res, nil +} diff --git a/controller/version/version_test.go b/controller/version/version_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9ae0dbaa713c0c7129782c425491bbcaab8281aa --- /dev/null +++ b/controller/version/version_test.go @@ -0,0 +1,468 @@ +package version + +import ( + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func Test_version_NewVersion(t *testing.T) { + // custom options for comparer + opts := cmp.Options{ + cmp.Comparer(func(left, right *Version) bool { + if left == nil && right == nil { + return true + } + if left.major == right.major && + left.minor == right.minor && + left.patch == right.patch { + return true + } + + return false + }), + } + + type args struct { + versionString string + } + + tests := []struct { + name string + args args + want *Version + wantErr bool + }{ + { + name: "default", + args: args{}, + want: &Version{ + major: 0, + minor: 0, + patch: 0, + BuildInformation: BuildInformation{}, + }, + wantErr: false, + }, + { + name: "faulty version string", + args: args{ + versionString: "faulty.version.string", + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This is not run in parallel since we change the versionString + // and this would therefore result in a data race. + // + // versionString is and should only be touched through ldflags with + // `go build`. + if tt.name != "default" { + versionString = tt.args.versionString + } + + got, err := NewVersion() + if (err != nil) != tt.wantErr { + t.Errorf("NewVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !cmp.Equal(got, tt.want, opts...) { + t.Errorf("NewVersion() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_version_Compare(t *testing.T) { + timeNormal, err := time.Parse(time.RFC3339, "2022-08-19T11:11:02+00:00") + if err != nil { + t.Errorf("Compare() error = %v", err) + } + + timeBefore, err := time.Parse(time.RFC3339, "2022-08-18T11:11:02+00:00") + if err != nil { + t.Errorf("Compare() error = %v", err) + } + + timeAfter, err := time.Parse(time.RFC3339, "2022-08-22T11:11:02+00:00") + if err != nil { + t.Errorf("Compare() error = %v", err) + } + + v := &Version{ + major: 1, + minor: 3, + patch: 101, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNormal, + modified: false, + }, + } + + type args struct { + inputVersion *Version + } + + tests := []struct { + name string + args args + want int + }{ + { + name: "input version is equal", + args: args{ + inputVersion: &Version{ + major: 1, + minor: 3, + patch: 101, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNormal, + modified: false, + }, + }, + }, + want: 0, + }, + { + name: "input version is smaller based on major", + args: args{ + inputVersion: &Version{ + major: 0, + minor: 0, + patch: 101, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNormal, + modified: false, + }, + }, + }, + want: 1, + }, + { + name: "input version is smaller based on minor", + args: args{ + inputVersion: &Version{ + major: 1, + minor: 0, + patch: 101, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNormal, + modified: false, + }, + }, + }, + want: 1, + }, + { + name: "input version is smaller based on patch", + args: args{ + inputVersion: &Version{ + major: 1, + minor: 3, + patch: 78, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNormal, + modified: false, + }, + }, + }, + want: 1, + }, + { + name: "input version is smaller based on commit", + args: args{ + inputVersion: &Version{ + major: 1, + minor: 3, + patch: 101, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "14379478a8c56269dd59419d04c9080c1103174d", + time: timeBefore, + modified: false, + }, + }, + }, + want: 1, + }, + { + name: "input version is larger based on major", + args: args{ + inputVersion: &Version{ + major: 2, + minor: 3, + patch: 78, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNormal, + modified: false, + }, + }, + }, + want: -1, + }, + { + name: "input version is larger based on minor", + args: args{ + inputVersion: &Version{ + major: 1, + minor: 4, + patch: 78, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNormal, + modified: false, + }, + }, + }, + want: -1, + }, + { + name: "input version is larger based on patch", + args: args{ + inputVersion: &Version{ + major: 1, + minor: 3, + patch: 103, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNormal, + modified: false, + }, + }, + }, + want: -1, + }, + { + name: "input version is larger based on commit", + args: args{ + inputVersion: &Version{ + major: 1, + minor: 3, + patch: 101, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "14379478a8c56269dd59419d04c9080c1103174d", + time: timeAfter, + modified: false, + }, + }, + }, + want: -1, + }, + { + name: "one of the versions is modified", + args: args{ + inputVersion: &Version{ + BuildInformation: BuildInformation{ + modified: true, + }, + }, + }, + want: -2, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := v.Compare(tt.args.inputVersion) + + if !cmp.Equal(got, tt.want) { + t.Errorf("Compare() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_version_String(t *testing.T) { + timeNow := time.Now() + v := &Version{ + major: 1, + minor: 3, + patch: 101, + BuildInformation: BuildInformation{ + goos: "linux", + goarch: "amd64", + revision: "34979478a8c56269dd59419d04d9080c1103174d", + time: timeNow, + modified: false, + }, + } + + tests := []struct { + name string + want string + }{ + { + name: "default", + want: fmt.Sprintf(`Version: 1.3.101 +Build Information: + - GOOS: linux + - GOARCH: amd64 + - Revision: 34979478a8c56269dd59419d04d9080c1103174d + - Time: %s + - Modified (dirty): false`, timeNow.String()), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := v.String() + + if !cmp.Equal(got, tt.want) { + t.Errorf("String() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_version_compareVersionSegment(t *testing.T) { + type args struct { + left, right int + } + + tests := []struct { + name string + args args + want int + }{ + { + name: "left is smaller && right is larger", + args: args{ + left: 5, + right: 8, + }, + want: -1, + }, + { + name: "left is larger && right is smaller", + args: args{ + left: 9, + right: 3, + }, + want: 1, + }, + { + name: "left and right are equal", + args: args{ + left: 4, + right: 4, + }, + want: 0, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := compareVersionSegment(tt.args.left, tt.args.right) + + if !cmp.Equal(got, tt.want) { + t.Errorf("compareVersionSegment() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_version_parseVersionString(t *testing.T) { + type args struct { + input string + } + + tests := []struct { + name string + args args + want []int + wantErr bool + }{ + { + name: "default", + args: args{ + input: "1.0.4", + }, + want: []int{1, 0, 4}, + wantErr: false, + }, + { + name: "too many values in version string", + args: args{ + input: "1.0.4.259", + }, + want: nil, + wantErr: true, + }, + { + name: "faulty version string - no numbers", + args: args{ + input: "this is wrong", + }, + want: nil, + wantErr: true, + }, + { + name: "faulty version string - wrong delimiter", + args: args{ + input: "1-2-187", + }, + want: nil, + wantErr: true, + }, + { + name: "faulty version string - mixed", + args: args{ + input: "1.abc.283", + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := parseVersionString(tt.args.input) + if (err != nil) != tt.wantErr { + t.Errorf("parseVersionString() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !cmp.Equal(got, tt.want) { + t.Errorf("parseVersionString() got = %v, want %v", got, tt.want) + } + }) + } +}