Skip to content

Commit

Permalink
New Cookbook Download Feature
Browse files Browse the repository at this point in the history
Adds two new functions for users to consume:
* `DownloadCookbook()` - download a cookbook to the current directory on disk.
* `DownloadCookbookAt()` - download a cookbook at a specific
path/directory.

Closes go-chef#114

Signed-off-by: Salim Afiune <afiune@chef.io>
  • Loading branch information
Salim Afiune committed Oct 15, 2019
1 parent 28591ee commit ac301c9
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 1 deletion.
2 changes: 1 addition & 1 deletion cookbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (c *CookbookService) GetAvailableVersions(name, numVersions string) (data C
// Chef API docs: https://docs.chef.io/api_chef_server.html#cookbooks-name-version
func (c *CookbookService) GetVersion(name, version string) (data Cookbook, err error) {
url := fmt.Sprintf("cookbooks/%s/%s", name, version)
c.client.magicRequestDecoder("GET", url, nil, &data)
err = c.client.magicRequestDecoder("GET", url, nil, &data)
return
}

Expand Down
109 changes: 109 additions & 0 deletions cookbook_download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// Author:: Salim Afiune <afiune@chef.io>
//

package chef

import (
"fmt"
"io"
"os"
"path"
)

// DownloadCookbook downloads a cookbook to the current directory on disk
func (c *CookbookService) DownloadCookbook(name, version string) error {
cwd, err := os.Getwd()
if err != nil {
return err
}

return c.DownloadCookbookAt(name, version, cwd)
}

// DownloadCookbookAt downloads a cookbook to the specified local directory on disk
func (c *CookbookService) DownloadCookbookAt(name, version, localDir string) error {
// If the version is set to 'latest' or it is empty ("") then,
// we will set the version to '_latest' which is the default endpoint
if version == "" || version == "latest" {
version = "_latest"
}

cookbook, err := c.GetVersion(name, version)
if err != nil {
return err
}

fmt.Printf("Downloading %s cookbook version %s\n", cookbook.CookbookName, cookbook.Version)

// We use 'cookbook.Name' since it returns the string '{NAME}-{VERSION}'. Ex: 'apache-0.1.0'
cookbookPath := path.Join(localDir, cookbook.Name)

downloadErrs := []error{
c.downloadCookbookItems(cookbook.RootFiles, "root_files", cookbookPath),
c.downloadCookbookItems(cookbook.Files, "files", path.Join(cookbookPath, "files")),
c.downloadCookbookItems(cookbook.Templates, "templates", path.Join(cookbookPath, "templates")),
c.downloadCookbookItems(cookbook.Attributes, "attributes", path.Join(cookbookPath, "attributes")),
c.downloadCookbookItems(cookbook.Recipes, "recipes", path.Join(cookbookPath, "recipes")),
c.downloadCookbookItems(cookbook.Definitions, "definitions", path.Join(cookbookPath, "definitions")),
c.downloadCookbookItems(cookbook.Libraries, "libraries", path.Join(cookbookPath, "libraries")),
c.downloadCookbookItems(cookbook.Providers, "providers", path.Join(cookbookPath, "providers")),
c.downloadCookbookItems(cookbook.Resources, "resources", path.Join(cookbookPath, "resources")),
}

for _, err := range downloadErrs {
if err != nil {
return err
}
}

fmt.Printf("Cookbook downloaded to %s\n", cookbookPath)
return nil
}

// downloadCookbookItems downloads all the provided cookbook items into the provided
// local path, it also ensures that the provided directory exists by creating it
func (c *CookbookService) downloadCookbookItems(items []CookbookItem, itemType, localPath string) error {
if len(items) == 0 {
return nil
}

fmt.Printf("Downloading %s\n", itemType)
if err := os.MkdirAll(localPath, 0755); err != nil {
return err
}

for _, item := range items {
itemPath := path.Join(localPath, item.Name)
if err := c.downloadCookbookFile(item.Url, itemPath); err != nil {
return err
}
}

return nil
}

// downloadCookbookFile downloads a single cookbook file to disk
func (c *CookbookService) downloadCookbookFile(url, file string) error {
request, err := c.client.NewRequest("GET", url, nil)
if err != nil {
return err
}
response, err := c.client.Do(request, nil)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return err
}

f, err := os.Create(file)
if err != nil {
return err
}

if _, err := io.Copy(f, response.Body); err != nil {
return err
}
return nil
}
154 changes: 154 additions & 0 deletions cookbook_download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// Author:: Salim Afiune <afiune@chef.io>
//

package chef

import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"testing"

"github.com/stretchr/testify/assert"
)

const emptyCookbookResponseFile = "test/empty_cookbook.json"

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

mux.HandleFunc("/cookbooks/foo/2.1.0", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Found", 404)
})

err := client.Cookbooks.DownloadCookbook("foo", "2.1.0")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "404")
}
}

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

mux.HandleFunc("/cookbooks/foo/_latest", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Found", 404)
})

err := client.Cookbooks.DownloadCookbook("foo", "")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "404")
}

err = client.Cookbooks.DownloadCookbook("foo", "latest")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "404")
}

err = client.Cookbooks.DownloadCookbook("foo", "_latest")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "404")
}
}

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

cbookResp, err := ioutil.ReadFile(emptyCookbookResponseFile)
if err != nil {
t.Error(err)
}

mux.HandleFunc("/cookbooks/foo/0.2.0", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, string(cbookResp))
})

err = client.Cookbooks.DownloadCookbook("foo", "0.2.0")
assert.Nil(t, err)
}

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

mockedCookbookResponseFile := `
{
"version": "0.2.1",
"name": "foo-0.2.1",
"cookbook_name": "foo",
"frozen?": false,
"chef_type": "cookbook_version",
"json_class": "Chef::CookbookVersion",
"attributes": [],
"definitions": [],
"files": [],
"libraries": [],
"providers": [],
"recipes": [
{
"name": "default.rb",
"path": "recipes/default.rb",
"checksum": "320sdk2w38020827kdlsdkasbd5454b6",
"specificity": "default",
"url": "` + server.URL + `/bookshelf/foo/default_rb"
}
],
"resources": [],
"root_files": [
{
"name": "metadata.rb",
"path": "metadata.rb",
"checksum": "14963c5b685f3a15ea90ae51bd5454b6",
"specificity": "default",
"url": "` + server.URL + `/bookshelf/foo/metadata_rb"
}
],
"templates": [],
"metadata": {},
"access": {}
}
`

tempDir, err := ioutil.TempDir("", "foo-cookbook")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(tempDir) // clean up

mux.HandleFunc("/cookbooks/foo/0.2.1", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, string(mockedCookbookResponseFile))
})
mux.HandleFunc("/bookshelf/foo/metadata_rb", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "name 'foo'")
})
mux.HandleFunc("/bookshelf/foo/default_rb", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "log 'this is a resource'")
})

err = client.Cookbooks.DownloadCookbookAt("foo", "0.2.1", tempDir)
assert.Nil(t, err)

var (
cookbookPath = path.Join(tempDir, "foo-0.2.1")
metadataPath = path.Join(cookbookPath, "metadata.rb")
recipesPath = path.Join(cookbookPath, "recipes")
defaultPath = path.Join(recipesPath, "default.rb")
)
assert.DirExistsf(t, cookbookPath, "the cookbook directory should exist")
assert.DirExistsf(t, recipesPath, "the recipes directory should exist")
if assert.FileExistsf(t, metadataPath, "a metadata.rb file should exist") {
metadataBytes, err := ioutil.ReadFile(metadataPath)
assert.Nil(t, err)
assert.Equal(t, "name 'foo'", string(metadataBytes))
}
if assert.FileExistsf(t, defaultPath, "the default.rb recipes should exist") {
recipeBytes, err := ioutil.ReadFile(defaultPath)
assert.Nil(t, err)
assert.Equal(t, "log 'this is a resource'", string(recipeBytes))
}
}
47 changes: 47 additions & 0 deletions test/empty_cookbook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"version": "0.2.0",
"name": "foo-0.2.0",
"cookbook_name": "foo",
"frozen?": false,
"chef_type": "cookbook_version",
"json_class": "Chef::CookbookVersion",
"attributes": [],
"definitions": [],
"files": [],
"libraries": [],
"providers": [],
"recipes": [],
"resources": [],
"root_files": [],
"templates": [],
"metadata": {
"name": "foo",
"description": "Installs/Configures foo",
"long_description": "Installs/Configures foo",
"maintainer": "The Authors",
"maintainer_email": "you@example.com",
"license": "All Rights Reserved",
"platforms": {},
"dependencies": {},
"recommendations": {},
"suggestions": {},
"conflicting": {},
"providing": {
"foo": ">= 0.0.0"
},
"replacing": {},
"attributes": {},
"groupings": {},
"recipes": {
"foo": ""
},
"version": "0.2.0"
},
"access": {
"read": true,
"create": true,
"grant": true,
"update": true,
"delete": true
}
}

0 comments on commit ac301c9

Please sign in to comment.