Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4d4cb80
Initial plan
Copilot Oct 16, 2025
370e781
Implement dynamic CORBA command registration for fabricv4
Copilot Oct 16, 2025
81e3714
Update documentation for dynamic command registration
Copilot Oct 16, 2025
3f3ee7b
Initial plan
Copilot Oct 16, 2025
8d7ba43
Refactor: Move fabricv4 client to internal/fabricv4 and create templates
Copilot Oct 16, 2025
f204629
feat: Add parameter generation using reflection in register package
Copilot Oct 16, 2025
aca4d49
chore: Remove unused retryablehttp dependency
Copilot Oct 16, 2025
6fbf310
fix: Add consistent credential error messages to cmd/api.go
Copilot Oct 17, 2025
ce7d57b
revert: Simplify credential error messages per feedback
Copilot Oct 17, 2025
de2741a
Merge pull request #28 from equinix/copilot/address-pr-27-feedback
displague Oct 18, 2025
1ba52af
Filter to only Execute methods and remove -execute suffix from comman…
Copilot Oct 18, 2025
e71f6f0
Implement basic command execution with --request flag support
Copilot Oct 18, 2025
cdb3bea
Update documentation for filtered Execute methods
Copilot Oct 18, 2025
95507a1
Implement parameter extraction and actual command execution
Copilot Oct 18, 2025
4067033
Update documentation with parameter flags
Copilot Oct 18, 2025
0f92bbd
Extract SDK descriptions using go/ast and embed in commands
Copilot Oct 18, 2025
1a5e23b
Fix parameter name extraction and add --debug flag
Copilot Oct 19, 2025
0d8af9b
Improve error messages and request body field hints
Copilot Oct 19, 2025
8950f84
Expand struct parameters into individual CLI flags and improve debug …
Copilot Oct 19, 2025
00657d1
Add struct field documentation extraction and improve API error handling
Copilot Oct 19, 2025
30709d7
Centralize debug transport in internal/api and improve error unmarsha…
Copilot Oct 19, 2025
813507d
Fix debug output not showing and setter parameters incorrectly marked…
Copilot Oct 19, 2025
73bd372
docs: make docs to remove incorrect "required" indicators
displague Oct 19, 2025
2ceec37
chore: "make fix" to address goimports validation
displague Oct 19, 2025
737e15f
Add make update target and fix make onboard to generate descriptions
Copilot Oct 21, 2025
6f22f4d
remove wip implementation of fabricv4 service
displague Oct 23, 2025
9683b98
docs: note the descriptions json file as onboard artifact
displague Oct 23, 2025
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
81 changes: 79 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: lint fix build docs docs-check
.PHONY: lint fix build docs docs-check onboard update
BINARY=equinix

GOLANGCI_LINT_VERSION=v2.3.0
Expand All @@ -22,4 +22,81 @@ docs-check: docs
if git status --porcelain | grep docs; then \
echo "Uncommitted changes detected. Run 'make docs' and commit changes."; \
exit 1; \
fi
fi

# update - Update an existing service integration by fetching latest SDK and regenerating descriptions
# Usage: make update SERVICE=fabricv4
# This will:
# 1. Update the SDK package to the latest version
# 2. Extract SDK descriptions and save to cmd/descriptions/<service>.json
update:
@if [ -z "$(SERVICE)" ]; then \
echo "Error: SERVICE parameter is required"; \
echo "Usage: make update SERVICE=fabricv4"; \
exit 1; \
fi
@echo "Updating service: $(SERVICE)"
@echo "Step 1: Updating SDK package..."
@go get -u github.com/equinix/equinix-sdk-go/services/$(SERVICE)
@go mod tidy
@echo ""
@echo "Step 2: Extracting SDK descriptions..."
@mkdir -p cmd/descriptions
@SDK_PATH=$$(go list -f '{{.Dir}}' -m github.com/equinix/equinix-sdk-go)/services/$(SERVICE); \
if [ -z "$$SDK_PATH" ] || [ ! -d "$$SDK_PATH" ]; then \
echo "Error: Could not find SDK path for $(SERVICE)"; \
echo "Make sure github.com/equinix/equinix-sdk-go/services/$(SERVICE) is a valid module"; \
exit 1; \
fi; \
echo "Extracting descriptions from: $$SDK_PATH"; \
go run cmd/extract-descriptions/main.go --sdk-path "$$SDK_PATH" --output cmd/descriptions/$(SERVICE).json
@echo ""
@echo "Service $(SERVICE) updated successfully!"
@echo "Description file: cmd/descriptions/$(SERVICE).json"
@echo ""
@echo "Next steps:"
@echo "1. Review the updated SDK integration"
@echo "2. Run 'make build' to verify the changes"
@echo "3. Run 'make docs' to update documentation"
@echo "4. Commit the changes including go.mod, go.sum, and cmd/descriptions/$(SERVICE).json"

# onboard - Scaffold a new service integration
# Usage: make onboard SERVICE=fabricv5
# This will create cmd/<service>.go, internal/<service>/<service>.go, and extract SDK descriptions
onboard:
@if [ -z "$(SERVICE)" ]; then \
echo "Error: SERVICE parameter is required"; \
echo "Usage: make onboard SERVICE=fabricv5"; \
exit 1; \
fi
@echo "Onboarding new service: $(SERVICE)"
@echo ""
@echo "Step 1: Creating service scaffolding..."
@mkdir -p cmd
@sed -e 's/{{SERVICE}}/$(SERVICE)/g' \
-e 's/{{SERVICE_DISPLAY}}/$(shell echo $(SERVICE) | sed 's/\([a-z]\)\([A-Z]\)/\1 \2/g' | sed 's/v\([0-9]\)/v\1/g' | sed 's/\b\(.\)/\u\1/g')/g' \
-e 's/{{SERVICE_ALIAS}}/$(shell echo $(SERVICE) | sed 's/v[0-9]*$$//')/g' \
templates/cmd/service.go.tmpl > cmd/$(SERVICE).go
@echo " - Created cmd/$(SERVICE).go"
@mkdir -p internal/$(SERVICE)
@sed -e 's/{{SERVICE}}/$(SERVICE)/g' \
-e 's/{{SERVICE_DISPLAY}}/$(shell echo $(SERVICE) | sed 's/\([a-z]\)\([A-Z]\)/\1 \2/g' | sed 's/v\([0-9]\)/v\1/g' | sed 's/\b\(.\)/\u\1/g')/g' \
templates/internal/service.go.tmpl > internal/$(SERVICE)/$(SERVICE).go
@echo " - Created internal/$(SERVICE)/$(SERVICE).go"
@echo ""
@echo "Step 2: Fetching SDK and extracting descriptions..."
@$(MAKE) update SERVICE=$(SERVICE)
@echo ""
@echo "Service $(SERVICE) onboarded successfully!"
@echo ""
@echo "Files created:"
@echo " - cmd/$(SERVICE).go"
@echo " - internal/$(SERVICE)/$(SERVICE).go"
@echo " - cmd/descriptions/$(SERVICE).json"
@echo ""
@echo "Next steps:"
@echo "1. Review and adjust the generated files as needed"
@echo "2. Add service-specific aliases in cmd/$(SERVICE).go if desired"
@echo "3. Run 'make build' to verify the integration"
@echo "4. Run 'make docs' to generate documentation"
@echo "5. Update README.md with information about the new service"
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,51 @@ brew install equinix

## Usage

The full CLI documentation can be found [in the docs directory](docs/equinix.md).
The full CLI documentation can be found [in the docs directory](docs/equinix.md).

### Dynamic Command Registration

The CLI uses reflection to automatically register commands from the Equinix SDK. This approach:

- **Automatically discovers** all API services in the SDK client
- **Generates commands** for each service and method at build time
- **Reduces maintenance** - new SDK services are automatically available
- **Ensures consistency** - command structure mirrors the SDK structure

## Development

### Adding New Services

To onboard a new Equinix service (e.g., fabricv5), use the `onboard` target:

```sh
make onboard SERVICE=fabricv5
```

This will scaffold:
- `cmd/<service>.go` - Command registration
- `internal/api/<service>.go` - API client setup
- `cmd/descriptions/<service>.json` - Field descriptions to embed for help

After scaffolding, you'll need to:
1. Review and adjust the generated files
2. Ensure the SDK package exists in `github.com/equinix/equinix-sdk-go/services/<service>`
3. Run `make build` to verify the integration

### Building

```sh
make build
```

### Linting

```sh
make lint
```

### Generating Documentation

```sh
make docs
```
43 changes: 43 additions & 0 deletions cmd/extract-descriptions/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Package main provides a CLI tool to extract SDK descriptions
package main

import (
"flag"
"fmt"
"os"

"github.com/equinix/cli/internal/parser"
)

func main() {
sdkPath := flag.String("sdk-path", "", "Path to the SDK source directory (required)")
outputFile := flag.String("output", "descriptions.json", "Output JSON file path")
flag.Parse()

if *sdkPath == "" {
fmt.Fprintln(os.Stderr, "Error: --sdk-path is required")
flag.Usage()
os.Exit(1)
}

fmt.Printf("Extracting descriptions from SDK at: %s\n", *sdkPath)
descriptions, err := parser.ExtractDescriptions(*sdkPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error extracting descriptions: %v\n", err)
os.Exit(1)
}

fmt.Printf("Found %d services\n", len(descriptions.Services))
for name, service := range descriptions.Services {
fmt.Printf(" - %s: %d methods, %d types\n", name, len(service.Methods), len(service.Types))
}
fmt.Printf("Found %d global types\n", len(descriptions.Types))

fmt.Printf("Saving descriptions to: %s\n", *outputFile)
if err := descriptions.SaveToFile(*outputFile); err != nil {
fmt.Fprintf(os.Stderr, "Error saving descriptions: %v\n", err)
os.Exit(1)
}

fmt.Println("Successfully extracted and saved SDK descriptions")
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ require (
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/validator.v2 v2.0.1 // indirect
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
57 changes: 55 additions & 2 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"io"
"log"
"net/http"
"net/http/httputil"
"os"

equinixoauth2 "github.com/equinix/equinix-sdk-go/extensions/equinixoauth2"
"github.com/spf13/viper"
Expand All @@ -30,9 +32,43 @@ type Client struct {
HTTPClient *http.Client
}

// debugTransport wraps an HTTP transport to log requests and responses when debug mode is enabled
type debugTransport struct {
transport http.RoundTripper
}

func (t *debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug transport is something I would like to see built in to the SDK, as a follow-on to the introduction of shared templates for consistent code across services. Wouldn't be a super impactful change here, since we'd still need to explicitly wire up a client in the CLI for common use across SDK and non-SDK requests, but this implementation could be copied into the SDK later.

// Log the request
fmt.Fprintf(os.Stderr, "\n==================== HTTP REQUEST ====================\n")
reqDump, err := httputil.DumpRequestOut(req, true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you said that the Authorization and X-Auth-Token headers are being obfuscated, but I don't see that happening here. How is that obfuscation set up?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

httputil.DumpRequestOut has some field-specific behavior, but Authorization is not one of the fields it is concerned with. https://cs.opensource.google/go/go/+/refs/tags/go1.25.3:src/net/http/httputil/dump.go;l=196-199

I added a fmt.Println("Authorization header:", req.Header.Get("Authorization")) here. The Authorization header is not present at the time this runs.

if err != nil {
fmt.Fprintf(os.Stderr, "Error dumping request: %v\n", err)
} else {
fmt.Fprintf(os.Stderr, "%s\n", string(reqDump))
}
fmt.Fprintf(os.Stderr, "======================================================\n")

// Execute the request
resp, err := t.transport.RoundTrip(req)

if resp != nil {
// Log the response
fmt.Fprintf(os.Stderr, "\n==================== HTTP RESPONSE ====================\n")
respDump, dumpErr := httputil.DumpResponse(resp, true)
if dumpErr != nil {
fmt.Fprintf(os.Stderr, "Error dumping response: %v\n", dumpErr)
} else {
fmt.Fprintf(os.Stderr, "%s\n", string(respDump))
}
fmt.Fprintf(os.Stderr, "=======================================================\n\n")
}

return resp, err
}

// NewStandardClient creates a new Client for Equinix APIs that exist under
// api.equinix.com and use OAuth2 client credentials for authentication
func NewStandardClient() (*Client, error) {
func NewStandardClient(options ...ClientOption) (*Client, error) {
client := &Client{
BaseURL: "https://api.equinix.com",
DefaultHeaders: standardHeaders,
Expand All @@ -51,11 +87,28 @@ func NewStandardClient() (*Client, error) {
BaseURL: client.BaseURL,
}
authTransport := authConfig.New()
client.HTTPClient.Transport = authTransport

// Apply options to potentially wrap the transport
transport := http.RoundTripper(authTransport)
for _, opt := range options {
transport = opt(transport)
}

client.HTTPClient.Transport = transport

return client, nil
}

// ClientOption is a function that can modify the HTTP transport
type ClientOption func(http.RoundTripper) http.RoundTripper

// WithDebug returns a ClientOption that enables debug logging of HTTP requests and responses
func WithDebug() ClientOption {
return func(transport http.RoundTripper) http.RoundTripper {
return &debugTransport{transport: transport}
}
}

// NewPortalClient creates a new Client for Equinix APIs that exist under
// portal.equinix.com and rely on Cookies to transmit OAuth2 tokens
func NewPortalClient() (*Client, error) {
Expand Down
Loading