forked from go-chef/chef
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
4 changed files
with
311 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |