Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filter by cluster uuid in subnet datasource #323

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
500cc1b
Add filter by cluster uuid in subnet datasource
shreevari Jan 12, 2022
2ae79a2
Fix lint errors
shreevari Jan 13, 2022
32c324e
Add filter by cluster uuid to documentation
shreevari Jan 13, 2022
165c563
add client side filtering
shreevari Feb 2, 2022
f6d6262
Rename ExtraFilter=>AdditionalFilter and move out base search paths
shreevari Feb 3, 2022
0a863e8
Remove specific filter from subnet datasource
shreevari Feb 3, 2022
8753d04
Remove unnecessary documentation changes
shreevari Feb 3, 2022
e9724c3
Change extra to additional
shreevari Feb 3, 2022
248cfec
Handle case where base search paths are not provided
shreevari Feb 3, 2022
612d6a1
Add API filter to filter by name
shreevari Feb 3, 2022
0123168
Format and lint changes
shreevari Feb 3, 2022
d63f12f
Move filtering to its own function for better testability
shreevari Feb 3, 2022
57cd1b1
Ignore interfacer lint since suggested changes fail to compile
shreevari Feb 3, 2022
f526d78
Add tests for filters
shreevari Feb 3, 2022
285d6e0
Lint code
shreevari Feb 3, 2022
9aaf37b
Handle errors during filter
shreevari Feb 7, 2022
e09eeaf
Use jsonpath instead of own implementation
shreevari Feb 7, 2022
f9e3e6f
Modify test
shreevari Feb 7, 2022
85c23c3
Rename additional filters to client side filters
shreevari Feb 7, 2022
8adfe04
Add example for subnet client side filters
shreevari Feb 7, 2022
77490fe
Add documentation for additional filters
shreevari Feb 7, 2022
43aecfe
Fix lint errors
shreevari Feb 7, 2022
23e0d2b
Merge remote-tracking branch 'origin/master' into bugfix/fix-308-subn…
shreevari Feb 9, 2022
24e7d82
Replace filter attributes to match terraform schema names
shreevari Feb 9, 2022
cc935a6
Fix lint error
shreevari Feb 9, 2022
209baa2
Add cluster_uuid mapping to base selector in subnet datasource filter
shreevari Feb 9, 2022
bfb614a
Remove unnecessary newlines
shreevari Feb 10, 2022
8e41d4b
Add an acceptance test for subnet datasource filters
shreevari Feb 10, 2022
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
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ issues:
linters:
- testpackage
# part of the golangci govet package is picking up things that go vet doesn't. Seems flaky, shutting that specific error off
- path: client/client.go
linters:
- interfacer
# interfacer lint on `filter` func suggests lint changes that dont compile
# Details: linter suggests to convert `body` param to io.Reader since we don't use Close method, but return type requires
## io.ReadCloser thus failing to compile

run:
# which dirs to skip: they won't be analyzed;
Expand Down
139 changes: 139 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"log"
"net/http"
"net/url"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/helper/logging"
)
Expand Down Expand Up @@ -61,6 +62,12 @@ type Credentials struct {
ProxyURL string
}

// AdditionalFilter specification for client side filters
type AdditionalFilter struct {
Name string
Values []string
}

// NewClient returns a new Nutanix API client.
func NewClient(credentials *Credentials, userAgent string, absolutePath string) (*Client, error) {
if userAgent == "" {
Expand Down Expand Up @@ -239,6 +246,138 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) error
return err
}

func searchSlice(slice []string, key string) bool {
for _, v := range slice {
if v == key {
return true
}
}
return false
}

// DoWithFilters performs request passed and filters entities in json response
func (c *Client) DoWithFilters(ctx context.Context, req *http.Request, v interface{}, filters []*AdditionalFilter, baseSearchPaths [][]string) error {
req = req.WithContext(ctx)
resp, err := c.client.Do(req)

if err != nil {
return err
}

defer func() {
yannickstruyf3 marked this conversation as resolved.
Show resolved Hide resolved
if rerr := resp.Body.Close(); err == nil {
err = rerr
}
}()

err = CheckResponse(resp)

if err != nil {
return err
}

resp.Body = filter(resp.Body, filters, baseSearchPaths)

if v != nil {
if w, ok := v.(io.Writer); ok {
_, err = io.Copy(w, resp.Body)
if err != nil {
fmt.Printf("Error io.Copy %s", err)

shreevari marked this conversation as resolved.
Show resolved Hide resolved
return err
}
} else {
err = json.NewDecoder(resp.Body).Decode(v)
if err != nil {
return fmt.Errorf("error unmarshalling json: %s", err)
}
}
}

if c.onRequestCompleted != nil {
c.onRequestCompleted(req, resp, v)
}

return err
}

func filter(body io.ReadCloser, filters []*AdditionalFilter, baseSearchPaths [][]string) io.ReadCloser {
if filters == nil {
return body
}
var res map[string]interface{}
b, _ := io.ReadAll(body)
shreevari marked this conversation as resolved.
Show resolved Hide resolved
json.Unmarshal(b, &res)

// Full search paths
searchPaths := map[string][][]string{}

filterMap := map[string]*AdditionalFilter{}
for _, filter := range filters {
filterMap[filter.Name] = filter

// Build search paths by appending target search paths to base paths
filterSearchPaths := [][]string{}
if len(baseSearchPaths) == 0 {
searchPath := strings.Split(filter.Name, ".")
filterSearchPaths = append(filterSearchPaths, searchPath)
}
for _, baseSearchPath := range baseSearchPaths {
searchPath := append(baseSearchPath, strings.Split(filter.Name, ".")...)
filterSearchPaths = append(filterSearchPaths, searchPath)
}
searchPaths[filter.Name] = filterSearchPaths
}

// Entities that pass filters
var filteredEntities []interface{}

entities := res["entities"].([]interface{})
shreevari marked this conversation as resolved.
Show resolved Hide resolved
for _, entity := range entities {
shreevari marked this conversation as resolved.
Show resolved Hide resolved
filtersPassed := 0
filter_loop:
for filter, filterSearchPaths := range searchPaths {
for _, searchPath := range filterSearchPaths {
// Start searching from the entity root
searchTarget := entity.(map[string]interface{})

shreevari marked this conversation as resolved.
Show resolved Hide resolved
pathOk := false

// Traverse till leaf
for _, pathElem := range searchPath[:len(searchPath)-1] {
if searchTarget, pathOk = searchTarget[pathElem].(map[string]interface{}); !pathOk {
break
}
}
if !pathOk {
continue // Could not find the filter key in this search path
}

// Stringify leaf value since we support only string values in filter
value := fmt.Sprint(searchTarget[searchPath[len(searchPath)-1]])
if searchSlice(filterMap[filter].Values, value) {
filtersPassed++
continue filter_loop
}
}
}

// Value must pass all filters since we perform logical AND b/w filters
if filtersPassed == len(filters) {
filteredEntities = append(filteredEntities, entity)
}
}
res["entities"] = filteredEntities

// Convert filtered result back to io.ReadCloser
filteredBody, jsonErr := json.Marshal(res)
if jsonErr == nil {
return io.NopCloser(bytes.NewReader(filteredBody))
}

return body
}

// CheckResponse checks errors if exist errors in request
func CheckResponse(r *http.Response) error {
c := r.StatusCode
Expand Down
61 changes: 61 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package client
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"regexp"
"strings"
"testing"
)
Expand Down Expand Up @@ -241,6 +243,65 @@ func TestDo_redirectLoop(t *testing.T) {
// }
// }

// *********** Filters tests ***********

func getEntity(name string, vlanID string, uuid string) string {
return fmt.Sprintf(`{"spec":{"cluster_reference":{"uuid":"%s"},"name":"%s","resources":{"vlan_id":%s}}}`, uuid, name, vlanID)
}

func removeWhiteSpace(input string) string {
whitespacePattern := regexp.MustCompile(`\s+`)
return whitespacePattern.ReplaceAllString(input, "")
}

func getFilter(name string, values []string) []*AdditionalFilter {
return []*AdditionalFilter{
{
Name: name,
Values: values,
},
}
}

func runTest(filters []*AdditionalFilter, inputString string, expected string) bool {
input := io.NopCloser(strings.NewReader(inputString))
fmt.Println(expected)
baseSearchPaths := [][]string{
{"spec"},
{"spec", "resources"},
}
actualBytes, _ := io.ReadAll(filter(input, filters, baseSearchPaths))
actual := string(actualBytes)
fmt.Println(actual)
return actual == expected
}

func TestDoWithFilters_filter(t *testing.T) {
entity1 := getEntity("subnet-01", "111", "012345-111")
entity2 := getEntity("subnet-01", "112", "012345-112")
entity3 := getEntity("subnet-02", "112", "012345-111")
input := fmt.Sprintf(`{"entities":[%s,%s,%s]}`, entity1, entity2, entity3)

filtersList := [][]*AdditionalFilter{
getFilter("name", []string{"subnet-01", "subnet-03"}),
getFilter("vlan_id", []string{"111", "subnet-03"}),
getFilter("cluster_reference.uuid", []string{"111", "012345-112"}),
}
expectedList := []string{
removeWhiteSpace(fmt.Sprintf(`{"entities":[%s,%s]}`, entity1, entity2)),
removeWhiteSpace(fmt.Sprintf(`{"entities":[%s]}`, entity1)),
removeWhiteSpace(fmt.Sprintf(`{"entities":[%s]}`, entity2)),
}

for i := 0; i < len(filtersList); i++ {
if ok := runTest(filtersList[i], input, expectedList[i]); !ok {
t.Fatal()
}
}
}

// *************************************

func TestClient_NewRequest(t *testing.T) {
type fields struct {
Credentials *Credentials
Expand Down
17 changes: 11 additions & 6 deletions client/v3/v3_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type Service interface {
DeleteVolumeGroup(uuid string) error
CreateVolumeGroup(request *VolumeGroupInput) (*VolumeGroupResponse, error)
ListAllVM(filter string) (*VMListIntentResponse, error)
ListAllSubnet(filter string) (*SubnetListIntentResponse, error)
ListAllSubnet(filter string, additionalFilters []*client.AdditionalFilter) (*SubnetListIntentResponse, error)
shreevari marked this conversation as resolved.
Show resolved Hide resolved
ListAllNetworkSecurityRule(filter string) (*NetworkSecurityRuleListIntentResponse, error)
ListAllImage(filter string) (*ImageListIntentResponse, error)
ListAllCluster(filter string) (*ClusterListIntentResponse, error)
Expand Down Expand Up @@ -293,8 +293,12 @@ func (op Operations) ListSubnet(getEntitiesRequest *DSMetadata) (*SubnetListInte
if err != nil {
return nil, err
}
baseSearchPaths := [][]string{
{"spec"},
{"spec", "resources"},
}

return subnetListIntentResponse, op.client.Do(ctx, req, subnetListIntentResponse)
return subnetListIntentResponse, op.client.DoWithFilters(ctx, req, subnetListIntentResponse, getEntitiesRequest.AdditionalFilters, baseSearchPaths)
}

/*UpdateSubnet Updates a subnet
Expand Down Expand Up @@ -938,13 +942,14 @@ func (op Operations) ListAllVM(filter string) (*VMListIntentResponse, error) {
}

// ListAllSubnet ...
func (op Operations) ListAllSubnet(filter string) (*SubnetListIntentResponse, error) {
func (op Operations) ListAllSubnet(filter string, additionalFilters []*client.AdditionalFilter) (*SubnetListIntentResponse, error) {
entities := make([]*SubnetIntentResponse, 0)

resp, err := op.ListSubnet(&DSMetadata{
Filter: &filter,
Kind: utils.StringPtr("subnet"),
Length: utils.Int64Ptr(itemsPerPage),
Filter: &filter,
Kind: utils.StringPtr("subnet"),
Length: utils.Int64Ptr(itemsPerPage),
AdditionalFilters: additionalFilters,
})

if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions client/v3/v3_structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package v3

import (
"time"

"github.com/terraform-providers/terraform-provider-nutanix/client"
)

// Reference ...
Expand Down Expand Up @@ -583,6 +585,9 @@ type DSMetadata struct {

// The sort order in which results are returned
SortOrder *string `json:"sort_order,omitempty" mapstructure:"sort_order,omitempty"`

// Additional filters for client side filtering api response
AdditionalFilters []*client.AdditionalFilter `json:"-"`
}

// VMIntentResource Response object for intentful operations on a vm
Expand Down
14 changes: 10 additions & 4 deletions nutanix/data_source_nutanix_subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/terraform-providers/terraform-provider-nutanix/client"
v3 "github.com/terraform-providers/terraform-provider-nutanix/client/v3"
"github.com/terraform-providers/terraform-provider-nutanix/utils"
)
Expand All @@ -31,6 +32,7 @@ func dataSourceNutanixSubnet() *schema.Resource {
Optional: true,
ConflictsWith: []string{"subnet_id"},
},
"additional_filter": DataSourceFiltersSchema(),
"api_version": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -283,9 +285,9 @@ func findSubnetByUUID(conn *v3.Client, uuid string) (*v3.SubnetIntentResponse, e
return conn.V3.GetSubnet(uuid)
}

func findSubnetByName(conn *v3.Client, name string) (*v3.SubnetIntentResponse, error) {
func findSubnetByName(conn *v3.Client, name string, additionalFilters []*client.AdditionalFilter) (*v3.SubnetIntentResponse, error) {
filter := fmt.Sprintf("name==%s", name)
resp, err := conn.V3.ListAllSubnet(filter)
resp, err := conn.V3.ListAllSubnet(filter, additionalFilters)
if err != nil {
return nil, err
}
Expand All @@ -300,7 +302,7 @@ func findSubnetByName(conn *v3.Client, name string) (*v3.SubnetIntentResponse, e
}

if len(found) > 1 {
return nil, fmt.Errorf("your query returned more than one result. Please use subnet_id argument instead")
return nil, fmt.Errorf("your query returned more than one result. Please use subnet_id argument or use additional filters instead")
}

if len(found) == 0 {
Expand All @@ -316,6 +318,10 @@ func dataSourceNutanixSubnetRead(d *schema.ResourceData, meta interface{}) error

subnetID, iok := d.GetOk("subnet_id")
subnetName, nok := d.GetOk("subnet_name")
var additionalFilters []*client.AdditionalFilter
if v, ok := d.GetOk("additional_filter"); ok {
additionalFilters = BuildFiltersDataSource(v.(*schema.Set))
}

if !iok && !nok {
return fmt.Errorf("please provide one of subnet_id or subnet_name attributes")
Expand All @@ -327,7 +333,7 @@ func dataSourceNutanixSubnetRead(d *schema.ResourceData, meta interface{}) error
if iok {
resp, reqErr = findSubnetByUUID(conn, subnetID.(string))
} else {
resp, reqErr = findSubnetByName(conn, subnetName.(string))
resp, reqErr = findSubnetByName(conn, subnetName.(string), additionalFilters)
}

if reqErr != nil {
Expand Down
2 changes: 1 addition & 1 deletion nutanix/data_source_nutanix_subnets.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ func dataSourceNutanixSubnetsRead(d *schema.ResourceData, meta interface{}) erro
req = buildDataSourceListMetadata(metadata.(*schema.Set))
}

resp, err := conn.V3.ListAllSubnet(utils.StringValue(req.Filter))
resp, err := conn.V3.ListAllSubnet(utils.StringValue(req.Filter), nil)
if err != nil {
return err
}
Expand Down
Loading