Skip to content

Commit 9a59569

Browse files
CLOUDP-292660: add a new 'foascli sunset list' command to foascli to list all endpoints with theirs sunset date
1 parent ef16431 commit 9a59569

File tree

10 files changed

+497
-2
lines changed

10 files changed

+497
-2
lines changed

tools/cli/internal/cli/changelog/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func (o *Opts) newOutputFilePath(fileName string) string {
116116
return fileName
117117
}
118118

119-
// Builder builds the merge command with the following signature:
119+
// CreateBuilder builds the merge command with the following signature:
120120
// changelog create -b path_folder -r path_folder --dry-run
121121
func CreateBuilder() *cobra.Command {
122122
opts := &Opts{

tools/cli/internal/cli/root/openapi/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/mongodb/openapi/tools/cli/internal/cli/changelog"
2323
"github.com/mongodb/openapi/tools/cli/internal/cli/merge"
2424
"github.com/mongodb/openapi/tools/cli/internal/cli/split"
25+
"github.com/mongodb/openapi/tools/cli/internal/cli/sunset"
2526
"github.com/mongodb/openapi/tools/cli/internal/cli/versions"
2627
"github.com/mongodb/openapi/tools/cli/internal/version"
2728
"github.com/spf13/cobra"
@@ -59,6 +60,7 @@ func Builder() *cobra.Command {
5960
versions.Builder(),
6061
changelog.Builder(),
6162
breakingchanges.Builder(),
63+
sunset.Builder(),
6264
)
6365
return rootCmd
6466
}

tools/cli/internal/cli/split/split.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (o *Opts) Run() error {
6868
}
6969

7070
func (o *Opts) filter(oas *openapi3.T, version string) (result *openapi3.T, err error) {
71-
log.Printf("Filtering OpenAPI document by version %s", version)
71+
log.Printf("Filtering OpenAPI document by version '%s'", version)
7272
apiVersion, err := apiversion.New(apiversion.WithVersion(version))
7373
if err != nil {
7474
return nil, err
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sunset
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"strings"
21+
22+
"github.com/mongodb/openapi/tools/cli/internal/cli/flag"
23+
"github.com/mongodb/openapi/tools/cli/internal/cli/usage"
24+
"github.com/mongodb/openapi/tools/cli/internal/openapi"
25+
"github.com/spf13/afero"
26+
"github.com/spf13/cobra"
27+
"gopkg.in/yaml.v3"
28+
)
29+
30+
type ListOpts struct {
31+
fs afero.Fs
32+
basePath string
33+
outputPath string
34+
format string
35+
}
36+
37+
func (o *ListOpts) Run() error {
38+
loader := openapi.NewOpenAPI3()
39+
specInfo, err := loader.CreateOpenAPISpecFromPath(o.basePath)
40+
if err != nil {
41+
return err
42+
}
43+
44+
bytes, err := o.newSunsetListBytes(openapi.NewSunsetListFromSpec(specInfo))
45+
if err != nil {
46+
return err
47+
}
48+
if o.outputPath != "" {
49+
return afero.WriteFile(o.fs, o.outputPath, bytes, 0o600)
50+
}
51+
52+
fmt.Println(string(bytes))
53+
return nil
54+
}
55+
56+
func (o *ListOpts) newSunsetListBytes(versions []*openapi.Sunset) ([]byte, error) {
57+
data, err := json.MarshalIndent(versions, "", " ")
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
if format := strings.ToLower(o.format); format == "json" {
63+
return data, nil
64+
}
65+
66+
var jsonData interface{}
67+
if mErr := json.Unmarshal(data, &jsonData); mErr != nil {
68+
return nil, mErr
69+
}
70+
71+
yamlData, err := yaml.Marshal(jsonData)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
return yamlData, nil
77+
}
78+
79+
// ListBuilder builds the merge command with the following signature:
80+
// changelog create -b path_folder -r path_folder --dry-run
81+
func ListBuilder() *cobra.Command {
82+
opts := &ListOpts{
83+
fs: afero.NewOsFs(),
84+
}
85+
86+
cmd := &cobra.Command{
87+
Use: "list -s spec.json -o json",
88+
Short: "List API endpoints with a Sunset date for a given OpenAPI spec.",
89+
Aliases: []string{"ls"},
90+
Args: cobra.NoArgs,
91+
RunE: func(_ *cobra.Command, _ []string) error {
92+
return opts.Run()
93+
},
94+
}
95+
96+
cmd.Flags().StringVarP(&opts.basePath, flag.Spec, flag.SpecShort, "", usage.Spec)
97+
cmd.Flags().StringVarP(&opts.outputPath, flag.Output, flag.OutputShort, "", usage.Output)
98+
cmd.Flags().StringVarP(&opts.format, flag.Format, flag.FormatShort, "json", usage.Format)
99+
100+
_ = cmd.MarkFlagRequired(flag.Spec)
101+
102+
return cmd
103+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sunset
16+
17+
import (
18+
"testing"
19+
20+
"github.com/spf13/afero"
21+
"github.com/stretchr/testify/assert"
22+
)
23+
24+
func TestList_Run(t *testing.T) {
25+
fs := afero.NewMemMapFs()
26+
opts := &ListOpts{
27+
basePath: "../../../test/data/base_spec.json",
28+
outputPath: "foas.json",
29+
fs: fs,
30+
}
31+
32+
if err := opts.Run(); err != nil {
33+
t.Fatalf("Run() unexpected error: %v", err)
34+
}
35+
36+
b, err := afero.ReadFile(fs, opts.outputPath)
37+
if err != nil {
38+
t.Fatalf("ReadFile() unexpected error: %v", err)
39+
}
40+
assert.NotEmpty(t, b)
41+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sunset
16+
17+
import (
18+
"github.com/spf13/cobra"
19+
)
20+
21+
func Builder() *cobra.Command {
22+
cmd := &cobra.Command{
23+
Use: "sunset",
24+
Short: "Manage the Sunset API for the OpenAPI spec.",
25+
Annotations: map[string]string{
26+
"toc": "true",
27+
},
28+
}
29+
30+
cmd.AddCommand(ListBuilder())
31+
32+
return cmd
33+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sunset
16+
17+
import (
18+
"testing"
19+
20+
"github.com/mongodb/openapi/tools/cli/internal/test"
21+
)
22+
23+
func TestBuilder(t *testing.T) {
24+
test.CmdValidator(
25+
t,
26+
Builder(),
27+
1,
28+
[]string{},
29+
)
30+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package openapi
16+
17+
import (
18+
"github.com/getkin/kin-openapi/openapi3"
19+
"github.com/tufin/oasdiff/load"
20+
)
21+
22+
const (
23+
sunsetExtensionName = "x-sunset"
24+
apiVersionExtensionName = "x-xgen-version"
25+
)
26+
27+
type Sunset struct {
28+
Operation string `json:"http_method" yaml:"http_method"`
29+
Path string `json:"path" yaml:"path"`
30+
Version string `json:"version" yaml:"version"`
31+
SunsetDate string `json:"sunset_date" yaml:"sunset_date"`
32+
}
33+
34+
func NewSunsetListFromSpec(spec *load.SpecInfo) []*Sunset {
35+
var sunsets []*Sunset
36+
paths := spec.Spec.Paths
37+
38+
for path, pathBody := range paths.Map() {
39+
for operationName, operationBody := range pathBody.Operations() {
40+
extensions := newExtensionsFrom2xxResponse(operationBody.Responses.Map())
41+
if extensions == nil {
42+
continue
43+
}
44+
45+
apiVersion, ok := extensions[apiVersionExtensionName]
46+
if !ok {
47+
continue
48+
}
49+
50+
sunsetExt, ok := extensions[sunsetExtensionName]
51+
if !ok {
52+
continue
53+
}
54+
55+
sunset := Sunset{
56+
Operation: operationName,
57+
Path: path,
58+
SunsetDate: sunsetExt.(string),
59+
Version: apiVersion.(string),
60+
}
61+
62+
sunsets = append(sunsets, &sunset)
63+
}
64+
}
65+
66+
return sunsets
67+
}
68+
69+
func newExtensionsFrom2xxResponse(responsesMap map[string]*openapi3.ResponseRef) map[string]any {
70+
if val, ok := responsesMap["200"]; ok {
71+
return newExtensionsFromContent(val.Value.Content)
72+
}
73+
if val, ok := responsesMap["201"]; ok {
74+
return newExtensionsFromContent(val.Value.Content)
75+
}
76+
if val, ok := responsesMap["202"]; ok {
77+
return newExtensionsFromContent(val.Value.Content)
78+
}
79+
if val, ok := responsesMap["204"]; ok {
80+
return newExtensionsFromContent(val.Value.Content)
81+
}
82+
83+
return nil
84+
}
85+
86+
func newExtensionsFromContent(content openapi3.Content) map[string]any {
87+
for _, v := range content {
88+
return v.Extensions
89+
}
90+
return nil
91+
}

0 commit comments

Comments
 (0)