├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── api_test.go ├── client.go ├── client_test.go ├── cookbook.go ├── cookbook_test.go ├── data.go ├── data_test.go ├── doc.go ├── environment.go ├── environment_test.go ├── node.go ├── node_test.go ├── principal.go ├── principal_test.go ├── role.go ├── role_test.go ├── search.go ├── search_test.go ├── test └── support │ ├── TEST_CONFIG.json │ ├── chef │ ├── environment.json │ ├── mysql_bag.json │ ├── node.json │ ├── role.json │ └── test_cook │ │ ├── Berksfile │ │ ├── Berksfile.lock │ │ ├── metadata.rb │ │ └── recipes │ │ └── compile_time.rb │ ├── chef_config.sh │ ├── ci.sh │ ├── keys │ ├── admin.pem │ ├── chef-validator.pem │ ├── chef-webui.pem │ └── neo4j.example.org.pem │ ├── knife.rb │ ├── regen_data.sh │ ├── seed_data │ ├── data │ └── index │ ├── start_server.sh │ └── stop_server.sh ├── user.go └── user_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | *.swp 3 | .DS_Store 4 | tags 5 | coverage.out 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | chef-golang is licensed under the MIT License 2 | 3 | Copyright (C) 2014 Mike Arpaia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | chef-golang [![Build Status](https://drone.io/github.com/marpaia/chef-golang/status.png)](https://drone.io/github.com/marpaia/chef-golang/latest) 2 | =========== 3 | This is a Go API client for Opscode's Chef. 4 | 5 | ## Installation 6 | 7 | Use `go get` to install chef-golang: 8 | ``` 9 | go get github.com/marpaia/chef-golang 10 | ``` 11 | 12 | ## Unit tests 13 | There is a helper script in `test/support/start_server.sh` that will setup a goiardi server primed 14 | with unit-testing data. there is also an example knife config and a script named `test/support/chef_config.sh`. 15 | 16 | If you don't have a local `~/.chef/knife.rb` you can simply run these 3 commands: 17 | ``` 18 | ./test/support/start_server.sh 19 | ./test/support/chef_config.sh 20 | go test -v 21 | ``` 22 | to shutdown the server run `test/support/stop_server.sh` 23 | 24 | The [goiardi](https://github.com/ctdk/goiardi) Server is listening on port `8443` and the keys and config are copied to /tmp/goiardi. If you want to setup your own `knife.rb` and not use the provided one. 25 | 26 | ## External dependencies 27 | 28 | This project has no external dependencies other than the Go standard library. 29 | 30 | ## Documentation 31 | 32 | Like most every other Golang project, this projects documentation can be found 33 | on godoc at [godoc.org/github.com/marpaia/chef-golang](http://godoc.org/github.com/marpaia/chef-golang). 34 | 35 | ## Examples 36 | ```go 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | "os" 42 | 43 | "github.com/marpaia/chef-golang" 44 | ) 45 | 46 | var findNode = "neo4j.example.org" 47 | var findCookbook = "neo4j" 48 | var findRole = "Neo4j" 49 | 50 | func main() { 51 | c, err := chef.Connect() 52 | if err != nil { 53 | fmt.Println("Error:", err) 54 | os.Exit(1) 55 | } 56 | c.SSLNoVerify = true 57 | 58 | // Print detailed information about a specific node 59 | node, ok, err := c.GetNode(findNode) 60 | if err != nil { 61 | fmt.Println("Error:", err) 62 | os.Exit(1) 63 | } 64 | if !ok { 65 | fmt.Println("\nCouldn't find that node!") 66 | } else { 67 | for i := 0; i < 80; i++ { 68 | fmt.Print("=") 69 | } 70 | 71 | fmt.Println("\nSystem info:", node.Name, "\n") 72 | fmt.Println(" [+] IP Address:", node.Info.IPAddress) 73 | fmt.Println(" [+] MAC Address:", node.Info.MACAddress) 74 | fmt.Println(" [+] Operating System:", node.Info.Platform) 75 | 76 | fmt.Println("\n [+] Filesystem Info") 77 | for partition, info := range node.Info.Filesystem { 78 | if info.PercentUsed != "" { 79 | fmt.Println(" - ", partition, "is", info.PercentUsed, "utilized") 80 | } 81 | } 82 | 83 | fmt.Println("\n [+] Roles") 84 | for _, role := range node.Info.Roles { 85 | fmt.Println(" - ", role) 86 | } 87 | 88 | fmt.Println() 89 | for i := 0; i < 80; i++ { 90 | fmt.Print("=") 91 | } 92 | } 93 | 94 | // Print detailed information about a specific cookbook 95 | cookbook, ok, err := c.GetCookbook(findCookbook) 96 | if err != nil { 97 | fmt.Println("Error:", err) 98 | os.Exit(1) 99 | } 100 | if !ok { 101 | fmt.Println("\nCouldn't find that cookbook!") 102 | } else { 103 | fmt.Println("\n\nCookbook info:", findCookbook) 104 | for _, version := range cookbook.Versions { 105 | currentVersion, ok, err := c.GetCookbookVersion(findCookbook, version.Version) 106 | if err != nil { 107 | fmt.Println("Error:", err) 108 | os.Exit(1) 109 | } 110 | if ok { 111 | if len(currentVersion.Files) > 0 { 112 | fmt.Println("\n [+]", findCookbook, currentVersion.Version, "Cookbook Files") 113 | for _, cookbookFile := range currentVersion.Files { 114 | fmt.Println(" - ", cookbookFile.Name) 115 | } 116 | } 117 | } 118 | } 119 | 120 | fmt.Println() 121 | for i := 0; i < 80; i++ { 122 | fmt.Print("=") 123 | } 124 | } 125 | 126 | // Print detailed information about a specific role 127 | role, ok, err := c.GetRole(findRole) 128 | if err != nil { 129 | fmt.Println("Error:", err) 130 | } 131 | if !ok { 132 | fmt.Println("\nCouldn't find that role!") 133 | } else { 134 | fmt.Println("\n\nRole information:", role.Name) 135 | fmt.Println("\n[+] Runlist") 136 | for _, recipe := range role.RunList { 137 | fmt.Println(" - ", recipe) 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | Which will output something like this: 144 | 145 | ``` 146 | ================================================================================ 147 | System info: neo4j.example.org 148 | 149 | [+] IP Address: 10.100.1.2 150 | [+] MAC Address: AA:BB:CC:DD:EE:FF 151 | [+] Operating System: centos 152 | 153 | [+] Filesystem Info 154 | - /dev/vda is 46% utilized 155 | - tmpfs is 1% utilized 156 | 157 | [+] Roles 158 | - Base 159 | - Neo4j 160 | 161 | ================================================================================ 162 | 163 | Cookbook info: neo4j 164 | 165 | [+] neo4j 0.1.6 Cookbook Files 166 | - neo4j-server.properties 167 | - neo4j-service 168 | - neo4j-wrapper.conf 169 | 170 | [+] neo4j 0.1.5 Cookbook Files 171 | - neo4j-service 172 | - neo4j-wrapper.conf 173 | 174 | [+] neo4j 0.1.4 Cookbook Files 175 | - neo4j-service 176 | 177 | [+] neo4j 0.1.3 Cookbook Files 178 | - neo4j-service 179 | 180 | [+] neo4j 0.1.2 Cookbook Files 181 | - neo4j-service 182 | 183 | ================================================================================ 184 | 185 | Role information: Neo4j 186 | 187 | [+] Runlist 188 | - recipe[yum] 189 | - recipe[ldap] 190 | - recipe[system] 191 | - recipe[neo4j::server] 192 | - recipe[neo4j::web-ui] 193 | ``` 194 | 195 | ## Contributing 196 | 197 | Please contribute and help improve this project! 198 | 199 | - Fork the repo 200 | - Make sure the tests pass 201 | - Improve the code 202 | - Make sure your feature has test coverage 203 | - Make sure the tests pass 204 | - Submit a pull request 205 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/rsa" 7 | "crypto/sha1" 8 | "crypto/tls" 9 | "crypto/x509" 10 | "encoding/base64" 11 | "encoding/pem" 12 | "errors" 13 | "fmt" 14 | "io" 15 | "io/ioutil" 16 | "math/big" 17 | "mime" 18 | "mime/multipart" 19 | "net/http" 20 | "net/url" 21 | "os" 22 | "path" 23 | "path/filepath" 24 | "regexp" 25 | "strings" 26 | "time" 27 | ) 28 | 29 | // Chef is the type that contains all of the relevant information about a Chef 30 | // server connection 31 | type Chef struct { 32 | Host string 33 | Url string 34 | Port string 35 | Version string 36 | Key *rsa.PrivateKey 37 | UserId string 38 | SSLNoVerify bool 39 | Organization string 40 | } 41 | 42 | // Connect looks for knife/chef configuration files and gather connection info 43 | // automagically 44 | func Connect(filename ...string) (*Chef, error) { 45 | knifeFiles := []string{} 46 | 47 | if len(filename) > 0 { 48 | for _, v := range filename { 49 | knifeFiles = append(knifeFiles, v) 50 | } 51 | } 52 | 53 | // Follow knife way of identifying knife.rb 54 | // Check current directory for a .chef directory 55 | knifeFiles = append(knifeFiles, ".chef/knife.rb") 56 | 57 | // Check ~/.chef 58 | homedir := os.Getenv("HOME") 59 | if homedir != "" { 60 | knifeFiles = append(knifeFiles, filepath.Join(homedir, ".chef/knife.rb")) 61 | } 62 | 63 | // Check client.rb 64 | knifeFiles = append(knifeFiles, "/etc/chef/client.rb") 65 | 66 | // Fall back on included test/support/knife.rb 67 | knifeFiles = append(knifeFiles, "test/support/knife.rb") 68 | var knifeFile string 69 | for _, each := range knifeFiles { 70 | if _, err := os.Stat(each); err == nil { 71 | knifeFile = each 72 | break 73 | } 74 | } 75 | 76 | if knifeFile == "" { 77 | return nil, errors.New("Configuration file not found") 78 | } 79 | 80 | file, err := os.Open(knifeFile) 81 | defer file.Close() 82 | if err != nil { 83 | return nil, err 84 | } 85 | scanner := bufio.NewScanner(file) 86 | chef := new(Chef) 87 | for scanner.Scan() { 88 | split := splitWhitespace(scanner.Text()) 89 | if len(split) == 2 { 90 | switch split[0] { 91 | case "node_name": 92 | chef.UserId = filterQuotes(split[1]) 93 | case "client_key": 94 | key, err := keyFromFile(filterQuotes(split[1])) 95 | if err != nil { 96 | return nil, err 97 | } 98 | chef.Key = key 99 | case "chef_server_url": 100 | parsedUrl := filterQuotes(split[1]) 101 | chef.Url = parsedUrl 102 | chefUrl, err := url.Parse(parsedUrl) 103 | if err != nil { 104 | return nil, err 105 | } 106 | hostPath := strings.Split(chefUrl.Path, "/") 107 | if len(hostPath) == 3 && hostPath[1] == "organizations" { 108 | chef.Organization = hostPath[2] 109 | } 110 | hostPort := strings.Split(chefUrl.Host, ":") 111 | if len(hostPort) == 2 { 112 | chef.Host = hostPort[0] 113 | chef.Port = hostPort[1] 114 | } else if len(hostPort) == 1 { 115 | chef.Host = hostPort[0] 116 | switch chefUrl.Scheme { 117 | case "http": 118 | chef.Port = "80" 119 | case "https": 120 | chef.Port = "443" 121 | default: 122 | return nil, errors.New("Invalid http scheme") 123 | } 124 | 125 | } else { 126 | return nil, errors.New("Invalid host format") 127 | } 128 | } 129 | } 130 | } 131 | 132 | if chef.Key == nil { 133 | return nil, errors.New("missing 'client_key' in knife.rb") 134 | } 135 | if chef.Version == "" { 136 | chef.Version = "11.6.0" 137 | } 138 | 139 | return chef, nil 140 | } 141 | 142 | // Given the appropriate connection parameters, ConnectChef returns a pointer to 143 | // a Chef type so that you can call request methods on it 144 | func ConnectBuilder(host, port, version, userid, key string, organization string) (*Chef, error) { 145 | chef := new(Chef) 146 | chef.Host = host 147 | chef.Port = port 148 | chef.Version = version 149 | chef.UserId = userid 150 | chef.Organization = organization 151 | 152 | var url string 153 | switch chef.Port { 154 | case "443": 155 | url = fmt.Sprintf("https://%s", chef.Host) 156 | case "80": 157 | url = fmt.Sprintf("http://%s", chef.Host) 158 | default: 159 | url = fmt.Sprintf("%s:%s", chef.Host, chef.Port) 160 | } 161 | 162 | if chef.Organization != "" { 163 | url += "/organizations/" + chef.Organization 164 | } 165 | 166 | chef.Url = url 167 | 168 | var rsaKey *rsa.PrivateKey 169 | var err error 170 | 171 | if strings.Contains(key, "-----BEGIN RSA PRIVATE KEY-----") { 172 | rsaKey, err = keyFromString([]byte(key)) 173 | } else { 174 | rsaKey, err = keyFromFile(key) 175 | } 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | chef.Key = rsaKey 181 | if chef.Version == "" { 182 | chef.Version = "11.6.0" 183 | } 184 | return chef, nil 185 | } 186 | 187 | // filterQuotes returns a string with surrounding quotes filtered 188 | func filterQuotes(s string) string { 189 | re1 := regexp.MustCompile(`^(\'|\")`) 190 | re2 := regexp.MustCompile(`(\'|\")$`) 191 | return re2.ReplaceAllString(re1.ReplaceAllString(s, ``), ``) 192 | } 193 | 194 | // Given a string with multiple consecutive spaces, splitWhitespace returns a 195 | // slice of strings which represent the given string split by \s characters with 196 | // all duplicates removed 197 | func splitWhitespace(s string) []string { 198 | re := regexp.MustCompile(`\s+`) 199 | return strings.Split(re.ReplaceAllString(s, `\s`), `\s`) 200 | } 201 | 202 | // keyFromFile reads an RSA private key given a filepath 203 | func keyFromFile(filename string) (*rsa.PrivateKey, error) { 204 | content, err := ioutil.ReadFile(filename) 205 | if err != nil { 206 | return nil, err 207 | } 208 | return keyFromString(content) 209 | } 210 | 211 | // keyFromString parses an RSA private key from a string 212 | func keyFromString(key []byte) (*rsa.PrivateKey, error) { 213 | block, _ := pem.Decode(key) 214 | if block == nil { 215 | return nil, fmt.Errorf("block size invalid for '%s'", string(key)) 216 | } 217 | rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) 218 | if err != nil { 219 | return nil, err 220 | } 221 | return rsaKey, nil 222 | } 223 | 224 | // assemble query string from params 225 | func (chef *Chef) buildQueryString(endpoint string, params map[string]string) (string, error) { 226 | u, err := url.Parse(chef.requestUrl(endpoint)) 227 | if err != nil { 228 | return "", err 229 | } 230 | 231 | query := u.Query() 232 | for k, v := range params { 233 | query.Set(k, v) 234 | } 235 | u.RawQuery = query.Encode() 236 | if err != nil { 237 | return "", err 238 | } 239 | 240 | return u.String(), nil 241 | } 242 | 243 | // Get makes an authenticated HTTP request to the Chef server for the supplied 244 | // endpoint 245 | func (chef *Chef) Get(endpoint string) (*http.Response, error) { 246 | request, _ := http.NewRequest("GET", chef.requestUrl(endpoint), nil) 247 | return chef.makeRequest(request) 248 | } 249 | 250 | // GetWithParams makes an authenticated HTTP request to the Chef server for the 251 | // supplied endpoint and also includes GET query string parameters 252 | func (chef *Chef) GetWithParams(endpoint string, params map[string]string) (*http.Response, error) { 253 | query, err := chef.buildQueryString(endpoint, params) 254 | if err != nil { 255 | return nil, err 256 | } 257 | 258 | request, err := http.NewRequest("GET", query, nil) 259 | if err != nil { 260 | return nil, err 261 | } 262 | return chef.makeRequest(request) 263 | } 264 | 265 | // Post post to the chef api 266 | func (chef *Chef) Post(endpoint string, contentType string, params map[string]string, body io.Reader) (*http.Response, error) { 267 | query, err := chef.buildQueryString(endpoint, params) 268 | if err != nil { 269 | return nil, err 270 | } 271 | 272 | request, err := http.NewRequest("POST", query, body) 273 | request.Header.Set("Content-Type", contentType) 274 | return chef.makeRequest(request) 275 | } 276 | 277 | // Put makes an authenticated PUT request to the Chef server for the supplied 278 | // endpoint 279 | func (chef *Chef) Put(endpoint string, params map[string]string, body io.Reader) (*http.Response, error) { 280 | //TODO: Finish this 281 | request, _ := http.NewRequest("PUT", chef.requestUrl(endpoint), body) 282 | request.Header.Set("Content-Type", "application/json") 283 | return chef.makeRequest(request) 284 | } 285 | 286 | // Delete makes an authenticated DELETE request to the Chef server for the 287 | // supplied endpoint 288 | func (chef *Chef) Delete(endpoint string, params map[string]string) (*http.Response, error) { 289 | //TODO: Finish this 290 | request, _ := http.NewRequest("DELETE", chef.requestUrl(endpoint), nil) 291 | return chef.makeRequest(request) 292 | } 293 | 294 | // 295 | // requestUrl generate the requestUrl from supplied endpoint and params 296 | // 297 | func (chef *Chef) requestUrl(endpoint string) string { 298 | return fmt.Sprintf("%s/%s", chef.Url, endpoint) 299 | } 300 | 301 | // makeRequest 302 | // Take a request object, Setup Auth headers and Send it to the server 303 | func (chef *Chef) makeRequest(request *http.Request) (*http.Response, error) { 304 | chef.apiRequestHeaders(request) 305 | var client *http.Client 306 | if chef.SSLNoVerify { 307 | tr := &http.Transport{ 308 | Proxy: http.ProxyFromEnvironment, 309 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 310 | } 311 | client = &http.Client{Transport: tr} 312 | } else { 313 | client = &http.Client{} 314 | } 315 | 316 | return client.Do(request) 317 | } 318 | 319 | // pulled from goiardi 320 | // encode a string suitable for auth header 321 | func hashStr(toHash string) string { 322 | h := sha1.New() 323 | io.WriteString(h, toHash) 324 | hashed := base64.StdEncoding.EncodeToString(h.Sum(nil)) 325 | return hashed 326 | } 327 | 328 | // also from goiardi calc and encodebody data 329 | func calcBodyHash(r *http.Request) (string, error) { 330 | var bodyStr string 331 | var err error 332 | if r.Body == nil { 333 | bodyStr = "" 334 | } else { 335 | save := r.Body 336 | save, r.Body, err = drainBody(r.Body) 337 | if err != nil { 338 | return "", err 339 | } 340 | mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 341 | if err != nil { 342 | return "", err 343 | } 344 | if strings.HasPrefix(mediaType, "multipart/form-data") { 345 | bodyStr, err = readFileFromRequest(r, params["boundary"]) 346 | if err != nil { 347 | return "", err 348 | } 349 | } else { 350 | buf := new(bytes.Buffer) 351 | buf.ReadFrom(r.Body) 352 | bodyStr = buf.String() 353 | } 354 | r.Body = save 355 | } 356 | chkHash := hashStr(bodyStr) 357 | return chkHash, err 358 | } 359 | 360 | // read a file from a multipart/form-data request body 361 | // and return it as a string needed to caculate the body hash 362 | func readFileFromRequest(r *http.Request, boundary string) (string, error) { 363 | mr := multipart.NewReader(r.Body, boundary) 364 | form, err := mr.ReadForm(1048576) 365 | if err != nil { 366 | return "", err 367 | } 368 | // we can safely assume only one tarball is added to the 369 | // multipart as this is how the Chef API is designed 370 | fp := form.File["tarball"][0] 371 | file, err := fp.Open() 372 | if err != nil { 373 | return "", err 374 | } 375 | buf, err := ioutil.ReadAll(file) 376 | if err != nil { 377 | return "", err 378 | } 379 | return string(buf), nil 380 | } 381 | 382 | // liberated from net/http/httputil 383 | // Copyright 2009 The Go Authors. All rights reserved. 384 | // Use of this source code is governed by a BSD-style 385 | // license that can be found in the LICENSE file. 386 | func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) { 387 | var buf bytes.Buffer 388 | if _, err = buf.ReadFrom(b); err != nil { 389 | return nil, nil, err 390 | } 391 | if err = b.Close(); err != nil { 392 | return nil, nil, err 393 | } 394 | return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewBuffer(buf.Bytes())), nil 395 | } 396 | 397 | // base64BlockEncode takes a byte slice and breaks it up into a slice of strings 398 | // where each string is 60 characters long 399 | func base64BlockEncode(content []byte) []string { 400 | resultString := base64.StdEncoding.EncodeToString(content) 401 | var resultSlice []string 402 | index := 0 403 | 404 | for i := 0; i < len(resultString)/60; i++ { 405 | resultSlice = append(resultSlice, resultString[index:index+60]) 406 | index += 60 407 | } 408 | 409 | if len(resultString)%60 != 0 { 410 | resultSlice = append(resultSlice, resultString[index:]) 411 | } 412 | 413 | return resultSlice 414 | } 415 | 416 | // getTimestamp returns an ISO-8601 formatted timestamp of the current time in 417 | // UTC 418 | func getTimestamp() string { 419 | return time.Now().UTC().Format(time.RFC3339) 420 | } 421 | 422 | // privateEncrypt implements OpenSSL's RSA_private_encrypt function 423 | func (chef *Chef) privateEncrypt(data []byte) (enc []byte, err error) { 424 | k := (chef.Key.N.BitLen() + 7) / 8 425 | tLen := len(data) 426 | // rfc2313, section 8: 427 | // The length of the data D shall not be more than k-11 octets 428 | if tLen > k-11 { 429 | err = errors.New("Data too long") 430 | return 431 | } 432 | em := make([]byte, k) 433 | em[1] = 1 434 | for i := 2; i < k-tLen-1; i++ { 435 | em[i] = 0xff 436 | } 437 | copy(em[k-tLen:k], data) 438 | c := new(big.Int).SetBytes(em) 439 | if c.Cmp(chef.Key.N) > 0 { 440 | err = nil 441 | return 442 | } 443 | var m *big.Int 444 | var ir *big.Int 445 | if chef.Key.Precomputed.Dp == nil { 446 | m = new(big.Int).Exp(c, chef.Key.D, chef.Key.N) 447 | } else { 448 | // We have the precalculated values needed for the CRT. 449 | m = new(big.Int).Exp(c, chef.Key.Precomputed.Dp, chef.Key.Primes[0]) 450 | m2 := new(big.Int).Exp(c, chef.Key.Precomputed.Dq, chef.Key.Primes[1]) 451 | m.Sub(m, m2) 452 | if m.Sign() < 0 { 453 | m.Add(m, chef.Key.Primes[0]) 454 | } 455 | m.Mul(m, chef.Key.Precomputed.Qinv) 456 | m.Mod(m, chef.Key.Primes[0]) 457 | m.Mul(m, chef.Key.Primes[1]) 458 | m.Add(m, m2) 459 | 460 | for i, values := range chef.Key.Precomputed.CRTValues { 461 | prime := chef.Key.Primes[2+i] 462 | m2.Exp(c, values.Exp, prime) 463 | m2.Sub(m2, m) 464 | m2.Mul(m2, values.Coeff) 465 | m2.Mod(m2, prime) 466 | if m2.Sign() < 0 { 467 | m2.Add(m2, prime) 468 | } 469 | m2.Mul(m2, values.R) 470 | m.Add(m, m2) 471 | } 472 | } 473 | 474 | if ir != nil { 475 | // Unblind. 476 | m.Mul(m, ir) 477 | m.Mod(m, chef.Key.N) 478 | } 479 | enc = m.Bytes() 480 | return 481 | } 482 | 483 | // generateRequestAuthorization returns a srting slice of the signed headers 484 | // It assumes you have calculated and put the required headers on the request 485 | func (chef *Chef) generateRequestAuthorization(request *http.Request) ([]string, error) { 486 | var content string 487 | content += fmt.Sprintf("Method:%s\n", request.Header.Get("Method")) 488 | content += fmt.Sprintf("Hashed Path:%s\n", request.Header.Get("Hashed-Path")) 489 | content += fmt.Sprintf("X-Ops-Content-Hash:%s\n", request.Header.Get("X-Ops-Content-Hash")) 490 | content += fmt.Sprintf("X-Ops-Timestamp:%s\n", request.Header.Get("X-Ops-Timestamp")) 491 | content += fmt.Sprintf("X-Ops-UserId:%s", request.Header.Get("X-Ops-UserId")) 492 | signature, err := chef.privateEncrypt([]byte(content)) 493 | if err != nil { 494 | return nil, err 495 | } 496 | return base64BlockEncode([]byte(string(signature))), nil 497 | } 498 | 499 | // apiRequestHeaders attache chef-server headers to the request 500 | func (chef *Chef) apiRequestHeaders(request *http.Request) error { 501 | body_hash, err := calcBodyHash(request) 502 | if err != nil { 503 | return err 504 | } 505 | 506 | // sanitize the path for the chef-server 507 | // chef-server doesn't support '//' in the Hash Path. 508 | path := path.Clean(request.URL.Path) 509 | request.URL.Path = path 510 | 511 | timestamp := getTimestamp() 512 | request.Header.Set("Method", request.Method) 513 | request.Header.Set("Hashed-Path", hashStr(path)) 514 | request.Header.Set("Accept", "application/json") 515 | request.Header.Set("X-Chef-Version", chef.Version) 516 | request.Header.Set("X-Ops-Timestamp", timestamp) 517 | request.Header.Set("X-Ops-Userid", chef.UserId) 518 | request.Header.Set("X-Ops-Sign", "version=1.0") 519 | request.Header.Set("X-Ops-Content-Hash", body_hash) 520 | 521 | // generate signed string of headers 522 | auths, err := chef.generateRequestAuthorization(request) 523 | if err != nil { 524 | return err 525 | } 526 | 527 | // roll over the auth slice and add the apropriate header 528 | for index, value := range auths { 529 | request.Header.Set(fmt.Sprintf("X-Ops-Authorization-%d", index+1), string(value)) 530 | } 531 | 532 | return nil 533 | } 534 | 535 | // Given an http response object, responseBody returns the response body 536 | func responseBody(resp *http.Response) ([]byte, error) { 537 | defer resp.Body.Close() 538 | 539 | if resp.StatusCode != http.StatusOK { 540 | return nil, errors.New(resp.Status) 541 | } 542 | 543 | body, err := ioutil.ReadAll(resp.Body) 544 | if err != nil { 545 | return nil, err 546 | } 547 | 548 | return body, nil 549 | } 550 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | var testRequiredHeaders []string 14 | 15 | var ConfigFilePath = "test/support/TEST_CONFIG.json" 16 | 17 | func init() { 18 | testRequiredHeaders = []string{ 19 | "Accept", 20 | "X-Ops-Timestamp", 21 | "X-Ops-Userid", 22 | "X-Ops-Sign", 23 | "X-Ops-Content-Hash", 24 | "X-Ops-Authorization-1", 25 | } 26 | } 27 | 28 | func testConnectionWrapper(t *testing.T) *Chef { 29 | chef, err := Connect() 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | chef.SSLNoVerify = true 34 | chef.Version = "11.6.0" 35 | 36 | return chef 37 | } 38 | 39 | type testConfigFile struct { 40 | RequiredCookbook struct { 41 | Name string `json:"name"` 42 | Version string `json:"version"` 43 | } `json:"required_cookbook"` 44 | RequiredNode struct { 45 | Name string `json:"name"` 46 | } `json:"required_node"` 47 | RequiredRecipe struct { 48 | Name string `json:"name"` 49 | } `json:"required_recipe"` 50 | RequiredRole struct { 51 | Name string `json:"name"` 52 | } `json:"required_role"` 53 | RequiredClient struct { 54 | Name string `json:"name"` 55 | } `json:"required_client"` 56 | RequiredEnvironment struct { 57 | Name string `json:"name"` 58 | } `json:"required_environment"` 59 | RequiredUser struct { 60 | Name string `json:"name"` 61 | } `json:"required_user"` 62 | RequiredData struct { 63 | Name string `json:"name"` 64 | } `json:"required_data"` 65 | SearchData struct { 66 | Index string `json:"index"` 67 | Query string `json:"query"` 68 | } `json:"search_data"` 69 | TestCredentials struct { 70 | Host string `json:"host"` 71 | Port string `json:"port"` 72 | Version string `json:"version"` 73 | UserId string `json:"user_name"` 74 | Key string `json:"key"` 75 | } `json:"test_credentials"` 76 | RequiredPrincipal struct { 77 | Name string `json:"name"` 78 | } `json:"required_principal"` 79 | KeyPath string `json:"key_path"` 80 | KeyString string `json:"key_string"` 81 | } 82 | 83 | func testConfig() *testConfigFile { 84 | file, err := ioutil.ReadFile(ConfigFilePath) 85 | t := new(testing.T) 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | 90 | var config *testConfigFile 91 | json.Unmarshal(file, &config) 92 | if config == nil { 93 | t.Error("Config is nil") 94 | } 95 | return config 96 | } 97 | 98 | func TestReadConfig(t *testing.T) { 99 | _ = testConfig() 100 | } 101 | 102 | func TestHashStr(t *testing.T) { 103 | if len(hashStr("hash_this")) != 28 { 104 | t.Error("Wrong length for hashAndBase64") 105 | } 106 | } 107 | 108 | func TestResponseBody(t *testing.T) { 109 | etsy, err := http.Get("https://www.etsy.com/") 110 | if err != nil { 111 | t.Error(err) 112 | } 113 | 114 | bytes, err := responseBody(etsy) 115 | if err != nil { 116 | t.Error(err) 117 | } 118 | 119 | etsyString := "Is code your craft? https://www.etsy.com/careers" 120 | if !strings.Contains(string(bytes), etsyString) { 121 | t.Error("Response body didn't return valid string") 122 | } 123 | } 124 | 125 | func TestConnectBuilderNoOrg(t *testing.T) { 126 | config := testConfig() 127 | host := config.TestCredentials.Host 128 | port := config.TestCredentials.Port 129 | version := config.TestCredentials.Version 130 | userid := config.TestCredentials.UserId 131 | key := config.TestCredentials.Key 132 | org := "" 133 | c, err := ConnectBuilder(host, port, version, userid, key, org) 134 | if err != nil { 135 | t.Error(err) 136 | } 137 | if c.UserId != config.TestCredentials.UserId { 138 | t.Fatal("credentials don't match") 139 | } 140 | 141 | } 142 | 143 | func TestConnectBuilderOrg(t *testing.T) { 144 | config := testConfig() 145 | host := config.TestCredentials.Host 146 | port := config.TestCredentials.Port 147 | version := config.TestCredentials.Version 148 | userid := config.TestCredentials.UserId 149 | key := config.TestCredentials.Key 150 | org := "myorg" 151 | c, err := ConnectBuilder(host, port, version, userid, key, org) 152 | if err != nil { 153 | t.Error(err) 154 | } 155 | if c.UserId != config.TestCredentials.UserId { 156 | t.Fatal("credentials don't match") 157 | } 158 | 159 | } 160 | 161 | func TestBuildQueryString(t *testing.T) { 162 | c := testConnectionWrapper(t) 163 | 164 | params := make(map[string]string) 165 | params["foo"] = "bar" 166 | s, err := c.buildQueryString("cookbooks", params) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | 171 | if s != "http://127.0.0.1:8443/cookbooks?foo=bar" { 172 | t.Fatal("assembled uri doesn't match", s) 173 | } 174 | } 175 | 176 | func TestGet(t *testing.T) { 177 | c := testConnectionWrapper(t) 178 | resp, err := c.Get("/cookbooks") 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | 183 | cookbooks := map[string]interface{}{} 184 | json.NewDecoder(resp.Body).Decode(&cookbooks) 185 | 186 | found := false 187 | config := testConfig() 188 | cookbook := config.RequiredCookbook.Name 189 | for name := range cookbooks { 190 | if name == cookbook { 191 | found = true 192 | break 193 | } 194 | } 195 | if !found { 196 | t.Error("Required cookbook not found") 197 | } 198 | } 199 | 200 | func TestGetWithParams(t *testing.T) { 201 | type searchResults struct { 202 | Total int `json:"total"` 203 | Start int `json:"start"` 204 | Rows []interface{} `json:"rows"` 205 | } 206 | 207 | c := testConnectionWrapper(t) 208 | params := make(map[string]string) 209 | params["q"] = "name:neo4j*" 210 | 211 | resp, err := c.GetWithParams("/search/node", params) 212 | if err != nil { 213 | t.Error(err) 214 | } 215 | 216 | body, err := ioutil.ReadAll(resp.Body) 217 | res := new(searchResults) 218 | json.Unmarshal(body, &res) 219 | 220 | if res.Total == 0 { 221 | t.Fatal("query result is empty: ", res) 222 | } 223 | } 224 | 225 | func TestPost(t *testing.T) { 226 | c := testConnectionWrapper(t) 227 | config := testConfig() 228 | cookbook := config.RequiredCookbook.Name 229 | run_list := strings.NewReader(fmt.Sprintf(`{ "run_list": [ "%s" ] }`, cookbook)) 230 | resp, err := c.Post("/environments/_default/cookbook_versions", "application/json", nil, run_list) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | 235 | body, err := ioutil.ReadAll(resp.Body) 236 | if err != nil { 237 | t.Error(err) 238 | } 239 | 240 | // This could or should be better. Be good to have another 241 | // test for unsolvable run_list 242 | cookbooks := map[string]interface{}{} 243 | json.Unmarshal(body, &cookbooks) 244 | found := false 245 | for name := range cookbooks { 246 | if name == cookbook { 247 | found = true 248 | break 249 | } 250 | } 251 | if !found { 252 | t.Error("Cookbook not solved") 253 | } 254 | 255 | // Test partial search via post. Should be in search probably, but function 256 | // is in api raw post method. 257 | partial_body := strings.NewReader(` { "name": [ "name" ] } `) 258 | params := make(map[string]string) 259 | params["q"] = "name:neo4j*" 260 | 261 | // For now this isn't supported in goiardi, but we can still submit it. 262 | resp, err = c.Post("/search/node", "application/json", params, partial_body) 263 | if err != nil { 264 | t.Error(err) 265 | } 266 | 267 | body, err = ioutil.ReadAll(resp.Body) 268 | if err != nil { 269 | t.Error(err) 270 | } 271 | 272 | search_result := map[string]interface{}{} 273 | json.Unmarshal(body, &search_result) 274 | 275 | if search_result["total"].(float64) != 1 { 276 | t.Error("partial search didn't return expect result") 277 | } 278 | } 279 | 280 | func TestPut(t *testing.T) { 281 | c := testConnectionWrapper(t) 282 | name := "put_test" 283 | env := strings.NewReader( 284 | fmt.Sprintf(`{ "name": "%s", "full_name": "%s environment", }`, name, name)) 285 | resp, err := c.Put(fmt.Sprintf("/organizations/%s", name), nil, env) 286 | if err != nil { 287 | t.Fatal(err) 288 | } 289 | 290 | _, err = ioutil.ReadAll(resp.Body) 291 | if err != nil { 292 | t.Error(err) 293 | } 294 | } 295 | 296 | func TestConnectNoFile(t *testing.T) { 297 | if _, err := Connect(); err != nil { 298 | t.Error(err) 299 | } 300 | } 301 | 302 | func TestConnectFile(t *testing.T) { 303 | if _, err := Connect("test/support/knife.rb"); err != nil { 304 | t.Error(err) 305 | } 306 | } 307 | 308 | func TestGenerateRequestAuthorization(t *testing.T) { 309 | chef := testConnectionWrapper(t) 310 | request, err := http.NewRequest("GET", chef.requestUrl("/cookbooks"), nil) 311 | auth, err := chef.generateRequestAuthorization(request) 312 | if err != nil { 313 | t.Fatal(err) 314 | } 315 | if len(auth[0]) != 60 { 316 | t.Error("Incorrect request authorization string") 317 | } 318 | } 319 | 320 | func TestApiRequestHeaders(t *testing.T) { 321 | chef := testConnectionWrapper(t) 322 | request, _ := http.NewRequest("GET", chef.requestUrl("/cookbooks"), nil) 323 | err := chef.apiRequestHeaders(request) 324 | if err != nil { 325 | println("failed to generate RequestHeaders") 326 | t.Fatal(err) 327 | } 328 | count := 0 329 | for _, requiredHeader := range testRequiredHeaders { 330 | for header := range request.Header { 331 | if strings.ToLower(requiredHeader) == strings.ToLower(header) { 332 | count += 1 333 | break 334 | } 335 | } 336 | } 337 | if count != len(testRequiredHeaders) { 338 | t.Error("apiRequestHeaders didn't return all of testRequiredHeaders") 339 | } 340 | } 341 | 342 | func TestPrivateEncrypt(t *testing.T) { 343 | chef := testConnectionWrapper(t) 344 | enc, err := chef.privateEncrypt([]byte("encrypt_this")) 345 | if err != nil { 346 | t.Error(err) 347 | } 348 | if len(enc) != 256 { 349 | t.Error("Wrong size of encrypted data") 350 | } 351 | } 352 | 353 | func TestBase64BlockEncode(t *testing.T) { 354 | toEncode := []byte("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz") 355 | results := base64BlockEncode(toEncode) 356 | expected := []string{"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXphYmNkZWZnaGlqa2xtbm9wcXJz", "dHV2d3h5emFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6"} 357 | if !reflect.DeepEqual(results, expected) { 358 | t.Error("Results not matching") 359 | } 360 | } 361 | 362 | func TestKeyFromString(t *testing.T) { 363 | config := testConfig() 364 | _, err := keyFromString([]byte(config.KeyString)) 365 | if err != nil { 366 | t.Error(err) 367 | } 368 | } 369 | 370 | func TestKeyFromFile(t *testing.T) { 371 | config := testConfig() 372 | _, err := keyFromFile(config.KeyPath) 373 | if err != nil { 374 | t.Error(err) 375 | } 376 | } 377 | 378 | func TestSplitWhitespace(t *testing.T) { 379 | str := "c h e f" 380 | if !reflect.DeepEqual(splitWhitespace(str), []string{"c", "h", "e", "f"}) { 381 | t.Error("splitWhitespace slices not equal") 382 | } 383 | } 384 | 385 | func TestFilterQuotes(t *testing.T) { 386 | known := map[string]string{ 387 | `'this`: "this", 388 | `this'`: "this", 389 | `"this`: "this", 390 | `this"`: "this", 391 | } 392 | 393 | for bad, good := range known { 394 | if filterQuotes(bad) != good { 395 | t.Error("filterQuotes didn't produce an expected string") 396 | } 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // chef.Client defines the relevant parameters of a Chef client. This includes 10 | // it's name, whether or not it's an admin, etc. 11 | type Client struct { 12 | Name string `json:"name"` 13 | Admin bool `json:"admin"` 14 | JSONClass string `json:"json_class"` 15 | ChefType string `json:"chef_type"` 16 | ClientName string `json:"clientname"` 17 | Org string `json:"orgname"` 18 | Validator bool `json:"validator"` 19 | Certificate string `json:"certificate"` 20 | PublicKey string `json:"public_key"` 21 | } 22 | 23 | // chef.GetClients returns a map of client name's to client REST urls as well as 24 | // an error indicating if the request was successful or not. 25 | // 26 | // Usage: 27 | // 28 | // clients, err := chef.GetClients() 29 | // if err != nil { 30 | // fmt.Println(err) 31 | // os.Exit(1) 32 | // } 33 | // // do what you please with the "clients" variable which is map of client 34 | // // names to client REST urls 35 | // for client := range clients { 36 | // fmt.Println("Client:", client) 37 | // } 38 | func (chef *Chef) GetClients() (map[string]string, error) { 39 | resp, err := chef.Get("clients") 40 | if err != nil { 41 | return nil, err 42 | } 43 | body, err := responseBody(resp) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | clients := map[string]string{} 49 | json.Unmarshal(body, &clients) 50 | 51 | return clients, nil 52 | } 53 | 54 | // GetClient accept a string representing the client name and returns a Client 55 | // type which illustrates various information about the client. It also returns 56 | // a bool indicating whether or not the client was found and an error indicating 57 | // if the request failed or not. 58 | // 59 | // Note that if the request is successful but no such client existed, the error 60 | // return value will be nil but the bool will be false. 61 | // 62 | // Usage: 63 | // 64 | // client, ok, err := chef.GetClient("clientname") 65 | // if err != nil { 66 | // fmt.Println(err) 67 | // os.Exit(1) 68 | // } 69 | // if !ok { 70 | // fmt.Println("Couldn't find that client!") 71 | // } else { 72 | // // do what you please with the "client" variable which is of the 73 | // // *Chef.Client type 74 | // fmt.Printf("%#v\n", client) 75 | // } 76 | func (chef *Chef) GetClient(name string) (*Client, bool, error) { 77 | resp, err := chef.Get(fmt.Sprintf("clients/%s", name)) 78 | if err != nil { 79 | return nil, false, err 80 | } 81 | body, err := responseBody(resp) 82 | if err != nil { 83 | if strings.Contains(err.Error(), "404") { 84 | return nil, false, nil 85 | } 86 | return nil, false, err 87 | } 88 | 89 | client := new(Client) 90 | json.Unmarshal(body, client) 91 | 92 | return client, true, nil 93 | } 94 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetClients(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | config := testConfig() 10 | clients, err := chef.GetClients() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | if clients[config.RequiredClient.Name] == "" { 15 | t.Error("Required client not found") 16 | } 17 | } 18 | 19 | func TestGetClient(t *testing.T) { 20 | chef := testConnectionWrapper(t) 21 | config := testConfig() 22 | _, ok, err := chef.GetClient(config.RequiredClient.Name) 23 | if !ok { 24 | t.Error("Couldn't find required client") 25 | } 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cookbook.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // chef.Cookbook defines the relevant parameters of a Chef cookbook. This 10 | // includes the RESTful URL of a cookbook and a slice of all of the cookbooks 11 | // versions. The versions each have two attributes: Url, which represents the 12 | // RESTful URL of the cookbook version and Version, which represents the version 13 | // number (identifier) of the cookbook version 14 | type Cookbook struct { 15 | Url string `json:"url"` 16 | Versions []struct { 17 | Url string `json:"url"` 18 | Version string `json:"version"` 19 | } `json:"versions"` 20 | } 21 | 22 | // chef.CookbookVersion defines the relevant parameters of a specific Chef 23 | // cookbook version. This includes, but is not limited to, information about 24 | // recipes, files, etc, various pieces of metadata about the cookbook at that 25 | // point in time, such as the name of the cookbook, the description, the 26 | // license, etc. 27 | type CookbookVersion struct { 28 | Files []struct { 29 | CookbookItem 30 | } `json:"files"` 31 | Definitions []struct { 32 | CookbookItem 33 | } `json:"definitions"` 34 | Libraries []struct { 35 | CookbookItem 36 | } `json:"libraries"` 37 | Attributes []struct { 38 | CookbookItem 39 | } `json:"attributes"` 40 | Recipes []struct { 41 | CookbookItem 42 | } `json:"recipes"` 43 | Providers []struct { 44 | CookbookItem 45 | } `json:"providers"` 46 | Resources []struct { 47 | CookbookItem 48 | } `json:"resources"` 49 | Templates []struct { 50 | CookbookItem 51 | } `json:"templates"` 52 | RootFiles []struct { 53 | CookbookItem 54 | } `json:"root_files"` 55 | Metadata struct { 56 | Name string `json:"name"` 57 | Description string `json:"description"` 58 | LongDescription string `json:"long_description"` 59 | Maintainer string `json:"maintainer"` 60 | MaintainerEmail string `json:"maintainer_email"` 61 | License string `json:"license"` 62 | Platforms map[string]string `json:"platforms"` 63 | Dependencies map[string]string `json:"dependencies"` 64 | Providing map[string]string `json:"providing"` 65 | Attributes map[string]interface{} `json:"attributes"` 66 | Recipes map[string]string `json:"recipes"` 67 | Version string `json:"version"` 68 | } `json:"metadata"` 69 | Name string `json:"cookbook_name"` 70 | Version string `json:"version"` 71 | FullName string `json:"name"` 72 | Frozen bool `json:"frozen?"` 73 | ChefType string `json:"chef_type"` 74 | JSONClass string `json:"json_class"` 75 | } 76 | 77 | // chef.CookbookItem defines the relevant parameters of various items that are 78 | // found in a chef Cookbook such as the name, checksum, etc. This type is 79 | // embedded in the chef.CookVersion type to reduce code repetition. 80 | type CookbookItem struct { 81 | Name string `json:"name"` 82 | Path string `json:"path"` 83 | Checksum string `json:"checksum"` 84 | Specificity string `json:"specificity"` 85 | Url string `json:"url"` 86 | } 87 | 88 | // chef.GetCookbooks returns a map of cookbook names to a pointer to the 89 | // chef.Cookbook type as well as an error indicating if the request was 90 | // successful or not. 91 | // 92 | // Usgae: 93 | // 94 | // cookbooks, err := chef.GetCookbooks() 95 | // if err != nil { 96 | // fmt.Println(err) 97 | // os.Exit(1) 98 | // } 99 | // // do what you please with the "cookbooks" variable which is a map of 100 | // // cookbook names to chef.Cookbook types 101 | // for name, cookbook := range cookbooks { 102 | // fmt.Println(name, cookbook.Version[0]) 103 | // } 104 | func (chef *Chef) GetCookbooks() (map[string]*Cookbook, error) { 105 | resp, err := chef.Get("cookbooks") 106 | if err != nil { 107 | return nil, err 108 | } 109 | body, err := responseBody(resp) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | cookbooks := map[string]*Cookbook{} 115 | json.Unmarshal(body, &cookbooks) 116 | 117 | return cookbooks, nil 118 | } 119 | 120 | // chef.GetCookbook returns a pointer to the chef.Cookbook type for a given 121 | // string that represents a cookbook name. It also returns a bool indicating 122 | // whether or not the client was found and an error indicating if the request 123 | // failed or not. 124 | // 125 | // Note that if the request is successful but no such client existed, the error 126 | // return value will be nil but the bool will be false. 127 | // 128 | // Usage: 129 | // 130 | // cookbook, ok, err := chef.GetCookbook("apache") 131 | // if err != nil { 132 | // fmt.Println(err) 133 | // os.Exit(1) 134 | // } 135 | // if !ok { 136 | // fmt.Println("Couldn't find that cookbook!") 137 | // } else { 138 | // // do what you please with the "cookbook" variable which is of the 139 | // // *Chef.Cookbook type 140 | // fmt.Printf("%#v\n", cookbook) 141 | // } 142 | func (chef *Chef) GetCookbook(name string) (*Cookbook, bool, error) { 143 | resp, err := chef.Get(fmt.Sprintf("cookbooks/%s", name)) 144 | if err != nil { 145 | return nil, false, err 146 | } 147 | body, err := responseBody(resp) 148 | if err != nil { 149 | if strings.Contains(err.Error(), "404") { 150 | return nil, false, nil 151 | } 152 | return nil, false, err 153 | } 154 | 155 | cookbook := map[string]*Cookbook{} 156 | json.Unmarshal(body, &cookbook) 157 | 158 | return cookbook[name], true, nil 159 | } 160 | 161 | // chef.GetCookbookVersion returns a pointer to the chef.CookbookVersion type 162 | // for a given string that represents a cookbook version. It also returns a bool 163 | // indicating whether or not the client was found and an error indicating if 164 | // the request failed or not. 165 | // 166 | // Note that if the request is successful but no such client existed, the error 167 | // return value will be nil but the bool will be false. 168 | // 169 | // Usage: 170 | // 171 | // cookbook, ok, err := chef.GetCookbookVersion("apache", "1.0.0") 172 | // if err != nil { 173 | // fmt.Println(err) 174 | // os.Exit(1) 175 | // } 176 | // if !ok { 177 | // fmt.Println("Couldn't find that cookbook version!") 178 | // } else { 179 | // // do what you please with the "cookbook" variable which is of the 180 | // // *Chef.CookbookVersion type 181 | // fmt.Printf("%#v\n", cookbook) 182 | // } 183 | func (chef *Chef) GetCookbookVersion(name, version string) (*CookbookVersion, bool, error) { 184 | resp, err := chef.Get(fmt.Sprintf("cookbooks/%s/%s", name, version)) 185 | if err != nil { 186 | return nil, false, err 187 | } 188 | body, err := responseBody(resp) 189 | if err != nil { 190 | if strings.Contains(err.Error(), "404") { 191 | return nil, false, nil 192 | } 193 | return nil, false, err 194 | } 195 | cookbook := new(CookbookVersion) 196 | json.Unmarshal(body, &cookbook) 197 | return cookbook, true, nil 198 | } 199 | -------------------------------------------------------------------------------- /cookbook_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetCookbooks(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | cookbooks, err := chef.GetCookbooks() 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | found := false 14 | config := testConfig() 15 | for cookbook := range cookbooks { 16 | if cookbook == config.RequiredCookbook.Name { 17 | found = true 18 | break 19 | } 20 | } 21 | if !found { 22 | t.Error("Couldn't find required cookbook") 23 | } 24 | } 25 | 26 | func TestGetCookbook(t *testing.T) { 27 | chef := testConnectionWrapper(t) 28 | config := testConfig() 29 | _, ok, err := chef.GetCookbook(config.RequiredCookbook.Name) 30 | if !ok { 31 | t.Error("Couldn't find required cookbook") 32 | } 33 | if err != nil { 34 | t.Error("Couldn't find required cookbook") 35 | } 36 | } 37 | 38 | func TestCookbookVersion(t *testing.T) { 39 | chef := testConnectionWrapper(t) 40 | config := testConfig() 41 | _, ok, err := chef.GetCookbookVersion(config.RequiredCookbook.Name, config.RequiredCookbook.Version) 42 | if !ok { 43 | t.Error(err) 44 | } 45 | if !ok { 46 | t.Error("Cookbook version not found") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // chef.GetData returns a map of databag names to their related REST URL 10 | // endpoint as well as an error indicating if the request was successful or not 11 | // 12 | // Usage: 13 | // 14 | // data, err := chef.GetData() 15 | // if err != nil { 16 | // fmt.Println(err) 17 | // os.Exit(1) 18 | // } 19 | // for d := range data { 20 | // fmt.Println(d) 21 | // } 22 | func (chef *Chef) GetData() (map[string]string, error) { 23 | resp, err := chef.Get("data") 24 | if err != nil { 25 | return nil, err 26 | } 27 | body, err := responseBody(resp) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | data := map[string]string{} 33 | json.Unmarshal(body, &data) 34 | 35 | return data, nil 36 | } 37 | 38 | // chef.GetDataByName accept a string which represents the name of a specific 39 | // databag and returns a map of information about that databag, a bool 40 | // indicating whether or not the databag was found and an error indicating if 41 | // the request failed or not. 42 | // 43 | // Note that if the request is successful but no such data item existed, the 44 | // error return value will be nil but the bool will be false 45 | // 46 | // Usage: 47 | // 48 | // data, ok, err := chef.GetDataByName("apache") 49 | // if err != nil { 50 | // fmt.Println(err) 51 | // os.Exit(1) 52 | // } 53 | // if !ok { 54 | // fmt.Println("Couldn't find that databag!") 55 | // } else { 56 | // // do what you please with the "data" variable which is of the 57 | // // map[string]string type 58 | // fmt.Println(data) 59 | // } 60 | func (chef *Chef) GetDataByName(name string) (map[string]string, bool, error) { 61 | resp, err := chef.Get(fmt.Sprintf("data/%s", name)) 62 | if err != nil { 63 | return nil, false, err 64 | } 65 | body, err := responseBody(resp) 66 | if err != nil { 67 | if strings.Contains(err.Error(), "404") { 68 | return nil, false, nil 69 | } 70 | return nil, false, err 71 | } 72 | 73 | data := map[string]string{} 74 | json.Unmarshal(body, &data) 75 | 76 | return data, true, nil 77 | } 78 | -------------------------------------------------------------------------------- /data_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetData(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | config := testConfig() 10 | data, err := chef.GetData() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | found := false 15 | for point := range data { 16 | if point == config.RequiredData.Name { 17 | found = true 18 | break 19 | } 20 | } 21 | if !found { 22 | t.Error("Couldn't find required data in data results") 23 | } 24 | } 25 | 26 | func TestGetDataByName(t *testing.T) { 27 | chef := testConnectionWrapper(t) 28 | config := testConfig() 29 | _, ok, err := chef.GetDataByName(config.RequiredData.Name) 30 | if !ok { 31 | t.Error("Couldn't find required data") 32 | } 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // This is a Go client for Opscode's Chef. 2 | // 3 | // Obviously many of the unit tests require a functioning Chef installation in 4 | // order to verify the results of API requests. Edit the `TEST_CONFIG.json` file 5 | // with the appropriate endpoints and information, and run `go test -v`. 6 | // 7 | // This project has no external dependencies other than the Go standard library. 8 | // 9 | // Like most every other Golang project, this projects documentation can be found 10 | // on godoc at http://godoc.org/github.com/marpaia/chef-golang 11 | // 12 | // The following is an example of a simple Go tool that displays a small amount 13 | // of information about a few different pieces of a Chef infrastructure: 14 | // 15 | // package main 16 | // 17 | // import ( 18 | // "fmt" 19 | // "github.com/marpaia/chef-golang" 20 | // "os" 21 | // ) 22 | // 23 | // var findNode = "neo4j.example.org" 24 | // var findCookbook = "neo4j" 25 | // var findRole = "Neo4j" 26 | // 27 | // func main() { 28 | // c, err := chef.Connect() 29 | // if err != nil { 30 | // fmt.Println("Error:", err) 31 | // os.Exit(1) 32 | // } 33 | // c.SSLNoVerify = true 34 | // 35 | // // Print detailed information about a specific node 36 | // node, ok, err := c.GetNode(findNode) 37 | // if err != nil { 38 | // fmt.Println("Error:", err) 39 | // os.Exit(1) 40 | // } 41 | // if !ok { 42 | // fmt.Println("\nCouldn't find that node!") 43 | // } else { 44 | // for i := 0; i < 80; i++ { 45 | // fmt.Print("=") 46 | // } 47 | // 48 | // fmt.Println("\nSystem info:", node.Name, "\n") 49 | // fmt.Println(" [+] IP Address:", node.Info.IPAddress) 50 | // fmt.Println(" [+] MAC Address:", node.Info.MACAddress) 51 | // fmt.Println(" [+] Operating System:", node.Info.Platform) 52 | // 53 | // fmt.Println("\n [+] Filesystem Info") 54 | // for partition, info := range node.Info.Filesystem { 55 | // if info.PercentUsed != "" { 56 | // fmt.Println(" - ", partition, "is", info.PercentUsed, "utilized") 57 | // } 58 | // } 59 | // 60 | // fmt.Println("\n [+] Roles") 61 | // for _, role := range node.Info.Roles { 62 | // fmt.Println(" - ", role) 63 | // } 64 | // 65 | // fmt.Println() 66 | // for i := 0; i < 80; i++ { 67 | // fmt.Print("=") 68 | // } 69 | // } 70 | // 71 | // // Print detailed information about a specific cookbook 72 | // cookbook, ok, err := c.GetCookbook(findCookbook) 73 | // if err != nil { 74 | // fmt.Println("Error:", err) 75 | // os.Exit(1) 76 | // } 77 | // if !ok { 78 | // fmt.Println("\nCouldn't find that cookbook!") 79 | // } else { 80 | // fmt.Println("\n\nCookbook info:", findCookbook) 81 | // for _, version := range cookbook.Versions { 82 | // currentVersion, ok, err := c.GetCookbookVersion(findCookbook, version.Version) 83 | // if err != nil { 84 | // fmt.Println("Error:", err) 85 | // os.Exit(1) 86 | // } 87 | // if ok { 88 | // if len(currentVersion.Files) > 0 { 89 | // fmt.Println("\n [+]", findCookbook, currentVersion.Version, "Cookbook Files") 90 | // for _, cookbookFile := range currentVersion.Files { 91 | // fmt.Println(" - ", cookbookFile.Name) 92 | // } 93 | // } 94 | // } 95 | // } 96 | // 97 | // fmt.Println() 98 | // for i := 0; i < 80; i++ { 99 | // fmt.Print("=") 100 | // } 101 | // } 102 | // 103 | // // Print detailed information about a specific role 104 | // role, ok, err := c.GetRole(findRole) 105 | // if err != nil { 106 | // fmt.Println("Error:", err) 107 | // } 108 | // if !ok { 109 | // fmt.Println("\nCouldn't find that role!") 110 | // } else { 111 | // fmt.Println("\n\nRole information:", role.Name) 112 | // fmt.Println("\n[+] Runlist") 113 | // for _, recipe := range role.RunList { 114 | // fmt.Println(" - ", recipe) 115 | // } 116 | // } 117 | // } 118 | // 119 | // Which will output something like this: 120 | // 121 | // ================================================================================ 122 | // System info: neo4j.example.org 123 | // 124 | // [+] IP Address: 10.100.1.2 125 | // [+] MAC Address: AA:BB:CC:DD:EE:FF 126 | // [+] Operating System: centos 127 | // 128 | // [+] Filesystem Info 129 | // - /dev/vda is 46% utilized 130 | // - tmpfs is 1% utilized 131 | // 132 | // [+] Roles 133 | // - Base 134 | // - Neo4j 135 | // 136 | // ================================================================================ 137 | // 138 | // Cookbook info: neo4j 139 | // 140 | // [+] neo4j 0.1.6 Cookbook Files 141 | // - neo4j-server.properties 142 | // - neo4j-service 143 | // - neo4j-wrapper.conf 144 | // 145 | // [+] neo4j 0.1.5 Cookbook Files 146 | // - neo4j-service 147 | // - neo4j-wrapper.conf 148 | // 149 | // [+] neo4j 0.1.4 Cookbook Files 150 | // - neo4j-service 151 | // 152 | // [+] neo4j 0.1.3 Cookbook Files 153 | // - neo4j-service 154 | // 155 | // [+] neo4j 0.1.2 Cookbook Files 156 | // - neo4j-service 157 | // 158 | // ================================================================================ 159 | // 160 | // Role information: Neo4j 161 | // 162 | // [+] Runlist 163 | // - recipe[yum] 164 | // - recipe[ldap] 165 | // - recipe[system] 166 | // - recipe[neo4j::server] 167 | // - recipe[neo4j::web-ui] 168 | package chef 169 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // chef.Environment dinfes the relevant parameters of a Chef environment. This 10 | // includes the name of the environment, the description strings, etc. 11 | type Environment struct { 12 | Name string `json:"name"` 13 | Description string `json:"description"` 14 | CookbookVersions map[string]string `json:"cookbook_versions"` 15 | JSONClass string `json:"json_class"` 16 | ChefType string `json:"chef_type"` 17 | DefaultAttributes map[string]interface{} `json:"default_attributes"` 18 | OverrideAttributes map[string]interface{} `json:"override_attributes"` 19 | } 20 | 21 | // chef.GetEnvironments returns a map of environment names to the environment's 22 | // RESTful URL as well as an error indicating if the request was successful or 23 | // not. 24 | // 25 | // Usage: 26 | // 27 | // environments, err := chef.GetEnvironments() 28 | // if err != nil { 29 | // fmt.Println(err) 30 | // os.Exit(1) 31 | // } 32 | // // do what you please with the "environments" variable which is a map of 33 | // // environment names to environment URLs 34 | // for environment := range environments { 35 | // fmt.Println(environment) 36 | // } 37 | func (chef *Chef) GetEnvironments() (map[string]string, error) { 38 | resp, err := chef.Get("environments") 39 | if err != nil { 40 | return nil, err 41 | } 42 | body, err := responseBody(resp) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | environments := map[string]string{} 48 | json.Unmarshal(body, &environments) 49 | 50 | return environments, nil 51 | } 52 | 53 | // chef.GetEnvironment accepts a string which represents the name of a Chef 54 | // environment and returns a chef.Environment type representing that environment 55 | // as well as a bool indicating whether or not the environment was found and an 56 | // error indicating if the request failed or not. 57 | // 58 | // Note that if the request is successful but no such client existed, the error 59 | // return value will be nil but the bool will be false. 60 | // 61 | // Usage: 62 | // 63 | // environment, ok, err := chef.GetEnvironment("production") 64 | // if err != nil { 65 | // fmt.Println(err) 66 | // os.Exit(1) 67 | // } 68 | // if !ok { 69 | // fmt.Println("Couldn't find that environment!") 70 | // } else { 71 | // // do what you please with the "environment" variable which is of the 72 | // // *Chef.Environment type 73 | // fmt.Printf("%#v\n", environment) 74 | // } 75 | func (chef *Chef) GetEnvironment(name string) (*Environment, bool, error) { 76 | resp, err := chef.Get(fmt.Sprintf("environments/%s", name)) 77 | if err != nil { 78 | return nil, false, err 79 | } 80 | body, err := responseBody(resp) 81 | if err != nil { 82 | if strings.Contains(err.Error(), "404") { 83 | return nil, false, nil 84 | } 85 | return nil, false, err 86 | } 87 | 88 | environment := new(Environment) 89 | json.Unmarshal(body, environment) 90 | 91 | return environment, true, nil 92 | } 93 | 94 | // chef.GetEnvironmentCookbooks accepts a string which represents the name of a 95 | // Chef environment and returns a map of cookbook names to a *Chef.Cookbook type 96 | // as well as an error indicating whether or not the request failed. 97 | // 98 | // Usage: 99 | // 100 | // cookbooks, err := chef.GetEnvironmentCookbooks("production") 101 | // if err != nil { 102 | // fmt.Println(err) 103 | // os.Exit(1) 104 | // } 105 | // // do what you please with the "cookbooks" variable which is a map of 106 | // // cookbook names to chef.Cookbook types 107 | // for name, cookbook := range cookbooks { 108 | // fmt.Println(name, cookbook.Version[0]) 109 | // } 110 | func (chef *Chef) GetEnvironmentCookbooks(name string) (map[string]*Cookbook, error) { 111 | resp, err := chef.Get(fmt.Sprintf("environments/%s/cookbooks", name)) 112 | if err != nil { 113 | return nil, err 114 | } 115 | body, err := responseBody(resp) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | cookbooks := map[string]*Cookbook{} 121 | json.Unmarshal(body, &cookbooks) 122 | 123 | return cookbooks, nil 124 | } 125 | 126 | // chef.GetEnvironmentCookbook accepts a string which represents the name of a 127 | // Chef environment as well as a string which represent the name of a cookbook 128 | // and returns a *Chef.Cookbook type, a bool indicating whether or not the 129 | // cookbook was found in the given environment as well as an error indicating 130 | // whether or not the request failed. 131 | // 132 | // Usage: 133 | // 134 | // cookbook, ok, err := chef.GetEnvironmentCookbook("production", "apache") 135 | // if err != nil { 136 | // fmt.Println(err) 137 | // os.Exit(1) 138 | // } 139 | // if !ok { 140 | // fmt.Println("Couldn't find that cookbook!") 141 | // } else { 142 | // // do what you please with the "cookbook" variable which is of the 143 | // // *Chef.Cookbook type 144 | // fmt.Printf("%#v\n", cookbook) 145 | // } 146 | func (chef *Chef) GetEnvironmentCookbook(env, cb string) (*Cookbook, bool, error) { 147 | resp, err := chef.Get(fmt.Sprintf("environments/%s/cookbooks/%s", env, cb)) 148 | if err != nil { 149 | return nil, false, err 150 | } 151 | body, err := responseBody(resp) 152 | if err != nil { 153 | if strings.Contains(err.Error(), "404") { 154 | return nil, false, nil 155 | } 156 | return nil, false, err 157 | } 158 | 159 | cookbook := map[string]*Cookbook{} 160 | json.Unmarshal(body, &cookbook) 161 | 162 | return cookbook[cb], true, nil 163 | } 164 | 165 | // chef.GetEnvironmentNodes accepts a string which represents the name of a 166 | // Chef environment as well as a string which represent the name of a node 167 | // and returns a map of node names to their corresponding RESTful URL, a bool 168 | // indicating whether or not the cookbook was found in the given environment as 169 | // well as an error indicating whether or not the request failed. 170 | // 171 | // Usage: 172 | // 173 | // nodes, err := chef.GetEnvironmentNodes("production") 174 | // if err != nil { 175 | // fmt.Println(err) 176 | // os.Exit(1) 177 | // } 178 | // // do what you please with the "nodes" variable which is a map of 179 | // // node names to their corresponding RESTful URL 180 | // for node := range nodes { 181 | // fmt.Println(node) 182 | // } 183 | func (chef *Chef) GetEnvironmentNodes(name string) (map[string]string, error) { 184 | resp, err := chef.Get(fmt.Sprintf("environments/%s/nodes", name)) 185 | if err != nil { 186 | return nil, err 187 | } 188 | body, err := responseBody(resp) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | nodes := map[string]string{} 194 | json.Unmarshal(body, &nodes) 195 | 196 | return nodes, nil 197 | } 198 | 199 | // chef.GetEnvironmentRecipes accepts a string which represents the name of a 200 | // Chef environment and returns a slice of recipe names as well as an error 201 | // indicating whether or not the request failed. 202 | // 203 | // Usage: 204 | // 205 | // recipes, err := chef.GetEnvironmentRecipes("production") 206 | // if err != nil { 207 | // fmt.Println(err) 208 | // os.Exit(1) 209 | // } 210 | // // do what you please with the "recipes" variable which is a slice of 211 | // // recipe names 212 | // for recipe := range recipes { 213 | // fmt.Println(recipe) 214 | // } 215 | func (chef *Chef) GetEnvironmentRecipes(name string) ([]string, error) { 216 | resp, err := chef.Get(fmt.Sprintf("environments/%s/recipes", name)) 217 | if err != nil { 218 | return nil, err 219 | } 220 | body, err := responseBody(resp) 221 | if err != nil { 222 | return nil, err 223 | } 224 | 225 | recipes := []string{} 226 | json.Unmarshal(body, &recipes) 227 | 228 | return recipes, nil 229 | } 230 | 231 | // chef.GetEnvironmentRole accepts a string which represents the name of a 232 | // Chef environment as well as a string which represent the name of a role 233 | // and returns a map of strings (which represent a role attribute like a 234 | // runlist) to a slice of strings which represents the relevant information with 235 | // regards to that attribute, a bool indicating whether or not the role was 236 | // found in the given environment as well as an error indicating whether or not 237 | // the request failed. 238 | // 239 | // Usage: 240 | // 241 | // role, ok, err := chef.GetEnvironmentRole("production", "base") 242 | // if err != nil { 243 | // fmt.Println(err) 244 | // os.Exit(1) 245 | // } 246 | // if !ok { 247 | // fmt.Println("Couldn't find that role!") 248 | // } else { 249 | // // do what you please with the "role" variable 250 | // fmt.Println(role) 251 | // } 252 | func (chef *Chef) GetEnvironmentRole(env, rol string) (map[string][]string, bool, error) { 253 | resp, err := chef.Get(fmt.Sprintf("environments/%s/roles/%s", env, rol)) 254 | if err != nil { 255 | return nil, false, err 256 | } 257 | body, err := responseBody(resp) 258 | if err != nil { 259 | if strings.Contains(err.Error(), "404") { 260 | return nil, false, nil 261 | } 262 | return nil, false, err 263 | } 264 | 265 | role := map[string][]string{} 266 | json.Unmarshal(body, &role) 267 | 268 | return role, true, nil 269 | } 270 | -------------------------------------------------------------------------------- /environment_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetEnvironments(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | config := testConfig() 10 | environments, err := chef.GetEnvironments() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | if environments[config.RequiredEnvironment.Name] == "" { 15 | t.Error("Required environment not found") 16 | } 17 | } 18 | 19 | func TestGetEnvironment(t *testing.T) { 20 | chef := testConnectionWrapper(t) 21 | config := testConfig() 22 | _, ok, err := chef.GetEnvironment(config.RequiredEnvironment.Name) 23 | if !ok { 24 | t.Error("Couldn't find required environment") 25 | } 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | } 30 | 31 | func TestGetEnvironmentCookbooks(t *testing.T) { 32 | chef := testConnectionWrapper(t) 33 | config := testConfig() 34 | _, err := chef.GetEnvironmentCookbooks(config.RequiredEnvironment.Name) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | } 39 | 40 | func TestGetEnvironmentCookbook(t *testing.T) { 41 | chef := testConnectionWrapper(t) 42 | config := testConfig() 43 | _, ok, err := chef.GetEnvironmentCookbook(config.RequiredEnvironment.Name, config.RequiredCookbook.Name) 44 | if !ok { 45 | t.Error("Couldn't find cookbook in environment") 46 | } 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | } 51 | 52 | func TestGetEnvironmentNodes(t *testing.T) { 53 | chef := testConnectionWrapper(t) 54 | config := testConfig() 55 | _, err := chef.GetEnvironmentNodes(config.RequiredEnvironment.Name) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | } 60 | 61 | func TestGetEnvironmentRecipes(t *testing.T) { 62 | chef := testConnectionWrapper(t) 63 | config := testConfig() 64 | recipes, err := chef.GetEnvironmentRecipes(config.RequiredEnvironment.Name) 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | found := false 69 | for _, recipe := range recipes { 70 | if recipe == config.RequiredRecipe.Name { 71 | found = true 72 | } 73 | } 74 | if !found { 75 | t.Error("Couldn't find required recipe") 76 | } 77 | } 78 | 79 | // For now I am skiping this test because it is a webui only endpoint 80 | func TestGetEnvironmentRole(t *testing.T) { 81 | chef := testConnectionWrapper(t) 82 | config := testConfig() 83 | println("Looking for role: ", config.RequiredRole.Name) 84 | println("In", config.RequiredEnvironment.Name) 85 | test, ok, err := chef.GetEnvironmentRole(config.RequiredEnvironment.Name, config.RequiredRole.Name) 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | if !ok { 90 | t.Error("Couldn't find required role in required environment") 91 | } 92 | t.Log(test) 93 | } 94 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // chef.Node represents the relevant parameters of a Chef node 10 | type Node struct { 11 | Name string `json:"name"` 12 | Environment string `json:"chef_environment"` 13 | JSONClass string `json:"json_class"` 14 | RunList []string `json:"run_list"` 15 | ChefType string `json:"chef_type"` 16 | Info struct { 17 | Languages map[string]interface{} `json:"languages"` 18 | Kernel struct { 19 | Name string `json:"name"` 20 | Release string `json:"release"` 21 | Version string `json:"version"` 22 | Machine string `json:"machine"` 23 | Modules map[string]map[string]interface{} `json:"modules"` 24 | } `json:"kernel"` 25 | OS string `json:"os"` 26 | OSVersion string `json:"os_version"` 27 | Hostname string `json:"hostname"` 28 | FQDN string `json:"fqdn"` 29 | Domain string `json:"domain"` 30 | Network struct { 31 | Interfaces map[string]struct { 32 | Type string `json:"type"` 33 | Encapsulation string `json:"encapsulation"` 34 | Addresses map[string]struct { 35 | Family string `json:"family"` 36 | Broadcast string `json:"broadcast"` 37 | Netmast string `json:"netmast"` 38 | } `json:"addresses"` 39 | Routes []struct { 40 | Destination string `json:"destination"` 41 | Family string `json:"family"` 42 | Metric string `json:"metric"` 43 | } `json:"routes"` 44 | State string `json:"state"` 45 | Flags []string `json:"flags"` 46 | MTU string `json:"mtu"` 47 | Arp map[string]string `json:"arp"` 48 | } `json:"interfaces"` 49 | DefaultInterface string `json:"default_interface"` 50 | DefaultGateway string `json:"default_gateway"` 51 | } `json:"network"` 52 | IPAddress string `json:"ipaddress"` 53 | MACAddress string `json:"macaddress"` 54 | ChefPackages map[string]map[string]string `json:"chef_packages"` 55 | Keys map[string]map[string]string `json:"keys"` 56 | Platform string `json:"platform"` 57 | PlatformVersion string `json:"platform_version"` 58 | PlatformFamily string `json:"platform_family"` 59 | CPU map[string]interface{} `json:"cpu"` 60 | Filesystem map[string]struct { 61 | KBSize interface{} `json:"ks_size"` 62 | KBUsed interface{} `json:"ks_used"` 63 | KBavailable interface{} `json:"ks_available"` 64 | PercentUsed interface{} `json:"percent_used"` 65 | Mount string `json:"mount"` 66 | FSType string `json:"fs_type"` 67 | MountOptions []string `json:"mount_options"` 68 | } `json:"filesystem"` 69 | Memory map[string]interface{} `json:"memory"` 70 | UptimeSeconds int `json:"uptime_seconds"` 71 | Uptime string `json:"uptime"` 72 | IdletimeSeconds int `json:"idletime_seconds"` 73 | Idletime string `json:"idletime"` 74 | BlockDevice map[string]interface{} `json:"block_device"` 75 | Recipes []string `json:"recipes"` 76 | Roles []string `json:"roles"` 77 | EC2 map[string]interface{} `json:"ec2"` 78 | } `json:"automatic"` 79 | Default map[string]interface{} `json:"default"` 80 | Normal map[string]interface{} `json:"normal"` 81 | Override map[string]interface{} `json:"override"` 82 | } 83 | 84 | // chef.GetNodes returns a map of nodes names to the nodes's RESTful URL as well 85 | // as an error indicating if the request was successful or not. 86 | // 87 | // Usage: 88 | // 89 | // nodes, err := chef.GetNodes() 90 | // if err != nil { 91 | // fmt.Println(err) 92 | // os.Exit(1) 93 | // } 94 | // // do what you please with the "node" variable which is a map of 95 | // // node names to node URLs 96 | // for node := range nodes { 97 | // fmt.Println(node) 98 | // } 99 | func (chef *Chef) GetNodes() (map[string]string, error) { 100 | resp, err := chef.Get("nodes") 101 | if err != nil { 102 | return nil, err 103 | } 104 | body, err := responseBody(resp) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | nodes := map[string]string{} 110 | json.Unmarshal(body, &nodes) 111 | 112 | return nodes, nil 113 | } 114 | 115 | // chef.GetNode accepts a string which represents the name of a Chef role and 116 | // returns a chef.Environment type representing that role as well as a bool 117 | // indicating whether or not the role was found and an error indicating if the 118 | // request failed or not. 119 | // 120 | // Note that if the request is successful but no such client existed, the error 121 | // return value will be nil but the bool will be false. 122 | // 123 | // Usage: 124 | // 125 | // node, ok, err := chef.GetNode("neo4j.example.com") 126 | // if err != nil { 127 | // fmt.Println(err) 128 | // os.Exit(1) 129 | // } 130 | // if !ok { 131 | // fmt.Println("Couldn't find that node!") 132 | // } else { 133 | // // do what you please with the "node" variable which is of the 134 | // // *Chef.Node type 135 | // fmt.Printf("%#v\n", node) 136 | // } 137 | func (chef *Chef) GetNode(name string) (*Node, bool, error) { 138 | resp, err := chef.Get(fmt.Sprintf("nodes/%s", name)) 139 | if err != nil { 140 | return nil, false, err 141 | } 142 | body, err := responseBody(resp) 143 | if err != nil { 144 | if strings.Contains(err.Error(), "404") { 145 | return nil, false, nil 146 | } 147 | return nil, false, err 148 | } 149 | 150 | node := new(Node) 151 | json.Unmarshal(body, node) 152 | 153 | return node, true, nil 154 | } 155 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetNodes(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | config := testConfig() 10 | nodes, err := chef.GetNodes() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | if nodes[config.RequiredNode.Name] == "" { 15 | t.Error("Required node not found") 16 | } 17 | } 18 | 19 | func TestGetNode(t *testing.T) { 20 | chef := testConnectionWrapper(t) 21 | config := testConfig() 22 | _, ok, err := chef.GetNode(config.RequiredNode.Name) 23 | if !ok { 24 | t.Error("Couldn't find required node") 25 | } 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /principal.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // chef.GetPrincipal returns a map of principal item names to their 10 | // corresponding RESTful url. It also returns a bool indicating whether or not 11 | // the client was found and an error indicating if the request failed or not. 12 | // 13 | // Note that if the request is successful but no such client existed, the error 14 | // return value will be nil but the bool will be false. 15 | // 16 | // Usage: 17 | // 18 | // principal, ok, err := chef.GetCookbookPrincipal("neo4j.example.org") 19 | // if err != nil { 20 | // fmt.Println(err) 21 | // os.Exit(1) 22 | // } 23 | // if !ok { 24 | // fmt.Println("Couldn't find that principal!") 25 | // } else { 26 | // // do what you please with the "principal" variable which is of the 27 | // // map[string]string type 28 | // fmt.Println(principal) 29 | // } 30 | func (chef *Chef) GetPrincipal(name string) (map[string]string, bool, error) { 31 | resp, err := chef.Get(fmt.Sprintf("principals/%s", name)) 32 | if err != nil { 33 | return nil, false, err 34 | } 35 | body, err := responseBody(resp) 36 | if err != nil { 37 | if strings.Contains(err.Error(), "404") { 38 | return nil, false, nil 39 | } 40 | return nil, false, err 41 | } 42 | 43 | principal := map[string]string{} 44 | json.Unmarshal(body, &principal) 45 | 46 | return principal, true, nil 47 | } 48 | -------------------------------------------------------------------------------- /principal_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetPrincipal(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | config := testConfig() 10 | _, ok, err := chef.GetPrincipal(config.RequiredPrincipal.Name) 11 | if !ok { 12 | t.Error("Couldn't find required principal") 13 | } 14 | if err != nil { 15 | t.Error(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /role.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // chef.Role represents the relevant attributes of a Chef role 10 | type Role struct { 11 | Name string `json:"name"` 12 | ChefType string `json:"chef_type"` 13 | JSONClass string `json:"json_class"` 14 | DefaultAttributes map[string]interface{} `json:"default_attributes"` 15 | OverrideAttributes map[string]interface{} `json:"override_attributes"` 16 | RunList []string `json:"run_list"` 17 | } 18 | 19 | // chef.GetRoles returns a map of role names to a string which represents the 20 | // role's RESTful URL as well as an error indicating if the request was 21 | // successful or not. 22 | // 23 | // Usgae: 24 | // 25 | // roles, err := chef.GetRoles() 26 | // if err != nil { 27 | // fmt.Println(err) 28 | // os.Exit(1) 29 | // } 30 | // // do what you please with the "roles" variable which is a map of 31 | // // role names to their RESTful URLs 32 | // for role := range roles { 33 | // fmt.Println(role) 34 | // } 35 | func (chef *Chef) GetRoles() (map[string]string, error) { 36 | resp, err := chef.Get("roles") 37 | if err != nil { 38 | return nil, err 39 | } 40 | body, err := responseBody(resp) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | roles := map[string]string{} 46 | json.Unmarshal(body, &roles) 47 | 48 | return roles, nil 49 | } 50 | 51 | // chef.GetRole returns a pointer to the chef.Role type for a given string that 52 | // represents a role name. It also returns a bool indicating whether or not the 53 | // client was found and an error indicating if the request failed or not. 54 | // 55 | // Note that if the request is successful but no such client existed, the error 56 | // return value will be nil but the bool will be false. 57 | // 58 | // Usage: 59 | // 60 | // role, ok, err := chef.GetRole("neo4j") 61 | // if err != nil { 62 | // fmt.Println(err) 63 | // os.Exit(1) 64 | // } 65 | // if !ok { 66 | // fmt.Println("Couldn't find that role!") 67 | // } else { 68 | // // do what you please with the "role" variable which is of the 69 | // // *Chef.Role 70 | // fmt.Printf("%#v\n", role) 71 | // } 72 | func (chef *Chef) GetRole(name string) (*Role, bool, error) { 73 | resp, err := chef.Get(fmt.Sprintf("roles/%s", name)) 74 | if err != nil { 75 | return nil, false, err 76 | } 77 | body, err := responseBody(resp) 78 | if err != nil { 79 | if strings.Contains(err.Error(), "404") { 80 | return nil, false, nil 81 | } 82 | return nil, false, err 83 | } 84 | 85 | role := new(Role) 86 | json.Unmarshal(body, role) 87 | 88 | return role, true, nil 89 | } 90 | -------------------------------------------------------------------------------- /role_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetRoles(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | config := testConfig() 10 | roles, err := chef.GetRoles() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | if roles[config.RequiredRole.Name] == "" { 15 | t.Error("Required role not found") 16 | } 17 | } 18 | 19 | func TestGetRole(t *testing.T) { 20 | chef := testConnectionWrapper(t) 21 | config := testConfig() 22 | _, ok, err := chef.GetRole(config.RequiredRole.Name) 23 | if !ok { 24 | t.Error("Couldn't find required role") 25 | } 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // chef.SearchParams represents the necessary parameters of a Chef search query 10 | type SearchParams struct { 11 | Index string 12 | Query string 13 | Sort string 14 | Rows int 15 | Start int 16 | chef *Chef 17 | } 18 | 19 | // chef.SearchResults represents the results of a Chef search query 20 | type SearchResults struct { 21 | Total int `json:"total"` 22 | Start int `json:"start"` 23 | Rows []json.RawMessage `json:"rows"` 24 | } 25 | 26 | // chef.GetSearchIndexes returns a map of search indexes to the indexes RESTful 27 | // URL as well as an error indicating if the request was successful or not. 28 | // 29 | // Usage: 30 | // 31 | // indexes, err := chef.GetSearchIndexes() 32 | // if err != nil { 33 | // fmt.Println(err) 34 | // os.Exit(1) 35 | // } 36 | // // do what you please with the "indexes" variable which is a map of 37 | // // index names to index URLs 38 | // for index := range indexes { 39 | // fmt.Println(index) 40 | // } 41 | func (chef *Chef) GetSearchIndexes() (map[string]string, error) { 42 | resp, err := chef.Get("search") 43 | if err != nil { 44 | return nil, err 45 | } 46 | body, err := responseBody(resp) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | results := map[string]string{} 52 | json.Unmarshal(body, &results) 53 | 54 | return results, nil 55 | } 56 | 57 | // chef.NewSearchQuery accepts an index and a query and returns a struct which 58 | // represents the appropriate search parameters. This is mostly just used by the 59 | // chef.Search method, but you can call it yourself if you'd like some more 60 | // control over your parameters 61 | func (chef *Chef) NewSearchQuery(index, query string) *SearchParams { 62 | params := new(SearchParams) 63 | params.Index = index 64 | params.Query = query 65 | params.Rows = -1 66 | params.Start = -1 67 | params.chef = chef 68 | return params 69 | } 70 | 71 | // chef.Search accepts an index and a query and returns a *Chef.Search results 72 | // type as well as an error indicating if the request was successful or not 73 | // 74 | // Usage: 75 | // 76 | // results, err := chef.Search("nodes", "hostname:memcached*") 77 | // if err != nil { 78 | // fmt.Println(err) 79 | // os.Exit(1) 80 | // } 81 | // // do what you please with the "results" variable which is of the type 82 | // // *Chef.SearchResults 83 | // fmt.Println(results) 84 | func (chef *Chef) Search(index, query string) (*SearchResults, error) { 85 | return chef.NewSearchQuery(index, query).Execute() 86 | } 87 | 88 | // chef.SearchWithParams is similar to chef.Search, but you can define 89 | // additional Chef search parameters 90 | func (chef *Chef) SearchWithParams(index, query string, params map[string]interface{}) (*SearchResults, error) { 91 | searchParams := chef.NewSearchQuery(index, query) 92 | if params["rows"] != nil { 93 | searchParams.Rows = params["rows"].(int) 94 | } 95 | if params["start"] != nil { 96 | searchParams.Start = params["start"].(int) 97 | } 98 | if params["sort"] != nil { 99 | searchParams.Sort = params["sort"].(string) 100 | } 101 | return searchParams.Execute() 102 | } 103 | 104 | // chef.Execute is a method on the chef.SearchParams type that executes a given 105 | // search that has a given set of paramters. This is mostly used by the 106 | // chef.Search method, but you can call it yourself if you'd like some more 107 | // control over your parameters 108 | func (search *SearchParams) Execute() (*SearchResults, error) { 109 | params := map[string]string{ 110 | "q": search.Query, 111 | } 112 | if search.Rows != -1 { 113 | params["rows"] = strconv.Itoa(search.Rows) 114 | } 115 | if search.Start != -1 { 116 | params["start"] = strconv.Itoa(search.Start) 117 | } 118 | if search.Sort != "" { 119 | params["sort"] = search.Sort 120 | } 121 | resp, err := search.chef.GetWithParams(fmt.Sprintf("search/%s", search.Index), params) 122 | if err != nil { 123 | return nil, err 124 | } 125 | body, err := responseBody(resp) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | results := new(SearchResults) 131 | json.Unmarshal(body, results) 132 | 133 | return results, nil 134 | } 135 | -------------------------------------------------------------------------------- /search_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetSearchIndexes(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | config := testConfig() 10 | results, err := chef.GetSearchIndexes() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | found := false 15 | for index := range results { 16 | if index == config.SearchData.Index { 17 | found = true 18 | } 19 | } 20 | if !found { 21 | t.Error("Couldn't find required index") 22 | } 23 | } 24 | 25 | func TestSearch(t *testing.T) { 26 | chef := testConnectionWrapper(t) 27 | config := testConfig() 28 | _, err := chef.Search(config.SearchData.Index, config.SearchData.Query) 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | } 33 | 34 | func TestSearchWithParams(t *testing.T) {} 35 | 36 | func TestNewSearchQuery(t *testing.T) { 37 | chef := testConnectionWrapper(t) 38 | config := testConfig() 39 | query := chef.NewSearchQuery(config.SearchData.Index, config.SearchData.Query) 40 | if query.Index != config.SearchData.Index { 41 | t.Error("Search index isn't correctly set") 42 | } 43 | if query.Query != config.SearchData.Query { 44 | t.Error("Search query isn't correctly set") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/support/TEST_CONFIG.json: -------------------------------------------------------------------------------- 1 | { 2 | "required_cookbook": { 3 | "name": "apache2", 4 | "version": "1.10.2" 5 | }, 6 | "required_role": { 7 | "name": "Neo4j" 8 | }, 9 | "required_client": { 10 | "name": "neo4j.example.org" 11 | }, 12 | "required_node": { 13 | "name": "neo4j.example.org" 14 | }, 15 | "required_environment": { 16 | "name": "production" 17 | }, 18 | "required_recipe": { 19 | "name": "test::compile_time" 20 | }, 21 | "required_user": { 22 | "name": "admin" 23 | }, 24 | "required_data": { 25 | "name": "mysql" 26 | }, 27 | "required_principal": { 28 | "name": "admin" 29 | }, 30 | "search_data": { 31 | "index": "node", 32 | "query": "nodename:neo4j*" 33 | }, 34 | "test_credentials": { 35 | "host": "loclahost", 36 | "port": "8443", 37 | "version": "11.6.0", 38 | "user_name":"admin", 39 | "key": "test/support/keys/admin.pem" 40 | }, 41 | "key_path": "test/support/keys/admin.pem", 42 | "key_string": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwJexi/Dyx4A8vE46AQ+nc+3LeG+xyMORKvZHnjQET+/nj8b6\nepNtbA3rtBD7zZU8o11Dneqx203d+rXcpNh9XZ1h4BjVNf+Vuignf2I1caJubPmI\nAhKjZuAgdpXdWnFVIHQhiTJIieHxa8F1Dduh8kRcx+/VNiTH9omaP96kzjEwpAZ1\nh+J2ffFhZKaa/IaHTH3i275RoBehiTxPMR18iwK0TiHKUNtD5ToU7Z7/fg/b734Z\nw08/F/McPZLC3kPp+DNgxrIvPKQOySg0eJBDPAkCZK9FTxL5XwACbqAXo9vr3Um4\nuO/zzfq86kmPP8TDEOkx4MVPRfRSw/5URUdTNQIDAQABAoIBAQC0Nj2AgPnsC+bJ\nHTnRfGBobf6St5FYFwdjeAgT1QCj/Axl0P7Ya9Nb5LrFR48XE6omA0MeJVo/p2Er\nE7le1uSYEIhhNVtsp7fm9WPgInl311IUznmh1ubngJlf3WO/GYVaVNB8nHyb6LwK\n5H8vdHDZTXaz74tGmqwx2Rho42C7PlSASobSHoSgoJbrUblksMhWwYOAlJxkv3U+\nNWCTecUKsEW/ZhAwAB0F/d2BzV39FT4hidPb5TZuUMyOdQ6ZU7r0FrMVvSZuF/tO\nM38aclHomv/RKhJs6AfO8hlaCre6Xw5wzMbgRePZ7MD3nVTKM9LIZ0IrJy3b3dDv\noAHeKTKRAoGBANvkFYa+eF7jKZq6HoRovunBCliBzhEWdRtoqdfsWFreyZyG5fhF\n7vPie46FTUzXwOUT78RFJ3h95RLCFpwmtyGcKbqUJuvE6XrJ8n1m7x9DQJ58w8xA\nSP6xAlhf785NeCiFmGKyNQTBTZd/7AYLkIhyjc16k2tmloPYdbekyuZ3AoGBAOA4\nBZvcYMNdpKlgvVX0GkAeydgOVB6zkIxh0Vb0PdcyZ3qIYekpOqO1wOX0uOTALVqm\nsI82FN0c57Pj+bvjga1b+w71xz4k5U2OCNMuNfhTxHbkHxa1rVsEC59rh8Cof/I7\nTx5d6ITzMUAMLY9zdEd1Y0eZ+GklEOjlfCYTzsKzAoGAbdKw3adrvxh9wwWKEF+w\nWldLr4DFRsuMma/ghDNKvbffwgmt+h4ThkNrqnbxcJuhf8hBTCmt2QQn0QKXAE5l\n0KeKP+GWvtKOOgF61x799wND1uRBdlX1Hx+RKEpiHwuaI8L1pPWUfVd+RVhirXJB\nMaTlJec+kbmfGyKVSK9Jl2kCgYEApXcyTG85yXMVzRljIEdgB+mwswz/7xg8mByn\nzC+6Y1EDJF35Q4P1jlLW49zRhZ582eUhbs5OX06xfwPCjHG8hR7YC5Y9TxMTVUR4\nJsB3e6b12XdHqg6wu/YkTaWhLQAsS6qRHKvYqCSAN+/Ev5RIP2PVvEVYGOANtpGE\nKOV9NBkCgYA4mQcjPjORWJXNFLQat4RBrwRkJnwb+ynKnxX710S8BbWfy3WDG8fD\niythdhdkuhh2A7kO1jqUCiNi36festwx4bC/wUNSqgGK0M5OtRaZbYofCCLB2bCX\n7De55k/yrx2xMrX+TaN0DGADMZrYWkpK4UIY8znEIpQyjWht4OC6Zw==\n-----END RSA PRIVATE KEY-----" 43 | } 44 | -------------------------------------------------------------------------------- /test/support/chef/environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "production", 3 | "description": "", 4 | "cookbook_versions": { 5 | }, 6 | "json_class": "Chef::Environment", 7 | "chef_type": "environment", 8 | "default_attributes": { 9 | }, 10 | "override_attributes": { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/support/chef/mysql_bag.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "mysql", 3 | "data": [ "somestuff" ] 4 | } 5 | -------------------------------------------------------------------------------- /test/support/chef/node.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neo4j.example.org", 3 | "chef_environment": "production", 4 | "json_class": "Chef::Node", 5 | "automatic": { 6 | }, 7 | "normal": { 8 | }, 9 | "chef_type": "node", 10 | "default": { 11 | }, 12 | "override": { 13 | }, 14 | "run_list": [ 15 | 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test/support/chef/role.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Neo4j", 3 | "description": "", 4 | "json_class": "Chef::Role", 5 | "default_attributes": { 6 | }, 7 | "override_attributes": { 8 | }, 9 | "chef_type": "role", 10 | "run_list": [ 11 | 12 | ], 13 | "env_run_lists": { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/support/chef/test_cook/Berksfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://api.berkshelf.com' 3 | 4 | metadata 5 | -------------------------------------------------------------------------------- /test/support/chef/test_cook/Berksfile.lock: -------------------------------------------------------------------------------- 1 | DEPENDENCIES 2 | test 3 | path: . 4 | metadata: true 5 | 6 | GRAPH 7 | apache2 (1.10.2) 8 | iptables (>= 0.0.0) 9 | logrotate (>= 0.0.0) 10 | pacman (>= 0.0.0) 11 | iptables (0.13.2) 12 | logrotate (1.5.0) 13 | pacman (1.1.1) 14 | test (0.0.0) 15 | apache2 (= 1.10.2) 16 | -------------------------------------------------------------------------------- /test/support/chef/test_cook/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'test' 2 | depends 'apache2', '1.10.2' 3 | -------------------------------------------------------------------------------- /test/support/chef/test_cook/recipes/compile_time.rb: -------------------------------------------------------------------------------- 1 | # 2 | # test recipe that does nothing 3 | # 4 | -------------------------------------------------------------------------------- /test/support/chef_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # setup the chef/knife config for use with local testing 3 | # 4 | # TODO: these are shares stuffs 5 | set -e 6 | set -x 7 | 8 | rundir="/tmp/goiardi" 9 | 10 | pushd $(dirname "${0}") > /dev/null 11 | basedir=$(pwd -L) 12 | # Use "pwd -P" for the path without links. man bash for more info. 13 | popd > /dev/null 14 | 15 | if [ -f ~/.chef/knife.rb ] ; then 16 | echo "looks like you go a knife.rb installed. exiting" 17 | exit 0 18 | else 19 | echo "no local knife config using our stub" 20 | mkdir -p ~/.chef 21 | cp $basedir/knife.rb ~/.chef/ 22 | fi 23 | -------------------------------------------------------------------------------- /test/support/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | test/support/chef_config.sh 5 | go get -v github.com/ctdk/goiardi 6 | test/support/start_server.sh 7 | go get code.google.com/p/go.tools/cmd/vet 8 | go install code.google.com/p/go.tools/cmd/vet 9 | go vet 10 | go test -v -cover 11 | test/support/stop_server.sh 12 | -------------------------------------------------------------------------------- /test/support/keys/admin.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAw+I0hxlZ/4QjHzQwAzzTokMAz87eFG++CqxohJxU5RMf6bAM 3 | HbobqchC/+KKbwxjg0HOduOhsCS0NhozeDHx/nLJPpc/uWvUBEz5gbfOuNF3aNU3 4 | xPavor9+uXyDECknbWCeFMaYXf/rng8x5SyTTPX+pBwhJg4TpaL/HhL5KaC9EFea 5 | aR6B3AMzHgVXQZG6nVfk18/IUhbPhekqcAF4fjm1IjSDrUNLQ7lMuS0elLo5IkP9 6 | 09CCgPilLLvhG+absSzU60TzIr7iRahUfAnMyYZBoZneVrDXHnTTkDwbJGrGUSD+ 7 | ute+n+6DZxfk/JSquUgjKqPINCFu124TVFzlKwIDAQABAoIBAQCWXZp86pUPc9uS 8 | JHmGwraipFSHMQGBJpX3k4lX4rm4bwncVWu3lhmOJ1BnJPunaxcotudKlEqV8Fb2 9 | AP7pkU12SZ76ERi1uXQzKPOPBA4ICaNkMehxpfwFLoKU2GOzkCPX7obq49gqsD2q 10 | Q3fFuIYKWfty9rTrhXrHd2vnJiFmNcfxwNLrzIyZGbIxW6PESAKMjhpnjBdK/HKY 11 | QoPStH5M1RBitbtbI3EsB3r8/NvxjifvuehKctWyRTH+ZGByRN8PDSkYH+viwwhV 12 | 5cId6WQrPTh4F24Dfi/YQ498y9jkurzB882UmDCV9ZRrw6/YmZ2tzjyJOnJk9g8k 13 | Yc7TOShhAoGBAMrftwgHbVPa4FJZr+bcSKmSmHLfQ6gtogbqPfekbiead07LxMSH 14 | qVL2N1LRf793L95VHJTqf9vIN0aV2M3t616EaxeOVXVJ6mWYcQq7ivDfrPD5Lf2W 15 | ezQZT4ilVUc2IWKGQrH7VgrmlLCXaYUmIblow6/ks01jQP4g8b191ZM1AoGBAPct 16 | 4B2QNIFzMRwGJrgv0KJe/gVE0uJUg8blCp7U0Jk+XGq8QodPSzdiaJlXHpl66q98 17 | w6GjTLOkKYCao/tFE8Jv2DCqVsVWz2xeKxh5n47t2gdqUepDDEeTH0TZn72uJrKu 18 | 9ZZ9poR9ibnj7vYnsnpn4omqfxDVG7Ep2c7HpkLfAoGBAJtcL0KKzYKF/3uRC0/0 19 | KgESLoTpzVoheA9rmwDmfyT8+ZjyG0GV0mmh/EQEA5yQnavZ6gVwf3HEJfB8GIe7 20 | R6AeMBTd/zvO2x3qW8coUAKeiEYRzNKCRt9i8rFmS6Q5I9/biX/bRc0bP4rsqPY6 21 | eWEPhaxRq6378G2stPO0RbZxAoGAM930gT+ZRPCfBXpk9ZFS/j9gnkpq1gNWA45c 22 | RCfz/bQQQHVwoGQLLHwB5utZCGu6g0hy5KAXoDp6WvoYHsnH86z0eIHF43Lr45l/ 23 | AYvD54IRlizBM58OHJbKoO/pjSvWAvijoAiNwI93LwKdShnaYcUazDhyoV2Wif52 24 | LeXHBZECgYEAs966Uo867OX025qBGK0UvxT3JdebjfwPoXkDgxNyA0ThhbC44p5V 25 | Ltaczbt1d/JDjvPkMTiZi3AX6WMCLFWSGc4NClIqPqmHaxyKdYe1vBiISdrRkDHb 26 | DFvE6SwJPDPIf/liA23jCtU/RLdH4muUBervYJCqoX4sVhZII0val24= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/support/keys/chef-validator.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAzIL1TCIPJDvbed7aTjCkphFjKWx3JlQ3IjFTCmR+7UqvvBYw 3 | qqtSNQKeOs8uY/V/n8oxNjrVTwqehVW1PyD5SGUAto33FwPcFrHdg/9d9sG1T/iw 4 | lGBi/aYYKt7bKZwv6AJk1yyBXgb4UTpOmJBs3kNbYFrikZJXiuYMxsIOm/TO5/Yg 5 | BNL9EYvTZ4F7xrWStYYwfFDcxZQ8mnqH3d0+WORx75WV/szjCzij8wELu4698/sC 6 | anF46P/R2icwDpH8aDoQpYqgE5TbYsQrT/x+aYh0P1vR2Ycd278nbyxCZDBV+j4n 7 | MxIyI+vSU82HvuL3LF+F2jzGulSCi3SV+g0U8QIDAQABAoIBAAz03wWLwtnQ0P7P 8 | l5UMj+HViCq/rGzJORT5ewFVjJUL/kY0QbEOWY7yM4QWTCIZE5TUAnolEA6J5bEc 9 | 1PbC6AgmHf1y0QysOe1WJ2qNay6O5yXB464CzUK0myNcKxZfxsk0HZ9kOm3ykxjS 10 | UNhpjfwm2EsqduAyHUm3eWe+M569rp0HwTAwUlqpYNeufwCpQss82/ryNFfNGTCp 11 | ND/sXqKcgGMLr55noQRtyuydv2xmu/Lu4TT0UCiuSSyocTc/ce61d/PBst7h3qWL 12 | JXtbjag3oysWg5E6DIU4E5fhAXXiSl5f2PemsDYAFW4ryq87px62uh24CL7rs5Am 13 | 1RsQCWECgYEA/B9bk2dYHMFRcS5A3l7IyZcpezzmtB4Ns1qlFF0Zh84DqMQxN5Al 14 | gOMl9gM+BfNqXH/PItSAc++fZo0V4ka2HM+DGKr3EtjfQS/SuzFHprVUVW3uLME3 15 | u9eMWp1Qy+51WKxwzDrB14P85tCgC28iGDZo6YcrgK6FavBdiLxAHUsCgYEAz6gm 16 | QCn3BR+oqDNfxWljcoV5XXXI6GhMw5d8XL5iIx8noT5gwHYZXHsDbaor1kv0g2K7 17 | piGvmCZb3Y4T5rWRUacOW4qBYsYCNZxjr9edGo3le+AfTNFtnEQJz0ntoXEPUhpd 18 | 64ZklgUIW5bVI9UI/OdMYtUnkJeemZaAxxUJXTMCgYEAw1ggghM+Z/k4+vKVfDYA 19 | aIFguIW1UrU+dEq1QRPpZhmvKCHq6FwdUsGsdfKJKd8x971nZlszs6Bj5vdFj26u 20 | R6BYuNIuYUH7qOw+4J8MHxS75P0p3y9N2ZLmwobFRVzbOvtQ4kxcSHmRh9z5RCK1 21 | TgFKRDZg9HFEGllmfz0siAUCgYBMp0V3agZFXepkLJKtzLlpDNxAI7qGD/pGOSUv 22 | Fxgi3t1CKESOxwmVmdMqKCcGdANOfKPIL6ciAwbmQgwWne+GtKQYJoicbXtjTtd9 23 | kZnss3SCxbjIIobTqEn2utN9RL7jD9ibmICI4FK1fHgxQfKPaNJJqWVxeoaRK31d 24 | /+X1AQKBgBppEurF0Mb7BfRR6gXu9uDkQ/lugOUOTC9FNawKZaPtDOLwDv1JF7r0 25 | uS8yhcDuvt/LooqYc5TzcTToyM3ZYg4FrwPdK4kYSQY4zW1CRpVbtGHC0tu8sk6W 26 | Rq2AsuocvVmnyxyxZ8aRuWubspnn9PQHA3VMasCvpjWVeNIFkx0u 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/support/keys/chef-webui.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEA6gnIyrwHRdeVGz+DSlnBpoMLeB6CPD/aKtfrN7+vfWO8AeGV 3 | 7yhnpzNB2TB72pFPVZjZpTMPIMBc0EKqLWjGfGzH1NUgEx2GMijCc0vvhHqBm2+O 4 | EHPp4+Tv0GQLa4I7JGtYhncKG1jEZR6uXd4JuP3Mt3wctB3l/W0tNi1UOu+wv0Cg 5 | I13MKdsucIO5i5vcQheSJwiEJsiCjRVwraSzkBOLIs9dEIP49ZLuz24OZRVxr3nE 6 | cE/CmP2tVFFlEY9AkApgYgC05TTYYaGCdxh5/ymGiMKCiF/gL9xnTb2ZjSqFgKKR 7 | CgsOA30cEja9MmP9SfJSNNz//bpu1RBbBzPMswIDAQABAoIBAQCcpF/YEXFstZX+ 8 | 3suo/9o08JDEnObeHH6RHmM4tPtoGVDEkZAguhJmPlBcZa7/4zIOQHersjj12spE 9 | Odpc+jufG8/EC8GkCaa/+7fqI3D0YmpM9qVHLc3l0l0QRWW/VzLudUqS9Sm2k/sf 10 | EyHxgaNQRc63IEK16yz8Kg6QrFYr+umyVBjlanEWMDIjvjMfjOXmw3yDaNNoxuLe 11 | HStFzmwE8stRFOIJz8zjYXi0zAnOQngaHA/UUSdwvMS6Y3TfODuQpM2CSadqSwAw 12 | I+GbKOsz+tMQBztHE4Pq2V2GvuZ0j5AE0UXR/3yawOMIzpbjafFUaPdq182Lp4w6 13 | GLHhEcJBAoGBAPt6uiy/dzFoLLvYLuqNaRfC4dju+CB4l1fyDSYmhGE17+OhQ3A1 14 | eh0BsUgWnAmHv2FyFeL6HF/lKI9J+pcSojgHEXejPSym4HxkVprLQjB/aH5QsYE/ 15 | SZT0Sy6ns+D+QUsJeU4as00JYCea8nqjEYejgQV4wv8G1argHcXjTbfFAoGBAO4+ 16 | y5RYKYUKykXX38ORLXW1vEOpHZASs6Sfm5bZJnlddg9qM1YmDujGFp7Ld/ZsjjGw 17 | jK0eyq9nGL0p/mDpsu9dOMqitUgpfeYUVWZkkuBrIPBAIV+zu8qomo6akrmcpj+f 18 | WoD3b5LhkaVFKLb+EL1IKZvOFOmNfC5abCO4ccIXAoGBALuBYYl15UiIdTpGUYz/ 19 | jUlzPHREkeucHrQrPVShyXi1/7zJBACJTxiLp4SpnwSYG8FrkBgiBsAVnG1iap7z 20 | /NIrqwF6TGWbs6vsIIrgXOTOSrpMLl3BM3sFv3z0+/EZgtORJ8ptrB0XjYNaS8vX 21 | BH4PEmdjCHmgvfXfjzN6bPtFAoGBAMCkBfjtUKVDFEtlQonf2eJUEOak2EKm3JtA 22 | c46GB5Ya/Ft6hAgkvel6QtAjtdyL6m5DU3yoaTtrIZXTXtz+GrFxr3oqFmtDIYno 23 | 5yTkLd9IwFmDgXjg2/gCXPbyBbIP+F15fLTLXmxl+F6qjUFqLh7fOsXA3Sf8P207 24 | sDUx7soBAoGBANzzxF/NgbpDoLshW5SrZFsvP7LlqT4my6mD9AoJQ0bzBy1ZeU1b 25 | NTZI5mFNxWaePCkc63Pm9fxKYCvBvgbygcxrgngQnKxlm6F+AFQotHQq7PtL6oAZ 26 | 4PBs+5n0nDDsRxrW9wEXXlPjmfh/Q9t1n91ORyu75N6DcKPQC3i1uNZm 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/support/keys/neo4j.example.org.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA9wHmTKrRwvkyCxLxFXMR0pd85iP82uFnYXjq/QOYipVOrY7z 3 | E/bCphg53NZ6njW32ku1cEqOb/Yg+RBuUzpjS7zF6Q9WTNlkRpSDrmEEhGOT4L0S 4 | 771eMTnJL5HBpmxVwqyXlaxXLbFdQ7VHa7cMqbDVMyTjFnV2CIl//Viae1adFiF0 5 | /VXqEqD2BFrRd9JuJeUrLUvAbrE+ykBPTXdsjQ8oEEnptZtyrCTRaoVg6mGjJfyV 6 | YWoo+krO8NwljKo7FUuWqWxQtNZdOPEYB3ytLgMQ7yw6glS4bfJGjt5KPJW5Ot8m 7 | gDAqT+hP6ECxbwUIRp6bNU2fM+bR8ErkROIhmwIDAQABAoIBAFqH47St1vuzAdh1 8 | NM87wYiB5VEkV4SRoKZk/xuEud/xIUkTX5wtkOH3dx2trntWDPCBVxq0+MeoWiVv 9 | FECE4efcZF0rNeIsu0fzHuBGxrUPGiAArHQZ3/AC0e4RafxpMHWNdHFzHPCuVRR/ 10 | 2JFuIcIAAeQg3OpaWsx+ON7OHrCdMdj+xBB3z8P7qxoZkpttiSaEpcpyB/RJv4sn 11 | k9FZNothi51usyhES20/LMDT4SxtJ1u8bfCPTp1aKS870vYykI7JQqqp3jupXOLK 12 | /zkDvmdFR3DHESX48e3165CSdO75D1y1E8d4PqkyxMgwu7hpC3eGVZuyKq9npSgV 13 | ROu5EmECgYEA/ELc2DVswOLSEJ0Aj/yKg0mVkGK2CfEP4sNn1QcyTfKQuGknQ/IG 14 | lA2mvoCMqLIh6rVUgXQ87kqgTNEeD1uzIa35fFX4VkhsqiU9ERGdwkUfAB1ojgm5 15 | gqLNbnCanDhOD373cymjxMwBo5H6j8WMg+ek6hzSSzI6AA1E7Mw9gf8CgYEA+qsa 16 | WopPY0uRLQw+HJhPq42Cau0eMTN6A0JTTxRLmcIuugMxpbt8yUmAOgyfuiKGXND2 17 | an/xQYrzzov45O5GG6YN64941B/aBthye0z6KkUl5uQclO16g1nqzpW86BuT8qg0 18 | bnC5Q7GAS3KQm+p5oHV3JKWix3pA9QSMVE+yKGUCgYA1o0XwFxQDw3blH20SMKGH 19 | HutVYxfP71jwX5pBWqFgn5YUEA/QS7MIYgq1cQH3kMDTdjD8wal/BZEmJETfASGK 20 | pMJqpKEEP6wTVY2xhmxj9WyNCrPDl9/BHjrOI9VMKFC4zMdyzkTLQJGMpSqAzYF+ 21 | Po/7U5uHb6uTfT7NlvPxIwKBgQDbirZHLDIBvnKFKN7RfpjwoLzj14+E+hfX+OQs 22 | M5NPNKJmKlv2UrIlPWm/7cSLaqeRbTcU6b+sbv3AHhm8r+/ZUWOJt5vU9ZyoODR0 23 | R2QnV26tvYZG7YNKvZsIml7cTyMR+T4KqFGqtzCtY8bBt+gUoCzcfPGHkkyIDBa3 24 | 1SEtfQKBgQDjXqIJaLIo0qAf8nH0YihVQlYDeYka8mpt6XWUbNI9SdMYUplquoz0 25 | Bfj2ydfwslemy/R+bSx2szVhcRfJ7+iVpF3Oz03pjAwT0fhIJkrbVUFBF2H24kkC 26 | iEp2dQ4DzN0p8a6JOMEDMQEWKvQAGQ8/e0v75qwr4F1i6Ni7G83ktg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/support/knife.rb: -------------------------------------------------------------------------------- 1 | log_level :info 2 | log_location STDOUT 3 | node_name 'admin' 4 | client_key '/tmp/goiardi/admin.pem' 5 | validation_client_name 'chef-validator' 6 | validation_key '/tmp/goiardi/chef-validator.pem' 7 | chef_server_url 'http://127.0.0.1:8443' 8 | -------------------------------------------------------------------------------- /test/support/regen_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script should only be used if you need ot regen the goiardi data/index and keys 4 | # 5 | # requires knife. We don't use this on each build beacuse of that. 6 | # TODO: at some point we can bootstrap with a tool thats not knife ;) 7 | # 8 | #------------------------------------------------------------------------------- 9 | rundir="/tmp/goiardi" 10 | 11 | if [ -d $rundir ]; then 12 | rm -f $rundir/* 13 | else 14 | mkdir -p $rundir 15 | fi 16 | 17 | pushd $(dirname "${0}") > /dev/null 18 | basedir=$(pwd -L) 19 | # Use "pwd -P" for the path without links. man bash for more info. 20 | popd > /dev/null 21 | 22 | # shut it if its running 23 | $basedir/stop_server.sh 24 | 25 | # remove old data 26 | rm $basedir/keys/* 27 | rm $basedir/seed_data/* 28 | 29 | set -x 30 | set -e 31 | 32 | go get -u github.com/ctdk/goiardi 33 | go install github.com/ctdk/goiardi 34 | 35 | cd $basedir/chef 36 | # start server with our support dirs 37 | goiardi -A -V -H localhost -P 8443 -D $basedir/seed_data/data -i $basedir/seed_data/index -F 30 --conf-root $basedir/keys & 38 | 39 | # dumb but we need to wait 1 for the keys to be populated. 40 | while [ ! -f $basedir/keys/admin.pem ] ; do 41 | echo 'waiting for admin key to be written' 42 | sleep 1 43 | done 44 | 45 | # set environment 46 | knife environment from file environment.json 47 | # setup node 48 | knife node from file node.json 49 | # set up role 50 | knife role from file role.json 51 | # bag 52 | knife data bag create mysql 53 | knife data bag from file mysql mysql_bag.json 54 | # client 55 | knife client create neo4j.example.org -d -f $basedir/keys/neo4j.example.org.pem 56 | 57 | # upload cook 58 | # berks3 Berksfile 59 | cd $basedir/chef/test_cook 60 | berks install 61 | berks upload 62 | -------------------------------------------------------------------------------- /test/support/seed_data/data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marpaia/chef-golang/5237b77e9f54dc5a60b008997f0cc6dfae41170f/test/support/seed_data/data -------------------------------------------------------------------------------- /test/support/seed_data/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marpaia/chef-golang/5237b77e9f54dc5a60b008997f0cc6dfae41170f/test/support/seed_data/index -------------------------------------------------------------------------------- /test/support/start_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # script to setup and pull goiardi into local 4 | # system, create users, and run tests against. 5 | # 6 | #------------------------------------------------------------------------------- 7 | set -e 8 | set -x 9 | 10 | rundir="/tmp/goiardi" 11 | 12 | if [ -d $rundir ]; then 13 | rm -f $rundir/* 14 | else 15 | mkdir -p $rundir 16 | fi 17 | 18 | pushd $(dirname "${0}") > /dev/null 19 | basedir=$(pwd -L) 20 | # Use "pwd -P" for the path without links. man bash for more info. 21 | popd > /dev/null 22 | 23 | go get github.com/ctdk/goiardi 24 | 25 | cp $basedir/keys/* $rundir/ 26 | cp $basedir/seed_data/* $rundir/ 27 | 28 | goiardi -A -V -H localhost -P 8443 -D $rundir/data -i $rundir/index -F 30 --conf-root $rundir & 29 | cd $basedir/../../ 30 | pwd 31 | -------------------------------------------------------------------------------- /test/support/stop_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pkill goiardi 4 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // chef.GetUsers returns a map of user names to the users RESTful URL as well 8 | // as an error indicating if the request was successful or not. 9 | // 10 | // Usage: 11 | // 12 | // users, err := chef.GetUsers() 13 | // if err != nil { 14 | // fmt.Println(err) 15 | // os.Exit(1) 16 | // } 17 | // // do what you please with the "user" variable which is a map of 18 | // // user names to user URLs 19 | // for user := range users { 20 | // fmt.Println(user) 21 | // } 22 | func (chef *Chef) GetUsers() (map[string]string, error) { 23 | resp, err := chef.Get("users") 24 | if err != nil { 25 | return nil, err 26 | } 27 | body, err := responseBody(resp) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | users := map[string]string{} 33 | json.Unmarshal(body, &users) 34 | 35 | return users, nil 36 | } 37 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | package chef 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetUsers(t *testing.T) { 8 | chef := testConnectionWrapper(t) 9 | config := testConfig() 10 | users, err := chef.GetUsers() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | found := false 15 | for user := range users { 16 | if user == config.RequiredUser.Name { 17 | found = true 18 | break 19 | } 20 | } 21 | if !found { 22 | t.Error("Couldn't find required user") 23 | } 24 | } 25 | --------------------------------------------------------------------------------