Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ docker-down:
# The default CI stays narrower via runtime guards such as requireGatewayURL,
# requireHTTPBin, and A7_E2E_ENABLE_GATEWAY_GROUP_CRUD.
test-e2e:
go run github.com/onsi/ginkgo/v2/ginkgo -r --procs=1 --tags=e2e --timeout=45m ./test/e2e/...
go run github.com/onsi/ginkgo/v2/ginkgo -r --procs=1 --tags=e2e --timeout=45m -v ./test/e2e/...

test-e2e-full:
go run github.com/onsi/ginkgo/v2/ginkgo -r --procs=1 --tags=e2e --timeout=45m ./test/e2e/...
go run github.com/onsi/ginkgo/v2/ginkgo -r --procs=1 --tags=e2e --timeout=45m -v ./test/e2e/...
3 changes: 1 addition & 2 deletions pkg/cmd/consumer/create/create.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package create

import (
"encoding/json"
"fmt"
"net/http"
"strings"
Expand Down Expand Up @@ -93,7 +92,7 @@ func actionRun(opts *Options) error {
if output == "" {
output = "json"
}
return cmdutil.NewExporter(output, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(output, opts.IO.Out).WriteAPIResponse(body)
}
if opts.Username == "" {
return fmt.Errorf("--username is required")
Expand Down
3 changes: 1 addition & 2 deletions pkg/cmd/consumer/update/update.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package update

import (
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -88,7 +87,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

body := api.Consumer{
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/credential/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

httpClient, err := opts.Client()
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/credential/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

pl := make(map[string]interface{})
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/gateway-group/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func createRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

if opts.Name == "" {
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/gateway-group/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func updateRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

labels := map[string]string{}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/global-rule/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}
if opts.ID == "" {
return fmt.Errorf("--id is required")
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/global-rule/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

plugins := make(map[string]interface{})
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/plugin-metadata/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}
if opts.PluginName == "" {
return fmt.Errorf("--plugin-name is required")
Expand Down Expand Up @@ -121,5 +121,5 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}
4 changes: 2 additions & 2 deletions pkg/cmd/plugin-metadata/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

metadata := map[string]interface{}{}
Expand All @@ -98,5 +98,5 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}
2 changes: 1 addition & 1 deletion pkg/cmd/proto/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

httpClient, err := opts.Client()
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/proto/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

labels := make(map[string]string)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/route/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}
if opts.URI == "" && len(opts.Paths) == 0 {
return fmt.Errorf("--path or --uri is required")
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/route/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

labels := make(map[string]string)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/service/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}
if opts.Name == "" {
return fmt.Errorf("--name is required")
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/service/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}

labels := make(map[string]string)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/ssl/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func actionRun(opts *Options) error {
func writeSSLResponse(format string, out io.Writer, body []byte) error {
var item api.SSL
if err := json.Unmarshal(body, &item); err != nil {
return cmdutil.NewExporter(format, out).Write(json.RawMessage(body))
return fmt.Errorf("failed to decode ssl response for safe export: %w", err)
}
return cmdutil.NewExporter(format, out).Write(api.RedactSSL(item))
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/ssl/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func actionRun(opts *Options) error {
func writeSSLResponse(format string, out io.Writer, body []byte) error {
var item api.SSL
if err := json.Unmarshal(body, &item); err != nil {
return cmdutil.NewExporter(format, out).Write(json.RawMessage(body))
return fmt.Errorf("failed to decode ssl response for safe export: %w", err)
}
return cmdutil.NewExporter(format, out).Write(api.RedactSSL(item))
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/stream-route/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func actionRun(opts *Options) error {
if format == "" {
format = "json"
}
return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body))
return cmdutil.NewExporter(format, opts.IO.Out).WriteAPIResponse(body)
}
if opts.ServiceID == "" {
return fmt.Errorf("--service-id is required for current API7 EE")
Expand Down
12 changes: 12 additions & 0 deletions pkg/cmdutil/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ func (e *Exporter) Write(data interface{}) error {
}
}

// WriteAPIResponse decodes a JSON API response body and writes it via the
// exporter. Writing a raw []byte (e.g. json.RawMessage) directly would cause
// yaml.v3 to emit the bytes as a sequence of integers; decoding first ensures
// both -o yaml and -o json render a proper object.
func (e *Exporter) WriteAPIResponse(body []byte) error {
var decoded interface{}
if err := json.Unmarshal(body, &decoded); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
return e.Write(decoded)
Comment thread
shreemaan-abhishek marked this conversation as resolved.
}

func (e *Exporter) writeJSON(data interface{}) error {
enc := json.NewEncoder(e.writer)
enc.SetIndent("", " ")
Expand Down
63 changes: 63 additions & 0 deletions pkg/cmdutil/exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cmdutil
Comment thread
shreemaan-abhishek marked this conversation as resolved.

import (
"bytes"
"encoding/json"
"strings"
"testing"

"gopkg.in/yaml.v3"
)

func TestExporter_WriteAPIResponse_YAML_DecodesObject(t *testing.T) {
// The original bug: writing json.RawMessage([]byte) directly to the YAML
// encoder serialized the bytes as a list of integers instead of a map.
// WriteAPIResponse must decode first so YAML emits a proper object.
body := []byte(`{"id":"r1","name":"demo","status":1}`)

var buf bytes.Buffer
if err := NewExporter("yaml", &buf).WriteAPIResponse(body); err != nil {
t.Fatalf("WriteAPIResponse failed: %v", err)
}

out := buf.String()
if strings.HasPrefix(strings.TrimSpace(out), "- ") {
t.Fatalf("yaml output looks like a byte sequence, not an object: %q", out)
}

var decoded map[string]interface{}
if err := yaml.Unmarshal(buf.Bytes(), &decoded); err != nil {
t.Fatalf("yaml output is not a valid map: %v (output: %q)", err, out)
}
if decoded["id"] != "r1" || decoded["name"] != "demo" {
t.Fatalf("yaml output missing expected fields: got %v", decoded)
}
}

func TestExporter_WriteAPIResponse_JSON_RoundTripsObject(t *testing.T) {
body := []byte(`{"id":"r1","name":"demo"}`)

var buf bytes.Buffer
if err := NewExporter("json", &buf).WriteAPIResponse(body); err != nil {
t.Fatalf("WriteAPIResponse failed: %v", err)
}

var decoded map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &decoded); err != nil {
t.Fatalf("json output is not a valid object: %v (output: %q)", err, buf.String())
}
if decoded["id"] != "r1" || decoded["name"] != "demo" {
t.Fatalf("json output missing expected fields: got %v", decoded)
}
}

func TestExporter_WriteAPIResponse_InvalidJSON_ReturnsError(t *testing.T) {
body := []byte(`not json`)
err := NewExporter("yaml", &bytes.Buffer{}).WriteAPIResponse(body)
if err == nil {
t.Fatal("expected error decoding invalid JSON")
}
if !strings.Contains(err.Error(), "failed to decode response") {
t.Fatalf("error did not mention decoding: %v", err)
}
}
3 changes: 3 additions & 0 deletions test/e2e/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func TestMain(m *testing.M) {
os.Exit(1)
}

fmt.Fprintln(os.Stderr, "Building a7 binary ...")
buildCmd := exec.Command("go", "build", "-o", binaryPath, "./cmd/a7")
buildCmd.Dir = modRoot
buildCmd.Stdout = os.Stdout
Expand All @@ -126,6 +127,7 @@ func TestMain(m *testing.M) {
// Wait for API7 EE Dashboard API to become healthy.
// Try the /api/status endpoint first, fall back to /api/gateway_groups.
healthURL := adminURL + "/api/gateway_groups"
fmt.Fprintln(os.Stderr, "Waiting for API7 EE dashboard at "+adminURL+" ...")
if err := waitForHealthy(healthURL, 120*time.Second); err != nil {
fmt.Fprintf(os.Stderr, "API7 EE not ready: %v\n", err)
os.Exit(1)
Expand All @@ -134,6 +136,7 @@ func TestMain(m *testing.M) {
// API7 EE uses UUID-style ids for runtime API calls. Resolve name -> id.
// An explicit name (incl. "default") is honored literally; only an
// unset/empty A7_GATEWAY_GROUP falls back to "first non-ingress group".
fmt.Fprintln(os.Stderr, "Resolving gateway group ...")
wanted := os.Getenv("A7_GATEWAY_GROUP")
ggID, err := resolveGatewayGroupID(wanted)
if err != nil {
Expand Down
Loading