diff --git a/version/feature_manager.go b/version/feature_manager.go new file mode 100644 index 0000000000..f116eac9fb --- /dev/null +++ b/version/feature_manager.go @@ -0,0 +1,87 @@ +package version + +import ( + "strings" + + gvers "github.com/hashicorp/go-version" +) + +type Metadata int + +const ( + OSS Metadata = iota + HCP +) + +type MetadataConstraint struct { + MetaInfo []Metadata + Constraints gvers.Constraints +} + +type Feature int + +const ( + UnknownFeature Feature = iota + MultiHopSessionFeature +) + +var featureMap map[Feature]MetadataConstraint + +func init() { + if featureMap == nil { + featureMap = make(map[Feature]MetadataConstraint) + } + /* + Add constraints here following this format after adding a Feature to the Feature iota: + featureConstraint, err := gvers.NewConstraint(">= 0.1.0") // This feature exists at 0.1.0 and above + featureMap[FEATURE] = MetadataConstraint{ + MetaInfo: []Metadata{OSS, HCP}, + Constraints: featureConstraint, + } + */ +} + +func metadataStringToMetadata(m string) Metadata { + if strings.Contains(strings.ToLower(m), "hcp") { + return HCP + } + + return OSS +} + +// Check returns a bool indicating if a version meets the metadata constraint for a feature +func (m MetadataConstraint) Check(version *gvers.Version) bool { + binaryMeta := metadataStringToMetadata(version.Metadata()) + + for _, v := range m.MetaInfo { + if v == binaryMeta { + return true + } + } + return false +} + +// Check returns a bool indicating if a version satisfies the feature constraints +func Check(binaryVersion *gvers.Version, featureConstraint MetadataConstraint) bool { + if !featureConstraint.Check(binaryVersion) { + return false + } + + return featureConstraint.Constraints.Check(binaryVersion) +} + +// SupportsFeature return a bool indicating whether or not this version supports the given feature +func SupportsFeature(version *gvers.Version, feature Feature) bool { + featureVersion, found := featureMap[feature] + if !found { + return false + } + + return Check(version, featureVersion) +} + +// GetReleaseVersion returns a go-version of this binary's Boundary version +func GetReleaseVersion() (*gvers.Version, error) { + ver := Get() + return gvers.NewVersion(ver.Version) +} diff --git a/version/feature_manager_test.go b/version/feature_manager_test.go new file mode 100644 index 0000000000..55774ca817 --- /dev/null +++ b/version/feature_manager_test.go @@ -0,0 +1,140 @@ +package version + +import ( + "testing" + + gvers "github.com/hashicorp/go-version" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHasFeature(t *testing.T) { + t.Parallel() + + DeprecatedFeature := Feature(999) + HCPOnlyFeature := Feature(998) + + deprecatedFeatureConstraint, _ := gvers.NewConstraint(">= 0.10.0, < 0.10.1") + featureMap[DeprecatedFeature] = MetadataConstraint{ + MetaInfo: []Metadata{OSS, HCP}, + Constraints: deprecatedFeatureConstraint, + } + + hcpOnlyFeature, _ := gvers.NewConstraint(">= 0.12.0+hcp") + featureMap[HCPOnlyFeature] = MetadataConstraint{ + MetaInfo: []Metadata{HCP}, + Constraints: hcpOnlyFeature, + } + + tests := []struct { + name string + version string + feature Feature + wantResult bool + }{ + { + name: "does-not-have-multihop-ENT", + version: "0.11.1+hcp", + feature: HCPOnlyFeature, + wantResult: false, + }, + { + name: "has-multihop-ENT", + version: "0.12.0+hcp", + feature: HCPOnlyFeature, + wantResult: true, + }, + { + name: "has-multihop-worker-ENT", + version: "0.12.0+hcp.int", + feature: HCPOnlyFeature, + wantResult: true, + }, + { + name: "does-not-have-multihop-OSS", + version: "0.12.0", + feature: HCPOnlyFeature, + wantResult: false, + }, + { + name: "deprecated-feature-before-deprecation", + version: "0.10.0", + feature: DeprecatedFeature, + wantResult: true, + }, + { + name: "deprecated-feature-after-deprecation", + version: "0.12.0", + feature: DeprecatedFeature, + wantResult: false, + }, + { + name: "bogus-feature", + version: "0.12.0", + feature: Feature(-1), + wantResult: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + testVersion, err := gvers.NewVersion(tt.version) + assert.NoError(err) + got := SupportsFeature(testVersion, tt.feature) + require.Equal(tt.wantResult, got) + }) + } + delete(featureMap, DeprecatedFeature) + _, ok := featureMap[DeprecatedFeature] + require.False(t, ok) + + delete(featureMap, HCPOnlyFeature) + _, ok = featureMap[HCPOnlyFeature] + require.False(t, ok) +} + +func TestEnableFeatureForTest(t *testing.T) { + t.Parallel() + + FutureFeature := Feature(997) + + futureVersionFeature, _ := gvers.NewConstraint(">= 99.99.99+hcp") + featureMap[FutureFeature] = MetadataConstraint{ + MetaInfo: []Metadata{HCP}, + Constraints: futureVersionFeature, + } + + tests := []struct { + name string + version string + feature Feature + wantResult bool + wantErr bool + }{ + { + name: "has-future-feature", + version: "0.11.1+hcp", + feature: FutureFeature, + wantResult: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + testVersion, err := gvers.NewVersion(tt.version) + assert.NoError(err) + + testFunc := func() bool { + EnableFeatureOnVersionForTest(t, testVersion, tt.feature) + got := SupportsFeature(testVersion, tt.feature) + return got + } + // Test that the feature was enabled + got := testFunc() + require.Equal(tt.wantResult, got) + }) + } + delete(featureMap, FutureFeature) + _, ok := featureMap[FutureFeature] + require.False(t, ok) +} diff --git a/version/testing.go b/version/testing.go new file mode 100644 index 0000000000..15baad09bc --- /dev/null +++ b/version/testing.go @@ -0,0 +1,43 @@ +package version + +import ( + "fmt" + "testing" + + gvers "github.com/hashicorp/go-version" + "github.com/stretchr/testify/require" +) + +// EnableFeatureForTest enables a feature for the current binary version +func EnableFeatureForTest(t *testing.T, feature Feature) { + require := require.New(t) + version, err := GetReleaseVersion() + require.NoError(err) + EnableFeatureOnVersionForTest(t, version, feature) +} + +// EnableFeatureForTest modifies the feature map to enable a feature for a version. +// This is intended to be used for testing before release of a version +// Test cleanup will reset the feature map to the original feature constraint +// Note: running any tests in parallel while using this function WILL result in surprising +// behavior because this modifies the global feature map +func EnableFeatureOnVersionForTest(t *testing.T, version *gvers.Version, feature Feature) { + featConstraint, ok := featureMap[feature] + require := require.New(t) + require.True(ok) + + versionNumber := version.String() + newConstraint, err := gvers.NewConstraint(fmt.Sprintf(">= %s", versionNumber)) + require.NoError(err) + + meta := metadataStringToMetadata(version.Metadata()) + featureMap[feature] = MetadataConstraint{ + MetaInfo: []Metadata{meta}, + Constraints: newConstraint, + } + + resetFunc := func() { + featureMap[feature] = featConstraint + } + t.Cleanup(resetFunc) +}