Skip to content

Commit e849055

Browse files
Refactor ApiClient to use generated OpenAPI client
This commit refactors the CLI's API client to use a strongly-typed Go client generated from the management API's OpenAPI specification. This improves maintainability, provides compile-time type safety, and reduces manual boilerplate code. The new implementation delegates all standard RESTful operations to the generated client while preserving the custom logic for non-RESTful endpoints. Key changes include: - Adds `oapi-codegen` as the tool for generating the Go client from the OpenAPI spec. - Introduces a `make generate-api-client` target to the `cli/Makefile` to automate the generation process. - Refactors `sdk.ApiClient` to wrap the generated client. - All RESTful methods (`Apply`, `Delete`, `GetResource`, `ListResources`) now use the generated client. - The existing manual implementations for `Watch` (streaming) and `ReadyWait` (long-polling) are preserved as they handle logic unsupported by the generator. - The generated client code is committed to `cli/sdk/generated/` to ensure build reproducibility. Signed-off-by: Aman Singh <[email protected]>
1 parent 5660760 commit e849055

File tree

4 files changed

+491
-70
lines changed

4 files changed

+491
-70
lines changed

cli/Makefile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ VERSION ?= latest
2323

2424
DRASI_LOCATION ?= $(if $(filter $(OS),Windows_NT),$(shell $$Env:ProgramFiles)\drasi\,/usr/local/bin/)
2525

26+
# Path to the OpenAPI specification file
27+
SPEC_FILE = ../control-planes/mgmt_api/openapi.yaml
28+
29+
# Output path for the generated client
30+
GENERATED_CLIENT = sdk/generated/api.gen.go
31+
2632
.PHONY: all
2733
all: fmt vet
2834
ifeq ($(IS_WINDOWS),true)
@@ -66,3 +72,15 @@ else
6672
cp ./bin/drasi $(DRASI_LOCATION)
6773
endif
6874

75+
## generate-api-client: Generates the Drasi API client from the OpenAPI spec
76+
.PHONY: generate-api-client
77+
generate-api-client:
78+
@echo "--> Generating Drasi API client..."
79+
@if ! command -v oapi-codegen &> /dev/null; then \
80+
echo "oapi-codegen not found. Please run: go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest"; \
81+
exit 1; \
82+
fi
83+
@mkdir -p sdk/generated
84+
oapi-codegen --generate "types,client" --package="generated" -o "$(GENERATED_CLIENT)" "$(SPEC_FILE)"
85+
@echo "--> API client generated successfully at $(GENERATED_CLIENT)"
86+

cli/sdk/api_client.go

Lines changed: 111 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package sdk
1616

1717
import (
1818
"bytes"
19+
"context"
1920
"encoding/json"
2021
"errors"
2122
"fmt"
@@ -27,6 +28,7 @@ import (
2728

2829
"drasi.io/cli/api"
2930
"drasi.io/cli/output"
31+
generated "drasi.io/cli/sdk/generated"
3032
)
3133

3234
// DrasiClient defines the interface for interacting with the Drasi Management API.
@@ -41,59 +43,121 @@ type DrasiClient interface {
4143
}
4244

4345
type ApiClient struct {
44-
stopCh chan struct{}
45-
port int32
46-
client *http.Client
47-
streamClient *http.Client
48-
prefix string
46+
stopCh chan struct{}
47+
port int32
48+
client *http.Client
49+
streamClient *http.Client
50+
prefix string
51+
generatedClient *generated.ClientWithResponses
4952
}
5053

5154
// Ensure ApiClient implements the new interface
5255
var _ DrasiClient = (*ApiClient)(nil)
5356

54-
func (t *ApiClient) Apply(manifests *[]api.Manifest, output output.TaskOutput) error {
55-
for _, mf := range *manifests {
56-
subject := "Apply: " + mf.Kind + "/" + mf.Name
57-
output.AddTask(subject, subject)
57+
// Helper function to route PUT operations to the appropriate generated client method
58+
func (t *ApiClient) putResource(ctx context.Context, kind string, name string, spec interface{}) (*http.Response, error) {
59+
switch strings.ToLower(kind) {
60+
case "source":
61+
body := generated.PutSourceJSONRequestBody(spec)
62+
return t.generatedClient.PutSource(ctx, name, body)
63+
case "continuousquery", "query":
64+
body := generated.PutContinuousQueryJSONRequestBody(spec)
65+
return t.generatedClient.PutContinuousQuery(ctx, name, body)
66+
case "reaction":
67+
body := generated.PutReactionJSONRequestBody(spec)
68+
return t.generatedClient.PutReaction(ctx, name, body)
69+
case "sourceprovider":
70+
body := generated.PutSourceProviderJSONRequestBody(spec)
71+
return t.generatedClient.PutSourceProvider(ctx, name, body)
72+
case "reactionprovider":
73+
body := generated.PutReactionProviderJSONRequestBody(spec)
74+
return t.generatedClient.PutReactionProvider(ctx, name, body)
75+
case "querycontainer":
76+
body := generated.PutQueryContainerJSONRequestBody(spec)
77+
return t.generatedClient.PutQueryContainer(ctx, name, body)
78+
default:
79+
return nil, fmt.Errorf("unsupported resource kind: %s", kind)
80+
}
81+
}
5882

59-
url := fmt.Sprintf("%v/%v/%v/%v", t.prefix, mf.ApiVersion, kindRoutes[strings.ToLower(mf.Kind)], mf.Name)
83+
// Helper function to route DELETE operations to the appropriate generated client method
84+
func (t *ApiClient) deleteResource(ctx context.Context, kind string, name string) (*http.Response, error) {
85+
switch strings.ToLower(kind) {
86+
case "source":
87+
return t.generatedClient.DeleteSource(ctx, name)
88+
case "continuousquery", "query":
89+
return t.generatedClient.DeleteContinuousQuery(ctx, name)
90+
case "reaction":
91+
return t.generatedClient.DeleteReaction(ctx, name)
92+
case "sourceprovider":
93+
return t.generatedClient.DeleteSourceProvider(ctx, name)
94+
case "reactionprovider":
95+
return t.generatedClient.DeleteReactionProvider(ctx, name)
96+
case "querycontainer":
97+
return t.generatedClient.DeleteQueryContainer(ctx, name)
98+
default:
99+
return nil, fmt.Errorf("unsupported resource kind: %s", kind)
100+
}
101+
}
60102

61-
if mf.Tag != "" {
62-
url = fmt.Sprintf("%v/%v/%v/%v", t.prefix, mf.ApiVersion, kindRoutes[strings.ToLower(mf.Kind)], mf.Name+":"+mf.Tag)
63-
}
64-
data, err := json.Marshal(mf.Spec)
65-
if err != nil {
66-
output.FailTask(subject, fmt.Sprintf("Error: %v: %v", subject, err.Error()))
67-
return err
68-
}
103+
// Helper function to route GET operations to the appropriate generated client method
104+
func (t *ApiClient) getResource(ctx context.Context, kind string, name string) (*http.Response, error) {
105+
switch strings.ToLower(kind) {
106+
case "source":
107+
return t.generatedClient.GetSource(ctx, name)
108+
case "continuousquery", "query":
109+
return t.generatedClient.GetContinuousQuery(ctx, name)
110+
case "reaction":
111+
return t.generatedClient.GetReaction(ctx, name)
112+
case "sourceprovider":
113+
return t.generatedClient.GetSourceProvider(ctx, name)
114+
case "reactionprovider":
115+
return t.generatedClient.GetReactionProvider(ctx, name)
116+
case "querycontainer":
117+
return t.generatedClient.GetQueryContainer(ctx, name)
118+
default:
119+
return nil, fmt.Errorf("unsupported resource kind: %s", kind)
120+
}
121+
}
69122

70-
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
71-
if err != nil {
72-
output.FailTask(subject, fmt.Sprintf("Error: %v: %v", subject, err.Error()))
73-
return err
74-
}
123+
// Helper function to route LIST operations to the appropriate generated client method
124+
func (t *ApiClient) listResources(ctx context.Context, kind string) (*http.Response, error) {
125+
switch strings.ToLower(kind) {
126+
case "source":
127+
return t.generatedClient.ListSources(ctx)
128+
case "continuousquery", "query":
129+
return t.generatedClient.ListContinuousQueries(ctx)
130+
case "reaction":
131+
return t.generatedClient.ListReactions(ctx)
132+
case "sourceprovider":
133+
return t.generatedClient.ListSourceProviders(ctx)
134+
case "reactionprovider":
135+
return t.generatedClient.ListReactionProviders(ctx)
136+
case "querycontainer":
137+
return t.generatedClient.ListQueryContainers(ctx)
138+
default:
139+
return nil, fmt.Errorf("unsupported resource kind: %s", kind)
140+
}
141+
}
75142

76-
req.Header.Set("Content-Type", "application/json")
77-
if mf.ApiVersion != "" {
78-
req.Header.Set("api-version", mf.ApiVersion)
79-
}
143+
func (t *ApiClient) Apply(manifests *[]api.Manifest, output output.TaskOutput) error {
144+
ctx := context.Background()
145+
for _, mf := range *manifests {
146+
subject := "Apply: " + mf.Kind + "/" + mf.Name
147+
output.AddTask(subject, subject)
80148

81-
resp, err := t.client.Do(req)
149+
resp, err := t.putResource(ctx, mf.Kind, mf.Name, mf.Spec)
82150
if err != nil {
83151
output.FailTask(subject, fmt.Sprintf("Error: %v: %v", subject, err.Error()))
84152
return err
85153
}
154+
defer resp.Body.Close()
86155

87156
if resp.StatusCode != http.StatusOK {
88-
msg := resp.Status
89-
90-
// Adding a space before the response body for better readability
91-
msg += " "
92-
157+
msg := resp.Status + " "
93158
if b, err := io.ReadAll(resp.Body); err == nil {
94159
msg += ": " + string(b)
95160
}
96-
97161
output.FailTask(subject, fmt.Sprintf("Error: %v: %v", subject, msg))
98162
return errors.New(msg)
99163
}
@@ -104,26 +168,17 @@ func (t *ApiClient) Apply(manifests *[]api.Manifest, output output.TaskOutput) e
104168
}
105169

106170
func (t *ApiClient) Delete(manifests *[]api.Manifest, output output.TaskOutput) error {
171+
ctx := context.Background()
107172
for _, mf := range *manifests {
108173
subject := "Delete: " + mf.Kind + "/" + mf.Name
109174
output.AddTask(subject, subject)
110175

111-
url := fmt.Sprintf("%v/%v/%v/%v", t.prefix, mf.ApiVersion, kindRoutes[strings.ToLower(mf.Kind)], mf.Name)
112-
113-
if mf.Tag != "" {
114-
url = fmt.Sprintf("%v/%v/%v/%v", t.prefix, mf.ApiVersion, kindRoutes[strings.ToLower(mf.Kind)], mf.Name+":"+mf.Tag)
115-
}
116-
req, err := http.NewRequest(http.MethodDelete, url, bytes.NewReader([]byte{}))
117-
if err != nil {
118-
output.FailTask(subject, fmt.Sprintf("Error: %v: %v", subject, err.Error()))
119-
return err
120-
}
121-
122-
resp, err := t.client.Do(req)
176+
resp, err := t.deleteResource(ctx, mf.Kind, mf.Name)
123177
if err != nil {
124178
output.FailTask(subject, fmt.Sprintf("Error: %v: %v", subject, err.Error()))
125179
return err
126180
}
181+
defer resp.Body.Close()
127182

128183
// Successful deletion should return 204 No Content
129184
if resp.StatusCode != http.StatusNoContent {
@@ -137,57 +192,43 @@ func (t *ApiClient) Delete(manifests *[]api.Manifest, output output.TaskOutput)
137192
}
138193

139194
func (t *ApiClient) GetResource(kind string, name string) (*api.Resource, error) {
140-
var result api.Resource
141-
142-
url := fmt.Sprintf("%v/%v/%v/%v", t.prefix, "v1", kindRoutes[strings.ToLower(kind)], name)
143-
resp, err := t.client.Get(url)
195+
ctx := context.Background()
196+
resp, err := t.getResource(ctx, kind, name)
144197
if err != nil {
145198
return nil, err
146199
}
147-
148200
defer resp.Body.Close()
149201

150202
if resp.StatusCode != http.StatusOK {
151203
return nil, errors.New(resp.Status)
152204
}
153205

154-
data, err := io.ReadAll(resp.Body)
155-
if err != nil {
156-
return nil, err
157-
}
158-
159-
if err = json.Unmarshal(data, &result); err != nil {
206+
var result api.Resource
207+
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
160208
return nil, err
161209
}
162210

163-
return &result, err
211+
return &result, nil
164212
}
165213

166214
func (t *ApiClient) ListResources(kind string) ([]api.Resource, error) {
167-
var result []api.Resource
168-
169-
url := fmt.Sprintf("%v/%v/%v", t.prefix, "v1", kindRoutes[strings.ToLower(kind)])
170-
resp, err := t.client.Get(url)
215+
ctx := context.Background()
216+
resp, err := t.listResources(ctx, kind)
171217
if err != nil {
172218
return nil, err
173219
}
174-
175220
defer resp.Body.Close()
176221

177222
if resp.StatusCode != http.StatusOK {
178223
return nil, errors.New(resp.Status)
179224
}
180225

181-
data, err := io.ReadAll(resp.Body)
182-
if err != nil {
183-
return nil, err
184-
}
185-
186-
if err = json.Unmarshal(data, &result); err != nil {
226+
var result []api.Resource
227+
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
187228
return nil, err
188229
}
189230

190-
return result, err
231+
return result, nil
191232
}
192233

193234
func (t *ApiClient) ReadyWait(manifests *[]api.Manifest, timeout int32, output output.TaskOutput) error {

0 commit comments

Comments
 (0)