From 24f90a23248036206eec697356ab4dbf2c65a454 Mon Sep 17 00:00:00 2001 From: klisiecka-splunk Date: Thu, 21 Nov 2024 11:54:13 +0100 Subject: [PATCH] Add validation for viz objects (#228) * validating functions added for viz objects * viz objects code refactored * update slo to return error after decoding --- chart.go | 104 ++++++++++++++--------------------------- chart_test.go | 24 ++++++++++ dashboard.go | 96 ++++++++++++++----------------------- dashboard_test.go | 24 ++++++++++ dashboardgroup.go | 94 +++++++++++++------------------------ dashboardgroup_test.go | 24 ++++++++++ slo.go | 11 +++-- 7 files changed, 183 insertions(+), 194 deletions(-) diff --git a/chart.go b/chart.go index 86fe1e9..09826a1 100644 --- a/chart.go +++ b/chart.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "io" - "io/ioutil" "net/http" "net/url" "strconv" @@ -20,106 +19,75 @@ const UpdateSloChartAPIURL = ChartAPIURL + "/updateSloChart" // CreateChart creates a chart. func (c *Client) CreateChart(ctx context.Context, chartRequest *chart.CreateUpdateChartRequest) (*chart.Chart, error) { - return c.internalCreateChart(ctx, chartRequest, ChartAPIURL) + return c.executeChartRequest(ctx, ChartAPIURL, http.MethodPost, http.StatusOK, chartRequest) } // CreateSloChart creates a SLO chart. func (c *Client) CreateSloChart(ctx context.Context, chartRequest *chart.CreateUpdateSloChartRequest) (*chart.Chart, error) { - return c.internalCreateChart(ctx, chartRequest, CreateSloChartAPIURL) -} - -func (c *Client) internalCreateChart(ctx context.Context, chartRequest interface{}, url string) (*chart.Chart, error) { - payload, err := json.Marshal(chartRequest) - if err != nil { - return nil, err - } - - resp, err := c.doRequest(ctx, "POST", url, nil, bytes.NewReader(payload)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusOK); err != nil { - return nil, err - } - - finalChart := &chart.Chart{} - - err = json.NewDecoder(resp.Body).Decode(finalChart) - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return finalChart, err + return c.executeChartRequest(ctx, CreateSloChartAPIURL, http.MethodPost, http.StatusOK, chartRequest) } // DeleteChart deletes a chart. func (c *Client) DeleteChart(ctx context.Context, id string) error { - resp, err := c.doRequest(ctx, "DELETE", ChartAPIURL+"/"+id, nil, nil) - if err != nil { - return err - } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusOK); err != nil { - return err + _, err := c.executeChartRequest(ctx, ChartAPIURL+"/"+id, http.MethodDelete, http.StatusOK, nil) + if err == io.EOF { + // Expected error as delete request returns status 200 instead of 204 + return nil } - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return nil + return err } // GetChart gets a chart. func (c *Client) GetChart(ctx context.Context, id string) (*chart.Chart, error) { - resp, err := c.doRequest(ctx, "GET", ChartAPIURL+"/"+id, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusOK); err != nil { - return nil, err - } - - finalChart := &chart.Chart{} - - err = json.NewDecoder(resp.Body).Decode(finalChart) - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return finalChart, err + return c.executeChartRequest(ctx, ChartAPIURL+"/"+id, http.MethodGet, http.StatusOK, nil) } // UpdateChart updates a chart. func (c *Client) UpdateChart(ctx context.Context, id string, chartRequest *chart.CreateUpdateChartRequest) (*chart.Chart, error) { - return c.internalUpdateChart(ctx, id, chartRequest, ChartAPIURL) + return c.executeChartRequest(ctx, ChartAPIURL+"/"+id, http.MethodPut, http.StatusOK, chartRequest) } // UpdateSloChart updates an SLO chart. func (c *Client) UpdateSloChart(ctx context.Context, id string, chartRequest *chart.CreateUpdateSloChartRequest) (*chart.Chart, error) { - return c.internalUpdateChart(ctx, id, chartRequest, UpdateSloChartAPIURL) + return c.executeChartRequest(ctx, UpdateSloChartAPIURL+"/"+id, http.MethodPut, http.StatusOK, chartRequest) } -func (c *Client) internalUpdateChart(ctx context.Context, id string, chartRequest interface{}, url string) (*chart.Chart, error) { - payload, err := json.Marshal(chartRequest) - if err != nil { - return nil, err +// ValidateChart validates a chart. +func (c *Client) ValidateChart(ctx context.Context, chartRequest *chart.CreateUpdateChartRequest) error { + _, err := c.executeChartRequest(ctx, ChartAPIURL+"/validate", http.MethodPost, http.StatusNoContent, chartRequest) + return err +} + +func (c *Client) executeChartRequest(ctx context.Context, url string, method string, expectedValidStatus int, chartRequest interface{}) (*chart.Chart, error) { + var body io.Reader + if chartRequest != nil { + payload, err := json.Marshal(chartRequest) + if err != nil { + return nil, err + } + + body = bytes.NewReader(payload) } - resp, err := c.doRequest(ctx, "PUT", url+"/"+id, nil, bytes.NewReader(payload)) + resp, err := c.doRequest(ctx, method, url, nil, body) if err != nil { return nil, err } defer resp.Body.Close() - if err = newResponseError(resp, http.StatusOK); err != nil { + if err = newResponseError(resp, expectedValidStatus); err != nil { return nil, err } - finalChart := &chart.Chart{} - - err = json.NewDecoder(resp.Body).Decode(finalChart) - _, _ = io.Copy(ioutil.Discard, resp.Body) + if expectedValidStatus == http.StatusNoContent { + _, _ = io.Copy(io.Discard, resp.Body) + return nil, nil + } - return finalChart, err + returnedChart := &chart.Chart{} + err = json.NewDecoder(resp.Body).Decode(returnedChart) + _, _ = io.Copy(io.Discard, resp.Body) + return returnedChart, err } // SearchCharts searches for charts, given a query string in `name`. @@ -147,7 +115,7 @@ func (c *Client) SearchCharts(ctx context.Context, limit int, name string, offse finalCharts := &chart.SearchResult{} err = json.NewDecoder(resp.Body).Decode(finalCharts) - _, _ = io.Copy(ioutil.Discard, resp.Body) + _, _ = io.Copy(io.Discard, resp.Body) return finalCharts, err } diff --git a/chart_test.go b/chart_test.go index a7dbf4f..964213a 100644 --- a/chart_test.go +++ b/chart_test.go @@ -197,3 +197,27 @@ func TestUpdateMissingSloChart(t *testing.T) { assert.Error(t, err, "Expected error updating chart") assert.Nil(t, result, "Expected nil result updating chart") } + +func TestValidateChart(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/chart/validate", verifyRequest(t, "POST", true, http.StatusNoContent, nil, "")) + + err := client.ValidateChart(context.Background(), &chart.CreateUpdateChartRequest{ + Name: "string", + }) + assert.NoError(t, err, "Unexpected error creating chart") +} + +func TestBadValidateChart(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/chart/validate", verifyRequest(t, "POST", true, http.StatusBadRequest, nil, "")) + + err := client.ValidateChart(context.Background(), &chart.CreateUpdateChartRequest{ + Name: "string", + }) + assert.Error(t, err, "Expected error creating bad chart") +} diff --git a/dashboard.go b/dashboard.go index 0845e8b..1e3b29a 100644 --- a/dashboard.go +++ b/dashboard.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "io" - "io/ioutil" "net/http" "net/url" "strconv" @@ -20,88 +19,65 @@ const DashboardAPIURL = "/v2/dashboard" // CreateDashboard creates a dashboard. func (c *Client) CreateDashboard(ctx context.Context, dashboardRequest *dashboard.CreateUpdateDashboardRequest) (*dashboard.Dashboard, error) { - payload, err := json.Marshal(dashboardRequest) - if err != nil { - return nil, err - } - - resp, err := c.doRequest(ctx, "POST", DashboardAPIURL, nil, bytes.NewReader(payload)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusOK); err != nil { - return nil, err - } - - finalDashboard := &dashboard.Dashboard{} - - err = json.NewDecoder(resp.Body).Decode(finalDashboard) - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return finalDashboard, err + return c.executeDashboardRequest(ctx, DashboardAPIURL, http.MethodPost, http.StatusOK, dashboardRequest) } // DeleteDashboard deletes a dashboard. func (c *Client) DeleteDashboard(ctx context.Context, id string) error { - resp, err := c.doRequest(ctx, "DELETE", DashboardAPIURL+"/"+id, nil, nil) - if err != nil { - return err + _, err := c.executeDashboardRequest(ctx, DashboardAPIURL+"/"+id, http.MethodDelete, http.StatusOK, nil) + if err == io.EOF { + // Expected error as delete request returns status 200 instead of 204 + return nil } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusOK); err != nil { - return err - } - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return nil + return err } // GetDashboard gets a dashboard. func (c *Client) GetDashboard(ctx context.Context, id string) (*dashboard.Dashboard, error) { - resp, err := c.doRequest(ctx, "GET", DashboardAPIURL+"/"+id, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusOK); err != nil { - return nil, err - } - - finalDashboard := &dashboard.Dashboard{} - - err = json.NewDecoder(resp.Body).Decode(finalDashboard) - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return finalDashboard, err + return c.executeDashboardRequest(ctx, DashboardAPIURL+"/"+id, http.MethodGet, http.StatusOK, nil) } // UpdateDashboard updates a dashboard. func (c *Client) UpdateDashboard(ctx context.Context, id string, dashboardRequest *dashboard.CreateUpdateDashboardRequest) (*dashboard.Dashboard, error) { - payload, err := json.Marshal(dashboardRequest) - if err != nil { - return nil, err + return c.executeDashboardRequest(ctx, DashboardAPIURL+"/"+id, http.MethodPut, http.StatusOK, dashboardRequest) +} + +// ValidateDashboard validates a dashboard. +func (c *Client) ValidateDashboard(ctx context.Context, dashboardRequest *dashboard.CreateUpdateDashboardRequest) error { + _, err := c.executeDashboardRequest(ctx, DashboardAPIURL+"/validate", http.MethodPost, http.StatusNoContent, dashboardRequest) + return err +} + +func (c *Client) executeDashboardRequest(ctx context.Context, url string, method string, expectedValidStatus int, dashboardRequest *dashboard.CreateUpdateDashboardRequest) (*dashboard.Dashboard, error) { + var body io.Reader + if dashboardRequest != nil { + payload, err := json.Marshal(dashboardRequest) + if err != nil { + return nil, err + } + + body = bytes.NewReader(payload) } - resp, err := c.doRequest(ctx, "PUT", DashboardAPIURL+"/"+id, nil, bytes.NewReader(payload)) + resp, err := c.doRequest(ctx, method, url, nil, body) if err != nil { return nil, err } defer resp.Body.Close() - if err = newResponseError(resp, http.StatusOK); err != nil { + if err := newResponseError(resp, expectedValidStatus); err != nil { return nil, err } - finalDashboard := &dashboard.Dashboard{} - - err = json.NewDecoder(resp.Body).Decode(finalDashboard) - _, _ = io.Copy(ioutil.Discard, resp.Body) + if expectedValidStatus == http.StatusNoContent { + _, _ = io.Copy(io.Discard, resp.Body) + return nil, nil + } - return finalDashboard, err + returnedDashboard := &dashboard.Dashboard{} + err = json.NewDecoder(resp.Body).Decode(returnedDashboard) + _, _ = io.Copy(io.Discard, resp.Body) + return returnedDashboard, err } // SearchDashboard searches for dashboards, given a query string in `name`. @@ -125,7 +101,7 @@ func (c *Client) SearchDashboard(ctx context.Context, limit int, name string, of finalDashboards := &dashboard.SearchResult{} err = json.NewDecoder(resp.Body).Decode(finalDashboards) - _, _ = io.Copy(ioutil.Discard, resp.Body) + _, _ = io.Copy(io.Discard, resp.Body) return finalDashboards, err } diff --git a/dashboard_test.go b/dashboard_test.go index 1fa66b8..4655e73 100644 --- a/dashboard_test.go +++ b/dashboard_test.go @@ -145,3 +145,27 @@ func TestUpdateMissingDashboard(t *testing.T) { assert.Error(t, err, "Should've gotten an error from a missing dashboard update") assert.Nil(t, result, "Should've gotten a nil dashboard from a missing dashboard update") } + +func TestValidateDashboard(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/dashboard/validate", verifyRequest(t, "POST", true, http.StatusNoContent, nil, "")) + + err := client.ValidateDashboard(context.Background(), &dashboard.CreateUpdateDashboardRequest{ + Name: "string", + }) + assert.NoError(t, err, "Unexpected error creating dashboard") +} + +func TestValidateBadDashboard(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/dashboard/validate", verifyRequest(t, "POST", true, http.StatusBadRequest, nil, "")) + + err := client.ValidateDashboard(context.Background(), &dashboard.CreateUpdateDashboardRequest{ + Name: "string", + }) + assert.Error(t, err, "Should have gotten an error code for a bad dashboard") +} diff --git a/dashboardgroup.go b/dashboardgroup.go index 1415d84..368e832 100644 --- a/dashboardgroup.go +++ b/dashboardgroup.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "io" - "io/ioutil" "net/http" "net/url" "strconv" @@ -20,93 +19,66 @@ const DashboardGroupAPIURL = "/v2/dashboardgroup" // CreateDashboardGroup creates a dashboard. func (c *Client) CreateDashboardGroup(ctx context.Context, dashboardGroupRequest *dashboard_group.CreateUpdateDashboardGroupRequest, skipImplicitDashboard bool) (*dashboard_group.DashboardGroup, error) { - payload, err := json.Marshal(dashboardGroupRequest) - if err != nil { - return nil, err - } - params := url.Values{} if skipImplicitDashboard { params.Add("empty", "true") } - resp, err := c.doRequest(ctx, "POST", DashboardGroupAPIURL, params, bytes.NewReader(payload)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusOK); err != nil { - return nil, err - } - - finalDashboardGroup := &dashboard_group.DashboardGroup{} - - err = json.NewDecoder(resp.Body).Decode(finalDashboardGroup) - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return finalDashboardGroup, err + return c.executeDashboardGroupRequest(ctx, DashboardGroupAPIURL, http.MethodPost, http.StatusOK, dashboardGroupRequest, params) } // DeleteDashboardGroup deletes a dashboard. func (c *Client) DeleteDashboardGroup(ctx context.Context, id string) error { - resp, err := c.doRequest(ctx, "DELETE", DashboardGroupAPIURL+"/"+id, nil, nil) - if err != nil { - return err - } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusNoContent); err != nil { - return err - } - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return nil + _, err := c.executeDashboardGroupRequest(ctx, DashboardGroupAPIURL+"/"+id, http.MethodDelete, http.StatusNoContent, nil, nil) + return err } // GetDashboardGroup gets a dashboard group. func (c *Client) GetDashboardGroup(ctx context.Context, id string) (*dashboard_group.DashboardGroup, error) { - resp, err := c.doRequest(ctx, "GET", DashboardGroupAPIURL+"/"+id, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if err = newResponseError(resp, http.StatusOK); err != nil { - return nil, err - } - - finalDashboardGroup := &dashboard_group.DashboardGroup{} - - err = json.NewDecoder(resp.Body).Decode(finalDashboardGroup) - _, _ = io.Copy(ioutil.Discard, resp.Body) - - return finalDashboardGroup, err + return c.executeDashboardGroupRequest(ctx, DashboardGroupAPIURL+"/"+id, http.MethodGet, http.StatusOK, nil, nil) } // UpdateDashboardGroup updates a dashboard group. func (c *Client) UpdateDashboardGroup(ctx context.Context, id string, dashboardGroupRequest *dashboard_group.CreateUpdateDashboardGroupRequest) (*dashboard_group.DashboardGroup, error) { - payload, err := json.Marshal(dashboardGroupRequest) - if err != nil { - return nil, err + return c.executeDashboardGroupRequest(ctx, DashboardGroupAPIURL+"/"+id, http.MethodPut, http.StatusOK, dashboardGroupRequest, nil) +} + +// ValidateDashboardGroup validates a dashboard grouop. +func (c *Client) ValidateDashboardGroup(ctx context.Context, dashboardGroupRequest *dashboard_group.CreateUpdateDashboardGroupRequest) error { + _, err := c.executeDashboardGroupRequest(ctx, DashboardGroupAPIURL+"/validate", http.MethodPost, http.StatusNoContent, dashboardGroupRequest, nil) + return err +} + +func (c *Client) executeDashboardGroupRequest(ctx context.Context, url string, method string, expectedValidStatus int, dashboardGroupRequest *dashboard_group.CreateUpdateDashboardGroupRequest, params url.Values) (*dashboard_group.DashboardGroup, error) { + var body io.Reader + if dashboardGroupRequest != nil { + payload, err := json.Marshal(dashboardGroupRequest) + if err != nil { + return nil, err + } + + body = bytes.NewReader(payload) } - resp, err := c.doRequest(ctx, "PUT", DashboardGroupAPIURL+"/"+id, nil, bytes.NewReader(payload)) + resp, err := c.doRequest(ctx, method, url, params, body) if err != nil { return nil, err } defer resp.Body.Close() - if err = newResponseError(resp, http.StatusOK); err != nil { + if err = newResponseError(resp, expectedValidStatus); err != nil { return nil, err } - finalDashboardGroup := &dashboard_group.DashboardGroup{} - - err = json.NewDecoder(resp.Body).Decode(finalDashboardGroup) - _, _ = io.Copy(ioutil.Discard, resp.Body) + if expectedValidStatus == http.StatusNoContent { + _, _ = io.Copy(io.Discard, resp.Body) + return nil, nil + } - return finalDashboardGroup, err + returnedDashboardGroup := &dashboard_group.DashboardGroup{} + err = json.NewDecoder(resp.Body).Decode(returnedDashboardGroup) + _, _ = io.Copy(io.Discard, resp.Body) + return returnedDashboardGroup, err } // SearchDashboardGroup searches for dashboard groups, given a query string in `name`. @@ -129,7 +101,7 @@ func (c *Client) SearchDashboardGroups(ctx context.Context, limit int, name stri finalDashboardGroups := &dashboard_group.SearchResult{} err = json.NewDecoder(resp.Body).Decode(finalDashboardGroups) - _, _ = io.Copy(ioutil.Discard, resp.Body) + _, _ = io.Copy(io.Discard, resp.Body) return finalDashboardGroups, err } diff --git a/dashboardgroup_test.go b/dashboardgroup_test.go index 025df36..864b470 100644 --- a/dashboardgroup_test.go +++ b/dashboardgroup_test.go @@ -146,3 +146,27 @@ func TestUpdateDashboardGroup(t *testing.T) { assert.Error(t, err, "Should have error updating missing dashboard group") assert.Nil(t, result, "Should have nil result") } + +func TestValidateDashboardGroup(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/dashboardgroup/validate", verifyRequest(t, "POST", true, http.StatusNoContent, nil, "")) + + err := client.ValidateDashboardGroup(context.Background(), &dashboard_group.CreateUpdateDashboardGroupRequest{ + Name: "string", + }) + assert.NoError(t, err, "Unexpected error validating dashboard group") +} + +func TestValidateDashboardGroupBad(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/dashboardgroup/validate", verifyRequest(t, "POST", true, http.StatusBadRequest, nil, "")) + + err := client.ValidateDashboardGroup(context.Background(), &dashboard_group.CreateUpdateDashboardGroupRequest{ + Name: "string", + }) + assert.Error(t, err, "Should have gotten an error from invalid dashboard group") +} diff --git a/slo.go b/slo.go index e90bec9..9c99f7c 100644 --- a/slo.go +++ b/slo.go @@ -56,12 +56,13 @@ func (c *Client) executeSloRequest(ctx context.Context, url string, method strin return nil, err } - if resp.Body != nil { - returnedSlo := &slo.SloObject{} - err = json.NewDecoder(resp.Body).Decode(returnedSlo) - return returnedSlo, nil - } else { + if expectedValidStatus == http.StatusNoContent { _, _ = io.Copy(io.Discard, resp.Body) return nil, nil } + + returnedSlo := &slo.SloObject{} + err = json.NewDecoder(resp.Body).Decode(returnedSlo) + _, _ = io.Copy(io.Discard, resp.Body) + return returnedSlo, err }