Skip to content

Commit

Permalink
Search Implementation and Query Type
Browse files Browse the repository at this point in the history
* adds functionality to http.go for encoding querystrings
* Consistency fis for JSONClass field name across the board (JsonClass)
* new search.go and examples implementing standard search
  • Loading branch information
spheromak committed Oct 10, 2014
1 parent 1041e6a commit 2d38462
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 7 deletions.
27 changes: 27 additions & 0 deletions examples/search/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAx12nDxxOwSPHRSJEDz67a0folBqElzlu2oGMiUTS+dqtj3FU
h5lJc1MjcprRVxcDVwhsSSo9948XEkk39IdblUCLohucqNMzOnIcdZn8zblN7Cnp
W03UwRM0iWX1HuwHnGvm6PKeqKGqplyIXYO0qlDWCzC+VaxFTwOUk31MfOHJQn4y
fTrfuE7h3FTElLBu065SFp3dPICIEmWCl9DadnxbnZ8ASxYQ9xG7hmZduDgjNW5l
3x6/EFkpym+//D6AbWDcVJ1ovCsJL3CfH/NZC3ekeJ/aEeLxP/vaCSH1VYC5VsYK
5Qg7SIa6Nth3+RZz1hYOoBJulEzwljznwoZYRQIDAQABAoIBADPQol+qAsnty5er
PTcdHcbXLJp5feZz1dzSeL0gdxja/erfEJIhg9aGUBs0I55X69VN6h7l7K8PsHZf
MzzJhUL4QJJETOYP5iuVhtIF0I+DTr5Hck/5nYcEv83KAvgjbiL4ZE486IF5awnL
2OE9HtJ5KfhEleNcX7MWgiIHGb8G1jCqu/tH0GI8Z4cNgUrXMbczGwfbN/5Wc0zo
Dtpe0Tec/Fd0DLFwRiAuheakPjlVWb7AGMDX4TyzCXfMpS1ul2jk6nGFk77uQozF
PQUawCRp+mVS4qecgq/WqfTZZbBlW2L18/kpafvsxG8kJ7OREtrb0SloZNFHEc2Q
70GbgKECgYEA6c/eOrI3Uour1gKezEBFmFKFH6YS/NZNpcSG5PcoqF6AVJwXg574
Qy6RatC47e92be2TT1Oyplntj4vkZ3REv81yfz/tuXmtG0AylH7REbxubxAgYmUT
18wUAL4s3TST2AlK4R29KwBadwUAJeOLNW+Rc4xht1galsqQRb4pUzkCgYEA2kj2
vUhKAB7QFCPST45/5q+AATut8WeHnI+t1UaiZoK41Jre8TwlYqUgcJ16Q0H6KIbJ
jlEZAu0IsJxjQxkD4oJgv8n5PFXdc14HcSQ512FmgCGNwtDY/AT7SQP3kOj0Rydg
N02uuRb/55NJ07Bh+yTQNGA+M5SSnUyaRPIAMW0CgYBgVU7grDDzB60C/g1jZk/G
VKmYwposJjfTxsc1a0gLJvSE59MgXc04EOXFNr4a+oC3Bh2dn4SJ2Z9xd1fh8Bur
UwCLwVE3DBTwl2C/ogiN4C83/1L4d2DXlrPfInvloBYR+rIpUlFweDLNuve2pKvk
llU9YGeaXOiHnGoY8iKgsQKBgQDZKMOHtZYhHoZlsul0ylCGAEz5bRT0V8n7QJlw
12+TSjN1F4n6Npr+00Y9ov1SUh38GXQFiLq4RXZitYKu6wEJZCm6Q8YXd1jzgDUp
IyAEHNsrV7Y/fSSRPKd9kVvGp2r2Kr825aqQasg16zsERbKEdrBHmwPmrsVZhi7n
rlXw1QKBgQDBOyUJKQOgDE2u9EHybhCIbfowyIE22qn9a3WjQgfxFJ+aAL9Bg124
fJIEzz43fJ91fe5lTOgyMF5TtU5ClAOPGtlWnXU0e5j3L4LjbcqzEbeyxvP3sn1z
dYkX7NdNQ5E6tcJZuJCGq0HxIAQeKPf3x9DRKzMnLply6BEzyuAC4g==
-----END RSA PRIVATE KEY-----
76 changes: 76 additions & 0 deletions examples/search/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"

"github.com/davecgh/go-spew/spew"
"github.com/go-chef/chef"
)

func main() {
// read a client key
key, err := ioutil.ReadFile("key.pem")
if err != nil {
fmt.Println("Couldn't read key.pem:", err)
os.Exit(1)
}

// build a client
client, err := chef.NewClient(&chef.Config{
Name: "foo",
Key: string(key),
// goiardi is on port 4545 by default. chef-zero is 8889
BaseURL: "http://localhost:4545",
})
if err != nil {
fmt.Println("Issue setting up client:", err)
os.Exit(1)
}

// List Indexes
indexes, err := client.Search.Indexes()
if err != nil {
log.Fatal("Couldn't list nodes: ", err)
}

// dump the Index list in Json
jsonData, err := json.MarshalIndent(indexes, "", "\t")
os.Stdout.Write(jsonData)
os.Stdout.WriteString("\n")

// build a seach query
query, err := client.Search.NewQuery("node", "name:*")
if err != nil {
log.Fatal("Error building query ", err)
}

// Run the query
res, err := query.Do(client)
if err != nil {
log.Fatal("Error running query ", err)
}

// <3 spew
spew.Dump(res)

// dump out results back in json for fun
jsonData, err = json.MarshalIndent(res, "", "\t")
os.Stdout.Write(jsonData)
os.Stdout.WriteString("\n")

// You can also use the service to run a query
res, err = client.Search.Exec("node", "name:*")
if err != nil {
log.Fatal("Error running Search.Exec() ", err)
}

// dump out results back in json for fun
jsonData, err = json.MarshalIndent(res, "", "\t")
os.Stdout.Write(jsonData)
os.Stdout.WriteString("\n")

}
9 changes: 8 additions & 1 deletion http.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type Client struct {
Nodes *NodeService
Roles *RoleService
Sandboxes *SandboxService
Search *SearchService
}

// Config contains the configuration options for a chef client. This is Used primarily in the NewClient() constructor in order to setup a proper client object
Expand Down Expand Up @@ -136,6 +137,7 @@ func NewClient(cfg *Config) (*Client, error) {
c.Nodes = &NodeService{client: c}
c.Roles = &RoleService{client: c}
c.Sandboxes = &SandboxService{client: c}
c.Search = &SearchService{client: c}
return c, nil
}

Expand All @@ -155,7 +157,7 @@ func (c *Client) magicRequestDecoder(method, path string, body io.Reader, v inte
return err
}

// NewRequest performs a signed request for the chef client
// NewRequest returns a signed request suitable for the chef server
func (c *Client) NewRequest(method string, requestUrl string, body io.Reader) (*http.Request, error) {
relativeUrl, err := url.Parse(requestUrl)
if err != nil {
Expand All @@ -169,6 +171,11 @@ func (c *Client) NewRequest(method string, requestUrl string, body io.Reader) (*
return nil, err
}

// parse and encode Querystring Values
values := req.URL.Query()
req.URL.RawQuery = values.Encode()
debug("Encoded url %+v", u)

myBody := &Body{body}

if body != nil {
Expand Down
2 changes: 1 addition & 1 deletion role.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Role struct {
RunList RunList `json:"run_list"`
DefaultAttributes interface{} `json:"default_attributes,omitempty"`
OverrideAttributes interface{} `json:"override_attributes,omitempty"`
JSONClass string `json:"json_class,omitempty"`
JsonClass string `json:"json_class,omitempty"`
}

// String makes RoleListResult implement the string result
Expand Down
10 changes: 5 additions & 5 deletions role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package chef
import (
"encoding/json"
"fmt"
. "github.com/smartystreets/goconvey/convey"
"io"
"log"
"net/http"
"os"
"reflect"
"testing"
. "github.com/smartystreets/goconvey/convey"
)

var (
Expand All @@ -20,7 +20,7 @@ var (
ChefType: "role",
Description: "Test Role",
RunList: []string{"recipe[foo]", "recipe[baz]", "role[banana]"},
JSONClass: "Chef::Role",
JsonClass: "Chef::Role",
DefaultAttributes: struct{}{},
OverrideAttributes: struct{}{},
}
Expand Down Expand Up @@ -99,7 +99,7 @@ func TestRolesService_Get(t *testing.T) {
want := &Role{
Name: "webserver",
ChefType: "role",
JSONClass: "Chef::Role",
JsonClass: "Chef::Role",
DefaultAttributes: "",
Description: "A webserver",
RunList: []string{"recipe[unicorn]", "recipe[apache2]"},
Expand All @@ -122,7 +122,7 @@ func TestRolesService_Create(t *testing.T) {
role := &Role{
Name: "webserver",
ChefType: "role",
JSONClass: "Chef::Role",
JsonClass: "Chef::Role",
DefaultAttributes: "",
Description: "A webserver",
RunList: []string{"recipe[unicorn]", "recipe[apache2]"},
Expand Down Expand Up @@ -160,7 +160,7 @@ func TestRolesService_Put(t *testing.T) {
role := &Role{
Name: "webserver",
ChefType: "role",
JSONClass: "Chef::Role",
JsonClass: "Chef::Role",
Description: "A webserver",
RunList: []string{"recipe[apache2]"},
}
Expand Down
98 changes: 98 additions & 0 deletions search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package chef

import (
"errors"
"fmt"
"strings"
)

type SearchService struct {
client *Client
}

// SearchQuery Is the struct for holding a query request
type SearchQuery struct {
// The index you want to search
Index string

// The query you want to execute. This is the 'chef' query ex: 'chef_environment:prod'
Query string

// Sort order you want the search results returned
SortBy string

// Starting position for search
Start int

// Number of rows to return
Rows int
}

// String implements the Stringer Interface for the SearchQuery
func (q SearchQuery) String() string {
return fmt.Sprintf("%s?q=%s&rows=%d&sort=%s&start=%d", q.Index, q.Query, q.Rows, q.SortBy, q.Start)
}

// search result will return a slice of interface{} of chef-like objects (roles/nodes/etc)
type SearchResult struct {
Total int
Start int
Rows []interface{}
}

// Do will execute the search query on the client
func (q SearchQuery) Do(client *Client) (res SearchResult, err error) {
fullUrl := fmt.Sprintf("/search/%s", q)
err = client.magicRequestDecoder("GET", fullUrl, nil, &res)
return
}

// NewSearch is a constructor for a SearchQuery struct. This is used by other search service methods to perform search requests on the server
func (e SearchService) NewQuery(idx, statement string) (query SearchQuery, err error) {
// validate statement
if !strings.Contains(statement, ":") {
err = errors.New("statement is malformed")
return
}

query = SearchQuery{
Index: idx,
Query: statement,
// These are the defaults in chef: https://github.com/opscode/chef/blob/master/lib/chef/search/query.rb#L102-L105
SortBy: "X_CHEF_id_CHEF_X asc",
Start: 0,
Rows: 1000,
}

return
}

// Exec runs the query on the index passed in. This is a helper method. If you wnat more controll over the query use NewQuery and its Do() method.
// BUG(spheromak): Should we use exec or SearchQuery.Do() or have both ?
func (e SearchService) Exec(idx, statement string) (res SearchResult, err error) {
// Copy-paste here till We decide which way to go with exec vs Do
if !strings.Contains(statement, ":") {
err = errors.New("statement is malformed")
return
}

query := SearchQuery{
Index: idx,
Query: statement,
// These are the defaults in chef: https://github.com/opscode/chef/blob/master/lib/chef/search/query.rb#L102-L105
SortBy: "X_CHEF_id_CHEF_X asc",
Start: 0,
Rows: 1000,
}

res, err = query.Do(e.client)
return
}

// List lists the nodes in the Chef server.
//
// Chef API docs: http://docs.opscode.com/api_chef_server.html#id25
func (e SearchService) Indexes() (data map[string]string, err error) {
err = e.client.magicRequestDecoder("GET", "search", nil, &data)
return
}
83 changes: 83 additions & 0 deletions search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package chef

import (
"fmt"
"net/http"
"reflect"
"testing"
)

func TestSearch_Get(t *testing.T) {
setup()
defer teardown()

mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{
"node": "http://localhost:4000/search/node",
"role": "http://localhost:4000/search/role",
"client": "http://localhost:4000/search/client",
"users": "http://localhost:4000/search/users"
}`)
})

indexes, err := client.Search.Indexes()
if err != nil {
t.Errorf("Search.Get returned error: %+v", err)
}
wantedIdx := map[string]string{
"node": "http://localhost:4000/search/node",
"role": "http://localhost:4000/search/role",
"client": "http://localhost:4000/search/client",
"users": "http://localhost:4000/search/users",
}
if !reflect.DeepEqual(indexes, wantedIdx) {
t.Errorf("Search.Get returned %+v, want %+v", indexes, wantedIdx)
}
}

func TestSearch_ExecDo(t *testing.T) {
setup()
defer teardown()

mux.HandleFunc("/search/nodes", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{
"total": 1,
"start": 0,
"rows": [
{
"overrides": {"hardware_type": "laptop"},
"name": "latte",
"chef_type": "node",
"json_class": "Chef::Node",
"attributes": {"hardware_type": "laptop"},
"run_list": ["recipe[unicorn]"],
"defaults": {}
}
]
}`)
})

// test the fail case
_, err := client.Search.NewQuery("foo", "failsauce")
if err == nil {
t.Errorf("Bad query wasn't caught")
}

// test the positive case
query, err := client.Search.NewQuery("nodes", "name:latte")
if err != nil {
t.Errorf("failed to create query")
}

// for now we aren't testing the result..
_, err = query.Do(client)
if err != nil {
t.Errorf("Search.Exec failed", err)
}

_, err = client.Search.Exec("nodes", "name:latte")
if err != nil {
t.Errorf("Search.Exec failed", err)
}

}

0 comments on commit 2d38462

Please sign in to comment.