Skip to content

Commit

Permalink
Merge branch 'develop' into obs-398-create-api-endpoint-for-applying-…
Browse files Browse the repository at this point in the history
…change-sets
  • Loading branch information
Julio-Oliveira-Encora committed Mar 1, 2024
2 parents fb97219 + 159ff9c commit f836389
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 1 deletion.
69 changes: 69 additions & 0 deletions diode-server/netboxdiodeplugin/client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package netboxdiodeplugin

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -203,6 +204,74 @@ func (c *Client) RetrieveDcimDeviceState(ctx context.Context, objectID int, quer
}, nil
}

// ApplyChangeSet applies a change set
func (c *Client) ApplyChangeSet(ctx context.Context, changeSet ChangeSetRequest) (*ChangeSetResponse, error) {
endpointURL, err := url.Parse(fmt.Sprintf("%s/apply-change-set", c.baseURL.String()))
if err != nil {
return nil, err
}

reqBody, err := json.Marshal(changeSet)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpointURL.String(), bytes.NewBuffer(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
c.logger.Warn("failed to close response body", "error", closeErr)
}
}()

respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body %w", err)
}

var changeSetResponse ChangeSetResponse
if err = json.Unmarshal(respBytes, &changeSetResponse); err != nil {
return nil, fmt.Errorf("failed to unmarshal response body %w", err)
}

if resp.StatusCode != http.StatusOK {
c.logger.Debug(fmt.Sprintf("request POST %s failed", req.URL.String()), "statusCode", resp.StatusCode, "response", changeSetResponse)
return &changeSetResponse, fmt.Errorf("request POST %s failed - %q", req.URL.String(), resp.Status)
}
return &changeSetResponse, nil
}

// ChangeSetRequest represents a apply change set request
type ChangeSetRequest struct {
ChangeSetID string `json:"change_set_id"`
ChangeSet []Change `json:"change_set"`
}

// Change represents a change to apply
type Change struct {
ChangeID string `json:"change_id"`
ChangeType string `json:"change_type"`
ObjectType string `json:"object_type"`
ObjectID *int `json:"object_id,omitempty"`
ObjectVersion *int `json:"object_version,omitempty"`
Data any `json:"data"`
}

// ChangeSetResponse represents an apply change set response
type ChangeSetResponse struct {
ChangeSetID string `json:"change_set_id"`
Result string `json:"result"`
Errors []string `json:"errors"`
}

// DcimDevice represents a DCIM device
type DcimDevice struct {
ID int `json:"id"`
Expand Down
110 changes: 109 additions & 1 deletion diode-server/netboxdiodeplugin/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func TestRetrieveDcimDeviceState(t *testing.T) {
{
name: "invalid server response",
objectID: 100,
apiKey: "bardfoo",
apiKey: "barfoo",
mockServerResponse: ``,
shouldError: true,
},
Expand Down Expand Up @@ -174,7 +174,115 @@ func TestRetrieveDcimDeviceState(t *testing.T) {
}
}

func TestApplyChangeSet(t *testing.T) {
tests := []struct {
name string
apiKey string
changeSetRequest netboxdiodeplugin.ChangeSetRequest
mockServerResponse string
mockStatusCode int
response any
shouldError bool
}{
{
name: "valid apply change set response",
apiKey: "foobar",
changeSetRequest: netboxdiodeplugin.ChangeSetRequest{
ChangeSetID: "00000000-0000-0000-0000-000000000000",
ChangeSet: []netboxdiodeplugin.Change{
{
ChangeID: "00000000-0000-0000-0000-000000000001",
ChangeType: "create",
ObjectType: "dcim.device",
ObjectID: nil,
ObjectVersion: nil,
Data: &netboxdiodeplugin.DcimDevice{
Name: "test",
},
},
{
ChangeID: "00000000-0000-0000-0000-000000000002",
ChangeType: "update",
ObjectType: "dcim.device",
ObjectID: ptrInt(1),
ObjectVersion: ptrInt(2),
Data: &netboxdiodeplugin.DcimDevice{
Name: "test",
},
},
},
},
mockServerResponse: `{"change_set_id":"00000000-0000-0000-0000-000000000000","result":"success"}`,
mockStatusCode: http.StatusOK,
response: &netboxdiodeplugin.ChangeSetResponse{
ChangeSetID: "00000000-0000-0000-0000-000000000000",
Result: "success",
},
shouldError: false,
},
{
name: "invalid request",
apiKey: "foobar",
changeSetRequest: netboxdiodeplugin.ChangeSetRequest{
ChangeSetID: "00000000-0000-0000-0000-000000000000",
ChangeSet: []netboxdiodeplugin.Change{
{
ChangeID: "00000000-0000-0000-0000-000000000001",
ChangeType: "create",
ObjectType: "",
ObjectID: nil,
ObjectVersion: nil,
Data: nil,
},
},
},
mockServerResponse: `{"change_set_id":"00000000-0000-0000-0000-000000000000","result":"failure","errors":["invalid object type"]}`,
mockStatusCode: http.StatusBadRequest,
shouldError: true,
},
}

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: false}))

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanUpEnvVars()

handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Method, http.MethodPost)
assert.Equal(t, r.URL.Path, "/api/diode/apply-change-set")
assert.Equal(t, r.Header.Get("Authorization"), fmt.Sprintf("Token %s", tt.apiKey))
assert.Equal(t, r.Header.Get("User-Agent"), fmt.Sprintf("%s/%s", netboxdiodeplugin.SDKName, netboxdiodeplugin.SDKVersion))
assert.Equal(t, r.Header.Get("Content-Type"), "application/json")
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockServerResponse))
}
mux := http.NewServeMux()
mux.HandleFunc("/api/diode/apply-change-set", handler)
ts := httptest.NewServer(mux)
defer ts.Close()

_ = os.Setenv(netboxdiodeplugin.BaseURLEnvVarName, fmt.Sprintf("%s/api/diode", ts.URL))

client, err := netboxdiodeplugin.NewClient(tt.apiKey, logger)
require.NoError(t, err)
resp, err := client.ApplyChangeSet(context.Background(), tt.changeSetRequest)
if tt.shouldError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.response, resp)
assert.Equal(t, tt.mockStatusCode, http.StatusOK)
})
}
}

func cleanUpEnvVars() {
_ = os.Unsetenv(netboxdiodeplugin.BaseURLEnvVarName)
_ = os.Unsetenv(netboxdiodeplugin.TimeoutSecondsEnvVarName)
}

func ptrInt(i int) *int {
return &i
}

0 comments on commit f836389

Please sign in to comment.