diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..4e156ec --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,52 @@ +# Golang CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-go/ for more details +version: 2 +workflows: + version: 2 + merge-to-master: + jobs: + - tester + - update_tag: + requires: + - tester + filters: + branches: + only: /master/ +jobs: + update_tag: + docker: + - image: circleci/golang:1.13.1 + working_directory: /go/src/github.com/go-chef/chefi + steps: + - add_ssh_keys: + fingerprints: + - ${TAG_TOKEN} + - checkout + - run: curl -s https://api.github.com/repos/pantheon-systems/autotag/releases/latest | grep browser_download | grep Linux | cut -d '"' -f 4 | xargs curl -o ./autotag -L && chmod 755 ./autotag + - run: ./autotag + - run: git push --tags origin + tester: + docker: + - image: circleci/golang:1.9 + #### TEMPLATE_NOTE: go expects specific checkout path representing url + #### expecting it in the form of + #### /go/src/github.com/circleci/go-tool + #### /go/src/bitbucket.org/circleci/go-tool + working_directory: /go/src/github.com/go-chef/chef + steps: + - add_ssh_keys: + fingerprints: + - 35:v45:41:ba:cf:3f:7f:d5:00:0f:11:6b:4d:c0:a1:90 + - checkout + # specify any bash command here prefixed with `run: ` + - run: pwd + - run: env + - run: ls -Rl + - run: go get + - run: go get github.com/ctdk/goiardi/chefcrypto + - run: go get github.com/r3labs/diff + - run: go get github.com/smartystreets/goconvey/convey + - run: go get github.com/stretchr/testify/assert + - run: go vet + - run: go test diff --git a/.gitignore b/.gitignore index 392a11e..43efa59 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ _testmain.go # vim *.swp + +# cookbook +*.kitchen diff --git a/README.md b/README.md index fc42833..1882bcb 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,14 @@ This is a Library that you can use to write tools to interact with the chef serv go get -t github.com/go-chef/chef go test -v github.com/go-chef/chef + examples::chefapi_tester kitchen verify ## SSL If you run into an SSL verification problem when trying to connect to a ssl server with self signed certs setup your config object with `SkipSSL: true` ## Usage -This example is setting up a basic client that you can use to interact with all the service endpoints (clients, nodes, cookbooks, etc.) +This example is setting up a basic client that you can use to interact with all the service endpoints (clients, nodes, cookbooks, etc. At [@chefapi](https://docs.chef.io/api_chef_server.html)) More usage examples can be found in the [examples](examples) directory. ```go @@ -79,6 +80,7 @@ improved, but is still an ongoing concern. |AJ Christensen |[@fujin](https://github.com/fujin) |Brad Beam |[@bradbeam](https://github.com/bradbeam) |Kraig Amador |[@bigkraig](https://github.com/bigkraig) +|Mark Gibbons |[@mark](https://github.com/markgibbons) ## COPYRIGHT diff --git a/association.go b/association.go new file mode 100644 index 0000000..996b255 --- /dev/null +++ b/association.go @@ -0,0 +1,155 @@ +package chef + +// import "fmt" +import "errors" + +type AssociationService struct { + client *Client +} + +// Chef API docs: https://docs.chef.io/api_chef_server.html#association-requests +// https://github.com/chef/chef-server/blob/master/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_org_invites.erl Invitation implementation +// https://github.com/chef/chef-server/blob/master/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_org_associations.erl user org associations + +// Association represents the response from creating an invitation to join an organization +// POST /organization/NAME/association_requests +type Association struct { + Uri string `json:"uri"` // the last part of the uri is the invitation id + OrganizationUser struct { + UserName string `json:"username,omitempty"` + } `json:"organization_user"` + Organization struct { + Name string `json:"name,omitempty"` + } `json:"organization"` + User struct { + Email string `json:"email,omitempty"` + FirstName string `json:"first_name,omitempty"` + } `json:"user"` +} + +// RescindInvite respresents the response from deleting an invitation +// DELETE /organization/NAME/association_requests/ID +type RescindInvite struct { + Id string `json:"id,omitempty"` + Orgname string `json:"orgname,omitempty"` + Username string `json:"username,omitempty"` +} + +// Invite represents an entry in the array of responses listing the outstanding invitations +// GET /organization/NAME/association_requests +type Invite struct { + Id string `json:"id,omitempty"` + UserName string `json:"username,omitempty"` +} + +// Request represents the body of the request to invite a user to an organization +// POST /organization/NAME/association_requests +type Request struct { + User string `json:"user"` +} + +// AddNow represents the body of the request to add a user to an organization +// POST /organization/NAME/users +type AddNow struct { + Username string `json:"username"` +} + +// Invite represents an entry in the array of responses listing the users in an organization +// GET /organization/NAME/association_requests +type OrgUserListEntry struct { + User struct { + Username string `json:"username,omitempty"` + } `json:"user,omitempty"` +} + +// OrgUser represents the detailed information about a user in an organization +// GET /organization/NAME/user/NAME +// DELETE /organization/NAME/user/NAME +type OrgUser struct { + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + DisplayName string `json:"display_name,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + PublicKey string `json:"public_key,omitempty"` +} + +// ListInvites gets a list of the pending invitations for an organization. +func (e *AssociationService) ListInvites() (invitelist []Invite, err error) { + err = e.client.magicRequestDecoder("GET", "association_requests", nil, &invitelist) + return +} + +// Invite creates an invitation for a user to join an organization on the chef server +func (e *AssociationService) Invite(invite Request) (data Association, err error) { + body, err := JSONReader(invite) + if err != nil { + return + } + err = e.client.magicRequestDecoder("POST", "association_requests/", body, &data) + return +} + +// DeleteInvite removes a pending invitation to an organization +func (e *AssociationService) DeleteInvite(id string) (rescind RescindInvite, err error) { + err = e.client.magicRequestDecoder("DELETE", "association_requests/"+id, nil, &rescind) + return +} + +// InviteID Finds an invitation id for a user +func (e *AssociationService) InviteId(user string) (id string, err error) { + var invitelist []Invite + err = e.client.magicRequestDecoder("GET", "association_requests", nil, &invitelist) + if err != nil { + return + } + // Find an invite for the user or return err + for _, in := range invitelist { + if in.UserName == user { + id = in.Id + } + } + if id == "" { + err = errors.New("User request not found") + } + return +} + +// AcceptInvite Accepts an invitation +// TODO: Gets a 405, code is in knife is it part of erchef? +func (e *AssociationService) AcceptInvite(id string) (data string, err error) { + body, err := JSONReader("{ \"accept\" }") + if err != nil { + return + } + err = e.client.magicRequestDecoder("PUT", "association_requests/"+id, body, &data) + return +} + +// List gets a list of the users in an organization +func (e *AssociationService) List() (data []OrgUserListEntry, err error) { + err = e.client.magicRequestDecoder("GET", "users", nil, &data) + return +} + +// Add a user immediately +func (e *AssociationService) Add(addme AddNow) (err error) { + body, err := JSONReader(addme) + if err != nil { + return + } + err = e.client.magicRequestDecoder("POST", "users", body, nil) + return +} + +// Get the details of a user in an organization +func (e *AssociationService) Get(name string) (data OrgUser, err error) { + err = e.client.magicRequestDecoder("GET", "users/"+name, nil, &data) + return +} + +// Delete removes a user from an organization +func (e *AssociationService) Delete(name string) (data OrgUser, err error) { + err = e.client.magicRequestDecoder("DELETE", "users/"+name, nil, &data) + return +} diff --git a/association_test.go b/association_test.go new file mode 100644 index 0000000..8a82dd4 --- /dev/null +++ b/association_test.go @@ -0,0 +1,166 @@ +package chef + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestAssociationMethods(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/association_requests", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET": + fmt.Fprintf(w, `[{"id":"1f", "username":"jollystranger"}, {"id":"2b", "username":"fredhamlet"}]`) + case r.Method == "POST": + fmt.Fprintf(w, `{ "uri": "http://chef/organizations/test/association_requests/1a" }`) + } + }) + + mux.HandleFunc("/association_requests/1f", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET": + fmt.Fprintf(w, `[ + { "user": {"username": "jollystranger"} + ]`) + case r.Method == "DELETE": + fmt.Fprintf(w, `{ + "id": "1f", + "orgname": "test", + "username": "jollystranger" + }`) + } + }) + + // test Invite - Invite a user + request := Request{ + User: "jollystranger", + } + invite, err := client.Associations.Invite(request) + if err != nil { + t.Errorf("Associations.Invite returned error: %v", err) + } + associationWant := Association{ + Uri: "http://chef/organizations/test/association_requests/1a", + } + if !reflect.DeepEqual(invite, associationWant) { + t.Errorf("Associations.Invite returned %+v, want %+v", invite, associationWant) + } + + // test ListInvites - return the existing invitations + invites, err := client.Associations.ListInvites() + if err != nil { + t.Errorf("Associations.List returned error: %v", err) + } + listWant := []Invite{ + Invite{Id: "1f", UserName: "jollystranger"}, + Invite{Id: "2b", UserName: "fredhamlet"}, + } + if !reflect.DeepEqual(invites, listWant) { + t.Errorf("Associations.InviteList returned %+v, want %+v", invites, listWant) + } + + // test DeleteInvite + deleted, err := client.Associations.DeleteInvite("1f") + if err != nil { + t.Errorf("Associations.Get returned error: %v", err) + } + wantInvite := RescindInvite{ + Id: "1f", + Orgname: "test", + Username: "jollystranger", + } + if !reflect.DeepEqual(deleted, wantInvite) { + t.Errorf("Associations.RescindInvite returned %+v, want %+v", deleted, wantInvite) + } + + // test InviteId + id, err := client.Associations.InviteId("fredhamlet") + if err != nil { + t.Errorf("Associations.InviteId returned error: %s", err.Error()) + } + wantId := "2b" + if !reflect.DeepEqual(id, wantId) { + t.Errorf("Associations.InviteId returned %+v, want %+v", id, wantId) + } +} + +func TestOrgUserMethods(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET": + // []OrgUserListEntry + fmt.Fprintf(w, `[{ "user": {"username": "jollystranger"}}]`) + case r.Method == "POST": + // err only + } + }) + + mux.HandleFunc("/users/jollystranger", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET": + // OrgUser + fmt.Fprintf(w, `{ "username": "jollystranger", "email": "jolly.stranger@domain.io", "display_name":"jolly" }`) + case r.Method == "DELETE": + // OrgUser + fmt.Fprintf(w, `{ "username": "jollystranger", "email": "jolly.stranger@domain.io", "display_name":"jolly" }`) + } + }) + + // test List the users + users, err := client.Associations.List() + if err != nil { + t.Errorf("Associations.List returned error: %v", err) + } + var a struct { + Username string `json:"username,omitempty"` + } + a.Username = "jollystranger" + var wuser OrgUserListEntry + wuser.User = a + wantUsers := []OrgUserListEntry{ + wuser, + } + if !reflect.DeepEqual(users, wantUsers) { + t.Errorf("Associations.List returned %+v, want %+v", users, wantUsers) + } + + // test Add user + addme := AddNow{ + Username: "jollystranger", + } + err = client.Associations.Add(addme) + if err != nil { + t.Errorf("Associations.Add returned error: %v", err) + } + + // test Get user details + user, err := client.Associations.Get("jollystranger") + if err != nil { + t.Errorf("Associations.Get returned error: %v", err) + } + wantUser := OrgUser{ + Username: "jollystranger", Email: "jolly.stranger@domain.io", DisplayName: "jolly", + } + if !reflect.DeepEqual(user, wantUser) { + t.Errorf("Associations.Get returned %+v, want %+v", user, wantUser) + } + + // test Delete user details + delu, err := client.Associations.Delete("jollystranger") + if err != nil { + t.Errorf("Associations.Delete returned error: %v", err) + } + delUser := OrgUser{ + Username: "jollystranger", Email: "jolly.stranger@domain.io", DisplayName: "jolly", + } + if !reflect.DeepEqual(delu, delUser) { + t.Errorf("Associations.Delete returned %+v, want %+v", delu, delUser) + } +} diff --git a/authenticate.go b/authenticate.go new file mode 100644 index 0000000..48da0f9 --- /dev/null +++ b/authenticate.go @@ -0,0 +1,20 @@ +package chef + +type AuthenticateUserService struct { + client *Client +} + +// Authenticate represents the body of the /authenticate_user request +type Authenticate struct { + UserName string `json:"username"` + Password string `json:"password"` +} + +// Authenticate performs an authentication attempt. +// +// https://docs.chef.io/api_chef_server.html#authenticate-user +func (e *AuthenticateUserService) Authenticate(authenticate_request Authenticate) (err error) { + body, err := JSONReader(authenticate_request) + err = e.client.magicRequestDecoder("POST", "authenticate_user", body, nil) + return +} diff --git a/authenticate_test.go b/authenticate_test.go new file mode 100644 index 0000000..417a005 --- /dev/null +++ b/authenticate_test.go @@ -0,0 +1,62 @@ +package chef + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "io" + "log" + "net/http" + "os" + "testing" +) + +var ( + testAuthenticateJSON = "test/authenticate.json" +) + +func TestAuthenticateFromJSONDecoder(t *testing.T) { + if file, err := os.Open(testAuthenticateJSON); err != nil { + t.Error("Unexpected error '", err, "' during os.Open on", testAuthenticateJSON) + } else { + dec := json.NewDecoder(file) + var g Authenticate + if err := dec.Decode(&g); err == io.EOF { + log.Fatal(g) + } else if err != nil { + log.Fatal(err) + } + } +} + +func TestAuthenticatesCreate(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/authenticate_user", func(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + var request Authenticate + dec.Decode(&request) + switch { + case r.Method == "POST": + if request.Password == "password" { + w.WriteHeader(200) + } else { + w.WriteHeader(401) + } + } + }) + var request Authenticate + request.UserName = "user1" + request.Password = "invalid" + err := client.AuthenticateUser.Authenticate(request) + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "401") + } + + request.UserName = "user1" + request.Password = "password" + err = client.AuthenticateUser.Authenticate(request) + if err != nil { + t.Errorf("Authenticate returned error: %+v", err) + } +} diff --git a/client_test.go b/client_test.go index ad7aff7..7daeeaf 100644 --- a/client_test.go +++ b/client_test.go @@ -40,15 +40,17 @@ func TestClientsService_List(t *testing.T) { mux.HandleFunc("/clients", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"client1": "http://localhost/clients/client1", "client2": "http://localhost/clients/client2"}`) }) - response, err := client.Clients.List() if err != nil { t.Errorf("Clients.List returned error: %v", err) } + // The order printed by the String function is not defined want := "client1 => http://localhost/clients/client1\nclient2 => http://localhost/clients/client2\n" - if response.String() != want { - t.Errorf("Clients.List returned:\n%+v\nwant:\n%+v\n", response.String(), want) + want2 := "client2 => http://localhost/clients/client2\nclient1 => http://localhost/clients/client1\n" + rstr := response.String() + if rstr != want && rstr != want2 { + t.Errorf("Clients.List returned:\n%+v\nwant:\n%+v\n", rstr, want) } } diff --git a/cookbook_download.go b/cookbook_download.go index 77115cb..909a98e 100644 --- a/cookbook_download.go +++ b/cookbook_download.go @@ -62,6 +62,12 @@ func (c *CookbookService) DownloadTo(name, version, localDir string) error { return nil } +// DownloadAt is a deprecated alias for DownloadTo +func (c *CookbookService) DownloadAt(name, version, localDir string) error { + err := c.DownloadTo(name, version, localDir) + return err +} + // downloadCookbookItems downloads all the provided cookbook items into the provided // local path, it also ensures that the provided directory exists by creating it func (c *CookbookService) downloadCookbookItems(items []CookbookItem, itemType, localPath string) error { diff --git a/cookbook_download_test.go b/cookbook_download_test.go index 89c7dc8..cd7c593 100644 --- a/cookbook_download_test.go +++ b/cookbook_download_test.go @@ -74,6 +74,7 @@ func TestCookbooksDownloadEmptyWithVersion(t *testing.T) { func cookbookData() string { return ` + { "version": "0.2.1", "name": "foo-0.2.1", diff --git a/cookbook_test.go b/cookbook_test.go index b70999f..c667549 100644 --- a/cookbook_test.go +++ b/cookbook_test.go @@ -4,7 +4,6 @@ import ( "fmt" "io/ioutil" "net/http" - //"os" "testing" "github.com/stretchr/testify/assert" diff --git a/databag.go b/databag.go index 4ad8e3e..f4d95c7 100644 --- a/databag.go +++ b/databag.go @@ -22,7 +22,7 @@ type DataBagCreateResult struct { } // DataBagListResult is the list of data bags returned by chef-api when listing -// http://docs.getchef.com/api_chef_server.html#data +// https://docs.chef.io/api_chef_server/#data type DataBagListResult map[string]string // String makes DataBagListResult implement the string result @@ -34,7 +34,7 @@ func (d DataBagListResult) String() (out string) { } // List returns a list of databags on the server -// Chef API Docs: http://docs.getchef.com/api_chef_server.html#id18 +// Chef API Docs: https://docs.chef.io/api_chef_server/#get-19 func (d *DataBagService) List() (data *DataBagListResult, err error) { path := fmt.Sprintf("data") err = d.client.magicRequestDecoder("GET", path, nil, &data) @@ -42,7 +42,7 @@ func (d *DataBagService) List() (data *DataBagListResult, err error) { } // Create adds a data bag to the server -// Chef API Docs: http://docs.getchef.com/api_chef_server.html#id19 +// Chef API Docs: https://docs.chef.io/api_chef_server/#post-7 func (d *DataBagService) Create(databag *DataBag) (result *DataBagCreateResult, err error) { body, err := JSONReader(databag) if err != nil { @@ -54,7 +54,7 @@ func (d *DataBagService) Create(databag *DataBag) (result *DataBagCreateResult, } // Delete removes a data bag from the server -// Chef API Docs: ???????????????? +// Chef API Docs: https://docs.chef.io/api_chef_server/#delete-7 func (d *DataBagService) Delete(name string) (result *DataBag, err error) { path := fmt.Sprintf("data/%s", name) err = d.client.magicRequestDecoder("DELETE", path, nil, &result) @@ -62,7 +62,7 @@ func (d *DataBagService) Delete(name string) (result *DataBag, err error) { } // ListItems gets a list of the items in a data bag. -// Chef API Docs: http://docs.getchef.com/api_chef_server.html#id20 +// Chef API Docs: https://docs.chef.io/api_chef_server/#get-20 func (d *DataBagService) ListItems(name string) (data *DataBagListResult, err error) { path := fmt.Sprintf("data/%s", name) err = d.client.magicRequestDecoder("GET", path, nil, &data) @@ -70,7 +70,7 @@ func (d *DataBagService) ListItems(name string) (data *DataBagListResult, err er } // CreateItem adds an item to a data bag -// Chef API Docs: http://docs.getchef.com/api_chef_server.html#id21 +// Chef API Docs: https://docs.chef.io/api_chef_server/#post-8 func (d *DataBagService) CreateItem(databagName string, databagItem DataBagItem) (err error) { body, err := JSONReader(databagItem) if err != nil { @@ -81,24 +81,24 @@ func (d *DataBagService) CreateItem(databagName string, databagItem DataBagItem) } // DeleteItem deletes an item from a data bag -// Chef API Docs: http://docs.getchef.com/api_chef_server.html#id22 -func (d *DataBagService) DeleteItem(databagName, databagItem string) (err error) { +// Chef API Docs: https://docs.chef.io/api_chef_server/#delete-8 +func (d *DataBagService) DeleteItem(databagName string, databagItem string) (err error) { path := fmt.Sprintf("data/%s/%s", databagName, databagItem) err = d.client.magicRequestDecoder("DELETE", path, nil, nil) return } // GetItem gets an item from a data bag -// Chef API Docs: http://docs.getchef.com/api_chef_server.html#id23 -func (d *DataBagService) GetItem(databagName, databagItem string) (item DataBagItem, err error) { +// Chef API Docs: https://docs.chef.io/api_chef_server/#get-21 +func (d *DataBagService) GetItem(databagName string, databagItem string) (item DataBagItem, err error) { path := fmt.Sprintf("data/%s/%s", databagName, databagItem) err = d.client.magicRequestDecoder("GET", path, nil, &item) return } // UpdateItem updates an item in a data bag -// Chef API Docs: http://docs.getchef.com/api_chef_server.html#id24 -func (d *DataBagService) UpdateItem(databagName, databagItemId string, databagItem DataBagItem) (err error) { +// Chef API Docs: https://docs.chef.io/api_chef_server/#put-6 +func (d *DataBagService) UpdateItem(databagName string, databagItemId string, databagItem DataBagItem) (err error) { body, err := JSONReader(databagItem) if err != nil { return diff --git a/databag_test.go b/databag_test.go index 5ea7514..071fc4b 100644 --- a/databag_test.go +++ b/databag_test.go @@ -157,8 +157,11 @@ func TestDataBagsService_UpdateItem(t *testing.T) { func TestDataBagsService_DataBagListResultString(t *testing.T) { e := &DataBagListResult{"bag1": "http://localhost/data/bag1", "bag2": "http://localhost/data/bag2"} + // The output order is not guarenteed by the String function, check for either order want := "bag1 => http://localhost/data/bag1\nbag2 => http://localhost/data/bag2\n" - if e.String() != want { - t.Errorf("DataBagListResult.String returned:\n%+v\nwant:\n%+v\n", e.String(), want) + want2 := "bag2 => http://localhost/data/bag2\nbag1 => http://localhost/data/bag1\n" + ebag := e.String() + if ebag != want && ebag != want2 { + t.Errorf("DataBagListResult.String returned:\n%+v\nwant:\n%+v\n", ebag, want) } } diff --git a/debug.go b/debug.go index d59297c..42360aa 100644 --- a/debug.go +++ b/debug.go @@ -1,5 +1,7 @@ // +build debug +// Add -tags debug to go run or go build to turn on debug output + package chef import "log" @@ -7,3 +9,7 @@ import "log" func debug(fmt string, args ...interface{}) { log.Printf(fmt, args...) } + +func debug_on() bool { + return true +} diff --git a/environment_test.go b/environment_test.go index 1bb7470..ff1b37a 100644 --- a/environment_test.go +++ b/environment_test.go @@ -143,16 +143,19 @@ func TestEnvironmentsService_Put(t *testing.T) { func TestEnvironmentsService_EnvironmentListResultString(t *testing.T) { e := &EnvironmentResult{"_default": "https://api.opscode.com/organizations/org_name/environments/_default", "webserver": "https://api.opscode.com/organizations/org_name/environments/webserver"} + estr := e.String() want := "_default => https://api.opscode.com/organizations/org_name/environments/_default\nwebserver => https://api.opscode.com/organizations/org_name/environments/webserver\n" - if e.String() != want { - t.Errorf("EnvironmentResult.String returned:\n%+v\nwant:\n%+v\n", e.String(), want) + want2 := "webserver => https://api.opscode.com/organizations/org_name/environments/webserver\n_default => https://api.opscode.com/organizations/org_name/environments/_default\n" + if estr != want && estr != want2 { + t.Errorf("EnvironmentResult.String returned:\n%+v\nwant:\n%+v\n", estr, want) } } func TestEnvironmentsService_EnvironmentCreateResultString(t *testing.T) { e := &EnvironmentResult{"uri": "http://localhost:4000/environments/dev"} + estr := e.String() want := "uri => http://localhost:4000/environments/dev\n" - if e.String() != want { - t.Errorf("EnvironmentResult.String returned %+v, want %+v", e.String(), want) + if estr != want { + t.Errorf("EnvironmentResult.String returned %+v, want %+v", estr, want) } } diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..aefe63c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,24 @@ +# Examples for using the api + +## Coded samples +The cmd directory has examples of using the client code to exercise the chef api end points. +The bin directory has commands for invoking the code. +The inspec directory has inspec tests to verify the output from invoking the code. +The chefapi_examples cookbook creates a chef server instance. Test kitchen can run the sample code and verify +the resulting output using inspec tests. + +## Cookbook and kitchen testing +Run kitchen converge to create a chef server instance in a linux image running under vagrant. +Run kitchen verify to run the go examples using the client api and to confirm +that the results are as expected. + +## Looking at the output from an api call +Sometimes you might want to see what output gets created by an api call. Inspec tends to hide +and mask the output. You can use kitchen login to access the linux image. Use "cd /go/src/chefapi_test/bin" +to access the bin directory and run any of the commands to run the api sample use code and see +the results. Running the bin commands by adding the -tags debug option will show more detail. + +## Creating a client +On the test image /go/src/chefapi_test/testapi/testapi.go has code that creates a client +for use by other api calls. For the purposes of testing it uses the pivotal user and key +for all the tests. diff --git a/examples/bin b/examples/bin new file mode 120000 index 0000000..a8a2909 --- /dev/null +++ b/examples/bin @@ -0,0 +1 @@ +./chefapi_examples/files/default/go/src/chefapi_test/bin \ No newline at end of file diff --git a/examples/chefapi_examples/Berksfile b/examples/chefapi_examples/Berksfile new file mode 100644 index 0000000..2e27c49 --- /dev/null +++ b/examples/chefapi_examples/Berksfile @@ -0,0 +1,4 @@ +source 'https://supermarket.chef.io' +source :chef_server + +metadata diff --git a/examples/chefapi_examples/Berksfile.lock b/examples/chefapi_examples/Berksfile.lock new file mode 100644 index 0000000..c250eba --- /dev/null +++ b/examples/chefapi_examples/Berksfile.lock @@ -0,0 +1,15 @@ +DEPENDENCIES + chefapi_examples + path: . + metadata: true + +GRAPH + chef-ingredient (3.1.2) + chef-server (5.5.2) + chef-ingredient (>= 2.1.10) + chefapi_examples (0.1.0) + chef-server (>= 0.0.0) + fileutils (>= 0.0.0) + line (>= 0.0.0) + fileutils (1.3.1) + line (2.5.0) diff --git a/examples/chefapi_examples/README.md b/examples/chefapi_examples/README.md new file mode 100644 index 0000000..5a7091f --- /dev/null +++ b/examples/chefapi_examples/README.md @@ -0,0 +1,11 @@ +# chefapi_examples + +Create a chef server and tests to run integration tests of the go-chef/chef api. + +Expected functions. + +* Spin up an instance of the chef server +* Configure enough objects to illustrate the tests +* Use inspec to run test and verify the results + +# Test Procedure diff --git a/examples/chefapi_examples/TODO.md b/examples/chefapi_examples/TODO.md new file mode 100644 index 0000000..d897b75 --- /dev/null +++ b/examples/chefapi_examples/TODO.md @@ -0,0 +1,26 @@ +* Allow for testing the local go-chef/chef verion +* Flag for ssl verify, figure out how not to turn of ssl authentication +* Fix the circleci pipeline + +* live testing +* sandbox + +Missing api stuff +* user authentication +* user update +* user verbose output +* all the user key stuff + +The search doc needs to be clearer. It refers to the knife search string but isn't clear that what it means + +Sandbox upload gets a 403. https://stackoverflow.com/questions/28807834/chef-upload-to-sandbox-fails-with-403-unauthorized +It's not using the client, going to a returned url and give an auth code. Not sure what to do with it. +embedded/lib/ruby/gems/2.6.0/gems/chef-15.6.10/lib/chef/cookbook_uploader.rb has the ruby code to do this stuff + +2879448ee0bb891f2103f21891234b2f:{Url:https://localhost:443/bookshelf/organization-87e84b155d98af0a68fb7e69e7adee0a/checksum-2879448ee0bb891f2103f21891234b2f?AWSAccessKeyId=3d54e25d43d14490faa55cf85826a2732e3c9411&Expires=1577447683&Signature=cQYAocsq%2B15ztGX3zEcmJ1C9HKk%3D Upload:true} 2b26f685cf7bce1b634787df20607aeb:{Url:https://localhost:443/bookshelf/organization-87e84b155d98af0a68fb7e69e7adee0a/checksum-2b26f685cf7bce1b634787df20607aeb?AWSAccessKeyId=3d54e25d43d14490faa55cf85826a2732e3c9411&Expires=1577447683&Signature=HBsn1DnQWuItPVxQNtCG7Rjc18w%3D Upload:true} 73008070bb42cbbe05dfa5427a8416b1:{Url:https://localhost:443/bookshelf/organization-87e84b155d98af0a68fb7e69e7adee0a/checksum-73008070bb42cbbe05dfa5427a8416b1?AWSAccessKeyId=3d54e25d43d14490faa55cf85826a2732e3c9411&Expires=1577447683&Signature=5RrhZNMvUU%2B4D5fZkYsuYdClWdQ%3D Upload:true}]} + +Uploading: 2879448ee0bb891f2103f21891234b2f ---> {https://localhost:443/bookshelf/organization-87e84b155d98af0a68fb7e69e7adee0a/checksum-2879448ee0bb891f2103f21891234b2f?AWSAccessKeyId=3d54e25d43d14490faa55cf85826a2732e3c9411&Expires=1577447683&Signature=cQYAocsq%2B15ztGX3zEcmJ1C9HKk%3D true} + +after request &{PUT https://localhost:443/bookshelf/organization-87e84b155d98af0a68fb7e69e7adee0a/checksum-2879448ee0bb891f2103f21891234b2f?AWSAccessKeyId=3d54e25d43d14490faa55cf85826a2732e3c9411&Expires=1577447683&Signature=cQYAocsq%2B15ztGX3zEcmJ1C9HKk%3D HTTP/1.1 1 1 map[X-Ops-Authorization-6:[bDFCEcSiDHAqCqwtvwNP8rX/0HgDwO4cCvaNJQT7uw==] Content-Type:[application/octet-stream] Method:[PUT] X-Ops-Userid:[pivotal] X-Ops-Authorization-2:[qMNYPwz2Ym6awXiCgOy6YXxGZeCvfvTngGPvdDa/FPVJLt5MUdYBNOBk/L1W] X-Ops-Authorization-5:[OG9NI2aOPOnah512Cav2l7H/mgdM4gr3+jyuz/sGQVxAv5jriu086zKqU/EJ] X-Ops-Content-Hash:[RWKT0i3TZ5n3l3km2zgS6JlPs9M=] X-Ops-Timestamp:[2019-12-27T11:39:43Z] X-Ops-Authorization-4:[PY95MHZVNgaH3gcYj+dD3GHCtDlk44C4vHEHv3N5OVTfrcs7Mi3tuprPARuh] Accept:[application/json] X-Ops-Authorization-1:[wFXZeEpvMefVCPDVGUZL/zNMlqx94yN5L78rq13TJticQNXCXJg45Y74fwKY] X-Ops-Authorization-3:[7V1BquFUsAJxsKkhiGsvT0l6qUTNFIKOGc2KOH6p0cGpPlzaDBoEyd8Qer0U] X-Chef-Version:[11.12.0] X-Ops-Sign:[algorithm=sha1;version=1.0]] {0xc42006ddd0} 0x604110 128 [] false localhost:443 map[] map[] map[] } +before upload +after upload PUT https://localhost:443/bookshelf/organization-87e84b155d98af0a68fb7e69e7adee0a/checksum-2879448ee0bb891f2103f21891234b2f?AWSAccessKeyId=3d54e25d43d14490faa55cf85826a2732e3c9411&Expires=1577447683&Signature=cQYAocsq%2B15ztGX3zEcmJ1C9HKk%3D: 403 diff --git a/examples/chefapi_examples/chefignore b/examples/chefapi_examples/chefignore new file mode 100644 index 0000000..38e7379 --- /dev/null +++ b/examples/chefapi_examples/chefignore @@ -0,0 +1,107 @@ +# Put files/directories that should be ignored in this file when uploading +# to a chef-server or supermarket. +# Lines that start with '# ' are comments. + +# OS generated files # +###################### +.DS_Store +Icon? +nohup.out +ehthumbs.db +Thumbs.db + +# SASS # +######## +.sass-cache + +# EDITORS # +########### +\#* +.#* +*~ +*.sw[a-z] +*.bak +REVISION +TAGS* +tmtags +*_flymake.* +*_flymake +*.tmproj +.project +.settings +mkmf.log + +## COMPILED ## +############## +a.out +*.o +*.pyc +*.so +*.com +*.class +*.dll +*.exe +*/rdoc/ + +# Testing # +########### +.watchr +.rspec +spec/* +spec/fixtures/* +test/* +features/* +examples/* +Guardfile +Procfile +.kitchen* +.rubocop.yml +spec/* +Rakefile +.travis.yml +.foodcritic +.codeclimate.yml + +# SCM # +####### +.git +*/.git +.gitignore +.gitmodules +.gitconfig +.gitattributes +.svn +*/.bzr/* +*/.hg/* +*/.svn/* + +# Berkshelf # +############# +Berksfile +Berksfile.lock +cookbooks/* +tmp + +# Policyfile # +############## +Policyfile.rb +Policyfile.lock.json + +# Cookbooks # +############# +CONTRIBUTING* +CHANGELOG* +TESTING* +MAINTAINERS.toml + +# Strainer # +############ +Colanderfile +Strainerfile +.colander +.strainer + +# Vagrant # +########### +.vagrant +Vagrantfile diff --git a/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/README.md b/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/README.md new file mode 100644 index 0000000..59d79a7 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/README.md @@ -0,0 +1 @@ +An example test cookbook diff --git a/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/metadata.rb b/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/metadata.rb new file mode 100644 index 0000000..af8789b --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/metadata.rb @@ -0,0 +1,3 @@ +name 'sampbook' +description 'A test cookbook that does nothing' +version '0.1.0' diff --git a/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/recipes/default.rb b/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/recipes/default.rb new file mode 100644 index 0000000..c452f47 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.1.0/sampbook/recipes/default.rb @@ -0,0 +1 @@ +# default recipe diff --git a/examples/chefapi_examples/files/default/cb/0.1.0/testbook/README.md b/examples/chefapi_examples/files/default/cb/0.1.0/testbook/README.md new file mode 100644 index 0000000..59d79a7 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.1.0/testbook/README.md @@ -0,0 +1 @@ +An example test cookbook diff --git a/examples/chefapi_examples/files/default/cb/0.1.0/testbook/metadata.rb b/examples/chefapi_examples/files/default/cb/0.1.0/testbook/metadata.rb new file mode 100644 index 0000000..792c6fd --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.1.0/testbook/metadata.rb @@ -0,0 +1,3 @@ +name 'testbook' +description 'A test cookbook that does nothing' +version '0.1.0' diff --git a/examples/chefapi_examples/files/default/cb/0.1.0/testbook/recipes/default.rb b/examples/chefapi_examples/files/default/cb/0.1.0/testbook/recipes/default.rb new file mode 100644 index 0000000..c452f47 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.1.0/testbook/recipes/default.rb @@ -0,0 +1 @@ +# default recipe diff --git a/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/README.md b/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/README.md new file mode 100644 index 0000000..59d79a7 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/README.md @@ -0,0 +1 @@ +An example test cookbook diff --git a/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/metadata.rb b/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/metadata.rb new file mode 100644 index 0000000..b45d45f --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/metadata.rb @@ -0,0 +1,3 @@ +name 'sampbook' +description 'A test cookbook that does nothing' +version '0.2.0' diff --git a/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/recipes/default.rb b/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/recipes/default.rb new file mode 100644 index 0000000..c452f47 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.2.0/sampbook/recipes/default.rb @@ -0,0 +1 @@ +# default recipe diff --git a/examples/chefapi_examples/files/default/cb/0.2.0/testbook/README.md b/examples/chefapi_examples/files/default/cb/0.2.0/testbook/README.md new file mode 100644 index 0000000..59d79a7 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.2.0/testbook/README.md @@ -0,0 +1 @@ +An example test cookbook diff --git a/examples/chefapi_examples/files/default/cb/0.2.0/testbook/metadata.rb b/examples/chefapi_examples/files/default/cb/0.2.0/testbook/metadata.rb new file mode 100644 index 0000000..0a2b110 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.2.0/testbook/metadata.rb @@ -0,0 +1,3 @@ +name 'testbook' +description 'A test cookbook that does nothing' +version '0.2.0' diff --git a/examples/chefapi_examples/files/default/cb/0.2.0/testbook/recipes/default.rb b/examples/chefapi_examples/files/default/cb/0.2.0/testbook/recipes/default.rb new file mode 100644 index 0000000..c452f47 --- /dev/null +++ b/examples/chefapi_examples/files/default/cb/0.2.0/testbook/recipes/default.rb @@ -0,0 +1 @@ +# default recipe diff --git a/examples/chefapi_examples/files/default/chef_objects.sh b/examples/chefapi_examples/files/default/chef_objects.sh new file mode 100644 index 0000000..e640885 --- /dev/null +++ b/examples/chefapi_examples/files/default/chef_objects.sh @@ -0,0 +1,30 @@ +#!/bin/bash -x + +# Add two users +chef-server-ctl user-create user1 user1 mid last user1.last@nordstrom.com dummuy1pass +chef-server-ctl user-create user2 user2 mid last user2.last@nordstrom.com dummuy1pass + +# Add a user to an org +./chef-server-ctl org-user-add ORG_NAME USER_NAME +# Add an admin user to an org +./chef-server-ctl org-user-add ORG_NAME USER_NAME -a + +# define the current node to the chef server +chef-client + +# Add a cookbook +knife cookbook upload + +# Add a role + +# Add the user to a group + +# Add an environment + +# Add a data bag + +# Add a chef vault item + +# Add a node and set the run list + +# tag a node diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/ExampleGoals.md b/examples/chefapi_examples/files/default/go/src/chefapi_test/ExampleGoals.md new file mode 100644 index 0000000..b86ccf8 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/ExampleGoals.md @@ -0,0 +1,34 @@ +# Chef API testing + +For each class of chef api object class (organizations, users, cookbooks, etc and each rest end point) +* Exercise the defined functions - mostly crud +* Define input structs for the calls to the api +* Create an example of the code to call each function +* Define the expected output in a struct +* Verify the output structs are populated as expected + +|Name| New | Function tests | input structs | calls | out struct | verify out | +acl +association_request * +authenticate_user * +client +containers * +cookbook +cookbook_download +databag +environment +group +license * +node +organization +policy * +policy_group * +principal +role +run_list +sandbox +status * +search +universe * +updated_since * +user diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/TODO b/examples/chefapi_examples/files/default/go/src/chefapi_test/TODO new file mode 100644 index 0000000..933ce06 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/TODO @@ -0,0 +1,90 @@ + +Organization + create 1 + create 2 + list + get 1 + update + delete + +ACL + get + update + +Client + create 2x + list + get + update * + delete + listkeys + getkey + +Cookbooks + List + List with version limits + GetVersion + List all recipes + GetAvailableVersion + Get specific version + Delete cookbook version + Add cookbook Version + Download cookbook files => create a cookbook on disk + +Databag + Create 2x + List + Delete + ListItems + CreateItem 2x + DeleteItem + GetItem + UpdateItem + +Environment + Create 2x + Get + Put ? + List + Delete + ListCookbooks + +Node + List + Get + Post + Put + Delete + +Organizations + List + Get + Create + Delete + Update + +Principal + Get + +Role + List + Create + Delete + Get + Put + +Sandbox + Post + Put + +Search + Do + PartialExec + Indexes + +Users + Create + List + Get + Update + Delete diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/Testing.md b/examples/chefapi_examples/files/default/go/src/chefapi_test/Testing.md new file mode 100644 index 0000000..26e30ad --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/Testing.md @@ -0,0 +1,5 @@ +## Global +https://test + +## Organization specific +https://test/organization/test/* diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/association b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/association new file mode 100755 index 0000000..7cfd9a8 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/association @@ -0,0 +1,11 @@ +#!/bin/bash + +# Group testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds +go run ${BASE}/../cmd/association/association_setup.go ${CHEFUSER} ${KEYFILE} ${CHEFGLOBALURL} ${SSLBYPASS} +go run ${BASE}/../cmd/association/association.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} +go run ${BASE}/../cmd/association/association_cleanup.go ${CHEFUSER} ${KEYFILE} ${CHEFGLOBALURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/authenticate b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/authenticate new file mode 100755 index 0000000..bd92929 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/authenticate @@ -0,0 +1,10 @@ +#!/bin/bash + +# Authenticate_user testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds + +go run ${BASE}/../cmd/authenticate/authenticate.go ${CHEFUSER} ${KEYFILE} ${CHEFGLOBALURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/cookbook b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/cookbook new file mode 100755 index 0000000..684d55d --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/cookbook @@ -0,0 +1,10 @@ +#!/bin/bash + +# Cookbook testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds + +go run ${BASE}/../cmd/cookbook/cookbook.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/creds b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/creds new file mode 100644 index 0000000..b663e81 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/creds @@ -0,0 +1,7 @@ +CHEFUSER='pivotal' +KEYFILE='/etc/opscode/pivotal.pem' +CHEFGLOBALURL='https://localhost/' +# The trailing / on the url is critically important. The api does not work without it. +CHEFORGANIZATIONURL='https://localhost/organizations/test/' +# Use the self signed certificates +SSLBYPASS='false' diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/databag b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/databag new file mode 100755 index 0000000..2df2cbf --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/databag @@ -0,0 +1,9 @@ +#!/bin/bash + +# Data Bag testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds +go run ${BASE}/../cmd/databag/databag.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/group b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/group new file mode 100755 index 0000000..653923d --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/group @@ -0,0 +1,9 @@ +#!/bin/bash + +# Group testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds +go run ${BASE}/../cmd/group/group.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/knife.rb b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/knife.rb new file mode 100644 index 0000000..bd98807 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/knife.rb @@ -0,0 +1,4 @@ +node_name "pivotal" +client_key "/etc/opscode/pivotal.pem" +chef_server_url "https://testhost/organizations/test" +ssl_verify_mode :verify_none diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/license b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/license new file mode 100755 index 0000000..effd2ca --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/license @@ -0,0 +1,10 @@ +#!/bin/bash + +# License testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds + +go run ${BASE}/../cmd/license/license.go ${CHEFUSER} ${KEYFILE} ${CHEFGLOBALURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/node b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/node new file mode 100755 index 0000000..ccf0049 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/node @@ -0,0 +1,10 @@ +#!/bin/bash + +# Node testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds + +go run ${BASE}/../cmd/node/nodes.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/organization b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/organization new file mode 100755 index 0000000..ad9c0aa --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/organization @@ -0,0 +1,13 @@ +#!/bin/bash + +# Organization testing + +# https://forfuncsake.github.io/post/2017/08/trust-extra-ca-cert-in-go-app/ +# TODO: Trust a self signed chef server cert + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds + +go run ${BASE}/../cmd/organization/organization.go ${CHEFUSER} ${KEYFILE} ${CHEFGLOBALURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/sandbox b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/sandbox new file mode 100755 index 0000000..f8c5105 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/sandbox @@ -0,0 +1,9 @@ +#!/bin/bash + +# Group testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds +go run ${BASE}/../cmd/sandbox/sandbox.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/search b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/search new file mode 100755 index 0000000..cd57caf --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/search @@ -0,0 +1,9 @@ +#!/bin/bash + +# Group testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds +go run ${BASE}/../cmd/search/search.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/search_pagination b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/search_pagination new file mode 100755 index 0000000..8b9647e --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/search_pagination @@ -0,0 +1,9 @@ +#!/bin/bash + +# Group testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds +go run ${BASE}/../cmd/search/search_pagination.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/setup b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/setup new file mode 100644 index 0000000..65e8ba4 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/setup @@ -0,0 +1 @@ +export GOPATH=/go diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/status b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/status new file mode 100755 index 0000000..7fc5ffb --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/status @@ -0,0 +1,10 @@ +#!/bin/bash + +# Status testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds + +go run ${BASE}/../cmd/status/status.go ${CHEFUSER} ${KEYFILE} ${CHEFGLOBALURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/universe b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/universe new file mode 100755 index 0000000..a67ffae --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/universe @@ -0,0 +1,10 @@ +#!/bin/bash + +# Universe testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds + +go run ${BASE}/../cmd/universe/universe.go ${CHEFUSER} ${KEYFILE} ${CHEFORGANIZATIONURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/user b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/user new file mode 100755 index 0000000..6b0135e --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/user @@ -0,0 +1,9 @@ +#!/bin/bash + +# Group testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds +go run ${BASE}/../cmd/user/user.go ${CHEFUSER} ${KEYFILE} ${CHEFGLOBALURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/userkeys b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/userkeys new file mode 100755 index 0000000..88369bf --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/bin/userkeys @@ -0,0 +1,9 @@ +#!/bin/bash + +# Group testing + +BASE=$(dirname $0) + +. ${BASE}/setup +. ${BASE}/creds +go run ${BASE}/../cmd/user/userkeys.go ${CHEFUSER} ${KEYFILE} ${CHEFGLOBALURL} ${SSLBYPASS} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association.go new file mode 100644 index 0000000..8ab694d --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association.go @@ -0,0 +1,106 @@ +// +// Test the go-chef/chef chef server api /organization/:org/user and /organization/:org/association_requests +// endpoints against a live server +// +package main + +import ( + "fmt" + "chefapi_test/testapi" + "github.com/go-chef/chef" + "os" +) + + +// main Exercise the chef server api +func main() { + client := testapi.Client() + + // Build stuctures to invite users + invite := chef.Request { + User: "usrinvite", + } + invite2 := chef.Request { + User: "usr2invite", + } + invitemissing := chef.Request { + User: "nouser", + } + add1 := chef.AddNow { + Username: "usradd", + } + + // Invite the user to the test org + out, err := client.Associations.Invite(invite) + if err != nil { + fmt.Fprintf(os.Stderr, "Issue inviting a user %+v %+v\n", invite, err) + } + fmt.Printf("Invited user %+v %+v\n", invite, out) + + // Invite a second user + out, err = client.Associations.Invite(invite2) + fmt.Printf("Invited user %+v %+v\n", invite2, out) + + // fail at inviting a missing user. Should get a 404 + out, err = client.Associations.Invite(invitemissing) + if err != nil { + fmt.Fprintf(os.Stderr, "Issue inviting a user %+v %+v\n", invitemissing, err) + } + fmt.Printf("Invited user %+v %+v\n", invitemissing, out) + + // Find a pending invitation by user name + id, err := client.Associations.InviteId("usr2invite") + if err != nil { + fmt.Fprintf(os.Stderr, "Issue finding an invitation for usr2invite %+v\n", err) + } + fmt.Printf("Invitation id for usr2invite %+v\n", id) + + // Accept the invite for invite2 + // outa, err := client.Associations.AcceptInvite(id) + // if err != nil { + // fmt.Fprintf(os.Stderr, "Issue accepting the invitation %+v\n", err) + // } + // fmt.Printf("Accept invitation %+v\n", outa) + + // List the invites + + outl, err := client.Associations.ListInvites() + if err != nil { + fmt.Fprintf(os.Stderr, "Issue listing the invitations %+v\n", err) + } + fmt.Printf("Invitation list %+v\n", outl) + + // Delete the invitations by id + for _, in := range outl { + outd, err := client.Associations.DeleteInvite(in.Id) + if err != nil { + fmt.Fprintf(os.Stderr, "Issue deleting an invitation for %s %+v\n", in.UserName, err) + } + fmt.Printf("Deleted invitation %s for %s %+v\n", in.Id, in.UserName, outd) + } + + // Add a user to the test organization + err = client.Associations.Add(add1) + if err != nil { + fmt.Fprintf(os.Stderr, "Issue adding user usradd: %+v\n", err) + } + fmt.Printf("User added: %+v\n", add1) + // List the users + ulist, err := client.Associations.List() + if err != nil { + fmt.Fprintf(os.Stderr, "Issue listing the users: %+v\n", err) + } + fmt.Printf("Users list: %+v\n", ulist) + // Get the user details + uget, err := client.Associations.Get("usradd") + if err != nil { + fmt.Fprintf(os.Stderr, "Issue getting user details: %+v\n", err) + } + fmt.Printf("User details: %+v\n", uget) + // Delete a user for the organization + udel, err := client.Associations.Get("usradd") + if err != nil { + fmt.Fprintf(os.Stderr, "Issue deleting usradd: %+v\n", err) + } + fmt.Printf("User deleted: %+v\n", udel) +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association_cleanup.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association_cleanup.go new file mode 100644 index 0000000..c4011af --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association_cleanup.go @@ -0,0 +1,31 @@ +// +// Test the go-chef/chef chef server api /organization/:org/user and /organization/:org/association_requests +// endpoints against a live server +// +package main + +import ( + "fmt" + "chefapi_test/testapi" + "github.com/go-chef/chef" + "os" +) + + +// main Exercise the chef server api +func main() { + client := testapi.Client() + deleteUser(client, "usrinvite") + deleteUser(client, "usr2invite") + deleteUser(client, "usradd") + +} + + // deleteUser uses the chef server api to delete a single user + func deleteUser(client *chef.Client, name string) (err error) { + err = client.Users.Delete(name) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting org:", err) + } + return + } diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association_setup.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association_setup.go new file mode 100644 index 0000000..62d4caa --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/association/association_setup.go @@ -0,0 +1,57 @@ +// +// Test the go-chef/chef chef server api /organization/:org/user and /organization/:org/association_requests +// endpoints against a live server +// +package main + +import ( + "fmt" + "chefapi_test/testapi" + "github.com/go-chef/chef" + "os" +) + + +// main Exercise the chef server api +func main() { + client := testapi.Client() + + // Create a user + var usr chef.User + usr = chef.User{ UserName: "usrinvite", + Email: "usrauth@domain.io", + FirstName: "usr", + LastName: "invite", + DisplayName: "Userauth Fullname", + Password: "Logn12ComplexPwd#", + } + createUser(client, usr) + + usr = chef.User{ UserName: "usr2invite", + Email: "usr22auth@domain.io", + FirstName: "usr22", + LastName: "invite", + DisplayName: "User22auth Fullname", + Password: "Logn12ComplexPwd#", + } + createUser(client, usr) + + usr = chef.User{ UserName: "usradd", + Email: "usradd@domain.io", + FirstName: "usr", + LastName: "add", + DisplayName: "UserAdd Fullname", + Password: "Logn12ComplexPwd#", + } + createUser(client, usr) + +} + +// createUser uses the chef server api to create a single user +func createUser(client *chef.Client, user chef.User) chef.UserResult { + usrResult, err := client.Users.Create(user) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue creating user:", err) + } + return usrResult +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/authenticate/authenticate.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/authenticate/authenticate.go new file mode 100644 index 0000000..760efec --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/authenticate/authenticate.go @@ -0,0 +1,62 @@ +// +// Test the go-chef/chef chef authenticate_user api endpoint against a live server +// +package main + +import ( + "fmt" + "os" + + "github.com/go-chef/chef" + "chefapi_test/testapi" +) + + +// main Exercise the chef server api +func main() { + // Create a client for access + client := testapi.Client() + + // Create a user + var usr chef.User + usr = chef.User{ UserName: "usrauth", + Email: "usrauth@domain.io", + FirstName: "usrauth", + LastName: "fullname", + DisplayName: "Userauth Fullname", + Password: "Logn12ComplexPwd#", + } + createUser(client, usr) + + var ar chef.Authenticate + // Authenticate with a valid password + ar.UserName = "usrauth" + ar.Password = "Logn12ComplexPwd#" + err := client.AuthenticateUser.Authenticate(ar) + fmt.Printf("Authenticate with a valid password %+vauthenticate\n", err) + + // Authenticate with an invalid password + ar.Password = "badpassword" + err = client.AuthenticateUser.Authenticate(ar) + fmt.Printf("Authenticate with an invalid password %+v\n", err) + + // Cleanup + deleteUser(client, "usrauth") +} + +// createUser uses the chef server api to create a single user +func createUser(client *chef.Client, user chef.User) chef.UserResult { + usrResult, err := client.Users.Create(user) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue creating user:", err) + } + return usrResult +} +// deleteUser uses the chef server api to delete a single user +func deleteUser(client *chef.Client, name string) (err error) { + err = client.Users.Delete(name) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting org:", err) + } + return +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/cookbook/cookbook.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/cookbook/cookbook.go new file mode 100644 index 0000000..43e6967 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/cookbook/cookbook.go @@ -0,0 +1,118 @@ +// +// Test the go-chef/chef chef server api /organizations/*/cookbooks endpoints against a live chef server +// +package main + +import ( + "fmt" + "os" + "os/exec" + + "chefapi_test/testapi" +) + + +// main Exercise the chef server api +func main() { + // Create a client for user access + client := testapi.Client() + + // Prep by adding a couple versions of some cookbooks before running this code + err := addSampleCookbooks() + // testbook version 0.1.0 and 0.2.0 + // sampbook version 0.1.0 and 0.2.0 + + fmt.Println("Starting cookbooks") + // Cookbooks + cookList, err := client.Cookbooks.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing cookbooks:", err) + } + fmt.Printf("List initial cookbooks %+v\nEndInitialList\n", cookList) + + // cookbook GET info + testbook, err := client.Cookbooks.Get("testbook") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting cookbook testbook:", err) + } + fmt.Printf("Get cookbook testbook %+v\n", testbook) + + // GET missing cookbook + nothere, err := client.Cookbooks.Get("nothere") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting cookbook nothere:", err) + } + fmt.Printf("Get cookbook nothere %+v\n", nothere) + + // list available versions of a cookbook + testbookversions, err := client.Cookbooks.GetAvailableVersions("testbook", "0") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting cookbook versions for testbook:", err) + } + fmt.Printf("Get cookbook versions testbook %+v\n", testbookversions) + + // list available versions of a cookbook + sampbookversions, err := client.Cookbooks.GetAvailableVersions("sampbook", "0") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting cookbook versions for sampbook:", err) + } + fmt.Printf("Get cookbook versions sampbook %+v\n", sampbookversions) + + // get specific versions of a cookbook + testbookversions1, err := client.Cookbooks.GetVersion("testbook", "0.1.0") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting specific cookbook versions for testbook:", err) + } + fmt.Printf("Get specific cookbook version testbook %+v\n", testbookversions1) + + // list all recipes + allRecipes, err := client.Cookbooks.ListAllRecipes() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting all recipes:", err) + } + fmt.Printf("Get all recipes %+v\n", allRecipes) + + // delete version + err = client.Cookbooks.Delete("testbook", "0.1.0") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting testbook 0.1.0:", err) + } + fmt.Printf("Delete testbook 0.1.0 %+v\n", err) + + // delete version + err = client.Cookbooks.Delete("testbook", "0.2.0") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting testbook 0.2.0:", err) + } + fmt.Printf("Delete testbook 0.2.0 %+v\n", err) + + // List cookbooks + cookList, err = client.Cookbooks.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing cookbooks:", err) + } + fmt.Printf("Final cookbook list %+v\n", cookList) + + // list available versions of a cookbook + sampbookversions, err = client.Cookbooks.GetAvailableVersions("sampbook", "0") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting cookbook versions for sampbook:", err) + } + fmt.Printf("Final cookbook versions sampbook %+v\n", sampbookversions) +} + +func addSampleCookbooks() (err error) { + cmd := exec.Command("knife", "cookbook", "upload", "sampbook", "testbook", "-c", "/go/src/chefapi_test/bin/knife.rb", "-o", "/fixtures/chef/cb/0.1.0") + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(os.Stderr, "Issue loading cookbook:", err) + } + fmt.Printf("Load 0.1.0 cookbook versions: %+v", string(out)) + cmd = exec.Command("knife", "cookbook", "upload", "sampbook", "testbook", "-c", "/go/src/chefapi_test/bin/knife.rb", "-o", "/fixtures/chef/cb/0.2.0") + out, err = cmd.CombinedOutput() + if err != nil { + fmt.Println(os.Stderr, "Issue loading cookbook:", err) + } + fmt.Printf("Load 0.2.0 cookbook versions: %+v", string(out)) + return +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/databag/databag.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/databag/databag.go new file mode 100644 index 0000000..44b6d14 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/databag/databag.go @@ -0,0 +1,123 @@ +// +// Test the go-chef/chef chef server api /databag endpoints against a live server +// +package main + +import ( + "fmt" + "chefapi_test/testapi" + "github.com/go-chef/chef" + "os" +) + + +// main Exercise the chef server api +func main() { + client := testapi.Client() + + // List the current databags + BagList, err := client.DataBags.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue printing the existing databags:", err) + } + fmt.Printf("List initial databags %+v\n", BagList) + + databag1 := chef.DataBag{ + Name: "databag1", + } + + // Add a new databag + databagAdd, err := client.DataBags.Create(&databag1) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue adding databag1:", err) + } + fmt.Println("Added databag1", databagAdd) + + // Add databag again + databagAdd, err = client.DataBags.Create(&databag1) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue recreating databag1:", err) + } + fmt.Println("Recreated databag1", databagAdd) + + // Try to get a missing databag + databagOutMissing, err := client.DataBags.ListItems("nothere") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting nothere:", err) + } + fmt.Println("Get nothere", databagOutMissing) + + // List databags after adding + BagList, err = client.DataBags.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue printing the existing databags:", err) + } + fmt.Printf("List databags after adding databag1 %+v\n", BagList) + + + // Add item to a data bag + item1data := map[string]string{ + "id": "item1", + "type": "password", + "value": "secret", + } + item1 := chef.DataBagItem(item1data) + err = client.DataBags.CreateItem("databag1", item1) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue updating databag1:", err) + } + fmt.Printf("Create databag1::item1 %+v\n", err) + + + + // Update a databag item + item1data["value"] = "next" + item1upd := chef.DataBagItem(item1data) + err = client.DataBags.UpdateItem("databag1", "item1", item1upd) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue updating databag1:", err) + } + fmt.Printf("Update databag1::item1 %+v\n", err) + + // list databag items + databagItems, err := client.DataBags.ListItems("databag1") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting databag1:", err) + } + fmt.Printf("List databag1 items %+v\n", databagItems) + + // Get the contents of a data bag item + dataItem, err := client.DataBags.GetItem("databag1", "item1") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting databag1::item1 :", err) + } + fmt.Printf("Get databag1::item1 %+v\n", dataItem) + + // Delete a databag item + err = client.DataBags.DeleteItem("databag1", "item1") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting databag1::item1 :", err) + } + fmt.Printf("Delete databag1::item1 %+v\n", err) + + // List items + databagItems, err = client.DataBags.ListItems("databag1") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting databag1:", err) + } + fmt.Printf("List databag1 items after delete %+v\n", databagItems) + + // Clean up + databag, err := client.DataBags.Delete("databag1") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting databag1:", err) + } + fmt.Printf("Delete databag1 %+v\n", databag) + + // Final list of databags + BagList, err = client.DataBags.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing the final databags:", err) + } + fmt.Printf("List databags after cleanup %+v\n", BagList) +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/group/group.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/group/group.go new file mode 100644 index 0000000..53157c8 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/group/group.go @@ -0,0 +1,85 @@ +// +// Test the go-chef/chef chef server api /group endpoints against a live server +// +package main + +import ( + "fmt" + "chefapi_test/testapi" + "github.com/go-chef/chef" + "os" +) + + +// main Exercise the chef server api +func main() { + client := testapi.Client() + + // List the current groups + groupList, err := client.Groups.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue printing the existing groups:", err) + } + fmt.Printf("List initial groups %+vEndInitialList\n", groupList) + + // Build a stucture to define a group + group1 := chef.Group { + Name: "group1", + GroupName: "group1", + } + + // Add a new group + groupAdd, err := client.Groups.Create(group1) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue adding group1:", err) + } + fmt.Println("Added group1", groupAdd) + + // Add group again + groupAdd, err = client.Groups.Create(group1) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue recreating group1:", err) + } + fmt.Println("Recreated group1", groupAdd) + + // List groups after adding + groupList, err = client.Groups.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue printing the existing groups:", err) + } + fmt.Printf("List groups after adding group1 %+vEndAddList\n", groupList) + + // Get new group + groupOut, err := client.Groups.Get("group1") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting group1:", err) + } + fmt.Printf("Get group1 %+v\n", groupOut) + + // Try to get a missing group + groupOutMissing, err := client.Groups.Get("nothere") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue getting nothere:", err) + } + fmt.Println("Get nothere", groupOutMissing) + + // Update a group + group1.GroupName = "group1-new" + group1.Users = append(group1.Users, "pivotal") + groupUpdate, err := client.Groups.Update(group1) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue updating group1:", err) + } + fmt.Printf("Update group1 %+v\n", groupUpdate) + + // Clean up + err = client.Groups.Delete("group1-new") + fmt.Println("Delete group1", err) + + // Final list of groups + groupList, err = client.Groups.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing the final groups:", err) + } + fmt.Printf("List groups after cleanup %+vEndFinalList\n", groupList) +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/license/license.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/license/license.go new file mode 100644 index 0000000..ef79f93 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/license/license.go @@ -0,0 +1,22 @@ +// +// Test the go-chef/chef chef server api /license endpoints against a live server +// +package main + +import ( + "fmt" + "chefapi_test/testapi" +) + + +// main Exercise the chef server api +func main() { + // Create a client for access + client := testapi.Client() + + license, err := client.License.Get() + if err != nil { + fmt.Println("Issue getting license information", err) + } + fmt.Printf("List license: %+v", license) +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/node/nodes.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/node/nodes.go new file mode 100644 index 0000000..0ae9128 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/node/nodes.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "os" + + "chefapi_test/testapi" + "github.com/go-chef/chef" +) + +func main() { + // Use the default test org + client := testapi.Client() + + // List initial nodes + nodeList, err := client.Nodes.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't list nodes: ", err) + } + fmt.Println("List initial nodes", nodeList) + + // Define a Node object + node1 := chef.NewNode("node1") + node1.RunList = []string{"pwn"} + fmt.Println("Define node1", node1) + + // Delete node1 ignoring errors :) + err = client.Nodes.Delete(node1.Name) + + // Create + nodeResult, err := client.Nodes.Post(node1) + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't create node node1. ", err) + } + fmt.Println("Added node1", nodeResult) + + // List nodes + nodeList, err = client.Nodes.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't list nodes: ", err) + } + fmt.Println("List nodes after adding node1", nodeList) + + // Create a second time + nodeResult, err = client.Nodes.Post(node1) + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't recreate node node1. ", err) + } + fmt.Println("Added node1", nodeResult) + + // Read node1 information + serverNode, err := client.Nodes.Get("node1") + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't get node: ", err) + } + fmt.Printf("Get node1 %+v\n", serverNode) + + // update node + node1.RunList = append(node1.RunList, "recipe[works]") + updateNode, err := client.Nodes.Put(node1) + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't update node: ", err) + } + fmt.Println("Update node1", updateNode) + + // Info after update + serverNode, err = client.Nodes.Get("node1") + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't get node: ", err) + } + fmt.Printf("Get node1 after update %+v\n", serverNode) + + // Delete node ignoring errors :) + err = client.Nodes.Delete(node1.Name) + fmt.Println("Delete node1", err) + + // List nodes + nodeList, err = client.Nodes.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't list nodes: ", err) + } + fmt.Println("List nodes after cleanup", nodeList) +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/organization/organization.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/organization/organization.go new file mode 100644 index 0000000..b1eb780 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/organization/organization.go @@ -0,0 +1,106 @@ +// +// Test the go-chef/chef chef server api /organizations endpoints against a live server +// +package main + +import ( + "fmt" + "os" + + "github.com/go-chef/chef" + "chefapi_test/testapi" +) + + +// main Exercise the chef server api +func main() { + // Create a client for access + client := testapi.Client() + + // Organization tests + org1 := "org1" + org2 := "org2" + + orgList := listOrganizations(client) + fmt.Println("List initial organizations", orgList) + + orgResult := createOrganization(client, chef.Organization{Name: "org1", FullName: "organization1"}) + fmt.Println("Added org1", orgResult) + + orgResult = createOrganization(client, chef.Organization{Name: "org1", FullName: "organization1"}) + fmt.Println("Added org1 again", orgResult) + + orgResult = createOrganization(client, chef.Organization{Name: "org2", FullName: "organization2"}) + fmt.Println("Added org2", orgResult) + + orgout := getOrganization(client, org1) + fmt.Println("Get org1", orgout) + + orgList = listOrganizations(client) + fmt.Println("List organizations After adding org1 and org2", orgList) + + orgresult := updateOrganization(client, chef.Organization{Name: "org1", FullName: "new_organization1"}) + fmt.Println("Update org1", orgresult) + + orgout = getOrganization(client, org1) + fmt.Println("Get org1 after update", orgout) + + orgErr := deleteOrganization(client, org2) + fmt.Println("Delete org2", orgErr) + + orgList = listOrganizations(client) + fmt.Println("List organizations after deleting org2", orgList) + + // Clean up + orgErr = deleteOrganization(client, org1) + fmt.Println("Result from deleting org1", orgErr) + + orgList = listOrganizations(client) + fmt.Println("List organizations after cleanup", orgList) + +} + +// createOrganization uses the chef server api to create a single organization +func createOrganization(client *chef.Client, org chef.Organization) chef.OrganizationResult { + orgResult, err := client.Organizations.Create(org) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue creating org:", org, err) + } + return orgResult +} + +// deleteOrganization uses the chef server api to delete a single organization +func deleteOrganization(client *chef.Client, name string) error { + err := client.Organizations.Delete(name) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting org:", name, err) + } + return err +} + +// getOrganization uses the chef server api to get information for a single organization +func getOrganization(client *chef.Client, name string) chef.Organization { + orgList, err := client.Organizations.Get(name) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing org:", name, err) + } + return orgList +} + +// listOrganizations uses the chef server api to list all organizations +func listOrganizations(client *chef.Client) map[string]string { + orgList, err := client.Organizations.List() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing orgs:", err) + } + return orgList +} + +// updateOrganization uses the chef server api to update information for a single organization +func updateOrganization(client *chef.Client, org chef.Organization) chef.Organization { + org_update, err := client.Organizations.Update(org) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue updating org:", org, err) + } + return org_update +} diff --git a/examples/sandboxes/sandboxes.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/sandbox/sandbox.go similarity index 53% rename from examples/sandboxes/sandboxes.go rename to examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/sandbox/sandbox.go index 21d06b5..44deac7 100644 --- a/examples/sandboxes/sandboxes.go +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/sandbox/sandbox.go @@ -4,14 +4,11 @@ import ( "bytes" "crypto/md5" "crypto/rand" - "encoding/json" "fmt" - "io/ioutil" "os" "github.com/cenkalti/backoff" - "github.com/davecgh/go-spew/spew" - "github.com/go-chef/chef" + "chefapi_test/testapi" ) //random_data makes random byte slice for building junk sandbox data @@ -22,45 +19,26 @@ func random_data(size int) (b []byte) { } 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) - } + client := testapi.Client() // create junk files and sums files := make(map[string][]byte) sums := make([]string, 10) for i := 0; i < 10; i++ { - data := random_data(1024) + data := random_data(128) hashstr := fmt.Sprintf("%x", md5.Sum(data)) files[hashstr] = data sums[i] = hashstr } - // post the new sums/files to the sandbox + // TODO: Find a sandbox delete method + + // post the new sums and get a new sandbox id postResp, err := client.Sandboxes.Post(sums) if err != nil { - fmt.Println("error making request: ", err) - os.Exit(1) + fmt.Fprintf(os.Stderr, "Issue making request: %+v\n", err) } - - // Dump the the server response data - j, err := json.MarshalIndent(postResp, "", " ") - fmt.Printf("%s", j) + fmt.Printf("Create sandboxes %+v\n", postResp) // Let's upload the files that postRep thinks we should upload for hash, item := range postResp.Checksums { @@ -71,9 +49,9 @@ func main() { // If you were writing this in your own tool you could just use the FH and let the Reader interface suck out the content instead of doing the convert. fmt.Printf("\nUploading: %s ---> %v\n\n", hash, item) req, err := client.NewRequest("PUT", item.Url, bytes.NewReader(files[hash])) + // TODO: headers = { "content-type" => "application/x-binary", "content-md5" => checksum64, "accept" => "application/json" } if err != nil { - fmt.Println("This shouldn't happen:", err.Error()) - os.Exit(1) + fmt.Println(os.Stderr, "Issue this shouldn't happen:", err) } // post the files @@ -82,22 +60,21 @@ func main() { return err } - // with exp backoff ! - err = backoff.Retry(upload, backoff.NewExponentialBackOff()) - if err != nil { - fmt.Println("error posting files to the sandbox: ", err.Error()) - } + // with exp backoff + err = upload + fmt.Println(os.Stderr, "Issue posting files to the sandbox: ", err) + // TODO: backoff of 4xx and 5xx doesn't make sense + // err = backoff.Retry(upload, backoff.NewExponentialBackOff()) + // if err != nil { + // fmt.Println(os.Stderr, "Issue posting files to the sandbox: ", err) + // } } } // Now lets tell the server we have uploaded all the things. sandbox, err := client.Sandboxes.Put(postResp.ID) if err != nil { - fmt.Println("Error commiting sandbox: ", err.Error()) - os.Exit(1) + fmt.Fprintln(os.Stderr, "Issue commiting sandbox: ", err) } - - // and heres yer commited sandbox - spew.Dump(sandbox) - + fmt.Printf("Resulting sandbox %+v\n", sandbox) } diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/search/search.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/search/search.go new file mode 100644 index 0000000..1935107 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/search/search.go @@ -0,0 +1,108 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "time" + + "github.com/go-chef/chef" + "chefapi_test/testapi" +) + +func main() { + // Add nodes + client := testapi.Client() + addNodes(client) + // Give the nodes time to end up in all of the data bases. An immediate search will show no nodes + time.Sleep(10 * time.Second) + + // TODO: Search limit is hardcoded to 1000, figure out how to do paging and to set the limit + + // List Indexes + indexes, err := client.Search.Indexes() + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing indexes ", err) + } + fmt.Printf("List indexes %+v EndIndex\n", indexes) + + // build an invalid seach query + query, err := client.Search.NewQuery("node", "name") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue building invalid query", err) + } + + // build a seach query + query, err = client.Search.NewQuery("node", "name:node*") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue building query ", err) + } + fmt.Printf("List new query %+v\n", query) + + // Run the query + res, err := query.Do(client) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue running query ", err) + } + fmt.Printf("List nodes from query %+v\n", res) + + // You can also use the service to run a query + res, err = client.Search.Exec("node", "name:node1") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue running Search.Exec() ", err) + } + fmt.Printf("List nodes from Exec query %+v\n", res) + // dump out results back in json as an example + fmt.Println("JSON output example") + jsonData, err := json.MarshalIndent(res, "", "\t") + os.Stdout.Write(jsonData) + os.Stdout.WriteString("\n") + + res, err = client.Search.Exec("node", "name:*") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue running Search.Exec() ", err) + } + fmt.Printf("List nodes from all nodes Exec query %+v\n", res) + + // Partial search + part := make(map[string]interface{}) + part["name"] = []string{"name"} + pres, err := client.Search.PartialExec("node", "*:*", part) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue running Search.PartialExec()", err) + } + fmt.Printf("List nodes from partial search %+v\n", pres) + + // Clean up nodes + deleteNodes(client) +} + +func addNodes(client *chef.Client) { + for i := 0; i < 2; i++ { + node := chef.NewNode("node" + fmt.Sprintf("%d", i)) + _, err := client.Nodes.Post(node) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue adding node", node, err) + } + bode := chef.NewNode("bode" + fmt.Sprintf("%d", i)) + _, err = client.Nodes.Post(bode) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue adding node", node, err) + } + } + return +} + +func deleteNodes(client *chef.Client) { + for i := 0; i < 2; i++ { + err := client.Nodes.Delete("node" + fmt.Sprintf("%d", i)) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting node", err) + } + err = client.Nodes.Delete("bode" + fmt.Sprintf("%d", i)) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting node", err) + } + } + return +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/search/search_pagination.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/search/search_pagination.go new file mode 100644 index 0000000..22cb516 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/search/search_pagination.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/go-chef/chef" + "chefapi_test/testapi" +) + +func main() { + // Add nodes + client := testapi.Client() + addNodes(client) + // Give the nodes time to end up in the search data bases. An immediate search will show no nodes + time.Sleep(10 * time.Second) + + // Stanard search + res, err := client.Search.Exec("node", "name:node*") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue running Search.Exec() ", err) + } + fmt.Printf("List nodes from Exec query Total:%+v\n", res.Total) + + // Partial search + part := make(map[string]interface{}) + part["name"] = []string{"name"} + pres, err := client.Search.PartialExec("node", "*:*", part) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue running Search.PartialExec()", err) + } + fmt.Printf("List nodes from partial search Total:%+v\n", pres.Total) + + // Clean up nodes + deleteNodes(client) +} + +func addNodes(client *chef.Client) { + for i := 0; i < 1200; i++ { + node := chef.NewNode("node" + fmt.Sprintf("%d", i)) + _, err := client.Nodes.Post(node) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue adding node", node, err) + } + } + return +} + +func deleteNodes(client *chef.Client) { + for i := 0; i < 1200; i++ { + err := client.Nodes.Delete("node" + fmt.Sprintf("%d", i)) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting node", err) + } + } + return +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/status/status.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/status/status.go new file mode 100644 index 0000000..b88b642 --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/status/status.go @@ -0,0 +1,22 @@ +// +// Test the go-chef/chef chef server api /_status endpoints against a live server +// +package main + +import ( + "fmt" + "chefapi_test/testapi" +) + + +// main Exercise the chef server api +func main() { + // Create a client for access + client := testapi.Client() + + status, err := client.Status.Get() + if err != nil { + fmt.Println("Issue getting status information", err) + } + fmt.Printf("List status: %+v", status) +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/universe/universe.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/universe/universe.go new file mode 100644 index 0000000..6232c9f --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/universe/universe.go @@ -0,0 +1,22 @@ +// +// Test the go-chef/chef chef server api /universe endpoints against a live server +// +package main + +import ( + "fmt" + "chefapi_test/testapi" +) + + +// main Exercise the chef server api +func main() { + // Create a client for access + client := testapi.Client() + + universe, err := client.Universe.Get() + if err != nil { + fmt.Println("Issue getting universe information", err) + } + fmt.Printf("List universe: %+v", universe) +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/user/user.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/user/user.go new file mode 100644 index 0000000..1469e0e --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/user/user.go @@ -0,0 +1,182 @@ +// +// Test the go-chef/chef chef server api /users endpoints against a live chef server +// +package main + +import ( + "fmt" + "github.com/go-chef/chef" + "chefapi_test/testapi" + "os" +) + + +// main Exercise the chef server api +func main() { + client := testapi.Client() + + // Create a new private key when adding the user + usr1 := chef.User{ UserName: "usr1", + Email: "user1@domain.io", + FirstName: "user1", + LastName: "fullname", + DisplayName: "User1 Fullname", + Password: "Logn12ComplexPwd#", + CreateKey: true, + } + + // Supply a public key + usr2 := chef.User{ UserName: "usr2", + Email: "user2@domain.io", + FirstName: "user2", + LastName: "lastname", + DisplayName: "User2 Lastname", + Password: "Logn12ComplexPwd#", + PublicKey: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoYyN0AIhUh7Fw1+gQtR+ \n0/HY3625IUlVheoUeUz3WnsTrUGSSS4fHvxUiCJlNni1sQvcJ0xC9Bw3iMz7YVFO\nWz5SeKmajqKEnNywN8/NByZhhlLdBxBX/UN04/7aHZMoZxrrjXGLcyjvXN3uxyCO\nyPY989pa68LJ9jXWyyfKjCYdztSFcRuwF7tWgqnlsc8pve/UaWamNOTXQnyrQ6Dp\ndn+1jiNbEJIdxiza7DJMH/9/i/mLIDEFCLRPQ3RqW4T8QrSbkyzPO/iwaHl9U196\n06Ajv1RNnfyHnBXIM+I5mxJRyJCyDFo/MACc5AgO6M0a7sJ/sdX+WccgcHEVbPAl\n1wIDAQAB \n-----END PUBLIC KEY-----\n\n", + } + + err := deleteUser(client, "usr2") + fmt.Println("Delete usr2", err) + + // Neither PublicKey nor CreateKey specified + usr3 := chef.User{ UserName: "usr3", + Email: "user3@domain.io", + FirstName: "user3", + LastName: "lastname", + DisplayName: "User3 Lastname", + Password: "Logn12ComplexPwd#", + } + + // Users + userList := listUsers(client) + fmt.Printf("List initial users %+v EndInitialList\n", userList) + + userout := getUser(client, "pivotal") + fmt.Printf("Pivotal user %+v\n", userout) + + userResult := createUser(client, usr1) + fmt.Printf("Add usr1 %+v\n", userResult) + + userResult = createUser(client, usr2) + fmt.Printf("Add usr2 %+v\n", userResult) + + err = deleteUser(client, "usr2") + fmt.Println("Delete usr2", err) + + userResult = createUser(client, usr3) + fmt.Printf("Add usr3 %+v\n", userResult) + + err = deleteUser(client, "usr3") + fmt.Println("Delete usr3", err) + + userList = listUsers(client, "email=user1@domain.io") + fmt.Printf("Filter users %+v\n", userList) + + userVerboseOut := listUsersVerbose(client) + fmt.Printf("Verbose out %v\n", userVerboseOut) + + userResult = createUser(client, usr1) + fmt.Printf("Add usr1 again %+v\n", userResult) + + userout = getUser(client, "usr1") + fmt.Printf("Get usr1 %+v\n", userout) + + userList = listUsers(client) + fmt.Printf("List after adding %+v EndAddList\n", userList) + + userbody := chef.User{ UserName: "usr1", DisplayName: "usr1", Email: "myuser@samp.com"} + userresult := updateUser(client, "usr1", userbody) + fmt.Printf("Update usr1 partial update %+v\n", userresult) + + userout = getUser(client, "usr1") + fmt.Printf("Get usr1 after partial update %+v\n", userout) + + userbody = chef.User{ UserName: "usr1", DisplayName: "usr1", FirstName: "user", MiddleName: "mid", LastName: "name", Email: "myuser@samp.com" } + userresult = updateUser(client, "usr1", userbody) + fmt.Printf("Update usr1 full update %+v\n", userresult) + + userout = getUser(client, "usr1") + fmt.Printf("Get usr1 after full update %+v\n", userout) + + userbody = chef.User{ UserName: "usr1new", DisplayName: "usr1", FirstName: "user", MiddleName: "mid", LastName: "name", Email: "myuser@samp.com" } + userresult = updateUser(client, "usr1", userbody) + fmt.Printf("Update usr1 rename %+v\n", userresult) + + userout = getUser(client, "usr1new") + fmt.Printf("Get usr1 after rename %+v\n", userout) + + userbody = chef.User{ UserName: "usr1new", DisplayName: "usr1", FirstName: "user", MiddleName: "mid", LastName: "name", Email: "myuser@samp.com", CreateKey: true} + userresult = updateUser(client, "usr1new", userbody) + fmt.Printf("Update usr1new create key %+v\n", userresult) + + userout = getUser(client, "usr1new") + fmt.Printf("Get usr1new after create key %+v\n", userout) + + err = deleteUser(client, "usr1") + fmt.Println("Delete usr1", err) + + err = deleteUser(client, "usr1new") + fmt.Println("Delete usr1new", err) + + userList = listUsers(client) + fmt.Printf("List after cleanup %+v EndCleanupList\n", userList) +} + +// createUser uses the chef server api to create a single user +func createUser(client *chef.Client, user chef.User) chef.UserResult { + usrResult, err := client.Users.Create(user) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue creating user:", err) + } + return usrResult +} + +// deleteUser uses the chef server api to delete a single user +func deleteUser(client *chef.Client, name string) (err error) { + err = client.Users.Delete(name) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting org:", err) + } + return +} + +// getUser uses the chef server api to get information for a single user +func getUser(client *chef.Client, name string) chef.User { + userList, err := client.Users.Get(name) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing user", err) + } + return userList +} + +// listUsers uses the chef server api to list all users +func listUsers(client *chef.Client, filters ...string) map[string]string { + var filter string + if len(filters) > 0 { + filter = filters[0] + } + userList, err := client.Users.List(filter) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue listing users:", err) + } + return userList +} + +// listUsersVerbose uses the chef server api to list all users and return verbose output +func listUsersVerbose(client *chef.Client) map[string]chef.UserVerboseResult { + userList, err := client.Users.VerboseList() + fmt.Printf("VERBOSE LIST %+v\n", userList) + if err != nil { + fmt.Println("Issue listing verbose users:", err) + } + return userList +} + +// updateUser uses the chef server api to update information for a single user +func updateUser(client *chef.Client, username string, user chef.User) chef.UserResult { + user_update, err := client.Users.Update(username, user) + if err != nil { + fmt.Println("Issue updating user:", err) + } + return user_update +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/user/userkeys.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/user/userkeys.go new file mode 100644 index 0000000..cdb0a2f --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/cmd/user/userkeys.go @@ -0,0 +1,167 @@ +// +// Test the go-chef/chef chef server api /users/USERNAME/keys endpoints against a live chef server +// +package main + +import ( + "fmt" + "github.com/go-chef/chef" + "chefapi_test/testapi" + "os" + "strings" +) + + +// main Exercise the chef server api +func main() { + client := testapi.Client() + + // Create a new private key when adding the user + usr1 := chef.User{ UserName: "usr1", + Email: "user1@domain.io", + FirstName: "user1", + LastName: "fullname", + DisplayName: "User1 Fullname", + Password: "Logn12ComplexPwd#", + CreateKey: true, + } + + // Supply a public key + usr2 := chef.User{ UserName: "usr2", + Email: "user2@domain.io", + FirstName: "user2", + LastName: "lastname", + DisplayName: "User2 Lastname", + Password: "Logn12ComplexPwd#", + PublicKey: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoYyN0AIhUh7Fw1+gQtR+ \n0/HY3625IUlVheoUeUz3WnsTrUGSSS4fHvxUiCJlNni1sQvcJ0xC9Bw3iMz7YVFO\nWz5SeKmajqKEnNywN8/NByZhhlLdBxBX/UN04/7aHZMoZxrrjXGLcyjvXN3uxyCO\nyPY989pa68LJ9jXWyyfKjCYdztSFcRuwF7tWgqnlsc8pve/UaWamNOTXQnyrQ6Dp\ndn+1jiNbEJIdxiza7DJMH/9/i/mLIDEFCLRPQ3RqW4T8QrSbkyzPO/iwaHl9U196\n06Ajv1RNnfyHnBXIM+I5mxJRyJCyDFo/MACc5AgO6M0a7sJ/sdX+WccgcHEVbPAl\n1wIDAQAB \n-----END PUBLIC KEY-----\n\n", + } + + // Neither PublicKey nor CreateKey specified + usr3 := chef.User{ UserName: "usr3", + Email: "user3@domain.io", + FirstName: "user3", + LastName: "lastname", + DisplayName: "User3 Lastname", + Password: "Logn12ComplexPwd#", + } + + _ = createUser(client, usr1) + fmt.Printf("Add usr1\n") + _ = createUser(client, usr2) + fmt.Printf("Add usr2\n") + _ = createUser(client, usr3) + fmt.Printf("Add usr3\n") + + // User Keys + userkeys := listUserKeys(client, "usr1") + fmt.Printf("List initial user usr1 keys %+v\n", userkeys) + + userkeys = listUserKeys(client, "usr2") + fmt.Printf("List initial user usr2 keys %+v\n", userkeys) + + userkeys = listUserKeys(client, "usr3") + fmt.Printf("List initial user usr3 keys %+v\n", userkeys) + + // Add a key to a user + keyadd := chef.UserKey{ + KeyName: "newkey", + PublicKey: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoYyN0AIhUh7Fw1+gQtR+ \n0/HY3625IUlVheoUeUz3WnsTrUGSSS4fHvxUiCJlNni1sQvcJ0xC9Bw3iMz7YVFO\nWz5SeKmajqKEnNywN8/NByZhhlLdBxBX/UN04/7aHZMoZxrrjXGLcyjvXN3uxyCO\nyPY989pa68LJ9jXWyyfKjCYdztSFcRuwF7tWgqnlsc8pve/UaWamNOTXQnyrQ6Dp\ndn+1jiNbEJIdxiza7DJMH/9/i/mLIDEFCLRPQ3RqW4T8QrSbkyzPO/iwaHl9U196\n06Ajv1RNnfyHnBXIM+I5mxJRyJCyDFo/MACc5AgO6M0a7sJ/sdX+WccgcHEVbPAl\n1wIDAQAB \n-----END PUBLIC KEY-----\n\n", + ExpirationDate: "infinity", + } + keyout, err := addUserKey(client, "usr1", keyadd) + fmt.Printf("Add usr1 key %+v\n", keyout) + // List the user keys after adding + userkeys = listUserKeys(client, "usr1") + fmt.Printf("List after add usr1 keys %+v\n", userkeys) + + // Add a defaultkey to user usr3 + keyadd.KeyName = "default" + keyout, err = addUserKey(client, "usr3", keyadd) + fmt.Printf("Add usr3 key %+v\n", keyout) + // List the user keys after adding + userkeys = listUserKeys(client, "usr3") + fmt.Printf("List after add usr3 keys %+v\n", userkeys) + + // Get key detail + keydetail, err := client.Users.GetUserKey("usr1", "default") + if err != nil { + fmt.Fprintf(os.Stderr, "Error displaying key detail %+v\n", err) + } + keyfold := strings.Replace(fmt.Sprintf("%+v", keydetail), "\n","",-1) + fmt.Printf("Key detail usr1 default %+v\n", keyfold) + + // update a key + keyadd.KeyName = "default" + keyupdate, err := client.Users.UpdateUserKey("usr1", "default", keyadd) + if err != nil { + fmt.Fprintf(os.Stderr, "Error updating usr1 default key%+v\n", err) + } + keyfold = strings.Replace(fmt.Sprintf("%+v", keyupdate), "\n","",-1) + fmt.Printf("Key update output usr1 default %+v\n", keyfold) + // Get key detail after update + keydetail, err = client.Users.GetUserKey("usr1", "default") + if err != nil { + fmt.Fprintf(os.Stderr, "Error displaying key detail %+v\n", err) + } + keyfold = strings.Replace(fmt.Sprintf("%+v", keydetail), "\n","",-1) + fmt.Printf("Updated key detail usr1 default %+v\n", keyfold) + + // delete the key + keydel, err := client.Users.DeleteUserKey("usr1", "default") + if err != nil { + fmt.Fprintf(os.Stderr, "Error deleting key %+v\n", err) + } + keyfold = strings.Replace(fmt.Sprintf("%+v", keydel), "\n","",-1) + fmt.Printf("List delete result usr1 keys %+v\n", keyfold) + // list the key after delete - expect 404 + keydetail, err = client.Users.GetUserKey("usr1", "default") + if err != nil { + fmt.Fprintf(os.Stderr, "Error displaying key detail %+v\n", err) + } + fmt.Printf("Deleted key detail usr1 default %+v\n", keydetail) + + // Delete the users + err = deleteUser(client, "usr1") + fmt.Printf("Delete usr1 %+v\n", err) + err = deleteUser(client, "usr2") + fmt.Printf("Delete usr2 %+v\n", err) + err = deleteUser(client, "usr3") + fmt.Printf("Delete usr3 %+v\n", err) + +} + +// listUserKeys uses the chef server api to show the keys for a user +func listUserKeys(client *chef.Client, name string) (userkeys []chef.UserKeyItem) { + userkeys, err := client.Users.ListUserKeys(name) + if err != nil { + fmt.Fprintf(os.Stderr, "Issue showing keys for user %s: %+v\n", name, err) + } + return userkeys +} + +// addUserKey uses the chef server api to add a key to user +func addUserKey(client *chef.Client, name string, keyadd chef.UserKey) (userkey chef.UserKeyItem, err error) { + userkey, err = client.Users.AddUserKey(name, keyadd) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting org:", err) + } + return +} + +// createUser uses the chef server api to create a single user +func createUser(client *chef.Client, user chef.User) chef.UserResult { + usrResult, err := client.Users.Create(user) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue creating user:", err) + } + return usrResult +} + +// deleteUser uses the chef server api to delete a single user +func deleteUser(client *chef.Client, name string) (err error) { + err = client.Users.Delete(name) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting org:", err) + } + return +} diff --git a/examples/chefapi_examples/files/default/go/src/chefapi_test/testapi/testapi.go b/examples/chefapi_examples/files/default/go/src/chefapi_test/testapi/testapi.go new file mode 100644 index 0000000..82ab37e --- /dev/null +++ b/examples/chefapi_examples/files/default/go/src/chefapi_test/testapi/testapi.go @@ -0,0 +1,79 @@ +// +// Test the go-chef/chef chef server api /organizations endpoints against a live server +// +package testapi + +import ( + "crypto/x509" + "fmt" + "io/ioutil" + "log" + "os" + "strconv" + + chef "github.com/go-chef/chef" +) + +// main Exercise the chef server api +func Client() *chef.Client { + // Pass in the database and chef-server api credentials. + user := os.Args[1] + keyfile := os.Args[2] + chefurl := os.Args[3] + skipssl, err := strconv.ParseBool(os.Args[4]) + if err != nil { + skipssl = true + } + + // Create a client for access + return buildClient(user, keyfile, chefurl, skipssl) +} + +// buildClient creates a connection to a chef server using the chef api. +// goiardi uses port 4545 by default, chef-zero uses 8889, chef-server uses 443 +func buildClient(user string, keyfile string, baseurl string, skipssl bool) *chef.Client { + key := clientKey(keyfile) + client, err := chef.NewClient(&chef.Config{ + Name: user, + Key: string(key), + BaseURL: baseurl, + SkipSSL: skipssl, + RootCAs: chefCerts(), + }) + + if err != nil { + fmt.Fprintln(os.Stderr, "Issue setting up client:", err) + os.Exit(1) + } + return client +} + +// clientKey reads the pem file containing the credentials needed to use the chef client. +func clientKey(filepath string) string { + key, err := ioutil.ReadFile(filepath) + if err != nil { + fmt.Fprintln(os.Stderr, "Couldn't read key.pem:", err) + os.Exit(1) + } + return string(key) +} + +// chefCerts creats a cert pool for the self signed certs +// reference https://forfuncsake.github.io/post/2017/08/trust-extra-ca-cert-in-go-app/ +func chefCerts() *x509.CertPool { + const localCertFile = "/var/opt/opscode/nginx/ca/localhost.crt" + certPool, _ := x509.SystemCertPool() + if certPool == nil { + certPool = x509.NewCertPool() + } + // Read in the cert file + certs, err := ioutil.ReadFile(localCertFile) + if err != nil { + log.Fatalf("Failed to append %q to RootCAs: %v", localCertFile, err) + } + // Append our cert to the system pool + if ok := certPool.AppendCertsFromPEM(certs); !ok { + log.Println("No certs appended, using system certs only") + } + return certPool +} diff --git a/examples/chefapi_examples/kitchen.yml b/examples/chefapi_examples/kitchen.yml new file mode 100644 index 0000000..0ee1590 --- /dev/null +++ b/examples/chefapi_examples/kitchen.yml @@ -0,0 +1,24 @@ +--- +driver: + name: vagrant + +provisioner: + name: chef_zero + +verifier: + name: inspec + +platforms: + - name: ubuntu-18.04 + +suites: + - name: default + run_list: + - recipe[chefapi_examples::setup] + - recipe[chefapi_examples::default] + - recipe[chefapi_examples::chef_objects] + - recipe[chefapi_examples::chefapi] + attributes: + chef-server: + accept_license: true + repo_version: <%= `git rev-parse --abbrev-ref HEAD` %> diff --git a/examples/chefapi_examples/metadata.rb b/examples/chefapi_examples/metadata.rb new file mode 100644 index 0000000..de79d08 --- /dev/null +++ b/examples/chefapi_examples/metadata.rb @@ -0,0 +1,9 @@ +name 'chefapi_examples' +maintainer 'go-chef/chef project maintainers' +license 'All Rights Reserved' +description 'Test the chef server api' +version '0.1.0' +chef_version '>= 13' +depends 'chef-server' +depends 'fileutils' +depends 'line' diff --git a/examples/chefapi_examples/recipes/chef_objects.rb b/examples/chefapi_examples/recipes/chef_objects.rb new file mode 100644 index 0000000..60c69c9 --- /dev/null +++ b/examples/chefapi_examples/recipes/chef_objects.rb @@ -0,0 +1,59 @@ +# Add chef objects to the server for testing + +execute 'Set the host name' do + command 'hostname testhost' +end + +# Create an organization + +execute 'create test organization' do + command '/opt/opscode/bin/chef-server-ctl org-create test test_org' + not_if '/opt/opscode/bin/chef-server-ctl org-list |grep test' +end + +execute 'get the ssl certificate for the chef server' do + command 'knife ssl fetch' + not_if { ::File.exist? '/root/.chef/trusted_certs/testhost' } +end + +# Register this node with the server +directory '/etc/chef' +file '/etc/chef/client.rb' do + content ' +chef_server_url "https://testhost/organizations/test" +client_fork true +file_backup_path "/var/chef/backup" +file_cache_path "/var/chef/cache" +log_location "/var/log/chef/client.log" +nodename "testhost" +validation_client_name "pivotal" +validation_key "/etc/opscode/pivotal.pem" +trusted_certs_dir "/root/.chef/trusted_certs" +ohai.disabled_plugins = [:C,:Cloud,:Rackspace,:Eucalyptus,:Command,:DMI,:Erlang,:Groovy,:IpScopes,:Java,:Lua,:Mono,:NetworkListeners,:Passwd,:Perl,:PHP,:Python] +' +end + +directory '/fixtures/bin' do + recursive true +end + +directory '/fixtures/chef/cookbooks' do + recursive true +end + +cookbook_file '/fixtures/bin/chef_objects.sh' do + source 'chef_objects.sh' + mode '0755' +end + +remote_directory '/fixtures/chef/cb' do + source 'cb' +end + +directory '/var/log/chef' do + recursive true +end + +directory '/var/chef' do + recursive true +end diff --git a/examples/chefapi_examples/recipes/chefapi.rb b/examples/chefapi_examples/recipes/chefapi.rb new file mode 100644 index 0000000..1fa478d --- /dev/null +++ b/examples/chefapi_examples/recipes/chefapi.rb @@ -0,0 +1,36 @@ +# recipe chef_tester::chefapi +# + +package 'git' + +package 'golang' + +directory '/go/src/github.com/go-chef' do + recursive true +end + +directory '/go/src/github.com/cenkalti' do + recursive true +end + +git '/go/src/github.com/go-chef/chef' do + revision node['repo_version'] + repository 'https://github.com/go-chef/chef.git' + action :sync +end + +git '/go/src/github.com/cenkalti/backoff' do + repository 'https://github.com/cenkalti/backoff' +end + +remote_directory 'local_go' do + files_backup false + path '/go' + purge false + recursive true + source 'go' +end + +fileutils '/go/src/chefapi_test/bin' do + file_mode ['+x'] +end diff --git a/examples/chefapi_examples/recipes/default.rb b/examples/chefapi_examples/recipes/default.rb new file mode 100644 index 0000000..3504234 --- /dev/null +++ b/examples/chefapi_examples/recipes/default.rb @@ -0,0 +1,7 @@ +# +# Cookbook:: chefapi_examples +# Recipe:: default +# +# Copyright:: 2019, The Authors, All Rights Reserved. + +include_recipe 'chef-server' diff --git a/examples/chefapi_examples/recipes/setup.rb b/examples/chefapi_examples/recipes/setup.rb new file mode 100644 index 0000000..cc5a439 --- /dev/null +++ b/examples/chefapi_examples/recipes/setup.rb @@ -0,0 +1,49 @@ +directory '/etc/chef/accepted_licenses' do + recursive true +end + +file '/etc/chef/accepted_licenses/chef_infra_client' do + content "--- +id: infra-client +name: Chef Infra Client +date_accepted: '2019-10-20T14:46:27+00:00' +accepting_product: infra-server +accepting_product_version: 0.6.0 +user: vagrant +file_format: 1" +end + +file '/etc/chef/accepted_licenses/chef_infra_server' do + content "--- + id: infra-server + name: Chef Infra Server + date_accepted: '2019-10-20T14:46:27+00:00' + accepting_product: infra-server + accepting_product_version: 0.6.0 + user: vagrant + file_format: 1" +end + +file '/etc/chef/accepted_licenses/inspec' do + content "--- +id: inspec +name: Chef InSpec +date_accepted: '2019-10-20T14:46:27+00:00' +accepting_product: infra-server +accepting_product_version: 0.6.0 +user: vagrant +file_format: 1" +end + +apt_update 'update' + +execute 'apt upgrade' do + command 'DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" dist-upgrade' + ignore_failure true +end + +replace_or_add 'testhost for cert compatibility' do + path '/etc/hosts' + pattern '^127.0.0.1' + line '127.0.0.1 localhost testhost' +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/association_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/association_spec.rb new file mode 100644 index 0000000..52ff0af --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/association_spec.rb @@ -0,0 +1,17 @@ +# Inspec tests for the associations chef api go module +# +describe command('/go/src/chefapi_test/bin/association') do + its('stderr') { should match(/^Issue inviting a user \{User:nouser\} .* 404/) } + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stderr') { should_not match(/testbook/) } + its('stderr') { should_not match(/sampbook/) } + its('stdout') { should match(%r{^Invited user \{User:usrinvite\} \{Uri:https://localhost/organizations/test/association_requests/[a-f0-9]+ OrganizationUser:\{UserName:pivotal\} Organization:\{Name:test\} User:\{Email:usrauth@domain.io FirstName:usr\}\}}) } + its('stdout') { should match(%r{^Invited user \{User:usr2invite\} \{Uri:https://localhost/organizations/test/association_requests/[a-f0-9]+ OrganizationUser:\{UserName:pivotal\} Organization:\{Name:test\} User:\{Email:usr22auth@domain.io FirstName:usr22\}\}}) } + its('stdout') { should match(/^Invitation id for usr2invite [a-f0-9]+/) } + its('stdout') { should match(/^Invitation list \[(?=.*\{Id:[a-f0-9]+ UserName:usr2invite\})(?=.*\{Id:[a-f0-9]+ UserName:usrinvite\})/) } + its('stdout') { should match(/^Deleted invitation [a-f0-9]+ for usrinvite \{Id:[a-f0-9]+ Orgname:test Username:usrinvite\}/) } + its('stdout') { should match(/^User added: \{Username:usradd\}/) } + its('stdout') { should match(/^Users list: \[\{User:\{Username:usradd\}\}\]/) } + its('stdout') { should match(/^User details: \{Username:usradd Email:usradd@domain.io DisplayName:UserAdd Fullname FirstName:usr LastName:add PublicKey:/) } + its('stdout') { should match(/^User deleted: \{Username:usradd Email:usradd@domain.io DisplayName:UserAdd Fullname FirstName:usr LastName:add PublicKey:/) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/authenticate.rb b/examples/chefapi_examples/test/integration/default/inspec/authenticate.rb new file mode 100644 index 0000000..f739bcf --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/authenticate.rb @@ -0,0 +1,8 @@ +# Inspec tests for the authenticate_user chef api go module +# + +describe command('/go/src/chefapi_test/bin/authenticate') do + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(/^Authenticate with a valid password \/) } + its('stdout') { should match(%r{^Authenticate with an invalid password POST https://localhost/authenticate_user: 401}) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/cookbook_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/cookbook_spec.rb new file mode 100644 index 0000000..243ea2a --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/cookbook_spec.rb @@ -0,0 +1,19 @@ +# Inspec tests for the cookbook chef api go module +# +describe command('/go/src/chefapi_test/bin/cookbook') do + its('stderr') { should match(%r{^Issue getting cookbook nothere: GET https://localhost/organizations/test/cookbooks/nothere: 404}) } + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stderr') { should_not match(/testbook/) } + its('stderr') { should_not match(/sampbook/) } + its('stderr') { should_not match(/Issue loading/) } + its('stdout') { should match(%r{^List initial cookbooks (?=.*sampbook => https://localhost/organizations/test/cookbooks/sampbook\n\s*\* 0.2.0)(?=.*testbook => https://localhost/organizations/test/cookbooks/testbook\n\s*\* 0.2.0).*EndInitialList}m) } + # output from get cookbook is odd + its('stdout') { should match(/^Get cookbook testbook/) } + its('stdout') { should match(%r{^Get cookbook versions testbook testbook => https://localhost/organizations/test/cookbooks/testbook\n\s*\* 0.2.0\n\s*\* 0.1.0}m) } + its('stdout') { should match(%r{^Get cookbook versions sampbook sampbook => https://localhost/organizations/test/cookbooks/sampbook\n\s*\* 0.2.0\n\s*\* 0.1.0}m) } + its('stdout') { should match(/^Get specific cookbook version testbook {CookbookName:testbook/) } + its('stdout') { should match(/^Get all recipes \[sampbook testbook\]/) } + its('stdout') { should match(/^Delete testbook 0.1.0 /) } + its('stdout') { should match(%r{^Final cookbook list sampbook => https://localhost/organizations/test/cookbooks/sampbook\n\s*\* 0.2.0}m) } + its('stdout') { should match(%r{^Final cookbook versions sampbook sampbook => https://localhost/organizations/test/cookbooks/sampbook\n\s*\* 0.2.0\n\s*\* 0.1.0}m) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/databag_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/databag_spec.rb new file mode 100644 index 0000000..e9a262e --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/databag_spec.rb @@ -0,0 +1,19 @@ +# Inspec tests for the databag chef api go module +# + +describe command('/go/src/chefapi_test/bin/databag') do + its('stderr') { should match(%r{^Issue recreating databag1. POST https://localhost/organizations/test/data: 409}) } + its('stderr') { should match(%r{^Issue getting nothere. GET https://localhost/organizations/test/data/nothere: 404}) } + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(/^List initial databags\s*$/) } + its('stdout') { should match(%r{^Added databag1 \&\{https://localhost/organizations/test/data/databag1\}}) } + its('stdout') { should match(%r{^List databags after adding databag1 databag1 => https://localhost/organizations/test/data/databag1}) } + its('stdout') { should match(/^Create databag1::item1 \/) } + its('stdout') { should match(/^Update databag1::item1 \/) } + its('stdout') { should match(%r{^List databag1 items item1 => https://localhost/organizations/test/data/databag1/item1}) } + its('stdout') { should match(/^Get databag1::item1 map\[id:item1 type:password value:next\]/) } + its('stdout') { should match(/^Delete databag1::item1 \/) } + its('stdout') { should match(/^List databag1 items after delete/) } + its('stdout') { should match(/^Delete databag1 \&\{Name:databag1 JsonClass:Chef::DataBag ChefType:data_bag/) } + its('stdout') { should match(/^List databags after cleanup\s*$/) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/group_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/group_spec.rb new file mode 100644 index 0000000..35f7b7a --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/group_spec.rb @@ -0,0 +1,14 @@ +# Inspec tests for the group chef api go module +# + +describe command('/go/src/chefapi_test/bin/group') do + its('stderr') { should match(%r{^Issue recreating group1. POST https://localhost/organizations/test/groups: 409}) } + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(%r{^List initial groups map\[(?=.*admins:https://localhost/organizations/test/groups/admins)(?=.*billing-admins:https://localhost/organizations/test/groups/billing-admins)(?=.*clients:https://localhost/organizations/test/groups/clients)(?=.*users:https://localhost/organizations/test/groups/users)(?=.*public_key_read_access:https://localhost/organizations/test/groups/public_key_read_access).*\]EndInitialList}) } + its('stdout') { should match(%r{^Added group1 \&\{https://localhost/organizations/test/groups/group1\}}) } + its('stdout') { should match(%r{^List groups after adding group1 map\[(?=.*group1:https://localhost/organizations/test/groups/group1)(?=.*admins:https://localhost/organizations/test/groups/admins)(?=.*billing-admins:https://localhost/organizations/test/groups/billing-admins)(?=.*clients:https://localhost/organizations/test/groups/clients)(?=.*users:https://localhost/organizations/test/groups/users)(?=.*public_key_read_access:https://localhost/organizations/test/groups/public_key_read_access).*\]EndAddList}) } + its('stdout') { should match(/^Get group1 \{Name:group1 GroupName:group1 OrgName:test Actors:\[\] Clients:\[\] Groups:\[\] Users:\[\]\}/) } + its('stdout') { should match(/^Update group1 \{Name:group1 GroupName:group1-new OrgName: Actors:\[\] Clients:\[\] Groups:\[\] Users:\[pivotal\]\}/) } + its('stdout') { should match(/^Delete group1 /) } + its('stdout') { should match(%r{^List groups after cleanup map\[(?=.*admins:https://localhost/organizations/test/groups/admins)(?=.*billing-admins:https://localhost/organizations/test/groups/billing-admins)(?=.*clients:https://localhost/organizations/test/groups/clients)(?=.*users:https://localhost/organizations/test/groups/users)(?=.*public_key_read_access:https://localhost/organizations/test/groups/public_key_read_access).*\]EndFinalList}) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/license.rb b/examples/chefapi_examples/test/integration/default/inspec/license.rb new file mode 100644 index 0000000..43649c0 --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/license.rb @@ -0,0 +1,8 @@ +# Inspec tests for the license chef api go module +# + +describe command('/go/src/chefapi_test/bin/license') do + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stderr') { should_not match(/Issue/) } + its('stdout') { should match(%r{^List license: {LimitExceeded:false NodeLicense:25 NodeCount:0 UpgradeUrl:http://www.chef.io/contact/on-premises-simple}}) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/node_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/node_spec.rb new file mode 100644 index 0000000..3ce6fc9 --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/node_spec.rb @@ -0,0 +1,16 @@ +# Inspec tests for the status chef api go module +# + +describe command('/go/src/chefapi_test/bin/node') do + its('stderr') { should match(%r{^Couldn't recreate node node1. POST https://localhost/organizations/test/nodes: 409}) } + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(/^List initial nodes map\[\]$/) } + its('stdout') { should match(/^Define node1 {node1 _default.*Chef::Node \[pwn\]/) } + its('stdout') { should match(%r{^Added node1 \&\{https://localhost/organizations/test/nodes/node1\}}) } + its('stdout') { should match(%r{^List nodes after adding node1 map\[node1:https://localhost/organizations/test/nodes/node1\]}) } + its('stdout') { should match(/^Get node1 {Name:node1 Environment:_default ChefType:node AutomaticAttributes:map\[\] NormalAttributes:map\[\] DefaultAttributes:map\[\] OverrideAttributes:map\[\] JsonClass:Chef::Node RunList:\[recipe\[pwn\]\] PolicyName: PolicyGroup/) } + its('stdout') { should match(/^Update node1/) } + its('stdout') { should match(/^Get node1 after update/) } + its('stdout') { should match(/^Delete node1 /) } + its('stdout') { should match(/^List nodes after cleanup map\[\]/) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/organization_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/organization_spec.rb new file mode 100644 index 0000000..e8c7fa2 --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/organization_spec.rb @@ -0,0 +1,19 @@ +# Inspec tests for the organization chef api go module +# + +describe command('/go/src/chefapi_test/bin/organization') do + its('stderr') { should match(%r{^Issue creating org: {org1 organization1 } POST https://localhost/organizations: 409$}) } + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(/^List initial organizations map\[test.*test\]$/) } + its('stdout') { should match(/^Added org1 {org1-validator -----BEGIN RSA PRIVATE KEY-----/) } + its('stdout') { should match(/^Added org1 again { }$/) } + its('stdout') { should match(/^Added org2 {org2-validator -----BEGIN RSA PRIVATE KEY-----.*$/) } + its('stdout') { should match(/^Get org1 {org1 organization1 [0-9a-f]+}$/) } + its('stdout') { should match(%r{^List organizations After adding org1 and org2 map(?=.*org2:https://localhost/organizations/org2)(?=.*test:https://localhost/organizations/test)(?=.*org1:https://localhost/organizations/org1)}) } + its('stdout') { should match(/^Update org1 {org1 new_organization1 }/) } + its('stdout') { should match(/^Get org1 after update {org1 new_organization1 [0-9a-f]+}/) } + its('stdout') { should match(/^Delete org2 /) } + its('stdout') { should match(%r{^List organizations after deleting org2 map(?=.*test:https://localhost/organizations/test)(?=.*org1:https://localhost/organizations/org1)}) } + its('stdout') { should match(/^Result from deleting org1 /) } + its('stdout') { should match(%r{^List organizations after cleanup map\[test:https://localhost/organizations/test\]}) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/sandbox_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/sandbox_spec.rb new file mode 100644 index 0000000..39877ad --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/sandbox_spec.rb @@ -0,0 +1,11 @@ +# Inspec tests for the sandbox chef api go module +# + +describe command('/go/src/chefapi_test/bin/sandbox') do + # TODO: Get the sandbox sample code to work - upload files is failing + + # its('stderr') { should_not match(%r{Issue}) } + # its('stderr') { should_not match(%r{error|no such file|cannot find|not used|undefined}) } + # its('stdout') { should match(/^Create sandboxes /) } + # its('stdout') { should match(/^Resulting sandboxes /) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/search_paginate_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/search_paginate_spec.rb new file mode 100644 index 0000000..344fdc5 --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/search_paginate_spec.rb @@ -0,0 +1,8 @@ +# Inspec tests for the search chef api go module +# + +describe command('/go/src/chefapi_test/bin/search_pagination') do + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(/^List nodes from Exec query Total:1200/) } + its('stdout') { should match(/^List nodes from partial search Total:1200/) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/search_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/search_spec.rb new file mode 100644 index 0000000..0b8450b --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/search_spec.rb @@ -0,0 +1,14 @@ +# Inspec tests for the search chef api go module +# + +describe command('/go/src/chefapi_test/bin/search') do + its('stderr') { should match(/^Issue building invalid query statement is malformed/) } + its('stderr') { should_not match(/node/) } + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(%r{^List indexes map\[(?=.*node:https://localhost/organizations/test/search/node)(?=.*role:https://localhost/organizations/test/search/role)(?=.*client:https://localhost/organizations/test/search/client)(?=.*environment:https://localhost/organizations/test/search/environment).*\] EndIndex}) } + its('stdout') { should match(/^List new query node\?q=name:node\*\&rows=1000\&sort=X_CHEF_id_CHEF_X asc\&start=0/) } + its('stdout') { should match(/^List nodes from query \{Total:2 Start:0 Rows:\[/) } + its('stdout') { should match(/^List nodes from Exec query \{Total:1 Start:0 Rows:\[/) } + its('stdout') { should match(/^List nodes from all nodes Exec query \{Total:4 Start:0 Rows:\[/) } + its('stdout') { should match(/^List nodes from partial search \{Total:4 Start:0 Rows:\[/) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/status_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/status_spec.rb new file mode 100644 index 0000000..34c6d30 --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/status_spec.rb @@ -0,0 +1,7 @@ +# Inspec tests for the status chef api go module +# + +describe command('/go/src/chefapi_test/bin/status') do + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(/List status: \{Status:pong Upstreams:map\[(?=.*chef_solr:pong)(?=.*chef_sql:pong)(?=.*chef_index:pong)(?=.*oc_chef_action:pong)(?=.*oc_chef_authz:pong).*\].*Keygen:map\[(?=.*keys:10)(?=.*max:10)(?=.*max_workers:1)(?=.*cur_max_workers:1)(?=.*inflight:0)(?=.*avail_workers:1)(?=.*start_size:0).*\]\}/) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/universe_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/universe_spec.rb new file mode 100644 index 0000000..09df3cd --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/universe_spec.rb @@ -0,0 +1,7 @@ +# Inspec tests for the universe chef api go module +# + +describe command('/go/src/chefapi_test/bin/universe') do + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(%r{^List universe: \{Books:map\[sampbook:\{Versions:map\[(?=.*0.2.0:\{LocationPath:https:\/\/localhost\/organizations\/test\/cookbooks\/sampbook\/0.2.0 LocationType:chef_server Dependencies:map\[\]\})(?=.*0.1.0:\{LocationPath:https:\/\/localhost\/organizations\/test\/cookbooks\/sampbook\/0.1.0 LocationType:chef_server Dependencies:map\[\]\}).*\]\}\]\}}) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/user_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/user_spec.rb new file mode 100644 index 0000000..cf9727b --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/user_spec.rb @@ -0,0 +1,30 @@ +# Inspec tests for the user chef api go module +# + +describe command('/go/src/chefapi_test/bin/user') do + its('stderr') { should match(%r{^Issue creating user: POST https://localhost/users: 409}) } + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stdout') { should match(%r{^List initial users map\[(?=.*pivotal:https://localhost/users/pivotal).*\] EndInitialList}) } + # might want a multi line match here to test for expirationdate, key uri and privatekey + its('stdout') { should match(%r{^Add usr1 \{Uri:https://localhost/users/usr1 ChefKey:\{Name:default PublicKey:-----BEGIN}) } + its('stdout') { should match(%r{^Add usr2 \{Uri:https://localhost/users/usr2 ChefKey:\{Name:default PublicKey:-----BEGIN}) } + its('stdout') { should match(%r{^Add usr3 \{Uri:https://localhost/users/usr3 ChefKey:\{Name: PublicKey: ExpirationDate: Uri: PrivateKey:\}\}}) } + its('stdout') { should match(%r{^Filter users map\[usr1:https://localhost/users/usr1\]}) } + its('stdout') { should match(/^Verbose out map\[(?=.*pivotal:)/) } + its('stdout') { should match(/^Get usr1 \{(?=.*UserName:usr1)(?=.*DisplayName:User1 Fullname)(?=.*Email:user1@domain.io)(?=.*ExternalAuthenticationUid:)(?=.*FirstName:user1)(?=.*LastName:fullname)(?=.*MiddleName:)(?=.*Password:)(?=.*PublicKey:)(?=.*RecoveryAuthenticationEnabled:false).*/) } + its('stdout') { should match(/^Pivotal user \{(?=.*UserName:pivotal)(?=.*DisplayName:Chef Server Superuser)(?=.*Email:root@localhost.localdomain)(?=.*ExternalAuthenticationUid:)(?=.*FirstName:Chef)(?=.*LastName:Server)(?=.*MiddleName:)(?=.*Password:)(?=.*PublicKey:)/) } + its('stdout') { should match(%r{^List after adding map\[(?=.*pivotal:https://localhost/users/pivotal)(?=.*usr1:https://localhost/users/usr1).*\] EndAddList}) } + its('stdout') { should match(/^Get usr1 \{(?=.*UserName:usr1)(?=.*DisplayName:User1 Fullname)(?=.*Email:user1@domain.io)(?=.*ExternalAuthenticationUid:)(?=.*FirstName:user1)(?=.*LastName:fullname)(?=.*MiddleName:)(?=.*Password:)(?=.*PublicKey:)/) } + its('stdout') { should match(%r{^List after adding map\[(?=.*pivotal:https://localhost/users/pivotal)(?=.*usr1:https://localhost/users/usr1).*\] EndAddList}) } + # TODO: - update and create new private key + # TODO - is admin a thing + its('stdout') { should match(%r{^Update usr1 partial update \{Uri:https://localhost/users/usr1 ChefKey:\{}) } + its('stdout') { should match(/^Get usr1 after partial update \{(UserName:usr1)(?=.*DisplayName:usr1)(?=.*Email:myuser@samp.com)(?=.*ExternalAuthenticationUid:)(?=.*FirstName:user1)(?=.*LastName:fullname)(?=.*MiddleName:)(?=.*Password:)(?=.*PublicKey:)(?=.*RecoveryAuthenticationEnabled:false).*\}/) } + its('stdout') { should match(%r{^Update usr1 full update \{Uri:https://localhost/users/usr1 ChefKey:\{Name: PublicKey: ExpirationDate: Uri: PrivateKey:\}}) } + its('stdout') { should match(/^Get usr1 after full update \{(UserName:usr1)(?=.*DisplayName:usr1)(?=.*Email:myuser@samp.com)(?=.*ExternalAuthenticationUid:)(?=.*FirstName:user)(?=.*LastName:name)(?=.*MiddleName:mid)(?=.*Password:)(?=.*PublicKey:)(?=.*RecoveryAuthenticationEnabled:false).*\}/) } + its('stdout') { should match(%r{^Update usr1 rename \{Uri:https://localhost/users/usr1new ChefKey:\{.*\}}) } + its('stdout') { should match(/^Get usr1 after rename \{(UserName:usr1new)(?=.*DisplayName:usr1)(?=.*Email:myuser@samp.com)(?=.*ExternalAuthenticationUid:)(?=.*FirstName:user)(?=.*LastName:name)(?=.*MiddleName:mid Password:)(?=.*PublicKey:)(?=.*RecoveryAuthenticationEnabled:false).*\}/) } + its('stdout') { should match(%r{^Delete usr1 DELETE https://localhost/users/usr1: 404}) } + its('stdout') { should match(/^Delete usr1new /) } + its('stdout') { should match(%r{^List after cleanup map\[(?=.*pivotal:https://localhost/users/pivotal).*\] EndCleanupList}) } +end diff --git a/examples/chefapi_examples/test/integration/default/inspec/userkeys_spec.rb b/examples/chefapi_examples/test/integration/default/inspec/userkeys_spec.rb new file mode 100644 index 0000000..71e5dfc --- /dev/null +++ b/examples/chefapi_examples/test/integration/default/inspec/userkeys_spec.rb @@ -0,0 +1,18 @@ +# Inspec tests for the user chef api go module +# + +describe command('/go/src/chefapi_test/bin/userkeys') do + its('stderr') { should_not match(/error|no such file|cannot find|not used|undefined/) } + its('stderr') { should match(%r{Error displaying key detail GET https://localhost/users/usr1/keys/default: 404}) } + its('stdout') { should match(%r{^List initial user usr1 keys \[\{KeyName:default Uri:https://localhost/users/usr1/keys/default Expired:false\}\]}) } + its('stdout') { should match(%r{^List initial user usr2 keys \[\{KeyName:default Uri:https://localhost/users/usr2/keys/default Expired:false\}\]}) } + its('stdout') { should match(/^List initial user usr3 keys \[\]/) } + its('stdout') { should match(%r{^Add usr1 key \{KeyName: Uri:https://localhost/users/usr1/keys/newkey Expired:false\}}) } + its('stdout') { should match(/^List after add usr1 keys \[\{(?=.*newkey)(?=.*default).*\}\]/) } + its('stdout') { should match(%r{^Add usr3 key \{KeyName: Uri:https://localhost/users/usr3/keys/default Expired:false\}}) } + its('stdout') { should match(/^List after add usr3 keys \[\{(?=.*default).*\}\]/) } + its('stdout') { should match(/^Key detail usr1 default \{KeyName:default/) } + its('stdout') { should match(/^Key update output usr1 default \{KeyName:default .*N0AIhUh7Fw1\+gQtR\+.*\}/) } + its('stdout') { should match(/^Updated key detail usr1 default \{KeyName:default .*N0AIhUh7Fw1\+gQtR\+.*\}/) } + its('stdout') { should match(/^List delete result usr1 keys \{KeyName:default .*N0AIhUh7Fw1\+gQtR\+.*\}/) } +end diff --git a/examples/cmd b/examples/cmd new file mode 120000 index 0000000..3f3cece --- /dev/null +++ b/examples/cmd @@ -0,0 +1 @@ +./chefapi_examples/files/default/go/src/chefapi_test/cmd \ No newline at end of file diff --git a/examples/cookbooks/cookbooks.go b/examples/cookbooks/cookbooks.go deleted file mode 100644 index d9cb1eb..0000000 --- a/examples/cookbooks/cookbooks.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "os" - - "github.com/go-chef/chef" -) - -func main() { - // simple arg parsing - //cookPath := flag.String("cookbook", "c", "Path to cookbook for upload") - // flag.Parse() - - // 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:8443", - }) - if err != nil { - fmt.Println("Issue setting up client:", err) - os.Exit(1) - } - - // List Cookbooks - cookList, err := client.Cookbooks.List() - if err != nil { - fmt.Println("Issue listing cookbooks:", err) - } - - // Print out the list - fmt.Println(cookList) - /* - *'parse' metadata... - this would prefer .json over .rb - if it's .rb lets maybe try to eval it ? - otherwise just extract name/version if they exist - */ - - /* - - - * generate sums - * create sandbox - * upload to sandbox - * - */ - -} diff --git a/examples/cookbooks/key.pem b/examples/cookbooks/key.pem deleted file mode 100644 index f44ac76..0000000 --- a/examples/cookbooks/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------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----- diff --git a/examples/inspec b/examples/inspec new file mode 120000 index 0000000..b141d77 --- /dev/null +++ b/examples/inspec @@ -0,0 +1 @@ +chefapi_examples/test/integration/default/inspec \ No newline at end of file diff --git a/examples/nodes/key.pem b/examples/nodes/key.pem deleted file mode 100644 index f44ac76..0000000 --- a/examples/nodes/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------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----- diff --git a/examples/nodes/nodes.go b/examples/nodes/nodes.go deleted file mode 100644 index 9262bec..0000000 --- a/examples/nodes/nodes.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "os" - - "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) - } - - // Create a Node object - // TOOD: should have a constructor for this - ranjib := chef.Node{ - Name: "ranjib", - Environment: "_default", - ChefType: "node", - JsonClass: "Chef::Node", - RunList: []string{"pwn"}, - } - - // Delete node ignoring errors :) - client.Nodes.Delete(ranjib.Name) - - // Create - _, err = client.Nodes.Post(ranjib) - if err != nil { - log.Fatal("Couldn't create node. ", err) - } - - // List nodes - nodeList, err := client.Nodes.List() - if err != nil { - log.Fatal("Couldn't list nodes: ", err) - } - - // dump the node list in Json - jsonData, err := json.MarshalIndent(nodeList, "", "\t") - os.Stdout.Write(jsonData) - os.Stdout.WriteString("\n") - - // dump the ranjib node we got from server in JSON! - serverNode, _ := client.Nodes.Get("ranjib") - if err != nil { - log.Fatal("Couldn't get node: ", err) - } - jsonData, err = json.MarshalIndent(serverNode, "", "\t") - os.Stdout.Write(jsonData) - os.Stdout.WriteString("\n") - - // update node - ranjib.RunList = append(ranjib.RunList, "recipe[works]") - jsonData, err = json.MarshalIndent(ranjib, "", "\t") - os.Stdout.Write(jsonData) - os.Stdout.WriteString("\n") - - _, err = client.Nodes.Put(ranjib) - if err != nil { - log.Fatal("Couldn't update node: ", err) - } - - // Delete node ignoring errors :) - client.Nodes.Delete(ranjib.Name) - -} diff --git a/examples/sandboxes/key.pem b/examples/sandboxes/key.pem deleted file mode 100644 index f44ac76..0000000 --- a/examples/sandboxes/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------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----- diff --git a/examples/search/key.pem b/examples/search/key.pem deleted file mode 100644 index f44ac76..0000000 --- a/examples/search/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------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----- diff --git a/examples/search/search.go b/examples/search/search.go deleted file mode 100644 index 0f1e16a..0000000 --- a/examples/search/search.go +++ /dev/null @@ -1,89 +0,0 @@ -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") - - // Partial search - log.Print("Partial Search") - part := make(map[string]interface{}) - part["name"] = []string{"name"} - pres, err := client.Search.PartialExec("node", "*:*", part) - if err != nil { - log.Fatal("Error running Search.PartialExec()", err) - } - - jsonData, err = json.MarshalIndent(pres, "", "\t") - os.Stdout.Write(jsonData) - os.Stdout.WriteString("\n") - -} diff --git a/go.mod b/go.mod index 6bb4eb8..898830a 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,16 @@ -module github.com/chef/go-chef +module github.com/go-chef/chef go 1.12 require ( github.com/cenkalti/backoff v2.1.1+incompatible - github.com/ctdk/goiardi v0.11.9 + github.com/ctdk/goiardi v0.11.10 github.com/davecgh/go-spew v1.1.1 - github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a + github.com/google/go-cmp v0.4.0 + github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de // indirect + github.com/r3labs/diff v0.0.0-20191120142937-b4ed99a31f5a + github.com/smartystreets/assertions v1.0.1 // indirect + github.com/smartystreets/goconvey v1.6.4 github.com/stretchr/testify v1.4.0 + gopkg.in/yaml.v2 v2.2.7 // indirect ) diff --git a/go.sum b/go.sum index 3941315..826d5c9 100644 --- a/go.sum +++ b/go.sum @@ -2,19 +2,31 @@ github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1q github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/ctdk/goiardi v0.11.9 h1:OkrfamLJkktXQGc6buUvg+VMd2L3F/63zrhJCINZE8s= github.com/ctdk/goiardi v0.11.9/go.mod h1:Pr6Cj6Wsahw45myttaOEZeZ0LE7p1qzWmzgsBISkrNI= +github.com/ctdk/goiardi v0.11.10 h1:IB/3Afl1pC2Q4KGwzmhHPAoJfe8VtU51wZ2V0QkvsL0= +github.com/ctdk/goiardi v0.11.10/go.mod h1:Pr6Cj6Wsahw45myttaOEZeZ0LE7p1qzWmzgsBISkrNI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de h1:F7WD09S8QB4LrkEpka0dFPLSotH11HRpCsLIbIcJ7sU= +github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/r3labs/diff v0.0.0-20191120142937-b4ed99a31f5a h1:2v4Ipjxa3sh+xn6GvtgrMub2ci4ZLQMvTaYIba2lfdc= +github.com/r3labs/diff v0.0.0-20191120142937-b4ed99a31f5a/go.mod h1:ozniNEFS3j1qCwHKdvraMn1WJOsUxHd7lYfukEIS4cs= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= +github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -23,6 +35,9 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/group.go b/group.go index fe0b0ff..2de3eb9 100644 --- a/group.go +++ b/group.go @@ -54,6 +54,8 @@ func (e *GroupService) Create(group Group) (data *GroupResult, err error) { // Update a group on the Chef server. // // Chef API docs: https://docs.chef.io/api_chef_server.html#groups +// Should this be name and group attributes? We might want to be +// able to change the name. func (e *GroupService) Update(g Group) (group Group, err error) { url := fmt.Sprintf("groups/%s", g.Name) body, err := JSONReader(g) diff --git a/http.go b/http.go index 971be32..39c56a2 100644 --- a/http.go +++ b/http.go @@ -20,7 +20,7 @@ import ( ) // ChefVersion that we pretend to emulate -const ChefVersion = "11.12.0" +const ChefVersion = "14.0.0" // Body wraps io.Reader and adds methods for calculating hashes and detecting content type Body struct { @@ -40,22 +40,28 @@ type Client struct { BaseURL *url.URL client *http.Client - ACLs *ACLService - Clients *ApiClientService - Cookbooks *CookbookService - DataBags *DataBagService - Environments *EnvironmentService - Groups *GroupService - Nodes *NodeService - Organizations *OrganizationService - Principals *PrincipalService - Roles *RoleService - Sandboxes *SandboxService - Search *SearchService - Users *UserService + ACLs *ACLService + Associations *AssociationService + AuthenticateUser *AuthenticateUserService + Clients *ApiClientService + Cookbooks *CookbookService + DataBags *DataBagService + Environments *EnvironmentService + Groups *GroupService + License *LicenseService + Nodes *NodeService + Organizations *OrganizationService + Principals *PrincipalService + Roles *RoleService + Sandboxes *SandboxService + Search *SearchService + Status *StatusService + Universe *UniverseService + UpdatedSince *UpdatedSinceService + Users *UserService } -// 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 +// Config contains the configuration options for a chef client. This structure is used primarily in the NewClient() constructor in order to setup a proper client object type Config struct { // This should be the user ID on the chef server Name string @@ -63,12 +69,15 @@ type Config struct { // This is the plain text private Key for the user Key string - // BaseURL is the chef server URL used to connect to. If using orgs you should include your org in the url + // BaseURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a "/" BaseURL string // When set to false (default) this will enable SSL Cert Verification. If you need to disable Cert Verification set to true SkipSSL bool + // RootCAs is a reference to x509.CertPool for TLS + RootCAs *x509.CertPool + // Time to wait in seconds before giving up on a request to the server Timeout int } @@ -132,13 +141,17 @@ func NewClient(cfg *Config) (*Client, error) { baseUrl, _ := url.Parse(cfg.BaseURL) + tlsConfig := &tls.Config{InsecureSkipVerify: cfg.SkipSSL} + if cfg.RootCAs != nil { + tlsConfig.RootCAs = cfg.RootCAs + } tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).Dial, - TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.SkipSSL}, + TLSClientConfig: tlsConfig, TLSHandshakeTimeout: 10 * time.Second, } @@ -154,17 +167,23 @@ func NewClient(cfg *Config) (*Client, error) { BaseURL: baseUrl, } c.ACLs = &ACLService{client: c} + c.AuthenticateUser = &AuthenticateUserService{client: c} + c.Associations = &AssociationService{client: c} c.Clients = &ApiClientService{client: c} c.Cookbooks = &CookbookService{client: c} c.DataBags = &DataBagService{client: c} c.Environments = &EnvironmentService{client: c} c.Groups = &GroupService{client: c} + c.License = &LicenseService{client: c} c.Nodes = &NodeService{client: c} c.Organizations = &OrganizationService{client: c} c.Principals = &PrincipalService{client: c} c.Roles = &RoleService{client: c} c.Sandboxes = &SandboxService{client: c} c.Search = &SearchService{client: c} + c.Status = &StatusService{client: c} + c.UpdatedSince = &UpdatedSinceService{client: c} + c.Universe = &UniverseService{client: c} c.Users = &UserService{client: c} return c, nil } @@ -176,12 +195,12 @@ func (c *Client) magicRequestDecoder(method, path string, body io.Reader, v inte return err } - debug("Request: %+v \n", req) + debug("\n\nRequest: %+v \n", req) res, err := c.Do(req, v) if res != nil { defer res.Body.Close() } - debug("Response: %+v \n", res) + debug("Response: %+v\n", res) if err != nil { return err } @@ -205,7 +224,7 @@ func (c *Client) NewRequest(method string, requestUrl string, body io.Reader) (* // parse and encode Querystring Values values := req.URL.Query() req.URL.RawQuery = values.Encode() - debug("Encoded url %+v", u) + debug("Encoded url %+v\n", u) myBody := &Body{body} @@ -243,16 +262,22 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { } // BUG(fujin) tightly coupled - err = CheckResponse(res) // <-- + err = CheckResponse(res) if err != nil { return res, err } + var resbuf bytes.Buffer + restee := io.TeeReader(res.Body, &resbuf) if v != nil { if w, ok := v.(io.Writer); ok { - io.Copy(w, res.Body) + io.Copy(w, restee) } else { - err = json.NewDecoder(res.Body).Decode(v) + err = json.NewDecoder(restee).Decode(v) + if debug_on() { + resbody, _ := ioutil.ReadAll(&resbuf) + debug("Response body: %+v\n", string(resbody)) + } if err != nil { return res, err } @@ -274,17 +299,18 @@ func (ac AuthConfig) SignRequest(request *http.Request) error { } vals := map[string]string{ - "Method": request.Method, - "Hashed Path": HashStr(endpoint), - "Accept": "application/json", - "X-Chef-Version": ChefVersion, - "X-Ops-Timestamp": time.Now().UTC().Format(time.RFC3339), - "X-Ops-UserId": ac.ClientName, - "X-Ops-Sign": "algorithm=sha1;version=1.0", - "X-Ops-Content-Hash": request.Header.Get("X-Ops-Content-Hash"), + "Method": request.Method, + "Hashed Path": HashStr(endpoint), + "Accept": "application/json", + "X-Chef-Version": ChefVersion, + "X-Ops-Server-API-Version": "1", + "X-Ops-Timestamp": time.Now().UTC().Format(time.RFC3339), + "X-Ops-UserId": ac.ClientName, + "X-Ops-Sign": "algorithm=sha1;version=1.0", + "X-Ops-Content-Hash": request.Header.Get("X-Ops-Content-Hash"), } - for _, key := range []string{"Method", "Accept", "X-Chef-Version", "X-Ops-Timestamp", "X-Ops-UserId", "X-Ops-Sign"} { + for _, key := range []string{"Method", "Accept", "X-Chef-Version", "X-Ops-Server-API-Version", "X-Ops-Timestamp", "X-Ops-UserId", "X-Ops-Sign"} { request.Header.Set(key, vals[key]) } diff --git a/http_test.go b/http_test.go index e3dc59a..056a47d 100644 --- a/http_test.go +++ b/http_test.go @@ -139,6 +139,10 @@ func createServer() *httptest.Server { return httptest.NewServer(http.HandlerFunc(checkHeader)) } +func createTLSServer() *httptest.Server { + return httptest.NewTLSServer(http.HandlerFunc(checkHeader)) +} + // publicKeyFromString parses an RSA public key from a string func publicKeyFromString(key []byte) (*rsa.PublicKey, error) { block, _ := pem.Decode(key) @@ -392,6 +396,69 @@ func TestRequestToEndpoint(t *testing.T) { } } +func TestTLSValidation(t *testing.T) { + ac, err := makeAuthConfig() + if err != nil { + panic(err) + } + // Self-signed server + server := createTLSServer() + defer server.Close() + + // Without RootCAs, TLS validation should fail + chefClient, _ := NewClient(&Config{ + Name: userid, + Key: privateKey, + BaseURL: server.URL, + }) + + request, err := chefClient.NewRequest("GET", server.URL, nil) + err = ac.SignRequest(request) + if err != nil { + t.Fatal("failed to generate RequestHeaders") + } + + client := chefClient.client + response, err := client.Do(request) + if err == nil { + t.Fatal("Request should fail due to TLS certification validation failure") + } + + // Success with RootCAs containing the server's certificate + certPool := x509.NewCertPool() + certPool.AddCert(server.Certificate()) + chefClient, _ = NewClient(&Config{ + Name: userid, + Key: privateKey, + BaseURL: server.URL, + RootCAs: certPool, + }) + + request, err = chefClient.NewRequest("GET", server.URL, nil) + err = ac.SignRequest(request) + if err != nil { + t.Fatal("failed to generate RequestHeaders") + } + + client = chefClient.client + response, err = client.Do(request) + if err != nil { + t.Fatal(err) + } + + if response.StatusCode != 200 { + t.Error("Non 200 return code: " + response.Status) + } + + buf := new(bytes.Buffer) + buf.ReadFrom(response.Body) + bodyStr := buf.String() + + if bodyStr != "" { + t.Error(bodyStr) + } +} + // More Goiardi <3 func assembleSignedHeader(r *http.Request) (string, error) { sHeadStore := make(map[int]string) diff --git a/license.go b/license.go new file mode 100644 index 0000000..9df09e0 --- /dev/null +++ b/license.go @@ -0,0 +1,21 @@ +package chef + +type LicenseService struct { + client *Client +} + +// License represents the body of the returned information. +type License struct { + LimitExceeded bool `json:"limit_exceeded"` + NodeLicense int `json:"node_license"` + NodeCount int `json:"node_count"` + UpgradeUrl string `json:"Upgrade_url"` +} + +// License gets license information. +// +// https://docs.chef.io/api_chef_server.html#license +func (e *LicenseService) Get() (data License, err error) { + err = e.client.magicRequestDecoder("GET", "license", nil, &data) + return +} diff --git a/license_test.go b/license_test.go new file mode 100644 index 0000000..4acb9c2 --- /dev/null +++ b/license_test.go @@ -0,0 +1,66 @@ +package chef + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "reflect" + "testing" +) + +var ( + testLicenseJSON = "test/license.json" +) + +func TestLicenseFromJSONDecoder(t *testing.T) { + if file, err := os.Open(testLicenseJSON); err != nil { + t.Error("Unexpected error '", err, "' during os.Open on", testLicenseJSON) + } else { + dec := json.NewDecoder(file) + var g License + if err := dec.Decode(&g); err == io.EOF { + log.Fatal(g) + } else if err != nil { + log.Fatal(err) + } + } +} + +func TestLicenseGet(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/license", func(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + var request License + dec.Decode(&request) + switch { + case r.Method == "GET": + fmt.Fprintf(w, `{ + "limit_exceeded": false, + "node_license": 25, + "node_count": 12, + "upgrade_url": "http://www.chef.io/contact/on-premises-simple" + }`) + } + }) + + wantLicense := License{ + LimitExceeded: false, + NodeLicense: 25, + NodeCount: 12, + UpgradeUrl: "http://www.chef.io/contact/on-premises-simple", + } + + license, err := client.License.Get() + if err != nil { + t.Errorf("License.Get returned error: %s", err.Error()) + } + if !reflect.DeepEqual(license, wantLicense) { + t.Errorf("License.Get returned %+v, want %+v", license, wantLicense) + } + +} diff --git a/release.go b/release.go index 34ac92d..f081cef 100644 --- a/release.go +++ b/release.go @@ -2,4 +2,9 @@ package chef -func debug(fmt string, args ...interface{}) {} +func debug(fmt string, args ...interface{}) { +} + +func debug_on() bool { + return false +} diff --git a/role_test.go b/role_test.go index 6ce09af..cfc9e66 100644 --- a/role_test.go +++ b/role_test.go @@ -178,17 +178,19 @@ func TestRolesService_Put(t *testing.T) { func TestRolesService_RoleListResultString(t *testing.T) { r := &RoleListResult{"foo": "http://localhost:4000/roles/foo"} + rstr := r.String() want := "foo => http://localhost:4000/roles/foo\n" - if r.String() != want { - t.Errorf("RoleListResult.String returned %+v, want %+v", r.String(), want) + if rstr != want { + t.Errorf("RoleListResult.String returned %+v, want %+v", rstr, want) } } func TestRolesService_RoleCreateResultString(t *testing.T) { r := &RoleCreateResult{"uri": "http://localhost:4000/roles/webserver"} + rstr := r.String() want := "uri => http://localhost:4000/roles/webserver\n" - if r.String() != want { - t.Errorf("RoleCreateResult.String returned %+v, want %+v", r.String(), want) + if rstr != want { + t.Errorf("RoleCreateResult.String returned %+v, want %+v", rstr, want) } } diff --git a/run_list_item.go b/run_list_item.go new file mode 100644 index 0000000..3ed26e4 --- /dev/null +++ b/run_list_item.go @@ -0,0 +1,93 @@ +package chef + +import ( + "fmt" + "regexp" +) + +const ( + qualifiedRecipe string = `^recipe\[([^\]@]+)(@([0-9]+(\.[0-9]+){1,2}))?\]$` + qualifiedRole string = `^role\[([^\]]+)\]$` + versionedUnqualifiedRecipe string = `^([^@]+)(@([0-9]+(\.[0-9]+){1,2}))$` + falseFriend string = `[\[\]]` +) + +var ( + qualifiedRecipeRegexp *regexp.Regexp = regexp.MustCompile(qualifiedRecipe) + qualifiedRoleRegexp *regexp.Regexp = regexp.MustCompile(qualifiedRole) + versionedUnqualifiedRecipeRegexp *regexp.Regexp = regexp.MustCompile(versionedUnqualifiedRecipe) + falseFriendRegexp *regexp.Regexp = regexp.MustCompile(falseFriend) +) + +// RunListItem external representation of a run list +// This module is a direct port of the Chef::RunList::RunListItem class +// see: https://github.com/chef/chef/blob/master/lib/chef/run_list/run_list_item.rb +type RunListItem struct { + Name string + Type string + Version string +} + +// NewRunListItem parses a single item from a run list and returns a structure +func NewRunListItem(item string) (rli RunListItem, err error) { + switch { + case qualifiedRecipeRegexp.MatchString(item): + // recipe[name] + // recipe[name@1.0.0] + rli.Type = "recipe" + + submatches := qualifiedRecipeRegexp.FindStringSubmatch(item) + rli.Name = submatches[1] + + if len(submatches) > 2 { + rli.Version = submatches[3] + } + case qualifiedRoleRegexp.MatchString(item): + // role[role_name] + rli.Type = "role" + + submatches := qualifiedRoleRegexp.FindStringSubmatch(item) + rli.Name = submatches[1] + case versionedUnqualifiedRecipeRegexp.MatchString(item): + // recipe_name@1.0.0 + rli.Type = "recipe" + submatches := versionedUnqualifiedRecipeRegexp.FindStringSubmatch(item) + + rli.Name = submatches[1] + + if len(submatches) > 2 { + rli.Version = submatches[3] + } + case falseFriendRegexp.MatchString(item): + // Recipe[recipe_name] + // roles[role_name] + err = fmt.Errorf("Invalid run-list item: %s", item) + return RunListItem{}, err + default: + rli.Type = "recipe" + rli.Name = item + } + + return rli, nil +} + +// String implements the String interface function +func (r RunListItem) String() (s string) { + if r.Version != "" { + s = fmt.Sprintf("%s[%s@%s]", r.Type, r.Name, r.Version) + } else { + s = fmt.Sprintf("%s[%s]", r.Type, r.Name) + } + + return s +} + +// IsRecipe Determines if the runlist item is a recipe +func (r RunListItem) IsRecipe() bool { + return r.Type == "recipe" +} + +// IsRole Determines if the runlist item is a role +func (r RunListItem) IsRole() bool { + return r.Type == "role" +} diff --git a/run_list_item_test.go b/run_list_item_test.go new file mode 100644 index 0000000..8c7a742 --- /dev/null +++ b/run_list_item_test.go @@ -0,0 +1,164 @@ +package chef + +import ( + "testing" +) + +func TestQualifiedRecipeWithVersion(t *testing.T) { + runListItem := "recipe[my_recipe@1.0.0]" + rli, err := NewRunListItem(runListItem) + if err != nil { + t.Errorf(`NewRunListItem("%s") did not correctly parse.`, runListItem) + } + if rli.Name != "my_recipe" { + t.Errorf(`NewRunListItem("%s") did not correctly set name`, runListItem) + } + if rli.Type != "recipe" { + t.Errorf(`NewRunListItem("%s") did not correctly set type`, runListItem) + } + if rli.Version != "1.0.0" { + t.Errorf(`NewRunListItem("%s") did not correctly set version`, runListItem) + } +} + +func TestQualifiedRecipe(t *testing.T) { + runListItem := "recipe[my_recipe]" + rli, err := NewRunListItem(runListItem) + if err != nil { + t.Errorf(`NewRunListItem("%s") did not correctly parse.`, runListItem) + } + if rli.Name != "my_recipe" { + t.Errorf(`NewRunListItem("%s") did not correctly set name`, runListItem) + } + if rli.Type != "recipe" { + t.Errorf(`NewRunListItem("%s") did not correctly set type`, runListItem) + } + if rli.Version != "" { + t.Errorf(`NewRunListItem("%s") did not correctly set version`, runListItem) + } +} + +func TestQualifiedRole(t *testing.T) { + runListItem := "role[my_role]" + rli, err := NewRunListItem(runListItem) + if err != nil { + t.Errorf(`NewRunListItem("%s") did not correctly parse.`, runListItem) + } + if rli.Name != "my_role" { + t.Errorf(`NewRunListItem("%s") did not correctly set name`, runListItem) + } + if rli.Type != "role" { + t.Errorf(`NewRunListItem("%s") did not correctly set type, %s`, runListItem, rli.Type) + } + if rli.Version != "" { + t.Errorf(`NewRunListItem("%s") did not correctly set version`, runListItem) + } +} + +func TestVersionedUnqualifiedRecipe(t *testing.T) { + runListItem := "my_recipe@1.0.0" + rli, err := NewRunListItem(runListItem) + if err != nil { + t.Errorf(`NewRunListItem("%s") did not correctly parse.`, runListItem) + } + if rli.Name != "my_recipe" { + t.Errorf(`NewRunListItem("%s") did not correctly set name`, runListItem) + } + if rli.Type != "recipe" { + t.Errorf(`NewRunListItem("%s") did not correctly set type`, runListItem) + } + if rli.Version != "1.0.0" { + t.Errorf(`NewRunListItem("%s") did not correctly set version`, runListItem) + } +} + +func TestRecipeNameAlone(t *testing.T) { + runListItem := "my_recipe" + rli, err := NewRunListItem(runListItem) + if err != nil { + t.Errorf(`NewRunListItem("%s") did not correctly parse.`, runListItem) + } + if rli.Name != "my_recipe" { + t.Errorf(`NewRunListItem("%s") did not correctly set name`, runListItem) + } + if rli.Type != "recipe" { + t.Errorf(`NewRunListItem("%s") did not correctly set type`, runListItem) + } + if rli.Version != "" { + t.Errorf(`NewRunListItem("%s") did not correctly set version`, runListItem) + } +} + +func TestBadCase(t *testing.T) { + runListItem := "Recipe[my_recipe]" // Capital R + _, err := NewRunListItem(runListItem) + + if err == nil { + t.Errorf(`NewRunListItem("%s") should have returned and error and it didn't.`, runListItem) + } +} + +func TestSpaceThrowsError(t *testing.T) { + runListItem := "recipe [my_recipe]" + _, err := NewRunListItem(runListItem) + + if err == nil { + t.Errorf(`NewRunListItem("%s") should have returned and error and it didn't.`, runListItem) + } +} + +func TestMissingClosingBracketThrowsError(t *testing.T) { + runListItem := "recipe[my_recipe" + _, err := NewRunListItem(runListItem) + + if err == nil { + t.Errorf(`NewRunListItem("%s") should have returned and error and it didn't.`, runListItem) + } +} + +func TestStringConversion(t *testing.T) { + runListItems := []string{ + "recipe[my_recipe@1.0.0]", + "recipe[my_recipe]", + "role[my_recipe]", + } + + for _, runListItem := range runListItems { + rli, err := NewRunListItem(runListItem) + if err != nil || rli.String() != runListItem { + t.Errorf(`NewRunListItem("%s").String() does not match %s`, runListItem, runListItem) + } + } +} + +func TestIsRecipe(t *testing.T) { + recipe := RunListItem{ + Type: "recipe", + } + if recipe.IsRecipe() != true { + t.Error(`IsRecipe() should return true for recipe`) + } + + role := RunListItem{ + Type: "role", + } + if role.IsRecipe() != false { + t.Error(`IsRecipe() should return false for role`) + } +} + +func TestIsRole(t *testing.T) { + recipe := RunListItem{ + Type: "role", + } + if recipe.IsRole() != true { + t.Error(`IsRole() should return true for role`) + } + + role := RunListItem{ + Type: "recipe", + } + if role.IsRole() != false { + t.Error(`IsRole() should return false for recipe`) + } +} diff --git a/sandbox.go b/sandbox.go index 43d48ec..6726ce2 100644 --- a/sandbox.go +++ b/sandbox.go @@ -15,20 +15,20 @@ type SandboxRequest struct { Checksums map[string]interface{} `json:"checksums"` } -// SandboxPostResponse is the struct returned from the chef-server for Post Requests to /sandbox +// SandboxPostResponse is the struct returned from the chef-server for Post Requests to /sandboxes type SandboxPostResponse struct { ID string `json:"sandbox_id"` Uri string `json:"uri"` Checksums map[string]SandboxItem } -// A SandbooxItem is embeddedinto the response from the chef-server and the actual sandbox It is the Url and state for a specific Item. +// A SandboxItem is embedded into the response from the chef-server and the actual sandbox is the Url and state for a specific Item. type SandboxItem struct { Url string `json:"url"` Upload bool `json:"needs_upload"` } -// Sandbox Is the structure of an actul sandbox that has been created and returned by the final PUT to the sandbox ID +// Sandbox Is the structure of an actual sandbox that has been created and returned by the final PUT to the sandbox ID type Sandbox struct { ID string `json:"guid"` Name string `json:"name"` @@ -51,11 +51,11 @@ func (s SandboxService) Post(sums []string) (data SandboxPostResponse, err error return } - err = s.client.magicRequestDecoder("POST", "/sandboxes", body, &data) + err = s.client.magicRequestDecoder("POST", "sandboxes", body, &data) return } -// Put is used to commit a sandbox ID to the chef server. To singal that the sandox you have Posted is now uploaded. +// Put is used to commit a sandbox ID to the chef server. To signal that the sandbox you have Posted is now uploaded. func (s SandboxService) Put(id string) (box Sandbox, err error) { answer := make(map[string]bool) answer["is_completed"] = true @@ -65,6 +65,6 @@ func (s SandboxService) Put(id string) (box Sandbox, err error) { return box, fmt.Errorf("must supply sandbox id to PUT request.") } - err = s.client.magicRequestDecoder("PUT", "/sandboxes/"+id, body, &box) + err = s.client.magicRequestDecoder("PUT", "sandboxes/"+id, body, &box) return } diff --git a/search.go b/search.go index 914b0a6..bafb624 100644 --- a/search.go +++ b/search.go @@ -53,7 +53,7 @@ func (q SearchQuery) DoPartial(client *Client, params map[string]interface{}) (r body, err := JSONReader(params) if err != nil { - debug("Problem encoding params for body", err.Error()) + debug("Problem encoding params for body %v", err.Error()) return } @@ -81,10 +81,10 @@ func (e SearchService) NewQuery(idx, statement string) (query SearchQuery, err e return } -// Exec runs the query on the index passed in. This is a helper method. If you want more controll over the query use NewQuery and its Do() method. -// BUG(spheromak): Should we use exec or SearchQuery.Do() or have both ? +// Exec runs the query on the index passed in. This is a helper method. If you want more control 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 + // 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 @@ -136,6 +136,27 @@ func (e SearchService) PartialExec(idx, statement string, params map[string]inte } err = e.client.magicRequestDecoder("POST", fullUrl, body, &res) + if err != nil { + return + } + + // the total rows available for this query across all pages + total := res.Total + // the maximum number of rows in each page + inc := query.Rows + paged_res := SearchResult{} + + for start := res.Start; start+inc <= total; start += inc { + query.Start = start + inc + fullUrl = fmt.Sprintf("search/%s", query) + + err = e.client.magicRequestDecoder("POST", fullUrl, body, &paged_res) + if err != nil { + return + } + // accumulate this page of results into the primary SearchResult instance + res.Rows = append(res.Rows, paged_res.Rows...) + } return } diff --git a/status.go b/status.go new file mode 100644 index 0000000..68d037c --- /dev/null +++ b/status.go @@ -0,0 +1,20 @@ +package chef + +type StatusService struct { + client *Client +} + +// Status represents the body of the returned information. +type Status struct { + Status string `json:"status"` + Upstreams map[string]string `json:"upstreams"` + Keygen map[string]int `json:"keygen"` +} + +// Status gets license information. +// +// https://docs.chef.io/api_chef_server.html#license +func (e *StatusService) Get() (data Status, err error) { + err = e.client.magicRequestDecoder("GET", "_status", nil, &data) + return +} diff --git a/status_test.go b/status_test.go new file mode 100644 index 0000000..529c9fe --- /dev/null +++ b/status_test.go @@ -0,0 +1,81 @@ +package chef + +import ( + "encoding/json" + "fmt" + // "github.com/google/go-cmp/cmp" + "io" + "log" + "net/http" + "os" + "reflect" + "testing" +) + +var ( + testStatusJSON = "test/status.json" +) + +func TestStatusFromJSONDecoder(t *testing.T) { + if file, err := os.Open(testStatusJSON); err != nil { + t.Error("Unexpected error '", err, "' during os.Open on", testStatusJSON) + } else { + dec := json.NewDecoder(file) + var g Status + if err := dec.Decode(&g); err == io.EOF { + log.Fatal(g) + } else if err != nil { + log.Fatal(err) + } + } +} + +func TestStatusGet(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/_status", func(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + var request Status + dec.Decode(&request) + switch { + case r.Method == "GET": + fmt.Fprintf(w, `{ + "status": "pong", + "upstreams": { + "chef_elasticsearch": "pong", + "chef_sql": "pong", + "chef_index": "pong", + "oc_chef_authz": "pong", + "data_collector": "pong" + }, + "keygen": { + "keys": 10, + "max": 10, + "max_workers": 2, + "cur_max_workers": 2, + "inflight": 0, + "avail_workers": 2, + "start_size": 0 + } + }`) + } + + }) + + wantStatus := Status{ + Status: "pong", + Upstreams: map[string]string{"chef_elasticsearch": "pong", "chef_index": "pong", "chef_sql": "pong", "data_collector": "pong", "oc_chef_authz": "pong"}, + Keygen: map[string]int{"avail_workers": 2, "cur_max_workers": 2, "inflight": 0, "keys": 10, "max": 10, "max_workers": 2, "start_size": 0}, + } + + status, err := client.Status.Get() + if err != nil { + t.Errorf("Status.Get returned error: %s", err.Error()) + } + + if !reflect.DeepEqual(status, wantStatus) { + t.Errorf("Status.Get returned %+v, want %+v, error %+v", status, wantStatus, err) + } + +} diff --git a/test/authenticate.json b/test/authenticate.json new file mode 100644 index 0000000..a1b91d4 --- /dev/null +++ b/test/authenticate.json @@ -0,0 +1,4 @@ +{ + "username": "grantmc", + "password": "p@ssw0rd" +} diff --git a/test/license.json b/test/license.json new file mode 100644 index 0000000..109a92c --- /dev/null +++ b/test/license.json @@ -0,0 +1,6 @@ +{ + "limit_exceeded": false, + "node_license": 25, + "node_count": 12, + "upgrade_url": "http://www.chef.io/contact/on-premises-simple" +} diff --git a/test/status.json b/test/status.json new file mode 100644 index 0000000..3768a1f --- /dev/null +++ b/test/status.json @@ -0,0 +1,19 @@ +{ + "status": "pong", + "upstreams": { + "chef_elasticsearch": "pong", + "chef_sql": "pong", + "chef_index": "pong", + "oc_chef_authz": "pong", + "data_collector": "pong" + }, + "keygen": { + "keys": 10, + "max": 10, + "max_workers": 2, + "cur_max_workers": 2, + "inflight": 0, + "avail_workers": 2, + "start_size": 0 + } +} diff --git a/test/verbose_user.json b/test/verbose_user.json new file mode 100644 index 0000000..692df9d --- /dev/null +++ b/test/verbose_user.json @@ -0,0 +1,4 @@ +{ + "janechef": { "email": "jane.chef@user.com", "first_name": "jane", "last_name": "chef_user" }, + "yaelsmith": { "email": "yeal.chef@user.com", "first_name": "yeal", "last_name": "smith" } +} diff --git a/universe.go b/universe.go new file mode 100644 index 0000000..279f695 --- /dev/null +++ b/universe.go @@ -0,0 +1,70 @@ +package chef + +type UniverseService struct { + client *Client +} + +// Universe represents the body of the returned information. +type Universe struct { + Books map[string]UniverseBook +} + +type UniverseBook struct { + Versions map[string]UniverseVersion +} + +type UniverseVersion struct { + LocationPath string + LocationType string + Dependencies map[string]string +} + +// Universe gets available cookbook version information. +// +// https://docs.chef.io/api_chef_server.html#universe +func (e *UniverseService) Get() (universe Universe, err error) { + var data map[string]interface{} + err = e.client.magicRequestDecoder("GET", "universe", nil, &data) + unpackUniverse(&universe, &data) + return +} + +func unpackUniverse(universe *Universe, data *map[string]interface{}) { + (*universe).Books = make(map[string]UniverseBook) + for bookn, versions := range *data { + ub := UniverseBook{} + ub.Versions = make(map[string]UniverseVersion) + switch versions.(type) { + case map[string]interface{}: + for vname, version := range versions.(map[string]interface{}) { + uv := UniverseVersion{} + switch version.(type) { + case map[string]interface{}: + for aname, attr := range version.(map[string]interface{}) { + deps := make(map[string]string) + switch aname { + case "dependencies": + for dname, dep := range attr.(map[string]interface{}) { + switch dep.(type) { + case string: + deps[dname] = dep.(string) + default: + } + } + uv.Dependencies = deps + case "location_path": + uv.LocationPath = attr.(string) + case "location_type": + uv.LocationType = attr.(string) + } + } + default: + } + ub.Versions[vname] = uv + } + default: + } + (*universe).Books[bookn] = ub + } + return +} diff --git a/universe_test.go b/universe_test.go new file mode 100644 index 0000000..9ea9135 --- /dev/null +++ b/universe_test.go @@ -0,0 +1,97 @@ +package chef + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestUniverseGet(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/universe", func(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + var request Universe + dec.Decode(&request) + switch { + case r.Method == "GET": + fmt.Fprintf(w, `{ + "ffmpeg": { + "0.1.0": { + "location_path": "http://supermarket.chef.io/api/v1/cookbooks/ffmpeg/0.1.0/download", + "location_type": "supermarket", + "dependencies": { + "git": ">= 0.0.0", + "build-essential": ">= 0.0.0", + "libvpx": "~> 0.1.1", + "x264": "~> 0.1.1" + } + }, + "0.1.1": { + "location_path": "http://supermarket.chef.io/api/v1/cookbooks/ffmpeg/0.1.1/download", + "location_type": "supermarket", + "dependencies": { + "git": ">= 0.0.0", + "build-essential": ">= 0.0.0", + "libvpx": "~> 0.1.1", + "x264": "~> 0.1.1" + } + } + }, + "pssh": { + "0.1.0": { + "location_path": "http://supermarket.chef.io/api/v1/cookbooks/pssh.1.0/download", + "location_type": "supermarket", + "dependencies": {} + } + } + }`) + } + }) + + wantU := Universe{} + wantU.Books = make(map[string]UniverseBook) + ffmpeg := UniverseBook{} + ffmpeg.Versions = make(map[string]UniverseVersion) + ffmpeg.Versions["0.1.0"] = UniverseVersion{ + LocationPath: "http://supermarket.chef.io/api/v1/cookbooks/ffmpeg/0.1.0/download", + LocationType: "supermarket", + Dependencies: map[string]string{ + "git": ">= 0.0.0", + "build-essential": ">= 0.0.0", + "libvpx": "~> 0.1.1", + "x264": "~> 0.1.1", + }, + } + ffmpeg.Versions["0.1.1"] = UniverseVersion{ + LocationPath: "http://supermarket.chef.io/api/v1/cookbooks/ffmpeg/0.1.1/download", + LocationType: "supermarket", + Dependencies: map[string]string{ + "git": ">= 0.0.0", + "build-essential": ">= 0.0.0", + "libvpx": "~> 0.1.1", + "x264": "~> 0.1.1", + }, + } + pssh := UniverseBook{} + pssh.Versions = make(map[string]UniverseVersion) + pssh.Versions["0.1.0"] = UniverseVersion{ + LocationPath: "http://supermarket.chef.io/api/v1/cookbooks/pssh.1.0/download", + LocationType: "supermarket", + Dependencies: map[string]string{}, + } + wantU.Books["ffmpeg"] = ffmpeg + wantU.Books["pssh"] = pssh + + universe, err := client.Universe.Get() + if err != nil { + t.Errorf("Universe.Get returned error: %s", err.Error()) + } + + if !reflect.DeepEqual(universe, wantU) { + t.Errorf("Universe.Get returned %+v, want %+v", universe, wantU) + } +} diff --git a/updated_since.go b/updated_since.go new file mode 100644 index 0000000..644d224 --- /dev/null +++ b/updated_since.go @@ -0,0 +1,31 @@ +package chef + +import ( + "errors" + "strconv" +) + +type UpdatedSinceService struct { + client *Client +} + +// UpdatedSince represents the body of the returned information. +type UpdatedSince struct { + Action string + Id int64 + Path string +} + +// Since gets available cookbook version information. +// +// https://docs.chef.io/api_chef_server/#updated_since +// This end point has long since been deprecated and is no longer available +// Calls will always return 404 not found errors +func (e UpdatedSinceService) Get(sequenceId int64) (updated []UpdatedSince, err error) { + url := "updated_since?seq=" + strconv.FormatInt(sequenceId, 10) + err = e.client.magicRequestDecoder("GET", url, nil, &updated) + if err != nil { + err = errors.New("Update_since is a deprecated endpoint and always returns 404.") + } + return +} diff --git a/updated_since_test.go b/updated_since_test.go new file mode 100644 index 0000000..bf95291 --- /dev/null +++ b/updated_since_test.go @@ -0,0 +1,25 @@ +package chef + +import ( + "fmt" + "net/http" + "strings" + "testing" +) + +func TestUpdatedSinceGet(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/updated_since", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET": + w.WriteHeader(404) + } + }) + + _, err := client.UpdatedSince.Get(1) + if !strings.Contains(fmt.Sprint(err), "404") { + t.Errorf("Non 404 return code: %+v", err) + } +} diff --git a/user.go b/user.go index 710efa3..abe7bfe 100644 --- a/user.go +++ b/user.go @@ -16,17 +16,31 @@ type User struct { Email string `json:"email,omitempty"` ExternalAuthenticationUid string `json:"external_authentication_uid,omitempty"` // this or password FirstName string `json:"first_name,omitempty"` - FullName string `json:"full_name,omitempty"` LastName string `json:"last_name,omitempty"` MiddleName string `json:"middle_name,omitempty"` Password string `json:"password,omitempty"` // Valid password - PublicKey string `json:"public_key,omitempty"` // not for Create + CreateKey bool `json:"create_key,omitempty"` // Cannot be passed with PublicKey + PublicKey string `json:"public_key,omitempty"` // Cannot be passed with CreateKey RecoveryAuthenticationEnabled bool `json:"recovery_authentication_enabled,omitempty"` } type UserResult struct { - Uri string `json:"uri,omitempty"` - PrivateKey string `json:"private_key,omitempty"` + Uri string `json:"uri,omitempty"` + ChefKey ChefKey `json:"chef_key,omitempty"` +} + +type ChefKey struct { + Name string `json:"name"` + PublicKey string `json:"public_key"` + ExpirationDate string `json:"expiration_date"` + Uri string `json:"uri"` + PrivateKey string `json:"private_key"` +} + +type UserVerboseResult struct { + Email string `json:"email,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` } type UserKey struct { @@ -35,15 +49,14 @@ type UserKey struct { ExpirationDate string `json:"expiration_date,omitempty"` } -type UserKeyResult struct { +type UserKeyItem struct { KeyName string `json:"name,omitempty"` Uri string `json:"uri,omitempty"` - Expired string `json:"expired,omitempty"` + Expired bool `json:"expired,omitempty"` } -// /users GET // List lists the users in the Chef server. -// +// /users GET // Chef API docs: https://docs.chef.io/api_chef_server.html#users func (e *UserService) List(filters ...string) (userlist map[string]string, err error) { url := "users" @@ -54,8 +67,21 @@ func (e *UserService) List(filters ...string) (userlist map[string]string, err e return } +// VerboseList lists the users in the Chef server in verbose format. +// /users GET +// Chef API docs: https://docs.chef.io/api_chef_server.html#users +func (e *UserService) VerboseList(filters ...string) (userlist map[string]UserVerboseResult, err error) { + url := "users" + filters = append(filters, "verbose=true") + if len(filters) > 0 { + url += "?" + strings.Join(filters, "&") + } + err = e.client.magicRequestDecoder("GET", url, nil, &userlist) + return +} + +// Create Creates a User on the chef server // /users POST -// Creates a User on the chef server // 201 = sucesss // 400 - invalid (missing display_name, email,( password or external) among other things) // username must be lower case without spaces @@ -100,12 +126,91 @@ func (e *UserService) Get(name string) (user User, err error) { return } -// TODO: -// API /users/USERNAME GET external_authentication_uid and email filters -// API /users/USERNAME GET verbose parameter -// API /users/USERNAME PUT -// API /users/USERNAME/keys GET -// API /users/USERNAME/keys POST -// API /users/USERNAME/keys/Key DELETE -// API /users/USERNAME/keys/Key GET -// API /users/USERNAME/keys/Key PUT +// Update updates a user on the Chef server. +// /users/USERNAME PUT +// 200 - updated +// 401 - not authenticated +// 403 - not authorizated +// 404 - user doesn't exist +// 409 - new user name is already in use +// +// Chef API docs: https://docs.chef.io/api_chef_server.html#users-name +func (e *UserService) Update(name string, user User) (userUpdate UserResult, err error) { + url := fmt.Sprintf("users/%s", name) + body, err := JSONReader(user) + err = e.client.magicRequestDecoder("PUT", url, body, &userUpdate) + return +} + +// ListUserKeys gets all the keys for a user. +// /users/USERNAME/keys GET +// 200 - successful +// 401 - not authenticated +// 403 - not authorizated +// 404 - user doesn't exist +// +// Chef API docs: https://docs.chef.io/api_chef_server/#usersuserkeys +func (e *UserService) ListUserKeys(name string) (userkeys []UserKeyItem, err error) { + url := fmt.Sprintf("users/%s/keys", name) + err = e.client.magicRequestDecoder("GET", url, nil, &userkeys) + return +} + +// AddUserKey add a key for a user on the Chef server. +// /users/USERNAME/keys POST +// 201 - created +// 401 - not authenticated +// 403 - not authorizated +// 404 - user doesn't exist +// 409 - new name is already in use +// +// Chef API docs: https://docs.chef.io/api_chef_server.html#users-name +func (e *UserService) AddUserKey(name string, keyadd UserKey) (userkey UserKeyItem, err error) { + url := fmt.Sprintf("users/%s/keys", name) + body, err := JSONReader(keyadd) + err = e.client.magicRequestDecoder("POST", url, body, &userkey) + return +} + +// DeleteUserKey delete a key for a user. +// /users/USERNAME/keys/KEYNAME DELETE +// 200 - successful +// 401 - not authenticated +// 403 - not authorizated +// 404 - user doesn't exist +// +// Chef API docs: https://docs.chef.io/api_chef_server/#usersuserkeys +func (e *UserService) DeleteUserKey(username string, keyname string) (userkey UserKey, err error) { + url := fmt.Sprintf("users/%s/keys/%s", username, keyname) + err = e.client.magicRequestDecoder("DELETE", url, nil, &userkey) + return +} + +// GetUserKey gets a key for a user. +// /users/USERNAME/keys/KEYNAME GET +// 200 - successful +// 401 - not authenticated +// 403 - not authorizated +// 404 - user doesn't exist +// +// Chef API docs: https://docs.chef.io/api_chef_server/#usersuserkeys +func (e *UserService) GetUserKey(username string, keyname string) (userkey UserKey, err error) { + url := fmt.Sprintf("users/%s/keys/%s", username, keyname) + err = e.client.magicRequestDecoder("GET", url, nil, &userkey) + return +} + +// UpdateUserKey updates a key for a user. +// /users/USERNAME/keys/KEYNAME PUT +// 200 - successful +// 401 - not authenticated +// 403 - not authorizated +// 404 - user doesn't exist +// +// Chef API docs: https://docs.chef.io/api_chef_server/#usersuserkeys +func (e *UserService) UpdateUserKey(username string, keyname string, keyupd UserKey) (userkey UserKey, err error) { + url := fmt.Sprintf("users/%s/keys/%s", username, keyname) + body, err := JSONReader(keyupd) + err = e.client.magicRequestDecoder("PUT", url, body, &userkey) + return +} diff --git a/user_test.go b/user_test.go index 163f5f0..e8d62fe 100644 --- a/user_test.go +++ b/user_test.go @@ -3,6 +3,7 @@ package chef import ( "encoding/json" "fmt" + "github.com/r3labs/diff" "io" "log" "net/http" @@ -12,7 +13,8 @@ import ( ) var ( - testUserJSON = "test/user.json" + testUserJSON = "test/user.json" + testVerboseUserJSON = "test/verbose_user.json" ) func TestUserFromJSONDecoder(t *testing.T) { @@ -29,6 +31,20 @@ func TestUserFromJSONDecoder(t *testing.T) { } } +func TestVerboseUserFromJSONDecoder(t *testing.T) { + if file, err := os.Open(testVerboseUserJSON); err != nil { + t.Error("Unexpected error '", err, "' during os.Open on", testVerboseUserJSON) + } else { + dec := json.NewDecoder(file) + var g User + if err := dec.Decode(&g); err == io.EOF { + log.Fatal(g) + } else if err != nil { + log.Fatal(err) + } + } +} + func TestUserslist(t *testing.T) { setup() defer teardown() @@ -51,6 +67,34 @@ func TestUserslist(t *testing.T) { } } +func TestVerboseUserslist(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET": + fmt.Fprintf(w, `{ + "janechef": { "email": "jane.chef@user.com", "first_name": "jane", "last_name": "chef_user" }, + "yaelsmith": { "email": "yael.chef@user.com", "first_name": "yael", "last_name": "smith" } + }`) + + } + }) + + // Test list + users, err := client.Users.VerboseList() + if err != nil { + t.Errorf("Verbose Users.List returned error: %v", err) + } + jane := UserVerboseResult{Email: "jane.chef@user.com", FirstName: "jane", LastName: "chef_user"} + yael := UserVerboseResult{Email: "yael.chef@user.com", FirstName: "yael", LastName: "smith"} + listWant := map[string]UserVerboseResult{"janechef": jane, "yaelsmith": yael} + if !reflect.DeepEqual(users, listWant) { + t.Errorf("Verbose Users.List returned %+v, want %+v", users, listWant) + } +} + func TestUserCreate(t *testing.T) { setup() defer teardown() @@ -59,8 +103,14 @@ func TestUserCreate(t *testing.T) { switch { case r.Method == "POST": fmt.Fprintf(w, `{ - "uri": "https://url/for/user_name1", - "private_key": "-----BEGIN RSA PRIVATE KEY-----" + "uri": "https://chefserver/users/user_name1", + "chef_key": { + "name": "default", + "public_key": "-----BEGIN RSA PUBLIC KEY-----", + "expiration_date": "infinity", + "uri": "https://chefserver/users/user_name1/keys/default", + "private_key": "-----BEGIN RSA PRIVATE KEY-----" + } }`) } }) @@ -71,7 +121,15 @@ func TestUserCreate(t *testing.T) { if err != nil { t.Errorf("Users.Create returned error: %v", err) } - Want := UserResult{Uri: "https://url/for/user_name1", PrivateKey: "-----BEGIN RSA PRIVATE KEY-----"} + Want := UserResult{Uri: "https://chefserver/users/user_name1", + ChefKey: ChefKey{ + Name: "default", + PublicKey: "-----BEGIN RSA PUBLIC KEY-----", + ExpirationDate: "infinity", + Uri: "https://chefserver/users/user_name1/keys/default", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----", + }, + } if !reflect.DeepEqual(userresult, Want) { t.Errorf("Users.Create returned %+v, want %+v", userresult, Want) } @@ -90,7 +148,6 @@ func TestUserGet(t *testing.T) { "email": "user1@mail.com", "external_authentication_uid": "user1", "first_name": "User", - "full_name": "User S Name", "last_name": "Name", "middle_name": "S", "public_key": "-----BEGIN RSA PUBLIC KEY-----", @@ -104,7 +161,7 @@ func TestUserGet(t *testing.T) { if err != nil { t.Errorf("User.Get returned error: %v", err) } - Want := User{UserName: "user1", DisplayName: "User Name", Email: "user1@mail.com", ExternalAuthenticationUid: "user1", FirstName: "User", FullName: "User S Name", LastName: "Name", MiddleName: "S", PublicKey: "-----BEGIN RSA PUBLIC KEY-----", RecoveryAuthenticationEnabled: true} + Want := User{UserName: "user1", DisplayName: "User Name", Email: "user1@mail.com", ExternalAuthenticationUid: "user1", FirstName: "User", LastName: "Name", MiddleName: "S", PublicKey: "-----BEGIN RSA PUBLIC KEY-----", RecoveryAuthenticationEnabled: true} if !reflect.DeepEqual(user, Want) { t.Errorf("Users.Get returned %+v, want %+v", user, Want) } @@ -129,3 +186,189 @@ func TestUserDelete(t *testing.T) { t.Errorf("User.Get returned error: %v", err) } } + +func TestUserUpdate(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/users/user_name1", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "PUT": + fmt.Fprintf(w, `{ + "uri": "https://chefserver/users/user_name1" + }`) + } + }) + + // Update User + user := User{UserName: "user_name1", Email: "user_name1@mail.com", Password: "dummypass"} + userresult, err := client.Users.Update("user_name1", user) + if err != nil { + t.Errorf("Users.Update returned error: %v", err) + } + Want := UserResult{Uri: "https://chefserver/users/user_name1"} + if !reflect.DeepEqual(userresult, Want) { + t.Errorf("Users.Update returned %+v, want %+v", userresult, Want) + } +} + +func TestListUserKeys(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/users/user1/keys", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET": + fmt.Fprintf(w, `[ + { + "name": "default", + "uri": "https://chefserver/users/user1/keys/default", + "expired": false + } + ]`) + } + }) + + keyresult, err := client.Users.ListUserKeys("user1") + if err != nil { + t.Errorf("Users.ListUserKeys returned error: %v", err) + } + defaultItem := UserKeyItem{ + KeyName: "default", + Uri: "https://chefserver/users/user1/keys/default", + Expired: false, + } + Want := []UserKeyItem{defaultItem} + if !reflect.DeepEqual(keyresult, Want) { + t.Errorf("Users.ListUserKeys returned %+v, want %+v", keyresult, Want) + } +} + +func TestAddUserKey(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/users/user1/keys", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "POST": + fmt.Fprintf(w, `{ + "name": "newkey", + "uri": "https://chefserver/users/user1/keys/newkey", + "expired": false + }`) + } + }) + + keyadd := UserKey{ + KeyName: "newkey", + PublicKey: "RSA KEY", + ExpirationDate: "infinity", + } + keyresult, err := client.Users.AddUserKey("user1", keyadd) + if err != nil { + t.Errorf("Users.AddUserKey returned error: %v", err) + } + Want := UserKeyItem{ + KeyName: "newkey", + Uri: "https://chefserver/users/user1/keys/newkey", + Expired: false, + } + if !reflect.DeepEqual(keyresult, Want) { + t.Errorf("Users.AddUserKey returned %+v, want %+v", keyresult, Want) + } +} + +func TestDeleteUserKey(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/users/user1/keys/newkey", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "DELETE": + fmt.Fprintf(w, `{ + "name": "newkey", + "public_key": "RSA KEY", + "expiration_date": "infinity" + }`) + } + }) + + keyresult, err := client.Users.DeleteUserKey("user1", "newkey") + if err != nil { + t.Errorf("Users.DeleteUserKey returned error: %v", err) + } + Want := UserKey{ + KeyName: "newkey", + PublicKey: "RSA KEY", + ExpirationDate: "infinity", + } + if !reflect.DeepEqual(keyresult, Want) { + diff, _ := diff.Diff(keyresult, Want) + t.Errorf("Users.DeleteUserKey returned %+v, want %+v, differences %+v", keyresult, Want, diff) + } +} + +func TestGetUserKey(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/users/user1/keys/newkey", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET": + fmt.Fprintf(w, `{ + "name": "newkey", + "public_key": "RSA KEY", + "expiration_date": "infinity" + }`) + } + }) + + keyresult, err := client.Users.GetUserKey("user1", "newkey") + if err != nil { + t.Errorf("Users.GetUserKey returned error: %v", err) + } + Want := UserKey{ + KeyName: "newkey", + PublicKey: "RSA KEY", + ExpirationDate: "infinity", + } + if !reflect.DeepEqual(keyresult, Want) { + diff, _ := diff.Diff(keyresult, Want) + t.Errorf("Users.GetUserKey returned %+v, want %+v, differences %+v", keyresult, Want, diff) + } +} + +func TestUpdateUserKey(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/users/user1/keys/newkey", func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "PUT": + fmt.Fprintf(w, `{ + "name": "newkey", + "public_key": "RSA NEW KEY", + "expiration_date": "infinity" + }`) + } + }) + + updkey := UserKey{ + KeyName: "newkey", + PublicKey: "RSA NEW KEY", + ExpirationDate: "infinity", + } + keyresult, err := client.Users.UpdateUserKey("user1", "newkey", updkey) + if err != nil { + t.Errorf("Users.UpdateUserKey returned error: %v", err) + } + Want := UserKey{ + KeyName: "newkey", + PublicKey: "RSA NEW KEY", + ExpirationDate: "infinity", + } + if !reflect.DeepEqual(keyresult, Want) { + diff, _ := diff.Diff(keyresult, Want) + t.Errorf("Users.UpdateUserKey returned %+v, want %+v, differences %+v", keyresult, Want, diff) + } +}