Skip to content

Commit 0459f89

Browse files
authored
Merge pull request #139 from bcgov/pre-release
add new gateway pattern command and stdin support
2 parents 42eb555 + 9e390e3 commit 0459f89

File tree

5 files changed

+342
-12
lines changed

5 files changed

+342
-12
lines changed

cmd/apply.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"os"
89
"path/filepath"
910

@@ -53,14 +54,27 @@ type ApplyOptions struct {
5354
func (o *ApplyOptions) Parse() error {
5455
var gatewayService = GatewayService{}
5556

56-
filePath := filepath.Join(o.cwd, o.input)
57-
ext := filepath.Ext(filePath)
58-
if ext != ".yaml" && ext != ".yml" {
59-
return fmt.Errorf("Invalid file type. %s is not a YAML file", o.input)
60-
}
61-
file, err := os.ReadFile(filePath)
62-
if err != nil {
63-
return err
57+
var file []byte
58+
if o.input == "-" {
59+
// read from stdin
60+
content, err := io.ReadAll(os.Stdin)
61+
if err != nil {
62+
return err
63+
}
64+
file = content
65+
} else {
66+
67+
filePath := filepath.Join(o.cwd, o.input)
68+
ext := filepath.Ext(filePath)
69+
if ext != ".yaml" && ext != ".yml" {
70+
return fmt.Errorf("Invalid file type. %s is not a YAML file", o.input)
71+
}
72+
content, err := os.ReadFile(filePath)
73+
if err != nil {
74+
return err
75+
}
76+
77+
file = content
6478
}
6579

6680
splitDocs, err := pkg.SplitYAML(file)

cmd/gatewayPattern.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/MakeNowJust/heredoc/v2"
12+
"github.com/bcgov/gwa-cli/pkg"
13+
"github.com/spf13/cobra"
14+
"gopkg.in/yaml.v3"
15+
)
16+
17+
type GatewayPatternOptions struct {
18+
input string
19+
}
20+
21+
func GatewayPatternCmd(ctx *pkg.AppContext) *cobra.Command {
22+
opts := &GatewayPatternOptions{}
23+
var gatewayPatternCmd = &cobra.Command{
24+
Use: "gateway-pattern input",
25+
Aliases: []string{"p"},
26+
Short: "Generate gateway configuration based on pattern",
27+
Long: heredoc.Doc(`
28+
`),
29+
Example: heredoc.Doc(`
30+
$ gwa gateway-pattern path/to/config1.yaml
31+
`),
32+
RunE: pkg.WrapError(ctx, func(_ *cobra.Command, args []string) error {
33+
if ctx.Gateway == "" {
34+
fmt.Println(heredoc.Doc(`
35+
A gateway must be set via the config command
36+
37+
Example:
38+
$ gwa config set gateway YOUR_GATEWAY_NAME
39+
`))
40+
return fmt.Errorf("no gateway has been set")
41+
}
42+
43+
if len(args) == 0 {
44+
return fmt.Errorf("a pattern input file is required")
45+
}
46+
47+
opts.input = args[0]
48+
config, err := PreparePatternFile(ctx, opts)
49+
if err != nil {
50+
return err
51+
}
52+
53+
result, err := GatewayPattern(ctx, opts, config)
54+
if err != nil {
55+
return err
56+
}
57+
58+
yamlContent, err := yaml.Marshal(result.Documents[0])
59+
if err != nil {
60+
return err
61+
}
62+
63+
fmt.Printf(`%s`, yamlContent)
64+
65+
return nil
66+
}),
67+
}
68+
return gatewayPatternCmd
69+
}
70+
71+
type GatewayPatternResponse struct {
72+
Documents []interface{} `json:"documents"`
73+
}
74+
75+
func PreparePatternFile(ctx *pkg.AppContext, opts *GatewayPatternOptions) (io.Reader, error) {
76+
var validFiles = []string{}
77+
78+
// validate all the inputs are YAML, if directory loop through
79+
var input = opts.input
80+
if input == "-" {
81+
// read from stdin
82+
content, err := io.ReadAll(os.Stdin)
83+
if err != nil {
84+
return nil, err
85+
}
86+
jsonContent, err := yamlToJson(content)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
return bytes.NewReader(jsonContent), nil
92+
}
93+
94+
filePath := filepath.Join(ctx.Cwd, input)
95+
info, err := os.Stat(filePath)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
if info.IsDir() {
101+
return nil, fmt.Errorf("must be a file")
102+
} else {
103+
if isYamlFile(input) {
104+
validFiles = append(validFiles, filePath)
105+
}
106+
}
107+
108+
if len(validFiles) == 0 {
109+
return nil, fmt.Errorf("this directory contains no yaml config files")
110+
}
111+
112+
// read yaml and convert to json
113+
content, err := os.ReadFile(validFiles[0])
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
jsonContent, err := yamlToJson(content)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
return bytes.NewReader(jsonContent), nil
124+
}
125+
126+
func yamlToJson(content []byte) ([]byte, error) {
127+
var data interface{}
128+
129+
var err = yaml.Unmarshal(content, &data)
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
jsonContent, err := json.Marshal(data)
135+
if err != nil {
136+
return nil, err
137+
}
138+
return jsonContent, nil
139+
}
140+
141+
func GatewayPattern(ctx *pkg.AppContext, opts *GatewayPatternOptions, configFile io.Reader) (GatewayPatternResponse, error) {
142+
var result GatewayPatternResponse
143+
144+
body := &bytes.Buffer{}
145+
146+
var _, err = io.Copy(body, configFile)
147+
if err != nil {
148+
return result, err
149+
}
150+
151+
path := fmt.Sprintf("/ds/api/%s/gateways/%s/pattern", ctx.ApiVersion, ctx.Gateway)
152+
URL, _ := ctx.CreateUrl(path, nil)
153+
r, err := pkg.NewApiPut[GatewayPatternResponse](ctx, URL, body)
154+
if err != nil {
155+
return result, err
156+
}
157+
r.Request.Header.Set("Content-Type", "application/json")
158+
contentLength := int64(body.Len())
159+
r.Request.ContentLength = contentLength
160+
161+
response, err := r.Do()
162+
if err != nil {
163+
return result, err
164+
}
165+
166+
result = response.Data
167+
168+
return result, nil
169+
}

cmd/gatewayPattern_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"testing"
8+
9+
"github.com/bcgov/gwa-cli/pkg"
10+
"github.com/jarcoal/httpmock"
11+
"github.com/spf13/cobra"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/zenizh/go-capturer"
14+
)
15+
16+
func TestGatewayPatternCommands(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
expect string
20+
method string
21+
gateway string
22+
payload string
23+
response httpmock.Responder
24+
}{
25+
{
26+
name: "eval gateway pattern",
27+
expect: `kind: GatewayService
28+
name: sdx.test-abc
29+
`,
30+
method: "PUT",
31+
payload: `{
32+
"pattern": "simple-service.r1",
33+
"parameters": {
34+
"gateway_id": "gw-1",
35+
"service_name": "test-abc",
36+
"service_url": "https://httpbun.com"
37+
}
38+
}`,
39+
gateway: "/ns-sampler/pattern",
40+
response: func(r *http.Request) (*http.Response, error) {
41+
return httpmock.NewJsonResponse(200, GatewayPatternResponse{
42+
Documents: []interface{}{
43+
map[string]interface{}{
44+
"kind": "GatewayService",
45+
"name": "sdx.test-abc",
46+
},
47+
},
48+
},
49+
)
50+
},
51+
},
52+
53+
{
54+
name: "eval missing parameter",
55+
expect: `Error: Invalid input`,
56+
method: "PUT",
57+
payload: `{
58+
"pattern": "simple-service.r1",
59+
"parameters": {
60+
"gateway_id": "gw-1",
61+
"service_url": "https://httpbun.com"
62+
}
63+
}`,
64+
gateway: "/ns-sampler/pattern",
65+
response: func(r *http.Request) (*http.Response, error) {
66+
return httpmock.NewJsonResponse(422, map[string]interface{}{
67+
"message": "Invalid input",
68+
"fields": map[string]interface{}{
69+
"service_name": map[string]interface{}{
70+
"message": "service_name is required",
71+
},
72+
},
73+
},
74+
)
75+
},
76+
},
77+
78+
{
79+
name: "eval invalid gateway pattern",
80+
expect: `Error: Invalid input`,
81+
method: "PUT",
82+
payload: `{
83+
"pattern": "simple-service.r1",
84+
"parameters": {
85+
"gateway_id": "gw-1",
86+
"service_url": "https://httpbun.com"
87+
}
88+
}`,
89+
gateway: "/ns-sampler/pattern",
90+
response: func(r *http.Request) (*http.Response, error) {
91+
return httpmock.NewJsonResponse(422, map[string]interface{}{
92+
"message": "Invalid input",
93+
"fields": map[string]interface{}{
94+
"pattern": map[string]interface{}{
95+
"message": "pattern not found",
96+
},
97+
},
98+
})
99+
},
100+
},
101+
}
102+
103+
for _, tt := range tests {
104+
t.Run(tt.name, func(t *testing.T) {
105+
dir := t.TempDir()
106+
setup(dir)
107+
108+
// prepare pattern.yaml
109+
patternFilePath := fmt.Sprintf("%s/pattern.yaml", dir)
110+
err := os.WriteFile(patternFilePath, []byte(tt.payload), 0644)
111+
if err != nil {
112+
t.Fatalf("failed to write pattern file: %v", err)
113+
}
114+
115+
if tt.response != nil {
116+
httpmock.Activate()
117+
defer httpmock.DeactivateAndReset()
118+
URL := fmt.Sprintf("https://api.gov.ca/ds/api/v3/gateways%s", tt.gateway)
119+
httpmock.RegisterResponder(tt.method, URL, tt.response)
120+
}
121+
ctx := &pkg.AppContext{
122+
ApiHost: "api.gov.ca",
123+
ApiVersion: "v3",
124+
Gateway: "ns-sampler",
125+
}
126+
args := append([]string{"gateway-pattern"}, patternFilePath)
127+
mainCmd := &cobra.Command{
128+
Use: "gwa",
129+
SilenceUsage: true,
130+
}
131+
mainCmd.AddCommand(GatewayPatternCmd(ctx))
132+
mainCmd.SetArgs(args)
133+
134+
out := capturer.CaptureOutput(func() {
135+
mainCmd.Execute()
136+
})
137+
assert.Contains(t, out, tt.expect, "Expect: %v\nActual: %v\n", tt.expect, out)
138+
})
139+
}
140+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func NewRootCommand(ctx *pkg.AppContext) *cobra.Command {
4040
rootCmd.AddCommand(NewGenerateConfigCmd(ctx))
4141
rootCmd.AddCommand(NewLoginCmd(ctx))
4242
rootCmd.AddCommand(NewGatewayCmd(ctx, nil))
43+
rootCmd.AddCommand(GatewayPatternCmd(ctx))
4344
rootCmd.AddCommand(NewStatusCmd(ctx, nil))
4445
// Disable these for now since they don't do anything
4546
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.gwa-confg.yaml)")

pkg/api.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,11 @@ func NewApiDelete[T any](ctx *AppContext, url string) (*NewApi[T], error) {
124124

125125
// API error parsing
126126
type ApiErrorResponse struct {
127-
Error string `json:"error"`
128-
ErrorMessage string `json:"error_description"`
129-
Message string `json:"message"`
130-
Results string `json:"results"`
127+
Error string `json:"error"`
128+
ErrorMessage string `json:"error_description"`
129+
Message string `json:"message"`
130+
Results string `json:"results"`
131+
Fields map[string]any `json:"fields"`
131132
Details struct {
132133
Item struct {
133134
Message string `json:"message"`
@@ -143,6 +144,11 @@ func (e *ApiErrorResponse) GetError() error {
143144
if e.Message != "" {
144145
result = append(result, e.Message)
145146
}
147+
if e.Fields != nil {
148+
for k, v := range e.Fields {
149+
result = append(result, fmt.Sprintf("%s: %s", k, v.(map[string]interface{})["message"].(string)))
150+
}
151+
}
146152
if e.Results != "" {
147153
result = append(result, e.Results)
148154
}

0 commit comments

Comments
 (0)