diff --git a/internal/cmd/backstage.go b/internal/cmd/backstage.go index ceb1c309..0f832d51 100644 --- a/internal/cmd/backstage.go +++ b/internal/cmd/backstage.go @@ -11,6 +11,7 @@ import ( "github.com/urfave/cli/v2" + "github.com/snyk/vervet/v8" "github.com/snyk/vervet/v8/config" "github.com/snyk/vervet/v8/internal/backstage" ) @@ -32,8 +33,8 @@ var BackstageCommand = cli.Command{ Aliases: []string{"P"}, Usage: fmt.Sprintf( "Pivot version after which new strategy versioning is used."+ - " Flag for testing only, recommend to use the default date(%s)", defaultPivotDate.String()), - Value: defaultPivotDate.String(), + " Flag for testing only, recommend to use the default date(%s)", vervet.DefaultPivotDate.String()), + Value: vervet.DefaultPivotDate.String(), }, }, Action: UpdateCatalog, @@ -51,8 +52,8 @@ var BackstageCommand = cli.Command{ Aliases: []string{"P"}, Usage: fmt.Sprintf( "Pivot version after which new strategy versioning is used."+ - " Flag for testing only, recommend to use the default date(%s)", defaultPivotDate.String()), - Value: defaultPivotDate.String(), + " Flag for testing only, recommend to use the default date(%s)", vervet.DefaultPivotDate.String()), + Value: vervet.DefaultPivotDate.String(), }, }, Action: PreviewCatalog, diff --git a/internal/cmd/compiler.go b/internal/cmd/compiler.go index f7afbc58..f011e084 100644 --- a/internal/cmd/compiler.go +++ b/internal/cmd/compiler.go @@ -12,7 +12,6 @@ import ( "github.com/snyk/vervet/v8/internal/simplebuild" ) -var defaultPivotDate = vervet.MustParseVersion("2024-10-15") var defaultVersioningUrl = "https://api.snyk.io/rest/openapi" var pivotDateCLIFlagName = "pivot-version" @@ -34,8 +33,8 @@ var buildFlags = []cli.Flag{ Aliases: []string{"P"}, Usage: fmt.Sprintf( "Pivot version after which new strategy versioning is used."+ - " Flag for testing only, recommend to use the default date(%s)", defaultPivotDate.String()), - Value: defaultPivotDate.String(), + " Flag for testing only, recommend to use the default date(%s)", vervet.DefaultPivotDate.String()), + Value: vervet.DefaultPivotDate.String(), }, &cli.StringFlag{ Name: versioningUrlCLIFlagName, diff --git a/version.go b/version.go index 9d210ce0..57ba3ec7 100644 --- a/version.go +++ b/version.go @@ -324,6 +324,26 @@ func (vi *VersionIndex) Resolve(query Version) (Version, error) { return Version{}, ErrNoMatchingVersion } +// ResolveGAorBetaStability returns the GA or beta version effective on the query version date at +// the given version date. Returns ErrNoMatchingVersion if no version matches or if query +// stability is not GA. +func (vi *VersionIndex) ResolveGAorBetaStability(query Version) (Version, error) { + i, err := vi.resolveIndex(query.Date) + if query.Stability != StabilityGA { + return Version{}, ErrNoMatchingVersion + } + if err != nil { + return Version{}, err + } + if stabDate := vi.effectiveVersions[i].stabilities[StabilityGA]; !stabDate.IsZero() { + return Version{Date: stabDate, Stability: StabilityGA}, nil + } + if stabDate := vi.effectiveVersions[i].stabilities[StabilityBeta]; !stabDate.IsZero() { + return Version{Date: stabDate, Stability: StabilityBeta}, nil + } + return Version{}, ErrNoMatchingVersion +} + // Versions returns each Version defined. func (vi *VersionIndex) Versions() VersionSlice { vs := make(VersionSlice, len(vi.versions)) @@ -498,3 +518,6 @@ func defaultLifecycleAt() time.Time { } return timeNow().UTC() } + +// DefaultPivotDate is the default pivot date after which the versioning strategy changes. +var DefaultPivotDate = MustParseVersion("2024-10-15") diff --git a/version_test.go b/version_test.go index 4a91b0f6..bd5867fa 100644 --- a/version_test.go +++ b/version_test.go @@ -2,6 +2,7 @@ package vervet_test import ( "fmt" + "reflect" "testing" "time" @@ -437,3 +438,122 @@ func TestLifecycleAtDefaultDate(t *testing.T) { }) } } + +func TestVersionIndex_ResolveGAorBetaStability(t *testing.T) { + tests := []struct { + name string + versions VersionSlice + query Version + want Version + wantErr bool + }{ + { + name: "no versions", + versions: VersionSlice{}, + query: MustParseVersion("2021-06-01"), + wantErr: true, + }, + { + name: "query is before first version", + versions: VersionSlice{ + MustParseVersion("2021-06-07"), + MustParseVersion("2021-06-08"), + MustParseVersion("2021-06-09"), + }, + query: MustParseVersion("2021-06-01"), + wantErr: true, + }, + { + name: "query is first version", + versions: VersionSlice{ + MustParseVersion("2021-06-07"), + MustParseVersion("2021-06-08"), + MustParseVersion("2021-06-09"), + }, + query: MustParseVersion("2021-06-07"), + want: MustParseVersion("2021-06-07"), + wantErr: false, + }, + { + name: "query is after last version", + versions: VersionSlice{ + MustParseVersion("2021-06-07"), + MustParseVersion("2021-06-08"), + MustParseVersion("2021-06-09"), + }, + query: MustParseVersion("2021-06-10"), + want: MustParseVersion("2021-06-09"), + wantErr: false, + }, + { + name: "return recent beta endpoint, when ga not available", + versions: VersionSlice{ + MustParseVersion("2021-06-07~beta"), + MustParseVersion("2021-06-08~beta"), + MustParseVersion("2021-06-09~beta"), + MustParseVersion("2021-06-10~beta"), + }, + query: MustParseVersion("2021-06-12"), + want: MustParseVersion("2021-06-10~beta"), + wantErr: false, + }, + { + name: "return latest beta endpoint, when ga not available", + versions: VersionSlice{ + MustParseVersion("2021-06-07~beta"), + MustParseVersion("2021-06-08~beta"), + MustParseVersion("2021-06-09~beta"), + MustParseVersion("2021-06-13~beta"), + }, + query: MustParseVersion("2021-06-10"), + want: MustParseVersion("2021-06-09~beta"), + wantErr: false, + }, + { + name: "return latest beta endpoint, when an older beta is requested, even when ga is available", + versions: VersionSlice{ + MustParseVersion("2021-06-07~beta"), + MustParseVersion("2021-06-08~beta"), + MustParseVersion("2021-06-09~beta"), + MustParseVersion("2021-06-13~beta"), + MustParseVersion("2021-06-14"), + }, + query: MustParseVersion("2021-06-10"), + want: MustParseVersion("2021-06-09~beta"), + wantErr: false, + }, + { + name: "error, for beta stability", + versions: VersionSlice{ + MustParseVersion("2021-06-07"), + MustParseVersion("2021-06-08"), + MustParseVersion("2021-06-09"), + }, + query: MustParseVersion("2021-06-09~beta"), + wantErr: true, + }, + { + name: "error, for experimental stability", + versions: VersionSlice{ + MustParseVersion("2021-06-07"), + MustParseVersion("2021-06-08"), + MustParseVersion("2021-06-09"), + }, + query: MustParseVersion("2021-06-09~experimental"), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vi := NewVersionIndex(tt.versions) + got, err := vi.ResolveGAorBetaStability(tt.query) + if (err != nil) != tt.wantErr { + t.Errorf("ResolveGAorBetaStability() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ResolveGAorBetaStability() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/versionware/handler.go b/versionware/handler.go index 10d2799c..647b1ab7 100644 --- a/versionware/handler.go +++ b/versionware/handler.go @@ -69,9 +69,18 @@ func (h *Handler) HandleErrors(errFunc VersionErrorHandler) { // Resolve returns the resolved version and its associated http.Handler for the // requested version. func (h *Handler) Resolve(requested vervet.Version) (*vervet.Version, http.Handler, error) { - resolvedVersion, err := h.index.ResolveForBuild(requested) - if err != nil { - return nil, nil, err + var resolvedVersion vervet.Version + var err error + if requested.Date.Compare(vervet.DefaultPivotDate.Date) < 0 { + resolvedVersion, err = h.index.ResolveForBuild(requested) + if err != nil { + return nil, nil, err + } + } else { + resolvedVersion, err = h.index.ResolveGAorBetaStability(requested) + if err != nil { + return nil, nil, err + } } return &resolvedVersion, h.handlers[resolvedVersion], nil } diff --git a/versionware/handler_test.go b/versionware/handler_test.go index b6a44eea..0950e5f9 100644 --- a/versionware/handler_test.go +++ b/versionware/handler_test.go @@ -80,6 +80,18 @@ func TestHandler(t *testing.T) { _, err := w.Write([]byte("sept")) c.Assert(err, qt.IsNil) }), + }, { + Version: vervet.MustParseVersion("2024-10-15"), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("on pivot date")) + c.Assert(err, qt.IsNil) + }), + }, { + Version: vervet.MustParseVersion("2024-10-20"), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("after pivot date")) + c.Assert(err, qt.IsNil) + }), }}...) tests := []struct { requested, resolved string @@ -107,6 +119,105 @@ func TestHandler(t *testing.T) { "2021-08-01~beta", "2021-08-01~beta", "aug beta", 200, }, { "2021-09-01~beta", "2021-09-01", "sept", 200, + }, { + "2024-10-14", "2021-11-01", "nov", 200, + }, { + "2024-10-14~beta", "2021-11-01", "nov", 200, + }, { + "2024-10-14~experimental", "2021-11-01", "nov", 200, + }, { + "2024-10-15", "2024-10-15", "on pivot date", 200, + }, { + "2024-10-16", "2024-10-15", "on pivot date", 200, + }, { + "2024-10-20", "2024-10-20", "after pivot date", 200, + }, { + "2024-10-20~beta", "", "Not Found\n", 404, + }, { + "2024-10-20~experimental", "", "Not Found\n", 404, + }} + for i, test := range tests { + c.Run(fmt.Sprintf("%d requested %s resolved %s", i, test.requested, test.resolved), func(c *qt.C) { + s := httptest.NewServer(h) + c.Cleanup(s.Close) + req, err := http.NewRequest("GET", s.URL+"?version="+test.requested, nil) + c.Assert(err, qt.IsNil) + resp, err := s.Client().Do(req) + c.Assert(err, qt.IsNil) + defer resp.Body.Close() + c.Assert(resp.StatusCode, qt.Equals, test.status) + contents, err := io.ReadAll(resp.Body) + c.Assert(err, qt.IsNil) + c.Assert(string(contents), qt.Equals, test.contents) + }) + } +} + +func TestHandler_BetaEndpoints(t *testing.T) { + c := qt.New(t) + h := versionware.NewHandler([]versionware.VersionHandler{{ + Version: vervet.MustParseVersion("2021-08-01~beta"), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("aug beta")) + c.Assert(err, qt.IsNil) + }), + }, { + Version: vervet.MustParseVersion("2021-10-01~beta"), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("oct beta")) + c.Assert(err, qt.IsNil) + }), + }, { + Version: vervet.MustParseVersion("2021-11-01~experimental"), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("nov experimental")) + c.Assert(err, qt.IsNil) + }), + }, { + Version: vervet.MustParseVersion("2024-10-15~beta"), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("beta on pivot date")) + c.Assert(err, qt.IsNil) + }), + }, { + Version: vervet.MustParseVersion("2024-10-20~beta"), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("beta after pivot date")) + c.Assert(err, qt.IsNil) + }), + }, { + Version: vervet.MustParseVersion("2024-10-25"), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("ga after pivot date")) + c.Assert(err, qt.IsNil) + }), + }}...) + tests := []struct { + requested, resolved string + contents string + status int + }{{ + "2021-07-31~beta", "", "Not Found\n", 404, + }, { + "2021-09-16~beta", "2021-08-01~beta", "aug beta", 200, + }, { + "2021-10-01~beta", "2021-10-01~beta", "oct beta", 200, + }, { + "2021-11-05~experimental", "2021-11-01~experimental", "nov experimental", 200, + }, { + "2024-10-15", "2024-10-15~beta", "beta on pivot date", 200, + }, { + "2024-10-15~beta", "", "Not Found\n", 404, + }, { + "2024-10-15~experimental", "", "Not Found\n", 404, + }, { + "2024-10-16", "2024-10-15~beta", "beta on pivot date", 200, + }, { + "2024-10-20", "2024-10-20~beta", "beta after pivot date", 200, + }, { + "2024-10-21", "2024-10-20~beta", "beta after pivot date", 200, + }, { + "2024-10-26", "2024-10-25", "ga after pivot date", 200, }} for i, test := range tests { c.Run(fmt.Sprintf("%d requested %s resolved %s", i, test.requested, test.resolved), func(c *qt.C) {