Skip to content

Commit

Permalink
Merge pull request #77 from 7h3-3mp7y-m4n/versionCheck
Browse files Browse the repository at this point in the history
Adding check-version subcommand
  • Loading branch information
mathieu-benoit authored Nov 4, 2024
2 parents 1edb8fc + 4ed8985 commit 361a6f5
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 1 deletion.
49 changes: 49 additions & 0 deletions internal/command/check_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2024 Humanitec
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package command

import (
"github.com/score-spec/score-k8s/internal/version"
"github.com/spf13/cobra"
)

var checkVersionCmd = &cobra.Command{
Use: "check-version [constraint]",
Short: "Assert that the version of score-k8s matches the required constraint",
Long: `score-k8s is commonly used in Makefiles and CI pipelines which may depend on a particular functionality
or a particular default provisioner provided by score-k8s init. This command provides a common way to check that
the version of score-k8s matches a required version.
`,
Example: `
# check that the version is exactly 1.2.3
score-k8s check-version =v1.2.3
# check that the version is 1.3.0 or greater
score-k8s check-version >v1.2
# check that the version is equal or greater to 1.2.3
score-k8s check-version >=1.2.3`,
Args: cobra.ExactArgs(1),
SilenceErrors: true,
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
return version.AssertVersion(args[0], version.Version)
},
}

func init() {
rootCmd.AddCommand(checkVersionCmd)
}
72 changes: 72 additions & 0 deletions internal/command/check_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2024 Humanitec
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package command

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCheckVersionHelp(t *testing.T) {
stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", "--help"})
assert.NoError(t, err)
assert.Equal(t, `score-k8s is commonly used in Makefiles and CI pipelines which may depend on a particular functionality
or a particular default provisioner provided by score-k8s init. This command provides a common way to check that
the version of score-k8s matches a required version.
Usage:
score-k8s check-version [constraint] [flags]
Examples:
# check that the version is exactly 1.2.3
score-k8s check-version =v1.2.3
# check that the version is 1.3.0 or greater
score-k8s check-version >v1.2
# check that the version is equal or greater to 1.2.3
score-k8s check-version >=1.2.3
Flags:
-h, --help help for check-version
Global Flags:
--quiet Mute any logging output
-v, --verbose count Increase log verbosity and detail by specifying this flag one or more times
`, stdout)
assert.Equal(t, "", stderr)

stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "check-version"})
assert.NoError(t, err)
assert.Equal(t, stdout, stdout2)
assert.Equal(t, "", stderr)
}

func TestCheckVersionPass(t *testing.T) {
stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", ">=0.0.0"})
assert.NoError(t, err)
assert.Equal(t, stdout, "")
assert.Equal(t, "", stderr)
}

func TestCheckVersionFail(t *testing.T) {
stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", ">99"})
assert.EqualError(t, err, "current version 0.0.0 does not match requested constraint >99")
assert.Equal(t, stdout, "")
assert.Equal(t, "", stderr)
}
49 changes: 48 additions & 1 deletion internal/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ package version

import (
"fmt"
"regexp"
"runtime/debug"
"strconv"
)

var (
Version string = "0.0.0"
Version string = "0.0.0"
semverPattern = regexp.MustCompile(`^(?:v?)(\d+)(?:\.(\d+))?(?:\.(\d+))?$`)
constraintAndSemver = regexp.MustCompile("^(>|>=|=)?" + semverPattern.String()[1:])
)

// BuildVersionString constructs a version string by looking at the build metadata injected at build time.
Expand Down Expand Up @@ -50,3 +54,46 @@ func BuildVersionString() string {
}
return fmt.Sprintf("%s (build: %s, sha: %s%s)", versionNumber, buildTime, gitSha, isDirtySuffix)
}

func semverToI(x string) (int, error) {
cpm := semverPattern.FindStringSubmatch(x)
if cpm == nil {
return 0, fmt.Errorf("invalid version: %s", x)
}
major, _ := strconv.Atoi(cpm[1])
minor, patch := 999, 999
if len(cpm) > 2 {
minor, _ = strconv.Atoi(cpm[2])
if len(cpm) > 3 {
patch, _ = strconv.Atoi(cpm[3])
}
}
return (major*1_000+minor)*1_000 + patch, nil
}

func AssertVersion(constraint string, current string) error {
if currentI, err := semverToI(current); err != nil {
return fmt.Errorf("current version is missing or invalid '%s'", current)
} else if m := constraintAndSemver.FindStringSubmatch(constraint); m == nil {
return fmt.Errorf("invalid constraint '%s'", constraint)
} else {
op := m[1]
compareI, err := semverToI(m[0][len(op):])
if err != nil {
return fmt.Errorf("failed to parse constraint: %w", err)
}
match := false
switch op {
case ">":
match = currentI > compareI
case ">=":
match = currentI >= compareI
case "=":
match = currentI == compareI
}
if !match {
return fmt.Errorf("current version %s does not match requested constraint %s", current, constraint)
}
return nil
}
}
85 changes: 85 additions & 0 deletions internal/version/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2024 Humanitec
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package version

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestAssertVersion_good(t *testing.T) {
for _, tup := range [][2]string{
{"=1.2.3", "v1.2.3"},
{">=1.2.3", "v1.2.3"},
{">=1.2.3", "v1.2.4"},
{">1.2.3", "v1.2.4"},
{">=1.1", "1.1.0"},
{">=1.1", "1.2.0"},
{">=1", "1.0.0"},
{">1", "2.0.0"},
} {
t.Run(fmt.Sprintf("%v", tup), func(t *testing.T) {
assert.NoError(t, AssertVersion(tup[0], tup[1]))
})
}
}

func TestAssertVersion_bad(t *testing.T) {
for _, tup := range [][3]string{
{"=1.2.3", "v1.2.0", "current version v1.2.0 does not match requested constraint =1.2.3"},
{">2", "v1.2.0", "current version v1.2.0 does not match requested constraint >2"},
{">1.2", "v1.2.0", "current version v1.2.0 does not match requested constraint >1.2"},
} {
t.Run(fmt.Sprintf("%v", tup), func(t *testing.T) {
assert.EqualError(t, AssertVersion(tup[0], tup[1]), tup[2])
})
}
}

func TestSemverToI(t *testing.T) {
validCases := []struct {
version string
expected int
}{
{"1.2.3", 1002003},
{"2.0.0", 2000000},
{"0.9.1", 9001},
{"1.2", 1002000},
{"1", 1000000},
}

for _, tc := range validCases {
t.Run(fmt.Sprintf("valid: %s", tc.version), func(t *testing.T) {
result, err := semverToI(tc.version)
assert.NoError(t, err)
assert.Equal(t, tc.expected, result)
})
}
invalidCases := []string{
"1.2.a",
"a.b.c",
"1..1",
"1.2.3.4",
}
for _, version := range invalidCases {
t.Run(fmt.Sprintf("invalid: %s", version), func(t *testing.T) {
result, err := semverToI(version)
assert.Error(t, err)
assert.Equal(t, 0, result)
})
}
}

0 comments on commit 361a6f5

Please sign in to comment.