From cde530e3632b3a175fc626c77089f63e1c7de36b Mon Sep 17 00:00:00 2001 From: RaduPetreTarean <156008349+RaduPetreTarean@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:21:27 +0300 Subject: [PATCH] feat: single-version-resources to be before latest version (#369) * feat: ensure single-version-resources to be before latest version --- internal/cmd/compiler.go | 17 ++++- internal/simplebuild/build.go | 99 +++++++++++++++++++++++++++++- internal/simplebuild/build_test.go | 46 ++++++++++++++ 3 files changed, 159 insertions(+), 3 deletions(-) diff --git a/internal/cmd/compiler.go b/internal/cmd/compiler.go index ece748fc..540ada54 100644 --- a/internal/cmd/compiler.go +++ b/internal/cmd/compiler.go @@ -13,7 +13,10 @@ import ( ) var defaultPivotDate = vervet.MustParseVersion("2024-09-01") +var defaultVersioningUrl = "https://api.snyk.io/rest/openapi" + var pivotDateCLIFlagName = "pivot-version" +var versioningUrlCLIFlagName = "versioning-url" var buildFlags = []cli.Flag{ &cli.StringFlag{ @@ -34,6 +37,12 @@ var buildFlags = []cli.Flag{ " Flag for testing only, recommend to use the default date(%s)", defaultPivotDate.String()), Value: defaultPivotDate.String(), }, + &cli.StringFlag{ + Name: versioningUrlCLIFlagName, + Aliases: []string{"U"}, + Usage: fmt.Sprintf("URL to fetch versioning information. Default is %q", defaultVersioningUrl), + Value: defaultVersioningUrl, + }, } // BuildCommand is the `vervet build` subcommand. @@ -73,7 +82,9 @@ func SimpleBuild(ctx *cli.Context) error { return fmt.Errorf("failed to parse pivot date %q: %w", pivotDate, err) } - err = simplebuild.Build(ctx.Context, project, pivotDate, false) + versioningURL := ctx.String(versioningUrlCLIFlagName) + + err = simplebuild.Build(ctx.Context, project, pivotDate, versioningURL, false) return err } @@ -89,6 +100,8 @@ func CombinedBuild(ctx *cli.Context) error { return fmt.Errorf("failed to parse pivot date %q: %w", pivotDate, err) } + versioningURL := ctx.String(versioningUrlCLIFlagName) + comp, err := compiler.New(ctx.Context, project) if err != nil { return err @@ -98,7 +111,7 @@ func CombinedBuild(ctx *cli.Context) error { return err } - return simplebuild.Build(ctx.Context, project, pivotDate, true) + return simplebuild.Build(ctx.Context, project, pivotDate, versioningURL, true) } func parsePivotDate(ctx *cli.Context) (vervet.Version, error) { diff --git a/internal/simplebuild/build.go b/internal/simplebuild/build.go index 81bca53c..7851a1dd 100644 --- a/internal/simplebuild/build.go +++ b/internal/simplebuild/build.go @@ -2,9 +2,14 @@ package simplebuild import ( "context" + "encoding/json" "fmt" + "io" + "net/http" "path/filepath" "slices" + "sort" + "strings" "time" "github.com/getkin/kin-openapi/openapi3" @@ -19,16 +24,39 @@ import ( // Build compiles the versioned resources in a project configuration based on // simplified versioning rules, after the start date. -func Build(ctx context.Context, project *config.Project, startDate vervet.Version, appendOutputFiles bool) error { +func Build( + ctx context.Context, + project *config.Project, + startDate vervet.Version, + versioningUrl string, + appendOutputFiles bool, +) error { if time.Now().Before(startDate.Date) { return nil } + + latestVersion, err := fetchLatestVersion(versioningUrl) + if err != nil { + return err + } + for _, apiConfig := range project.APIs { if apiConfig.Output == nil { fmt.Printf("No output specified for %s, skipping\n", apiConfig.Name) continue } + for _, resource := range apiConfig.Resources { + paths, err := ResourceSpecFiles(resource) + if err != nil { + return err + } + + if err := CheckSingleVersionResourceToBeBeforeLatestVersion(paths, latestVersion); err != nil { + return err + } + } + operations, err := LoadPaths(ctx, apiConfig) if err != nil { return err @@ -314,3 +342,72 @@ func CheckBreakingChanges(docs DocSet) error { } return nil } + +func fetchLatestVersion(versioningURL string) (vervet.Version, error) { + resp, err := http.Get(versioningURL) + if err != nil { + return vervet.Version{}, fmt.Errorf("failed to fetch versioning information from %q: %w", versioningURL, err) + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + fmt.Println("failed to close response body") + } + }(resp.Body) + + if resp.StatusCode != http.StatusOK { + return vervet.Version{}, fmt.Errorf("failed to fetch versioning information, status code: %d", resp.StatusCode) + } + + var versions []string + if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil { + return vervet.Version{}, fmt.Errorf("failed to parse versioning information: %w", err) + } + + var dates = make([]string, 0, len(versions)) + + for _, version := range versions { + parts := strings.Split(version, "~") + dates = append(dates, parts[0]) + } + sort.Strings(dates) + + latestVersion, err := vervet.ParseVersion(dates[len(dates)-1]) + if err != nil { + return vervet.Version{}, fmt.Errorf("failed to parse latest version date %q: %w", dates[len(dates)-1], err) + } + + return latestVersion, nil +} + +func CheckSingleVersionResourceToBeBeforeLatestVersion(paths []string, latestVersion vervet.Version) error { + resourceVersions := make(map[string][]string) + + for _, path := range paths { + resourceDir := filepath.Dir(filepath.Dir(path)) + versionDir := filepath.Base(filepath.Dir(path)) + resourceVersions[resourceDir] = append(resourceVersions[resourceDir], versionDir) + } + + for _, versions := range resourceVersions { + if len(versions) == 1 { + versionStr := versions[0] + version, err := vervet.ParseVersion(versionStr) + if err != nil { + return fmt.Errorf("invalid version %q", versionStr) + } + + if version.Date.After(latestVersion.Date) { + return fmt.Errorf( + "version %s is after the last released version of the global API %s. "+ + "Please change the version date to be before %s or at the same date", + version.Date.Format("2006-01-02"), + latestVersion.Date.Format("2006-01-02"), + latestVersion.Date.Format("2006-01-02"), + ) + } + } + } + return nil +} diff --git a/internal/simplebuild/build_test.go b/internal/simplebuild/build_test.go index 09904d94..3bfeacc1 100644 --- a/internal/simplebuild/build_test.go +++ b/internal/simplebuild/build_test.go @@ -561,6 +561,52 @@ func TestCheckBreakingChanges(t *testing.T) { c.Assert(err, qt.Not(qt.IsNil), qt.Commentf("expected no breaking changes")) }) } + +func TestCheckSingleVersionResource(t *testing.T) { + c := qt.New(t) + + c.Run("no error when version is before or equal to the latest version", func(c *qt.C) { + paths := []string{ + "internal/api/hidden/resources/apps/2023-07-31/spec.yaml", + } + latestVersion := vervet.MustParseVersion("2024-01-01") + + err := simplebuild.CheckSingleVersionResourceToBeBeforeLatestVersion(paths, latestVersion) + c.Assert(err, qt.IsNil) + }) + + c.Run("error when version is after the latest version", func(c *qt.C) { + paths := []string{ + "internal/api/hidden/resources/apps/2025-07-31/spec.yaml", + } + latestVersion := vervet.MustParseVersion("2024-01-01") + + err := simplebuild.CheckSingleVersionResourceToBeBeforeLatestVersion(paths, latestVersion) + c.Assert(err, qt.ErrorMatches, "version .* is after the last released version .*") + }) + + c.Run("no error when multiple versions are present", func(c *qt.C) { + paths := []string{ + "internal/api/hidden/resources/apps/2023-07-31/spec.yaml", + "internal/api/hidden/resources/apps/2024-01-01/spec.yaml", + } + latestVersion := vervet.MustParseVersion("2024-01-01") + + err := simplebuild.CheckSingleVersionResourceToBeBeforeLatestVersion(paths, latestVersion) + c.Assert(err, qt.IsNil) + }) + + c.Run("handles version parsing error gracefully", func(c *qt.C) { + paths := []string{ + "internal/api/hidden/resources/apps/invalid-version/spec.yaml", + } + latestVersion := vervet.MustParseVersion("2024-01-01") + + err := simplebuild.CheckSingleVersionResourceToBeBeforeLatestVersion(paths, latestVersion) + c.Assert(err, qt.ErrorMatches, "invalid version .*") + }) +} + func compareDocs(a, b simplebuild.VersionedDoc) int { return a.VersionDate.Compare(b.VersionDate) }