Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding check-version subcommand #77

Merged
merged 3 commits into from
Nov 4, 2024
Merged
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
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)
})
}
}
Loading