From bc017707408fe8f59a1bda0c478dfde630ad0d1a Mon Sep 17 00:00:00 2001 From: Matthew Newell <18470637+wheatevo@users.noreply.github.com> Date: Sat, 20 Jan 2024 07:48:21 -0600 Subject: [PATCH] Add Cookbook Uploads * Add support for cookbook uploads (V0 and V2). * Add support for chefignore files * Improve metadata.rb parsing to conform with Chef API * Add ServerApiVersion customization to http * Minor linting changes for newer golang versions Resolves #250 --- README.md | 3 +- association_test.go | 4 +- chefignore.go | 72 +++++ chefignore_test.go | 135 +++++++++ config_rb_reader.go | 4 +- cookbook.go | 269 ++++++++++++++++- cookbook_artifacts_download_test.go | 7 +- cookbook_artifacts_test.go | 8 +- cookbook_download.go | 20 -- cookbook_download_test.go | 35 +-- cookbook_manifest.go | 283 ++++++++++++++++++ cookbook_manifest_test.go | 258 ++++++++++++++++ cookbook_test.go | 137 ++++++++- cookbook_upload.go | 201 +++++++++++++ cookbook_upload_test.go | 1 + digest.go | 46 +++ digest_test.go | 56 ++++ doc.go | 3 +- environment.go | 2 +- http.go | 33 +- http2_test.go | 3 +- http_test.go | 18 +- policy_group_test.go | 12 +- policy_test.go | 8 +- reader_test.go | 3 +- search_test.go | 6 +- test/chefignore | 113 +++++++ test/cookbooks/testcomplex/README.md | 3 + .../testcomplex/attributes/default.rb | 1 + test/cookbooks/testcomplex/chefignore | 3 + .../testcomplex/definitions/write_a_file.rb | 6 + test/cookbooks/testcomplex/files/test.txt | 1 + .../testcomplex/files/windows/test.txt | 1 + .../testcomplex/libraries/testlib.rb | 5 + .../testcomplex/manifests/chefv0.json | 145 +++++++++ .../testcomplex/manifests/chefv2.json | 148 +++++++++ test/cookbooks/testcomplex/metadata.rb | 15 + .../cookbooks/testcomplex/other/otherfile.txt | 1 + .../providers/provider_resource.rb | 3 + test/cookbooks/testcomplex/recipes/default.rb | 5 + test/cookbooks/testcomplex/recipes/test.rb | 1 + .../testcomplex/resources/a_resource.rb | 7 + .../resources/provider_resource.rb | 3 + .../templates/default/template.txt.erb | 2 + .../templates/windows/template.txt.erb | 2 + test/cookbooks/testdeps/attributes/default.rb | 2 + test/cookbooks/testdeps/metadata.rb | 15 + test/cookbooks/testdeps/recipes/default.rb | 1 + test_chef_server/Berksfile.lock | 2 +- test_chef_server/recipes/chefapi.rb | 7 +- .../default/inspec/cookbook_spec.rb | 4 + testapi/bin/setup | 3 + testapi/cookbook.go | 40 +++ testapi/policy.go | 5 +- testapi/testapi.go | 5 +- testapi/testcase/testcase.go | 2 +- 56 files changed, 2055 insertions(+), 123 deletions(-) create mode 100644 chefignore.go create mode 100644 chefignore_test.go create mode 100644 cookbook_manifest.go create mode 100644 cookbook_manifest_test.go create mode 100644 cookbook_upload.go create mode 100644 cookbook_upload_test.go create mode 100644 digest.go create mode 100644 digest_test.go create mode 100644 test/chefignore create mode 100644 test/cookbooks/testcomplex/README.md create mode 100644 test/cookbooks/testcomplex/attributes/default.rb create mode 100644 test/cookbooks/testcomplex/chefignore create mode 100644 test/cookbooks/testcomplex/definitions/write_a_file.rb create mode 100644 test/cookbooks/testcomplex/files/test.txt create mode 100644 test/cookbooks/testcomplex/files/windows/test.txt create mode 100644 test/cookbooks/testcomplex/libraries/testlib.rb create mode 100644 test/cookbooks/testcomplex/manifests/chefv0.json create mode 100644 test/cookbooks/testcomplex/manifests/chefv2.json create mode 100644 test/cookbooks/testcomplex/metadata.rb create mode 100644 test/cookbooks/testcomplex/other/otherfile.txt create mode 100644 test/cookbooks/testcomplex/providers/provider_resource.rb create mode 100644 test/cookbooks/testcomplex/recipes/default.rb create mode 100644 test/cookbooks/testcomplex/recipes/test.rb create mode 100644 test/cookbooks/testcomplex/resources/a_resource.rb create mode 100644 test/cookbooks/testcomplex/resources/provider_resource.rb create mode 100644 test/cookbooks/testcomplex/templates/default/template.txt.erb create mode 100644 test/cookbooks/testcomplex/templates/windows/template.txt.erb create mode 100644 test/cookbooks/testdeps/attributes/default.rb create mode 100644 test/cookbooks/testdeps/metadata.rb create mode 100644 test/cookbooks/testdeps/recipes/default.rb diff --git a/README.md b/README.md index b3c1b42d..e6fc5b4f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ package main import ( "fmt" - "io/ioutil" "os" "github.com/go-chef/chef" @@ -36,7 +35,7 @@ import ( func main() { // read a client key - key, err := ioutil.ReadFile("key.pem") + key, err := os.ReadFile("key.pem") if err != nil { fmt.Println("Couldn't read key.pem:", err) os.Exit(1) diff --git a/association_test.go b/association_test.go index 8a82dd45..51ba1403 100644 --- a/association_test.go +++ b/association_test.go @@ -56,8 +56,8 @@ func TestAssociationMethods(t *testing.T) { t.Errorf("Associations.List returned error: %v", err) } listWant := []Invite{ - Invite{Id: "1f", UserName: "jollystranger"}, - Invite{Id: "2b", UserName: "fredhamlet"}, + {Id: "1f", UserName: "jollystranger"}, + {Id: "2b", UserName: "fredhamlet"}, } if !reflect.DeepEqual(invites, listWant) { t.Errorf("Associations.InviteList returned %+v, want %+v", invites, listWant) diff --git a/chefignore.go b/chefignore.go new file mode 100644 index 00000000..f1e6f607 --- /dev/null +++ b/chefignore.go @@ -0,0 +1,72 @@ +package chef + +import ( + "bufio" + "os" + "path/filepath" + "regexp" + "strings" +) + +// Matches all lines that only contain comments or regexp +const commentsAndWhitespaceRegex = `^\s*(?:#.*)?$` + +// chefignore object +type Chefignore struct { + Path string + Ignores []string +} + +// Creates a new Chefignore from a path +func NewChefignore(path string) Chefignore { + chefignore := Chefignore{ + Path: path, + Ignores: make([]string, 0), + } + + // Parse ignored lines, respecting comments + chefignore.Ignores = parseChefignoreContent(path) + return chefignore +} + +// Returns true if the given path should be ignored, false otherwise +func (chefignore *Chefignore) Ignore(path string) bool { + for _, ignorePattern := range chefignore.Ignores { + // TODO: Handle antd style wildcards (**) since these are supported in the spec + // but not by filepath.Match + matched, err := filepath.Match(ignorePattern, path) + if err != nil { + // Skip malformed patterns + continue + } + + if matched { + return true + } + } + return false +} + +// Parses .chefignore content +func parseChefignoreContent(path string) []string { + skipLineRegex := regexp.MustCompile(commentsAndWhitespaceRegex) + + // Read content line-by-line + file, err := os.Open(path) + if err != nil { + return []string{} + } + defer file.Close() + + ignores := make([]string, 0) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if !skipLineRegex.MatchString(line) { + ignores = append(ignores, strings.TrimSpace(line)) + } + } + + return ignores +} diff --git a/chefignore_test.go b/chefignore_test.go new file mode 100644 index 00000000..40850f82 --- /dev/null +++ b/chefignore_test.go @@ -0,0 +1,135 @@ +package chef_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/go-chef/chef" + "github.com/stretchr/testify/assert" +) + +var ( + testChefignore = "test/chefignore" + expectedIgnores = []string{ + ".DS_Store", + "ehthumbs.db", + "Icon?", + "nohup.out", + "Thumbs.db", + ".envrc", + ".#*", + ".project", + ".settings", + "*_flymake", + "*_flymake.*", + "*.bak", + "*.sw[a-z]", + "*.tmproj", + "*~", + "\\#*", + "REVISION", + "TAGS*", + "tmtags", + ".vscode", + ".editorconfig", + "*.class", + "*.com", + "*.dll", + "*.exe", + "*.o", + "*.pyc", + "*.so", + "*/rdoc/", + "a.out", + "mkmf.log", + ".circleci/*", + ".codeclimate.yml", + ".delivery/*", + ".foodcritic", + ".kitchen*", + ".mdlrc", + ".overcommit.yml", + ".rspec", + ".rubocop.yml", + ".travis.yml", + ".watchr", + ".yamllint", + "azure-pipelines.yml", + "Dangerfile", + "examples/*", + "features/*", + "Guardfile", + "kitchen.yml*", + "mlc_config.json", + "Procfile", + "Rakefile", + "spec/*", + "test/*", + ".git", + ".gitattributes", + ".gitconfig", + ".github/*", + ".gitignore", + ".gitkeep", + ".gitmodules", + ".svn", + "*/.bzr/*", + "*/.git", + "*/.hg/*", + "*/.svn/*", + "Berksfile", + "Berksfile.lock", + "cookbooks/*", + "tmp", + "vendor/*", + "Gemfile", + "Gemfile.lock", + "Policyfile.rb", + "Policyfile.lock.json", + "CODE_OF_CONDUCT*", + "CONTRIBUTING*", + "documentation/*", + "TESTING*", + "UPGRADING*", + ".vagrant", + "Vagrantfile", + } + unignoredPaths = []string{ + "metadata.rb", + "metadata.json", + "recipes/default.rb", + "attributes/default.rb", + "resources/custom.rb", + "providers/custom.rb", + "templates/default/temp.txt.erb", + } +) + +func TestChefignoreNew(t *testing.T) { + c := chef.NewChefignore(testChefignore) + + assert.Equal(t, testChefignore, c.Path) + assert.Equal(t, expectedIgnores, c.Ignores) +} + +func TestChefignoreIgnore(t *testing.T) { + c := chef.NewChefignore(testChefignore) + + // Ensure appropriate files will be ignored + for _, ignore := range c.Ignores { + ignoreStr := strings.ReplaceAll(ignore, "*", "test") + + // Replace contents of [*] matchers + re := regexp.MustCompile(`\[.*\]`) + ignoreStr = re.ReplaceAllString(ignoreStr, "a") + + // Handle \# + ignoreStr = strings.ReplaceAll(ignoreStr, "\\#", "#") + assert.Equal(t, true, c.Ignore(ignoreStr), "Did not ignore ", ignoreStr, " as expected") + } + + for _, path := range unignoredPaths { + assert.Equal(t, false, c.Ignore(path)) + } +} diff --git a/config_rb_reader.go b/config_rb_reader.go index 38d0b23b..e3dbdf01 100644 --- a/config_rb_reader.go +++ b/config_rb_reader.go @@ -2,7 +2,7 @@ package chef import ( "errors" - "io/ioutil" + "os" "path/filepath" "strings" ) @@ -46,7 +46,7 @@ func configKeyParser(s []string, path string, c *ConfigRb) error { size := len(data) if size > 0 { keyPath := filepath.Join(path, data[size-1]) - keyData, err := ioutil.ReadFile(keyPath) + keyData, err := os.ReadFile(keyPath) if err != nil { return err } diff --git a/cookbook.go b/cookbook.go index fe483c79..21173602 100644 --- a/cookbook.go +++ b/cookbook.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io/fs" "os" "path/filepath" "strings" @@ -105,10 +105,41 @@ type Cookbook struct { Providers []CookbookItem `json:"providers,omitempty"` Resources []CookbookItem `json:"resources,omitempty"` RootFiles []CookbookItem `json:"root_files,omitempty"` + otherFiles []CookbookItem Metadata CookbookMeta `json:"metadata,omitempty"` Access CookbookAccess `json:"access,omitempty"` } +// Returns a slice of all items in the cookbook +func (c *Cookbook) AllItems() []CookbookItem { + var allItems []CookbookItem + + allItems = append(allItems, c.Files...) + allItems = append(allItems, c.Templates...) + allItems = append(allItems, c.Attributes...) + allItems = append(allItems, c.Recipes...) + allItems = append(allItems, c.Definitions...) + allItems = append(allItems, c.Libraries...) + allItems = append(allItems, c.Providers...) + allItems = append(allItems, c.Resources...) + allItems = append(allItems, c.RootFiles...) + allItems = append(allItems, c.otherFiles...) + + return allItems +} + +// Returns a map of all items in the cookbook keyed by the item checksum +func (c *Cookbook) AllItemsByChecksum() map[string]CookbookItem { + itemMap := make(map[string]CookbookItem) + allItems := c.AllItems() + + for _, item := range allItems { + itemMap[item.Checksum] = item + } + + return itemMap +} + // String makes CookbookListResult implement the string result func (c CookbookListResult) String() (out string) { for k, v := range c { @@ -198,7 +229,7 @@ func ReadMetaData(path string) (m CookbookMeta, err error) { fileName = filepath.Join(path, metaRbName) } - file, err := ioutil.ReadFile(fileName) + file, err := os.ReadFile(fileName) if err != nil { fmt.Println(err.Error()) os.Exit(1) @@ -241,6 +272,204 @@ func clearWhiteSpace(s []string) (result []string) { return result } +func clearComments(s []string) (result []string) { + for _, i := range s { + if len(i) > 0 { + // once a comment is found, break out of line parsing + if i == "#" { + break + } + result = append(result, i) + } + } + return result +} + +func clearQuotesAndCommas(s []string) (result []string) { + for _, i := range s { + i = strings.Trim(i, ",") + i = trimQuotes(i) + + result = append(result, i) + } + return result +} + +// Creates a new Cookbook object from a given cookbook path. +// Parses in the cookbook's metadata and all cookbook files, respecting ChefIgnore. +func NewCookbookFromPath(cookbookPath string) (*Cookbook, error) { + cookbook := Cookbook{ + JsonClass: "Chef::CookbookVersion", + ChefType: "cookbook_version", + } + + // Parse cookbook metadata + meta, err := ReadMetaData(cookbookPath) + if err != nil { + return nil, err + } + + // Update cookbook with metadata information + cookbook.Version = meta.Version + cookbook.CookbookName = meta.Name + cookbook.Name = fmt.Sprintf("%s-%s", cookbook.CookbookName, cookbook.Version) + cookbook.Metadata = meta + + chefignore := NewChefignore(filepath.Join(cookbookPath, "chefignore")) + + // Find all files in the cookbook and classify them. + // Allowed types: + // + // * Attributes (attributes/) + // * Definitions (definitions/) + // * Files (files/) + // * Libraries (libraries/) + // * Providers (providers/) + // * Recipes (recipes/) + // * Resources (resources/) + // * RootFiles (/) + // * Templates (templates/) + // + // Gather directories under the root of the cookbook and root files. + rootPaths, _ := filepath.Glob(filepath.Join(cookbookPath, "*")) + var rootDirs []string + + for _, rootPath := range rootPaths { + fileInfo, err := os.Stat(rootPath) + if err != nil { + return nil, err + } + + basePath := filepath.Base(rootPath) + + if chefignore.Ignore(basePath) { + continue + } + + if fileInfo.Mode().IsDir() { + rootDirs = append(rootDirs, basePath) + } else if fileInfo.Mode().IsRegular() { + checksum, err := fileMD5Checksum(rootPath) + if err != nil { + return nil, err + } + + // Add the file as a root file + newItem := CookbookItem{ + Path: basePath, + Name: "root_files/" + basePath, + Specificity: "default", + Checksum: checksum, + } + cookbook.RootFiles = append(cookbook.RootFiles, newItem) + } + } + + for _, rootDir := range rootDirs { + fullPath := filepath.Join(cookbookPath, rootDir) + items, err := walkCookbookDir(fullPath, &chefignore) + + if err != nil { + return nil, err + } + + switch rootDir { + case "attributes": + cookbook.Attributes = items + case "definitions": + cookbook.Definitions = items + case "files": + cookbook.Files = items + case "libraries": + cookbook.Libraries = items + case "providers": + cookbook.Providers = items + case "recipes": + cookbook.Recipes = items + case "resources": + cookbook.Resources = items + case "templates": + cookbook.Templates = items + default: + // Store other non-standard files for use with V2 manifests + cookbook.otherFiles = append(cookbook.otherFiles, items...) + } + + if err != nil { + return nil, err + } + } + + return &cookbook, nil +} + +// Walks a cookbook directory to parse cookbook items from a given path +func walkCookbookDir(dir string, chefignore *Chefignore) ([]CookbookItem, error) { + baseDir := filepath.Base(dir) + parentDir := filepath.Dir(dir) + var items []CookbookItem + + err := filepath.WalkDir(dir, func(path string, file fs.DirEntry, err error) error { + if err != nil { + return err + } + + if file.Type().IsRegular() { + // Path should be relative to the target directory + chefPath, err := filepath.Rel(parentDir, path) + if err != nil { + return err + } + + if chefignore.Ignore(chefPath) { + // Return early for ignored files + return nil + } + + // Parse specificity from path if template or cookbook file + specificity := "default" + if baseDir == "templates" || baseDir == "files" { + splitPath := splitCookbookDir(chefPath) + if len(splitPath) == 2 { + specificity = "root_default" + } else if len(splitPath) > 2 { + specificity = splitPath[1] + } + } + + itemChecksum, err := fileMD5Checksum(path) + if err != nil { + return err + } + + newItem := CookbookItem{ + Path: chefPath, + Name: baseDir + "/" + filepath.Base(path), + Specificity: specificity, + Checksum: itemChecksum, + } + items = append(items, newItem) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return items, nil +} + +// Splits the cookbook directory into a slice of strings by directory separator +func splitCookbookDir(path string) []string { + dir, last := filepath.Split(path) + if dir == "" { + return []string{last} + } + return append(splitCookbookDir(filepath.Clean(dir)), last) +} + func NewMetaData(data string) (m CookbookMeta, err error) { linesData := strings.Split(data, "\n") if len(linesData) < 3 { @@ -302,6 +531,10 @@ func metaSourceUrlParser(s []string, m *CookbookMeta) error { return nil } func metaGemParser(s []string, m *CookbookMeta) error { + s = clearWhiteSpace(s) + s = clearComments(s) + s = clearQuotesAndCommas(s) + m.Gems = append(m.Gems, s) return nil } @@ -326,37 +559,51 @@ func metaPrivacyParser(s []string, m *CookbookMeta) error { } func metaSupportsParser(s []string, m *CookbookMeta) error { s = clearWhiteSpace(s) + s = clearComments(s) + + // Remove surrounding spaces, commas, and quotes from keys + k := strings.TrimSpace(s[0]) + k = strings.Trim(k, ",") + k = trimQuotes(k) + switch len(s) { case 1: if s[0] != "os" { - m.Platforms[strings.TrimSpace(s[0])] = ">= 0.0.0" + m.Platforms[k] = ">= 0.0.0" } case 2: - m.Platforms[strings.TrimSpace(s[0])] = s[1] + m.Platforms[k] = s[1] case 3: v := trimQuotes(s[1] + " " + s[2]) - m.Platforms[strings.TrimSpace(s[0])] = v + m.Platforms[k] = v } if len(s) > 3 { return errors.New(`<<~OBSOLETED - The dependency specification syntax you are using is no longer valid. You may not - specify more than one version constraint for a particular cookbook. + The supports specification syntax you are using is no longer valid. You may not + specify more than one version constraint for a particular supported platform. Consult https://docs.chef.io/config_rb_metadata/ for the updated syntax.`) } return nil } func metaDependsParser(s []string, m *CookbookMeta) error { s = clearWhiteSpace(s) + s = clearComments(s) + + // Remove surrounding spaces, commas, and quotes from keys + k := strings.TrimSpace(s[0]) + k = strings.Trim(k, ",") + k = trimQuotes(k) + switch len(s) { case 1: - m.Depends[strings.TrimSpace(s[0])] = ">= 0.0.0" + m.Depends[k] = ">= 0.0.0" case 2: - m.Depends[strings.TrimSpace(s[0])] = s[1] + m.Depends[k] = s[1] case 3: v := trimQuotes(s[1] + " " + s[2]) - m.Depends[strings.TrimSpace(s[0])] = v + m.Depends[k] = v } if len(s) > 3 { @@ -385,6 +632,7 @@ func metaSupportsRubyParser(s []string, m *CookbookMeta) error { } return nil } + func init() { metaRegistry = make(map[string]metaFunc, 15) metaRegistry["name"] = metaNameParser @@ -404,5 +652,4 @@ func init() { metaRegistry["chef_version"] = metaChefVersionParser metaRegistry["ohai_version"] = metaOhaiVersionParser metaRegistry["gem"] = metaGemParser - } diff --git a/cookbook_artifacts_download_test.go b/cookbook_artifacts_download_test.go index 70d34f10..54470048 100644 --- a/cookbook_artifacts_download_test.go +++ b/cookbook_artifacts_download_test.go @@ -2,7 +2,6 @@ package chef import ( "fmt" - "io/ioutil" "net/http" "os" "path" @@ -30,7 +29,7 @@ func TestCBADownloadTo(t *testing.T) { defer teardown() mockedCBAResponseFile := cbaData() - tempDir, err := ioutil.TempDir("", "seven_zip-cookbook") + tempDir, err := os.MkdirTemp("", "seven_zip-cookbook") if err != nil { t.Error(err) } @@ -58,12 +57,12 @@ func TestCBADownloadTo(t *testing.T) { 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) + metadataBytes, err := os.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) + recipeBytes, err := os.ReadFile(defaultPath) assert.Nil(t, err) assert.Equal(t, "log 'this is a resource'", string(recipeBytes)) } diff --git a/cookbook_artifacts_test.go b/cookbook_artifacts_test.go index eccc3b40..7162c8b4 100644 --- a/cookbook_artifacts_test.go +++ b/cookbook_artifacts_test.go @@ -2,8 +2,8 @@ package chef import ( "fmt" - "io/ioutil" "net/http" + "os" "testing" "github.com/stretchr/testify/assert" @@ -17,7 +17,7 @@ func TestListCBA(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(CBAListResponseFile) + file, err := os.ReadFile(CBAListResponseFile) if err != nil { t.Error(err) } @@ -53,7 +53,7 @@ func TestGetCBA(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(CBAGetResponseFile) + file, err := os.ReadFile(CBAGetResponseFile) if err != nil { t.Error(err) } @@ -87,7 +87,7 @@ func TestGetVersionCBA(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(CBAGetVersionResponseFile) + file, err := os.ReadFile(CBAGetVersionResponseFile) if err != nil { t.Error(err) } diff --git a/cookbook_download.go b/cookbook_download.go index 909a98e3..4c811e4f 100644 --- a/cookbook_download.go +++ b/cookbook_download.go @@ -5,7 +5,6 @@ package chef import ( - "crypto/md5" "fmt" "io" "os" @@ -132,22 +131,3 @@ func (c *CookbookService) downloadCookbookFile(item CookbookItem, localPath stri item.Checksum, ) } - -func verifyMD5Checksum(filePath, checksum string) bool { - file, err := os.Open(filePath) - if err != nil { - return false - } - defer file.Close() - - hash := md5.New() - if _, err := io.Copy(hash, file); err != nil { - return false - } - - md5String := fmt.Sprintf("%x", hash.Sum(nil)) - if md5String == checksum { - return true - } - return false -} diff --git a/cookbook_download_test.go b/cookbook_download_test.go index cd7c5939..03025d71 100644 --- a/cookbook_download_test.go +++ b/cookbook_download_test.go @@ -6,7 +6,6 @@ package chef import ( "fmt" - "io/ioutil" "net/http" "os" "path" @@ -59,7 +58,7 @@ func TestCookbooksDownloadEmptyWithVersion(t *testing.T) { setup() defer teardown() - cbookResp, err := ioutil.ReadFile(emptyCookbookResponseFile) + cbookResp, err := os.ReadFile(emptyCookbookResponseFile) if err != nil { t.Error(err) } @@ -117,7 +116,7 @@ func TestCookbooksDownloadTo(t *testing.T) { defer teardown() mockedCookbookResponseFile := cookbookData() - tempDir, err := ioutil.TempDir("", "foo-cookbook") + tempDir, err := os.MkdirTemp("", "foo-cookbook") if err != nil { t.Error(err) } @@ -145,12 +144,12 @@ func TestCookbooksDownloadTo(t *testing.T) { 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) + metadataBytes, err := os.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) + recipeBytes, err := os.ReadFile(defaultPath) assert.Nil(t, err) assert.Equal(t, "log 'this is a resource'", string(recipeBytes)) } @@ -162,7 +161,7 @@ func TestCookbooksDownloadTo_caching(t *testing.T) { defer teardown() mockedCookbookResponseFile := cookbookData() - tempDir, err := ioutil.TempDir("", "foo-cookbook") + tempDir, err := os.MkdirTemp("", "foo-cookbook") if err != nil { t.Error(err) } @@ -190,12 +189,12 @@ func TestCookbooksDownloadTo_caching(t *testing.T) { 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) + metadataBytes, err := os.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) + recipeBytes, err := os.ReadFile(defaultPath) assert.Nil(t, err) assert.Equal(t, "log 'this is a resource'", string(recipeBytes)) } @@ -243,26 +242,8 @@ func TestCookbooksDownloadTo_caching(t *testing.T) { // Finally, make sure the modified-and-replaced metadata.rb is matching // what we expect after we have redownloaded the cookbook: if assert.FileExistsf(t, metadataPath, "a metadata.rb file should exist") { - metadataBytes, err := ioutil.ReadFile(metadataPath) + metadataBytes, err := os.ReadFile(metadataPath) assert.Nil(t, err) assert.Equal(t, "name 'foo'", string(metadataBytes)) } } - -func TestVerifyMD5Checksum(t *testing.T) { - tempDir, err := ioutil.TempDir("", "md5-test") - if err != nil { - t.Error(err) - } - defer os.RemoveAll(tempDir) // clean up - - var ( - // if someone changes the test data, - // you have to also update the below md5 sum - testData = []byte("hello\nchef\n") - filePath = path.Join(tempDir, "dat") - ) - err = ioutil.WriteFile(filePath, testData, 0644) - assert.Nil(t, err) - assert.True(t, verifyMD5Checksum(filePath, "70bda176ac4db06f1f66f96ae0693be1")) -} diff --git a/cookbook_manifest.go b/cookbook_manifest.go new file mode 100644 index 00000000..3f38401e --- /dev/null +++ b/cookbook_manifest.go @@ -0,0 +1,283 @@ +package chef + +import ( + "fmt" + "io" + "path/filepath" + "strconv" + "strings" +) + +// Cookbook Manifest meta format for Chef API versions 0-2 +type CookbookManifestMeta struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Description string `json:"description,omitempty"` + LongDescription string `json:"long_description"` + Maintainer string `json:"maintainer,omitempty"` + MaintainerEmail string `json:"maintainer_email,omitempty"` + License string `json:"license,omitempty"` + Platforms map[string]interface{} `json:"platforms"` + Depends map[string]string `json:"dependencies"` + Recommends map[string]string `json:"recommendations,omitempty"` + Suggests map[string]string `json:"suggestions,omitempty"` + Conflicts map[string]string `json:"conflicting,omitempty"` + Provides map[string]interface{} `json:"providing,omitempty"` + Replaces map[string]string `json:"replacing,omitempty"` + Attributes map[string]interface{} `json:"attributes,omitempty"` + Groupings map[string]interface{} `json:"groupings,omitempty"` + Recipes map[string]string `json:"recipes,omitempty"` + SourceUrl string `json:"source_url"` + IssueUrl string `json:"issues_url"` + ChefVersions [][]string `json:"chef_versions"` + OhaiVersions [][]string `json:"ohai_versions"` + Gems [][]string `json:"gems"` + EagerLoadLibraries bool `json:"eager_load_libraries"` + Privacy bool `json:"privacy"` +} + +// Cookbook Manifest format for Chef API versions 0-1 +type CookbookManifestV0 struct { + CookbookName string `json:"cookbook_name"` + Name string `json:"name"` + Version string `json:"version,omitempty"` + ChefType string `json:"chef_type,omitempty"` + Frozen bool `json:"frozen?"` + JsonClass string `json:"json_class,omitempty"` + Files []CookbookItem `json:"files,omitempty"` + Templates []CookbookItem `json:"templates,omitempty"` + Attributes []CookbookItem `json:"attributes,omitempty"` + Recipes []CookbookItem `json:"recipes,omitempty"` + Definitions []CookbookItem `json:"definitions,omitempty"` + Libraries []CookbookItem `json:"libraries,omitempty"` + Providers []CookbookItem `json:"providers,omitempty"` + Resources []CookbookItem `json:"resources,omitempty"` + RootFiles []CookbookItem `json:"root_files,omitempty"` + Metadata CookbookManifestMeta `json:"metadata"` +} + +// Cookbook Manifest format for Chef API version 2 +type CookbookManifestV2 struct { + CookbookName string `json:"cookbook_name"` + Name string `json:"name"` + Version string `json:"version,omitempty"` + ChefType string `json:"chef_type,omitempty"` + Frozen bool `json:"frozen?"` + JsonClass string `json:"json_class,omitempty"` + AllFiles []CookbookItem `json:"all_files"` + Metadata CookbookManifestMeta `json:"metadata"` +} + +// Generate a CookbookManifestMeta from an existing CookbookMeta object +func (meta *CookbookMeta) Manifest() *CookbookManifestMeta { + chefVersions := make([][]string, 0) + ohaiVersions := make([][]string, 0) + + if meta.ChefVersion != "" { + chefVersionList := []string{meta.ChefVersion} + chefVersions = append(chefVersions, chefVersionList) + } + + if meta.OhaiVersion != "" { + ohaiVersionList := []string{meta.OhaiVersion} + ohaiVersions = append(ohaiVersions, ohaiVersionList) + } + + if meta.Gems == nil { + // Ensure the gems array exists, nil will break the client + meta.Gems = make([][]string, 0) + } + + // Set eager load libraries to true since this is default behavior + meta.EagerLoadLibraries = true + + manifestMeta := CookbookManifestMeta{ + Name: meta.Name, + Version: meta.Version, + Description: meta.Description, + LongDescription: meta.LongDescription, + Maintainer: meta.Maintainer, + MaintainerEmail: meta.MaintainerEmail, + License: meta.License, + Platforms: meta.Platforms, + Depends: meta.Depends, + Provides: meta.Provides, + Recipes: meta.Recipes, + Recommends: meta.Reccomends, + Suggests: meta.Suggests, + Conflicts: meta.Conflicts, + Replaces: meta.Replaces, + Attributes: meta.Attributes, + Groupings: meta.Groupings, + SourceUrl: meta.SourceUrl, + IssueUrl: meta.IssueUrl, + Gems: meta.Gems, + EagerLoadLibraries: meta.EagerLoadLibraries, + Privacy: meta.Privacy, + ChefVersions: chefVersions, + OhaiVersions: ohaiVersions, + } + + return &manifestMeta +} + +func (c *Cookbook) populateProvidesMetadata() { + if c.Metadata.Provides == nil { + c.Metadata.Provides = make(map[string]interface{}) + + for _, recipe := range c.Recipes { + recipeName := strings.Split(filepath.Base(recipe.Name), ".")[0] + recipeKey := fmt.Sprintf("%s::%s", c.CookbookName, recipeName) + + if recipeName == "default" { + recipeKey = c.CookbookName + } + + c.Metadata.Provides[recipeKey] = ">= 0.0.0" + } + } +} + +func (c *Cookbook) populateRecipesMetadata() { + if c.Metadata.Recipes == nil { + c.Metadata.Recipes = make(map[string]string) + + for _, recipe := range c.Recipes { + recipeName := strings.Split(filepath.Base(recipe.Name), ".")[0] + recipeKey := fmt.Sprintf("%s::%s", c.CookbookName, recipeName) + + if recipeName == "default" { + recipeKey = c.CookbookName + } + + c.Metadata.Recipes[recipeKey] = "" + } + } +} + +// Generate a CookbookManifestV0 from an existing Cookbook object +func (c *Cookbook) ManifestV0() *CookbookManifestV0 { + c.populateProvidesMetadata() + c.populateRecipesMetadata() + + manifest := CookbookManifestV0{ + CookbookName: c.CookbookName, + Name: c.Name, + Version: c.Version, + ChefType: "cookbook_version", + Frozen: false, + JsonClass: "Chef::CookbookVersion", + Metadata: *c.Metadata.Manifest(), + } + + for i := range c.Files { + if manifest.Files == nil { + manifest.Files = make([]CookbookItem, len(c.Files)) + } + manifest.Files[i] = *manifestV0CookbookItem(&c.Files[i]) + } + + for i := range c.Templates { + if manifest.Templates == nil { + manifest.Templates = make([]CookbookItem, len(c.Templates)) + } + manifest.Templates[i] = *manifestV0CookbookItem(&c.Templates[i]) + } + + for i := range c.Attributes { + if manifest.Attributes == nil { + manifest.Attributes = make([]CookbookItem, len(c.Attributes)) + } + manifest.Attributes[i] = *manifestV0CookbookItem(&c.Attributes[i]) + } + + for i := range c.Recipes { + if manifest.Recipes == nil { + manifest.Recipes = make([]CookbookItem, len(c.Recipes)) + } + manifest.Recipes[i] = *manifestV0CookbookItem(&c.Recipes[i]) + } + + for i := range c.Definitions { + if manifest.Definitions == nil { + manifest.Definitions = make([]CookbookItem, len(c.Definitions)) + } + manifest.Definitions[i] = *manifestV0CookbookItem(&c.Definitions[i]) + } + + for i := range c.Libraries { + if manifest.Libraries == nil { + manifest.Libraries = make([]CookbookItem, len(c.Libraries)) + } + manifest.Libraries[i] = *manifestV0CookbookItem(&c.Libraries[i]) + } + + for i := range c.Providers { + if manifest.Providers == nil { + manifest.Providers = make([]CookbookItem, len(c.Providers)) + } + manifest.Providers[i] = *manifestV0CookbookItem(&c.Providers[i]) + } + + for i := range c.Resources { + if manifest.Resources == nil { + manifest.Resources = make([]CookbookItem, len(c.Resources)) + } + manifest.Resources[i] = *manifestV0CookbookItem(&c.Resources[i]) + } + + for i := range c.RootFiles { + if manifest.RootFiles == nil { + manifest.RootFiles = make([]CookbookItem, len(c.RootFiles)) + } + manifest.RootFiles[i] = *manifestV0CookbookItem(&c.RootFiles[i]) + } + + return &manifest +} + +// Generate a new cookbook item that follows ManifestV0 naming syntax +func manifestV0CookbookItem(item *CookbookItem) *CookbookItem { + return &CookbookItem{ + Name: filepath.Base(item.Name), + Path: item.Path, + Url: item.Url, + Checksum: item.Checksum, + Specificity: item.Specificity, + } +} + +// Generate a CookbookManifestV2 from an existing Cookbook object +func (c *Cookbook) ManifestV2() *CookbookManifestV2 { + c.populateProvidesMetadata() + c.populateRecipesMetadata() + + manifest := CookbookManifestV2{ + CookbookName: c.CookbookName, + Name: c.Name, + Version: c.Version, + ChefType: "cookbook_version", + Frozen: c.Frozen, + JsonClass: "Chef::CookbookVersion", + AllFiles: c.AllItems(), + Metadata: *c.Metadata.Manifest(), + } + + return &manifest +} + +func (c *Cookbook) ManifestJsonForApi(serverApiVersion string) (reader io.Reader, err error) { + apiVersionInt, _ := strconv.Atoi(serverApiVersion) + + if apiVersionInt >= 2 { + reader, err = JSONReader(c.ManifestV2()) + } else { + reader, err = JSONReader(c.ManifestV0()) + } + + if err != nil { + return nil, err + } + + return reader, nil +} diff --git a/cookbook_manifest_test.go b/cookbook_manifest_test.go new file mode 100644 index 00000000..bafeec76 --- /dev/null +++ b/cookbook_manifest_test.go @@ -0,0 +1,258 @@ +package chef + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +const _sourceManifestCookbookPath = "test/cookbooks/testdeps" +const _sourceComplexManifestCookbookPath = "test/cookbooks/testcomplex" + +func TestCookbookMetaManifest(t *testing.T) { + meta, err := ReadMetaData(_sourceManifestCookbookPath) + assert.Nil(t, err) + + metaManifest := meta.Manifest() + + assert.Equal(t, &CookbookManifestMeta{ + Name: "testdeps", + Version: "0.1.0", + Description: "Installs/Configures testdeps", + Maintainer: "The Authors", + MaintainerEmail: "you@example.com", + License: "All Rights Reserved", + Platforms: map[string]interface{}{ + "redhat": ">= 0.0.0", + "ubuntu": ">= 20.04", + }, + Depends: map[string]string{ + "lvm": "~> 6.1", + "vagrant": ">= 4.0.14", + }, + ChefVersions: [][]string{ + {">= 18.0"}, + }, + OhaiVersions: [][]string{}, + Gems: [][]string{ + {"json", ">1.0.0"}, + }, + EagerLoadLibraries: true, + }, metaManifest) +} + +func TestCookbookManifestV0(t *testing.T) { + cookbook, err := NewCookbookFromPath(_sourceManifestCookbookPath) + assert.Nil(t, err) + + manifest := cookbook.ManifestV0() + + assert.Equal(t, &CookbookManifestV0{ + CookbookName: "testdeps", + Name: "testdeps-0.1.0", + Version: "0.1.0", + ChefType: "cookbook_version", + Frozen: false, + JsonClass: "Chef::CookbookVersion", + Files: nil, + Templates: nil, + Attributes: []CookbookItem{{ + Name: "default.rb", + Path: "attributes/default.rb", + Checksum: "553637b4fba46b5148f88d6dd3877e2f", + Specificity: "default", + }}, + Recipes: []CookbookItem{{ + Name: "default.rb", + Path: "recipes/default.rb", + Checksum: "4e15b1e5593d717685323c5dac86b99e", + Specificity: "default", + }}, + Definitions: nil, + Libraries: nil, + Providers: nil, + Resources: nil, + RootFiles: []CookbookItem{{ + Name: "metadata.rb", + Path: "metadata.rb", + Checksum: "ba208d0ffc0dd8cbe9c71fb40fb207b2", + Specificity: "default", + }}, + Metadata: *cookbook.Metadata.Manifest(), + }, manifest) + + var expectedManifest *CookbookManifestV0 + expectedMetadataBytes, err := os.ReadFile(filepath.Join(_sourceComplexManifestCookbookPath, "manifests", "chefv0.json")) + assert.Nil(t, err) + json.Unmarshal(expectedMetadataBytes, &expectedManifest) + + cookbook, err = NewCookbookFromPath(_sourceComplexManifestCookbookPath) + assert.Nil(t, err) + + parsedManifest := cookbook.ManifestV0() + + assert.Equal(t, expectedManifest, parsedManifest) +} + +func TestCookbookManifestV2(t *testing.T) { + cookbook, err := NewCookbookFromPath(_sourceManifestCookbookPath) + assert.Nil(t, err) + + manifest := cookbook.ManifestV2() + + assert.Equal(t, &CookbookManifestV2{ + CookbookName: "testdeps", + Name: "testdeps-0.1.0", + Version: "0.1.0", + ChefType: "cookbook_version", + Frozen: false, + JsonClass: "Chef::CookbookVersion", + AllFiles: []CookbookItem{ + { + Name: "attributes/default.rb", + Path: "attributes/default.rb", + Checksum: "553637b4fba46b5148f88d6dd3877e2f", + Specificity: "default", + }, + { + Name: "recipes/default.rb", + Path: "recipes/default.rb", + Checksum: "4e15b1e5593d717685323c5dac86b99e", + Specificity: "default", + }, + { + Name: "root_files/metadata.rb", + Path: "metadata.rb", + Checksum: "ba208d0ffc0dd8cbe9c71fb40fb207b2", + Specificity: "default", + }, + }, + Metadata: *cookbook.Metadata.Manifest(), + }, manifest) + + var expectedManifest *CookbookManifestV2 + expectedMetadataBytes, err := os.ReadFile(filepath.Join(_sourceComplexManifestCookbookPath, "manifests", "chefv2.json")) + assert.Nil(t, err) + json.Unmarshal(expectedMetadataBytes, &expectedManifest) + + cookbook, err = NewCookbookFromPath(_sourceComplexManifestCookbookPath) + assert.Nil(t, err) + + parsedManifest := cookbook.ManifestV2() + + assert.Equal(t, expectedManifest.ChefType, parsedManifest.ChefType) + assert.Equal(t, expectedManifest.CookbookName, parsedManifest.CookbookName) + assert.Equal(t, expectedManifest.Frozen, parsedManifest.Frozen) + assert.Equal(t, expectedManifest.JsonClass, parsedManifest.JsonClass) + assert.Equal(t, expectedManifest.Name, parsedManifest.Name) + assert.Equal(t, expectedManifest.Version, parsedManifest.Version) + assert.Equal(t, expectedManifest.Metadata, parsedManifest.Metadata) + + assert.ElementsMatch(t, expectedManifest.AllFiles, parsedManifest.AllFiles) +} + +func TestCookbookManifestJsonForApi(t *testing.T) { + cookbook, err := NewCookbookFromPath(_sourceManifestCookbookPath) + assert.Nil(t, err) + + // Ensure API versions 0-1 use the same manifest format + v0ManifestJsonReader, err := cookbook.ManifestJsonForApi("0") + assert.Nil(t, err) + v1ManifestJsonReader, err := cookbook.ManifestJsonForApi("1") + assert.Nil(t, err) + + v0ManifestJson, err := io.ReadAll(v0ManifestJsonReader) + assert.Nil(t, err) + v1ManifestJson, err := io.ReadAll(v1ManifestJsonReader) + assert.Nil(t, err) + assert.Equal(t, v0ManifestJson, v1ManifestJson) + + // Ensure API versions 2+ use the same manifest format + v2ManifestJsonReader, err := cookbook.ManifestJsonForApi("2") + assert.Nil(t, err) + v3ManifestJsonReader, err := cookbook.ManifestJsonForApi("3") + assert.Nil(t, err) + + v2ManifestJson, err := io.ReadAll(v2ManifestJsonReader) + assert.Nil(t, err) + v3ManifestJson, err := io.ReadAll(v3ManifestJsonReader) + assert.Nil(t, err) + assert.Equal(t, v2ManifestJson, v3ManifestJson) + + // Verify JSON content + var parsedV0Json CookbookManifestV0 + err = json.Unmarshal(v0ManifestJson, &parsedV0Json) + assert.Nil(t, err) + + assert.Equal(t, CookbookManifestV0{ + CookbookName: "testdeps", + Name: "testdeps-0.1.0", + Version: "0.1.0", + ChefType: "cookbook_version", + Frozen: false, + JsonClass: "Chef::CookbookVersion", + Files: nil, + Templates: nil, + Attributes: []CookbookItem{{ + Name: "default.rb", + Path: "attributes/default.rb", + Checksum: "553637b4fba46b5148f88d6dd3877e2f", + Specificity: "default", + }}, + Recipes: []CookbookItem{{ + Name: "default.rb", + Path: "recipes/default.rb", + Checksum: "4e15b1e5593d717685323c5dac86b99e", + Specificity: "default", + }}, + Definitions: nil, + Libraries: nil, + Providers: nil, + Resources: nil, + RootFiles: []CookbookItem{{ + Name: "metadata.rb", + Path: "metadata.rb", + Checksum: "ba208d0ffc0dd8cbe9c71fb40fb207b2", + Specificity: "default", + }}, + Metadata: *cookbook.Metadata.Manifest(), + }, parsedV0Json) + + var parsedV2Json CookbookManifestV2 + err = json.Unmarshal(v2ManifestJson, &parsedV2Json) + assert.Nil(t, err) + + assert.Equal(t, CookbookManifestV2{ + CookbookName: "testdeps", + Name: "testdeps-0.1.0", + Version: "0.1.0", + ChefType: "cookbook_version", + Frozen: false, + JsonClass: "Chef::CookbookVersion", + AllFiles: []CookbookItem{ + { + Name: "attributes/default.rb", + Path: "attributes/default.rb", + Checksum: "553637b4fba46b5148f88d6dd3877e2f", + Specificity: "default", + }, + { + Name: "recipes/default.rb", + Path: "recipes/default.rb", + Checksum: "4e15b1e5593d717685323c5dac86b99e", + Specificity: "default", + }, + { + Name: "root_files/metadata.rb", + Path: "metadata.rb", + Checksum: "ba208d0ffc0dd8cbe9c71fb40fb207b2", + Specificity: "default", + }, + }, + Metadata: *cookbook.Metadata.Manifest(), + }, parsedV2Json) +} diff --git a/cookbook_test.go b/cookbook_test.go index fa0709c1..4d55d3f4 100644 --- a/cookbook_test.go +++ b/cookbook_test.go @@ -2,7 +2,6 @@ package chef import ( "fmt" - "io/ioutil" "net/http" "os" "testing" @@ -10,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) +const _testdepsCookbookPath = "test/cookbooks/testdeps" const cookbookListResponseFile = "test/cookbooks_response.json" const cookbookResponseFile = "test/cookbook.json" const _IssueUrl = "https://github.com//apache/issues" @@ -22,13 +22,142 @@ const _Version = "0.1.0" const _ChefVersion = ">= 15.0" const _Description = "Installs/Configures apache" -var _Gems = [][]string{[]string{"foobar"}, []string{"aws-sdk-ec2", "~> 1.214.0"}} +var _Gems = [][]string{{"foobar"}, {"aws-sdk-ec2", "~> 1.214.0"}} + +func TestNewCookbookFromPath(t *testing.T) { + cookbook, err := NewCookbookFromPath(_testdepsCookbookPath) + assert.Nil(t, err) + assert.Equal(t, "testdeps", cookbook.CookbookName) + assert.Equal( + t, + []CookbookItem{{ + Name: "attributes/default.rb", + Path: "attributes/default.rb", + Checksum: "553637b4fba46b5148f88d6dd3877e2f", + Specificity: "default", + }}, + cookbook.Attributes, + ) + assert.Equal(t, "cookbook_version", cookbook.ChefType) + assert.Nil(t, cookbook.Definitions) + assert.Nil(t, cookbook.Files) + assert.False(t, cookbook.Frozen) + assert.Equal(t, "Chef::CookbookVersion", cookbook.JsonClass) + assert.Nil(t, cookbook.Libraries) + assert.Equal( + t, + CookbookMeta{ + Name: "testdeps", + Version: "0.1.0", + Description: "Installs/Configures testdeps", + Maintainer: "The Authors", + MaintainerEmail: "you@example.com", + License: "All Rights Reserved", + Platforms: map[string]interface{}{ + "redhat": ">= 0.0.0", + "ubuntu": ">= 20.04", + }, + Depends: map[string]string{ + "lvm": "~> 6.1", + "vagrant": ">= 4.0.14", + }, + ChefVersion: ">= 18.0", + EagerLoadLibraries: false, + Gems: [][]string{ + {"json", ">1.0.0"}, + }, + }, + cookbook.Metadata, + ) + assert.Equal(t, "testdeps-0.1.0", cookbook.Name) + assert.Nil(t, cookbook.Providers) + assert.Equal( + t, + []CookbookItem{{ + Name: "recipes/default.rb", + Path: "recipes/default.rb", + Checksum: "4e15b1e5593d717685323c5dac86b99e", + Specificity: "default", + }}, + cookbook.Recipes, + ) + assert.Nil(t, cookbook.Resources) + assert.Equal( + t, + []CookbookItem{{ + Name: "root_files/metadata.rb", + Path: "metadata.rb", + Checksum: "ba208d0ffc0dd8cbe9c71fb40fb207b2", + Specificity: "default", + }}, + cookbook.RootFiles, + ) + assert.Nil(t, cookbook.Templates) + assert.Equal(t, "0.1.0", cookbook.Version) + assert.Equal(t, "testdeps", cookbook.CookbookName) +} + +func TestCookbookAllItems(t *testing.T) { + cookbook, _ := NewCookbookFromPath(_testdepsCookbookPath) + + allItems := cookbook.AllItems() + + assert.Len(t, allItems, 3) + assert.Contains(t, allItems, CookbookItem{ + Name: "root_files/metadata.rb", + Path: "metadata.rb", + Checksum: "ba208d0ffc0dd8cbe9c71fb40fb207b2", + Specificity: "default", + }) + + assert.Contains(t, allItems, CookbookItem{ + Name: "recipes/default.rb", + Path: "recipes/default.rb", + Checksum: "4e15b1e5593d717685323c5dac86b99e", + Specificity: "default", + }) + + assert.Contains(t, allItems, CookbookItem{ + Name: "attributes/default.rb", + Path: "attributes/default.rb", + Checksum: "553637b4fba46b5148f88d6dd3877e2f", + Specificity: "default", + }) +} + +func TestCookbookAllItemsByChecksum(t *testing.T) { + cookbook, _ := NewCookbookFromPath(_testdepsCookbookPath) + + allItemsByChecksum := cookbook.AllItemsByChecksum() + + assert.Equal(t, + map[string]CookbookItem{ + "ba208d0ffc0dd8cbe9c71fb40fb207b2": { + Name: "root_files/metadata.rb", + Path: "metadata.rb", + Checksum: "ba208d0ffc0dd8cbe9c71fb40fb207b2", + Specificity: "default", + }, + "4e15b1e5593d717685323c5dac86b99e": { + Name: "recipes/default.rb", + Path: "recipes/default.rb", + Checksum: "4e15b1e5593d717685323c5dac86b99e", + Specificity: "default", + }, + "553637b4fba46b5148f88d6dd3877e2f": { + Name: "attributes/default.rb", + Path: "attributes/default.rb", + Checksum: "553637b4fba46b5148f88d6dd3877e2f", + Specificity: "default", + }, + }, allItemsByChecksum) +} func TestGetVersion(t *testing.T) { setup() defer teardown() - cbookResp, err := ioutil.ReadFile(cookbookResponseFile) + cbookResp, err := os.ReadFile(cookbookResponseFile) if err != nil { t.Error(err) } @@ -83,7 +212,7 @@ func TestCookbookList(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(cookbookListResponseFile) + file, err := os.ReadFile(cookbookListResponseFile) if err != nil { t.Error(err) } diff --git a/cookbook_upload.go b/cookbook_upload.go new file mode 100644 index 00000000..9085f0eb --- /dev/null +++ b/cookbook_upload.go @@ -0,0 +1,201 @@ +package chef + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +const MaxCookbookUploadRetries = 5 +const MaxCookbookUploadRetrySleep = 2 * time.Second + +func (c *CookbookService) UploadFromForce(path string) (*Cookbook, error) { + return c.UploadFrom(path, true, true) +} + +func (c *CookbookService) UploadFromFrozen(path string) (*Cookbook, error) { + return c.UploadFrom(path, true, false) +} + +// UploadFrom uploads a cookbook from a given path to the Chef server +// frozen indicates whether the cookbook should be frozen, and force indicates +// whether the cookbook should be force uploaded (override an existing frozen version) +// +// * Parses local cookbook. +// * Creates a sandbox with the cookbook's item checksums. +// * Uploads each missing cookbook item into the sandbox. +// * Commits the sandbox. +// * Uploads the cookbook version manifest. +func (c *CookbookService) UploadFrom(path string, frozen bool, force bool) (*Cookbook, error) { + cookbook, err := NewCookbookFromPath(path) + if err != nil { + return nil, err + } + cookbook.Frozen = frozen + + metadataJsonExists := false + cookbookItems := cookbook.AllItemsByChecksum() + checksums := []string{} + for k, v := range cookbookItems { + checksums = append(checksums, k) + if v.Path == "metadata.json" { + metadataJsonExists = true + } + } + + // If the metadata JSON does not exist, generate it from the metadata + if !metadataJsonExists { + metadataJsonBytes, err := json.MarshalIndent(cookbook.Metadata, "", " ") + if err != nil { + return nil, err + } + + err = os.WriteFile(filepath.Join(path, "metadata.json"), metadataJsonBytes, os.ModePerm) + if err != nil { + return nil, err + } + // Ensure the generated metadata.json is removed + defer os.Remove(filepath.Join(path, "metadata.json")) + + metadataJsonChecksum, err := fileMD5Checksum(filepath.Join(path, "metadata.json")) + if err != nil { + return nil, err + } + + metadataJsonCi := CookbookItem{ + Name: "metadata.json", + Path: "metadata.json", + Checksum: metadataJsonChecksum, + Specificity: "default", + } + + // Update associated data + cookbook.RootFiles = append(cookbook.RootFiles, metadataJsonCi) + cookbookItems[metadataJsonChecksum] = metadataJsonCi + checksums = append(checksums, metadataJsonChecksum) + } + + // Create the new sandbox + sandboxPostResp, err := c.client.Sandboxes.Post(checksums) + if err != nil { + return nil, err + } + + sandboxId := sandboxPostResp.ID + sandboxChecksums := sandboxPostResp.Checksums + + // Upload cookbook files to sandbox + for checksum, checksumDetails := range sandboxChecksums { + // Skip files that do not require upload + if !checksumDetails.Upload { + continue + } + + // TODO: Parallelize the uploads + cookbookItem := cookbookItems[checksum] + itemPath := filepath.Join(path, cookbookItem.Path) + err = c.UploadCookbookItem(itemPath, &cookbookItem, checksumDetails.Url) + if err != nil { + return nil, err + } + } + + // Commit the sandbox + // + // Retries are performed to reflect the Ruby implementation + // (the upload target may not yet have replicated the uploaded data) + retries := 0 + + for retries < MaxCookbookUploadRetries { + _, err = c.client.Sandboxes.Put(sandboxId) + if err != nil { + // Retry 400 errors until max retries + if strings.Contains(err.Error(), ": 400") { + time.Sleep(MaxCookbookUploadRetrySleep) + retries++ + continue + } + + return nil, err + } else { + break + } + } + + if retries >= MaxCookbookUploadRetries { + return nil, err + } + + err = c.UploadVersion(cookbook, force) + if err != nil { + return nil, err + } + + return cookbook, nil +} + +// UploadVersion puts a specific version of a cookbooks to the server api +// If force is true, ?force=true is appended to the cookbook URL. +// +// PUT /cookbook/foo/1.2.3 +// Chef API docs: https://docs.chef.io/server/api_chef_server/#put-6 +func (c *CookbookService) UploadVersion(cookbook *Cookbook, force bool) error { + url := fmt.Sprintf("cookbooks/%s/%s", cookbook.CookbookName, cookbook.Version) + + if force { + url = fmt.Sprintf("%s?force=true", url) + } + + manifest, err := cookbook.ManifestJsonForApi(c.client.Auth.ServerApiVersion) + if err != nil { + return err + } + + req, err := c.client.NewRequest("PUT", url, manifest) + if err != nil { + return err + } + + _, err = c.client.Do(req, nil) + + return err +} + +// Uploads a single cookbook item to a given URL. +// Requires the cookbook item's local path, the CookbookItem, and the target URL. +func (c *CookbookService) UploadCookbookItem(itemPath string, item *CookbookItem, url string) error { + md5B64Checksum, err := md5Base64Checksum(item.Checksum) + if err != nil { + return err + } + + itemFile, err := os.Open(itemPath) + if err != nil { + return err + } + defer itemFile.Close() + + uploadReq, err := c.client.NewRequest("PUT", url, itemFile) + if err != nil { + return err + } + + // Get file length for content-length header (required to prevent the Go http client from chunking the request) + fileInfo, err := os.Stat(itemPath) + if err != nil { + return err + } + + // Set minimum headers for file upload + uploadReq.ContentLength = fileInfo.Size() + uploadReq.Header.Set("content-md5", md5B64Checksum) + uploadReq.Header.Set("content-type", "application/x-binary") + uploadReq.Header.Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) + + _, err = c.client.Do(uploadReq, nil) + + return err +} diff --git a/cookbook_upload_test.go b/cookbook_upload_test.go new file mode 100644 index 00000000..7d0dd79b --- /dev/null +++ b/cookbook_upload_test.go @@ -0,0 +1 @@ +package chef diff --git a/digest.go b/digest.go new file mode 100644 index 00000000..b6c92d09 --- /dev/null +++ b/digest.go @@ -0,0 +1,46 @@ +package chef + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "os" +) + +// Generates an MD5 checksum from a file at filePath +func fileMD5Checksum(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + hash := md5.New() + if _, err := io.Copy(hash, file); err != nil { + return "", err + } + + md5String := fmt.Sprintf("%x", hash.Sum(nil)) + + return md5String, nil +} + +// Verifies the MD5 checksum for a file at filePath +func verifyMD5Checksum(filePath, checksum string) bool { + md5String, err := fileMD5Checksum(filePath) + if err != nil { + return false + } + return md5String == checksum +} + +// Converts the MD5 checksum to base64 for compatibility with the sandbox upload API +func md5Base64Checksum(md5Checksum string) (string, error) { + bytes, err := hex.DecodeString(md5Checksum) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(bytes), nil +} diff --git a/digest_test.go b/digest_test.go new file mode 100644 index 00000000..0db785d1 --- /dev/null +++ b/digest_test.go @@ -0,0 +1,56 @@ +package chef + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVerifyMD5Checksum(t *testing.T) { + tempDir, err := os.MkdirTemp("", "md5-test") + if err != nil { + t.Error(err) + } + defer os.RemoveAll(tempDir) // clean up + + var ( + // if someone changes the test data, + // you have to also update the below md5 sum + testData = []byte("hello\nchef\n") + filePath = path.Join(tempDir, "dat") + ) + err = os.WriteFile(filePath, testData, 0644) + assert.Nil(t, err) + assert.True(t, verifyMD5Checksum(filePath, "70bda176ac4db06f1f66f96ae0693be1")) +} + +func TestFileMD5Checksum(t *testing.T) { + tempDir, err := os.MkdirTemp("", "md5-test") + if err != nil { + t.Error(err) + } + defer os.RemoveAll(tempDir) // clean up + + var ( + // if someone changes the test data, + // you have to also update the below md5 sum + testData = []byte("hello\nchef\n") + filePath = path.Join(tempDir, "dat") + ) + err = os.WriteFile(filePath, testData, 0644) + assert.Nil(t, err) + + checksum, err := fileMD5Checksum(filePath) + assert.Nil(t, err) + assert.Equal(t, "70bda176ac4db06f1f66f96ae0693be1", checksum) +} + +func TestMd5Base64Checksum(t *testing.T) { + result, err := md5Base64Checksum("70bda176ac4db06f1f66f96ae0693be1") + if err != nil { + t.Error(err) + } + assert.Equal(t, "cL2hdqxNsG8fZvlq4Gk74Q==", result) +} diff --git a/doc.go b/doc.go index dcf2dd8b..0cd1bc1a 100644 --- a/doc.go +++ b/doc.go @@ -15,7 +15,6 @@ This is an example code generating a new node on a Chef Infra Server: import ( "encoding/json" "fmt" - "io/ioutil" "log" "os" @@ -24,7 +23,7 @@ This is an example code generating a new node on a Chef Infra Server: func main() { // read a client key - key, err := ioutil.ReadFile("key.pem") + key, err := os.ReadFile("key.pem") if err != nil { fmt.Println("Couldn't read key.pem:", err) os.Exit(1) diff --git a/environment.go b/environment.go index eb334feb..761a66c1 100644 --- a/environment.go +++ b/environment.go @@ -30,7 +30,7 @@ type EnvironmentRecipesResult []string func strMapToStr(e map[string]string) (out string) { keys := make([]string, len(e)) - for k, _ := range e { + for k := range e { keys = append(keys, k) } sort.Strings(keys) diff --git a/http.go b/http.go index 786674e7..063da6b4 100644 --- a/http.go +++ b/http.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "log" "net" "net/http" @@ -24,6 +23,9 @@ import ( // ChefVersion that we pretend to emulate const ChefVersion = "14.0.0" +// Default to Server API version 1. Current Chef server API versions include 0, 1, and 2. +const DefaultServerApiVersion = "1" + // Body wraps io.Reader and adds methods for calculating hashes and detecting content type Body struct { io.Reader @@ -36,6 +38,7 @@ type AuthConfig struct { PrivateKey *rsa.PrivateKey ClientName string AuthenticationVersion string + ServerApiVersion string } // Client is vessel for public methods used against the chef-server @@ -92,6 +95,9 @@ type Config struct { // Time to wait in seconds before giving up on a request to the server Timeout int + // Server API Version + ServerApiVersion string + // Authentication Protocol Version AuthenticationVersion string @@ -238,11 +244,16 @@ func NewClient(cfg *Config) (*Client, error) { tr.Proxy = cfg.Proxy } + if cfg.ServerApiVersion == "" { + cfg.ServerApiVersion = DefaultServerApiVersion + } + c := &Client{ Auth: &AuthConfig{ PrivateKey: pk, ClientName: cfg.Name, AuthenticationVersion: cfg.AuthenticationVersion, + ServerApiVersion: cfg.ServerApiVersion, }, BaseURL: baseUrl, } @@ -446,7 +457,7 @@ func CheckResponse(r *http.Response) error { return nil } errorResponse := &ErrorResponse{Response: r} - data, err := ioutil.ReadAll(r.Body) + data, err := io.ReadAll(r.Body) debug("Response Error Body: %+v\n", string(data)) if err == nil && data != nil { json.Unmarshal(data, errorResponse) @@ -511,16 +522,16 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { // add the body back to the response so // subsequent calls to res.Body contain data - res.Body = ioutil.NopCloser(&resBuf) + res.Body = io.NopCloser(&resBuf) // no response interface specified if v == nil { if debug_on() { // show the response body as a string - resbody, _ := ioutil.ReadAll(resTee) + resbody, _ := io.ReadAll(resTee) debug("Response body: %+v\n", string(resbody)) } else { - _, _ = ioutil.ReadAll(resTee) + _, _ = io.ReadAll(resTee) } debug("No response body requested\n") return res, nil @@ -538,11 +549,11 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { err = json.NewDecoder(resTee).Decode(v) if debug_on() { // show the response body as a string - resbody, _ := ioutil.ReadAll(&resBuf) + resbody, _ := io.ReadAll(&resBuf) debug("Response body: %+v\n", string(resbody)) var repBuffer bytes.Buffer repBuffer.Write(resbody) - res.Body = ioutil.NopCloser(&repBuffer) + res.Body = io.NopCloser(&repBuffer) } debug("Response body specifies content as JSON: %+v Err: %+v\n", v, err) return res, err @@ -550,7 +561,7 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { // response interface, v, is type string and the content is plain text if _, ok := v.(*string); ok && hasTextContentType(res) { - resbody, _ := ioutil.ReadAll(resTee) + resbody, _ := io.ReadAll(resTee) if err != nil { return res, err } @@ -564,11 +575,11 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { err = json.NewDecoder(resTee).Decode(v) if debug_on() { // show the response body as a string - resbody, _ := ioutil.ReadAll(&resBuf) + resbody, _ := io.ReadAll(&resBuf) debug("Response body: %+v\n", string(resbody)) var repBuffer bytes.Buffer repBuffer.Write(resbody) - res.Body = ioutil.NopCloser(&repBuffer) + res.Body = io.NopCloser(&repBuffer) } debug("Response body defaulted to JSON parsing: %+v Err: %+v\n", v, err) return res, err @@ -599,7 +610,7 @@ func (ac AuthConfig) SignRequest(request *http.Request) error { "Method": request.Method, "Accept": "application/json", "X-Chef-Version": ChefVersion, - "X-Ops-Server-API-Version": "1", + "X-Ops-Server-API-Version": ac.ServerApiVersion, "X-Ops-Timestamp": time.Now().UTC().Format(time.RFC3339), "X-Ops-Content-Hash": request.Header.Get("X-Ops-Content-Hash"), "X-Ops-UserId": ac.ClientName, diff --git a/http2_test.go b/http2_test.go index 2e0408a2..a1a57071 100755 --- a/http2_test.go +++ b/http2_test.go @@ -4,12 +4,13 @@ package chef import ( - "github.com/stretchr/testify/assert" "net/http" "net/url" "os" "reflect" "testing" + + "github.com/stretchr/testify/assert" ) // TestNewClientProxy2 diff --git a/http_test.go b/http_test.go index c912da62..38f38c2a 100755 --- a/http_test.go +++ b/http_test.go @@ -7,10 +7,7 @@ import ( "encoding/pem" "errors" "fmt" - "github.com/hashicorp/go-retryablehttp" - "github.com/stretchr/testify/assert" "io" - "io/ioutil" "math/big" "net/http" "net/http/httptest" @@ -21,6 +18,9 @@ import ( "testing" "time" + "github.com/hashicorp/go-retryablehttp" + "github.com/stretchr/testify/assert" + . "github.com/ctdk/goiardi/chefcrypto" ) @@ -746,7 +746,7 @@ func TestDoText(t *testing.T) { res, err := client.Do(request, &getdata) assert.Nil(t, err, "text request err") assert.Equal(t, pigText, getdata, "Plain text returned in string") - resData, err := ioutil.ReadAll(res.Body) + resData, err := io.ReadAll(res.Body) assert.Nil(t, err, "Read the response body") assert.Equal(t, pigText, string(resData), "Plain text from the response body") } @@ -767,7 +767,7 @@ func TestDoJSON(t *testing.T) { res, err := client.Do(request, &getdata) assert.Nil(t, err, "Json returned") assert.Equal(t, getdata, wantdata, "Json returned data") - resData, err := ioutil.ReadAll(res.Body) + resData, err := io.ReadAll(res.Body) assert.Nil(t, err, "Read the response body") assert.Equal(t, jsonText, string(resData), "Plain text from the response body") } @@ -789,7 +789,7 @@ func TestDoDefaultParse(t *testing.T) { res, err := client.Do(request, &getdata) assert.Nil(t, err, "Default parse err") assert.Equal(t, getdata, wantdata, "Default parse of json data") - resData, err := ioutil.ReadAll(res.Body) + resData, err := io.ReadAll(res.Body) assert.Nil(t, err, "Read the response body") assert.Equal(t, jsonText, string(resData), "Default parse text from the response body") } @@ -808,7 +808,7 @@ func TestDoNoResponseInterface(t *testing.T) { request, _ := client.NewRequest("GET", "hashrocket", nil) res, err := client.Do(request, nil) assert.Nil(t, err, "No interface parse err") - resData, err := ioutil.ReadAll(res.Body) + resData, err := io.ReadAll(res.Body) assert.Nil(t, err, "Read the response body") assert.Equal(t, jsonText, string(resData), "No Interface from the response body") } @@ -828,11 +828,11 @@ func TestDoIOWriter(t *testing.T) { request, _ := client.NewRequest("GET", "hashrocket", nil) res, err := client.Do(request, buf) assert.Nil(t, err, "No interface parse err") - byteData, err := ioutil.ReadAll(buf) + byteData, err := io.ReadAll(buf) wantdata := string(byteData) assert.Nil(t, err, "Readable IO stream") assert.Equal(t, jsonText, wantdata, "IO writer parse") - resData, err := ioutil.ReadAll(res.Body) + resData, err := io.ReadAll(res.Body) assert.Nil(t, err, "Read the response body") assert.Equal(t, jsonText, string(resData), "IO Writer from the response body") } diff --git a/policy_group_test.go b/policy_group_test.go index bfcd402e..be539eec 100644 --- a/policy_group_test.go +++ b/policy_group_test.go @@ -2,8 +2,8 @@ package chef import ( "fmt" - "io/ioutil" "net/http" + "os" "testing" ) @@ -15,7 +15,7 @@ func TestPolicyGroupList(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(policyGroupResponseFile) + file, err := os.ReadFile(policyGroupResponseFile) if err != nil { t.Error(err) } @@ -47,7 +47,7 @@ func TestPolicyGroupGet(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(policyGroupFile) + file, err := os.ReadFile(policyGroupFile) if err != nil { t.Error(err) } @@ -71,7 +71,7 @@ func TestPolicyGroupDelete(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(policyGroupFile) + file, err := os.ReadFile(policyGroupFile) if err != nil { t.Error(err) } @@ -95,7 +95,7 @@ func TestPolicyGroupGetPolicy(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(revisionDetailsResponseFile) + file, err := os.ReadFile(revisionDetailsResponseFile) if err != nil { t.Error(err) } @@ -119,7 +119,7 @@ func TestPolicyGroupDeletePolicy(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(revisionDetailsResponseFile) + file, err := os.ReadFile(revisionDetailsResponseFile) if err != nil { t.Error(err) } diff --git a/policy_test.go b/policy_test.go index f4fcaffb..b20de9e2 100644 --- a/policy_test.go +++ b/policy_test.go @@ -2,8 +2,8 @@ package chef import ( "fmt" - "io/ioutil" "net/http" + "os" "testing" ) @@ -14,7 +14,7 @@ func TestListPolicies(t *testing.T) { setup() defer teardown() - file, err := ioutil.ReadFile(policyListResponseFile) + file, err := os.ReadFile(policyListResponseFile) if err != nil { t.Error(err) } @@ -86,7 +86,7 @@ func TestGetPolicyRevision(t *testing.T) { const policyName = "base" const policyRevision = "8228b0e381fe1de3ee39bf51e93029dbbdcecc61fb5abea4ca8c82591c0b529b" - file, err := ioutil.ReadFile(policyRevisionResponseFile) + file, err := os.ReadFile(policyRevisionResponseFile) if err != nil { t.Error(err) } @@ -139,7 +139,7 @@ func TestDeletePolicyRevision(t *testing.T) { const policyName = "base" const policyRevision = "8228b0e381fe1de3ee39bf51e93029dbbdcecc61fb5abea4ca8c82591c0b529b" - file, err := ioutil.ReadFile(policyRevisionResponseFile) + file, err := os.ReadFile(policyRevisionResponseFile) if err != nil { t.Error(err) } diff --git a/reader_test.go b/reader_test.go index 986ed463..97964aaa 100644 --- a/reader_test.go +++ b/reader_test.go @@ -2,7 +2,6 @@ package chef import ( "io" - "io/ioutil" "os" "testing" ) @@ -14,7 +13,7 @@ type TestEncoder struct { } func TestEncoderJSONReader(t *testing.T) { - f, err := ioutil.TempFile("test/", "reader") + f, err := os.CreateTemp("test/", "reader") if err != nil { t.Error(err) } diff --git a/search_test.go b/search_test.go index 0ad7111a..6ffeae55 100644 --- a/search_test.go +++ b/search_test.go @@ -3,8 +3,8 @@ package chef import ( "encoding/json" "fmt" - "io/ioutil" "net/http" + "os" "testing" "github.com/stretchr/testify/assert" @@ -132,10 +132,10 @@ func TestSearch_PartialExecMultipleCalls(t *testing.T) { setup() defer teardown() - searchResponseOne, err := ioutil.ReadFile(partialSearchResponseFile_1) + searchResponseOne, err := os.ReadFile(partialSearchResponseFile_1) assert.Nil(t, err, "Read response file 1 failed") - searchResponseTwo, err := ioutil.ReadFile(partialSearchResponseFile_2) + searchResponseTwo, err := os.ReadFile(partialSearchResponseFile_2) assert.Nil(t, err, "Read response file 2 failed") mux.HandleFunc("/search/node", func(w http.ResponseWriter, r *http.Request) { diff --git a/test/chefignore b/test/chefignore new file mode 100644 index 00000000..93f27d4f --- /dev/null +++ b/test/chefignore @@ -0,0 +1,113 @@ +# Test chefignore file + +# OS generated files # +###################### +.DS_Store +ehthumbs.db +Icon? +nohup.out +Thumbs.db +.envrc + +# EDITORS # +########### +.#* +.project +.settings +*_flymake +*_flymake.* +*.bak +*.sw[a-z] +*.tmproj +*~ +\#* +REVISION +TAGS* +tmtags +.vscode +.editorconfig + +## COMPILED ## +############## +*.class +*.com +*.dll +*.exe +*.o +*.pyc +*.so +*/rdoc/ +a.out +mkmf.log + +# Testing # +########### +.circleci/* +.codeclimate.yml +.delivery/* +.foodcritic +.kitchen* +.mdlrc +.overcommit.yml +.rspec +.rubocop.yml +.travis.yml +.watchr +.yamllint +azure-pipelines.yml +Dangerfile +examples/* +features/* +Guardfile +kitchen.yml* +mlc_config.json +Procfile +Rakefile +spec/* +test/* + +# SCM # +####### +.git +.gitattributes +.gitconfig +.github/* +.gitignore +.gitkeep +.gitmodules +.svn +*/.bzr/* +*/.git +*/.hg/* +*/.svn/* + +# Berkshelf # +############# +Berksfile +Berksfile.lock +cookbooks/* +tmp + +# Bundler # +########### +vendor/* +Gemfile +Gemfile.lock + +# Policyfile # +############## +Policyfile.rb +Policyfile.lock.json + +# Documentation # +############# +CODE_OF_CONDUCT* +CONTRIBUTING* +documentation/* +TESTING* +UPGRADING* + +# Vagrant # +########### +.vagrant +Vagrantfile diff --git a/test/cookbooks/testcomplex/README.md b/test/cookbooks/testcomplex/README.md new file mode 100644 index 00000000..410e444b --- /dev/null +++ b/test/cookbooks/testcomplex/README.md @@ -0,0 +1,3 @@ +# testcomplex + +A test cookbook containing all cookbook file types. Used to compare parsing done by go-chef to upstream chef. diff --git a/test/cookbooks/testcomplex/attributes/default.rb b/test/cookbooks/testcomplex/attributes/default.rb new file mode 100644 index 00000000..a24f33aa --- /dev/null +++ b/test/cookbooks/testcomplex/attributes/default.rb @@ -0,0 +1 @@ +default['testcomplex']['feature'] = true diff --git a/test/cookbooks/testcomplex/chefignore b/test/cookbooks/testcomplex/chefignore new file mode 100644 index 00000000..dfed8fe8 --- /dev/null +++ b/test/cookbooks/testcomplex/chefignore @@ -0,0 +1,3 @@ +README.md +chefignore +manifests/* diff --git a/test/cookbooks/testcomplex/definitions/write_a_file.rb b/test/cookbooks/testcomplex/definitions/write_a_file.rb new file mode 100644 index 00000000..3fd0d3a2 --- /dev/null +++ b/test/cookbooks/testcomplex/definitions/write_a_file.rb @@ -0,0 +1,6 @@ +# Legacy resource definition +define :write_a_file, path: nil do + path = params[:path] || name + + file path +end diff --git a/test/cookbooks/testcomplex/files/test.txt b/test/cookbooks/testcomplex/files/test.txt new file mode 100644 index 00000000..13ac68ef --- /dev/null +++ b/test/cookbooks/testcomplex/files/test.txt @@ -0,0 +1 @@ +Default root test cookbook file diff --git a/test/cookbooks/testcomplex/files/windows/test.txt b/test/cookbooks/testcomplex/files/windows/test.txt new file mode 100644 index 00000000..e1c8eb04 --- /dev/null +++ b/test/cookbooks/testcomplex/files/windows/test.txt @@ -0,0 +1 @@ +Windows test cookbook file diff --git a/test/cookbooks/testcomplex/libraries/testlib.rb b/test/cookbooks/testcomplex/libraries/testlib.rb new file mode 100644 index 00000000..39d423bd --- /dev/null +++ b/test/cookbooks/testcomplex/libraries/testlib.rb @@ -0,0 +1,5 @@ +module Testcomplex + def testmethod? + true + end +end diff --git a/test/cookbooks/testcomplex/manifests/chefv0.json b/test/cookbooks/testcomplex/manifests/chefv0.json new file mode 100644 index 00000000..a67a3385 --- /dev/null +++ b/test/cookbooks/testcomplex/manifests/chefv0.json @@ -0,0 +1,145 @@ +{ + "metadata": { + "name": "testcomplex", + "description": "Installs/Configures testcomplex", + "long_description": "", + "maintainer": "The Authors", + "maintainer_email": "you@example.com", + "license": "All Rights Reserved", + "platforms": { + "ubuntu": ">= 20.04", + "redhat": ">= 0.0.0" + }, + "dependencies": { + "lvm": "~> 6.1", + "vagrant": ">= 4.0.14" + }, + "providing": { + "testcomplex": ">= 0.0.0", + "testcomplex::test": ">= 0.0.0" + }, + "recipes": { + "testcomplex": "", + "testcomplex::test": "" + }, + "version": "1.2.3", + "source_url": "", + "issues_url": "", + "privacy": false, + "chef_versions": [ + [ + ">= 18.0" + ] + ], + "ohai_versions": [], + "gems": [ + [ + "json", + ">1.0.0" + ] + ], + "eager_load_libraries": true + }, + "version": "1.2.3", + "name": "testcomplex-1.2.3", + "cookbook_name": "testcomplex", + "templates": [ + { + "name": "template.txt.erb", + "path": "templates/default/template.txt.erb", + "checksum": "9fa6c96ead0d25afb74a7269c581f6bf", + "specificity": "default" + }, + { + "name": "template.txt.erb", + "path": "templates/windows/template.txt.erb", + "checksum": "65599b78af4e8a01eb9cbe4ef4bfee18", + "specificity": "windows" + } + ], + "attributes": [ + { + "name": "default.rb", + "path": "attributes/default.rb", + "checksum": "c286b84ab6605c49692fcad18e92ac63", + "specificity": "default" + } + ], + "root_files": [ + { + "name": "metadata.rb", + "path": "metadata.rb", + "checksum": "d35110e2a5617dccfecd5e1c24a73181", + "specificity": "default" + } + ], + "definitions": [ + { + "name": "write_a_file.rb", + "path": "definitions/write_a_file.rb", + "checksum": "7fb4867a82c2463045a21298d4759944", + "specificity": "default" + } + ], + "providers": [ + { + "name": "provider_resource.rb", + "path": "providers/provider_resource.rb", + "checksum": "92f62e7f709d8367dc184a0270938711", + "specificity": "default" + } + ], + "recipes": [ + { + "name": "default.rb", + "path": "recipes/default.rb", + "checksum": "37b0d4fee115fbdcc48941d49707d924", + "specificity": "default" + }, + { + "name": "test.rb", + "path": "recipes/test.rb", + "checksum": "da333f01b6f5896d3b73be14be36acff", + "specificity": "default" + } + ], + "libraries": [ + { + "name": "testlib.rb", + "path": "libraries/testlib.rb", + "checksum": "6999b70bad7055c842018196a319ad44", + "specificity": "default" + } + ], + "resources": [ + { + "name": "a_resource.rb", + "path": "resources/a_resource.rb", + "checksum": "ca753be0661ba6217b8f4c544ccb37ae", + "specificity": "default" + }, + { + "name": "provider_resource.rb", + "path": "resources/provider_resource.rb", + "checksum": "ddd4a4df29e395bc2e20b9c93fdcbf07", + "specificity": "default" + } + ], + "files": [ + { + "name": "test.txt", + "path": "files/test.txt", + "checksum": "3047c9959bd06478aba8574a8716adba", + "specificity": "root_default" + }, + { + "name": "test.txt", + "path": "files/windows/test.txt", + "checksum": "2b38ee6807f71ac34103818dc8a1d641", + "specificity": "windows" + } + ], + "frozen?": false, + "chef_type": "cookbook_version", + "json_class": "Chef::CookbookVersion" +} diff --git a/test/cookbooks/testcomplex/manifests/chefv2.json b/test/cookbooks/testcomplex/manifests/chefv2.json new file mode 100644 index 00000000..860ea204 --- /dev/null +++ b/test/cookbooks/testcomplex/manifests/chefv2.json @@ -0,0 +1,148 @@ +{ + "all_files": [ + { + "name": "templates/template.txt.erb", + "path": "templates/default/template.txt.erb", + "checksum": "9fa6c96ead0d25afb74a7269c581f6bf", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/templates/default/template.txt.erb" + }, + { + "name": "templates/template.txt.erb", + "path": "templates/windows/template.txt.erb", + "checksum": "65599b78af4e8a01eb9cbe4ef4bfee18", + "specificity": "windows", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/templates/windows/template.txt.erb" + }, + { + "name": "attributes/default.rb", + "path": "attributes/default.rb", + "checksum": "c286b84ab6605c49692fcad18e92ac63", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/attributes/default.rb" + }, + { + "name": "root_files/metadata.rb", + "path": "metadata.rb", + "checksum": "d35110e2a5617dccfecd5e1c24a73181", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/metadata.rb" + }, + { + "name": "other/otherfile.txt", + "path": "other/otherfile.txt", + "checksum": "e7ae50f38eb44cf66440397beffa072f", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/other/otherfile.txt" + }, + { + "name": "definitions/write_a_file.rb", + "path": "definitions/write_a_file.rb", + "checksum": "7fb4867a82c2463045a21298d4759944", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/definitions/write_a_file.rb" + }, + { + "name": "providers/provider_resource.rb", + "path": "providers/provider_resource.rb", + "checksum": "92f62e7f709d8367dc184a0270938711", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/providers/provider_resource.rb" + }, + { + "name": "recipes/default.rb", + "path": "recipes/default.rb", + "checksum": "37b0d4fee115fbdcc48941d49707d924", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/recipes/default.rb" + }, + { + "name": "recipes/test.rb", + "path": "recipes/test.rb", + "checksum": "da333f01b6f5896d3b73be14be36acff", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/recipes/test.rb" + }, + { + "name": "libraries/testlib.rb", + "path": "libraries/testlib.rb", + "checksum": "6999b70bad7055c842018196a319ad44", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/libraries/testlib.rb" + }, + { + "name": "resources/a_resource.rb", + "path": "resources/a_resource.rb", + "checksum": "ca753be0661ba6217b8f4c544ccb37ae", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/resources/a_resource.rb" + }, + { + "name": "resources/provider_resource.rb", + "path": "resources/provider_resource.rb", + "checksum": "ddd4a4df29e395bc2e20b9c93fdcbf07", + "specificity": "default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/resources/provider_resource.rb" + }, + { + "name": "files/test.txt", + "path": "files/test.txt", + "checksum": "3047c9959bd06478aba8574a8716adba", + "specificity": "root_default", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/files/test.txt" + }, + { + "name": "files/test.txt", + "path": "files/windows/test.txt", + "checksum": "2b38ee6807f71ac34103818dc8a1d641", + "specificity": "windows", + "full_path": "/home/matty/go/chef/test/cookbooks/testcomplex/files/windows/test.txt" + } + ], + "metadata": { + "name": "testcomplex", + "description": "Installs/Configures testcomplex", + "long_description": "", + "maintainer": "The Authors", + "maintainer_email": "you@example.com", + "license": "All Rights Reserved", + "platforms": { + "ubuntu": ">= 20.04", + "redhat": ">= 0.0.0" + }, + "dependencies": { + "lvm": "~> 6.1", + "vagrant": ">= 4.0.14" + }, + "providing": { + "testcomplex": ">= 0.0.0", + "testcomplex::test": ">= 0.0.0" + }, + "recipes": { + "testcomplex": "", + "testcomplex::test": "" + }, + "version": "1.2.3", + "source_url": "", + "issues_url": "", + "privacy": false, + "chef_versions": [ + [ + ">= 18.0" + ] + ], + "ohai_versions": [], + "gems": [ + [ + "json", + ">1.0.0" + ] + ], + "eager_load_libraries": true + }, + "version": "1.2.3", + "name": "testcomplex-1.2.3", + "cookbook_name": "testcomplex", + "chef_type": "cookbook_version", + "json_class": "Chef::CookbookVersion" +} diff --git a/test/cookbooks/testcomplex/metadata.rb b/test/cookbooks/testcomplex/metadata.rb new file mode 100644 index 00000000..3459553c --- /dev/null +++ b/test/cookbooks/testcomplex/metadata.rb @@ -0,0 +1,15 @@ +name 'testcomplex' +maintainer 'The Authors' +maintainer_email 'you@example.com' +license 'All Rights Reserved' +description 'Installs/Configures testcomplex' +version '1.2.3' +chef_version '>= 18.0' + +supports 'ubuntu', '>= 20.04' +supports 'redhat' + +depends 'lvm', '~> 6.1' # Needed for VG and LV management +depends 'vagrant', '>= 4.0.14' + +gem 'json', '>1.0.0' diff --git a/test/cookbooks/testcomplex/other/otherfile.txt b/test/cookbooks/testcomplex/other/otherfile.txt new file mode 100644 index 00000000..05fd43ad --- /dev/null +++ b/test/cookbooks/testcomplex/other/otherfile.txt @@ -0,0 +1 @@ +This is a file outside of the usual structure. diff --git a/test/cookbooks/testcomplex/providers/provider_resource.rb b/test/cookbooks/testcomplex/providers/provider_resource.rb new file mode 100644 index 00000000..8f35c48b --- /dev/null +++ b/test/cookbooks/testcomplex/providers/provider_resource.rb @@ -0,0 +1,3 @@ +action :create do + Chef::Log.debug('Test provider create method.') +end diff --git a/test/cookbooks/testcomplex/recipes/default.rb b/test/cookbooks/testcomplex/recipes/default.rb new file mode 100644 index 00000000..67b0a354 --- /dev/null +++ b/test/cookbooks/testcomplex/recipes/default.rb @@ -0,0 +1,5 @@ +file '/tmp/testcomplex' + +a_resource 'test resource' + +include_recipe 'testcomplex::test' diff --git a/test/cookbooks/testcomplex/recipes/test.rb b/test/cookbooks/testcomplex/recipes/test.rb new file mode 100644 index 00000000..ae466eba --- /dev/null +++ b/test/cookbooks/testcomplex/recipes/test.rb @@ -0,0 +1 @@ +file '/tmp/testcomplex_test.txt' diff --git a/test/cookbooks/testcomplex/resources/a_resource.rb b/test/cookbooks/testcomplex/resources/a_resource.rb new file mode 100644 index 00000000..31ab058e --- /dev/null +++ b/test/cookbooks/testcomplex/resources/a_resource.rb @@ -0,0 +1,7 @@ +unified_mode true if respond_to?(:unified_mode) + +property :a_prop, String, default: 'default' + +action :create do + file ::File.join('/tmp', new_resource.a_prop) +end diff --git a/test/cookbooks/testcomplex/resources/provider_resource.rb b/test/cookbooks/testcomplex/resources/provider_resource.rb new file mode 100644 index 00000000..e98e4428 --- /dev/null +++ b/test/cookbooks/testcomplex/resources/provider_resource.rb @@ -0,0 +1,3 @@ +unified_mode true if respond_to?(:unified_mode) + +default_action :create diff --git a/test/cookbooks/testcomplex/templates/default/template.txt.erb b/test/cookbooks/testcomplex/templates/default/template.txt.erb new file mode 100644 index 00000000..eb8d792b --- /dev/null +++ b/test/cookbooks/testcomplex/templates/default/template.txt.erb @@ -0,0 +1,2 @@ +# A test template +<%= node['hostname'] %> diff --git a/test/cookbooks/testcomplex/templates/windows/template.txt.erb b/test/cookbooks/testcomplex/templates/windows/template.txt.erb new file mode 100644 index 00000000..55a836a6 --- /dev/null +++ b/test/cookbooks/testcomplex/templates/windows/template.txt.erb @@ -0,0 +1,2 @@ +# A test Windows template +Windows: <%= node['hostname'] %> diff --git a/test/cookbooks/testdeps/attributes/default.rb b/test/cookbooks/testdeps/attributes/default.rb new file mode 100644 index 00000000..ae669c70 --- /dev/null +++ b/test/cookbooks/testdeps/attributes/default.rb @@ -0,0 +1,2 @@ +# Test attribute +default['some']['attribute'] = 123 diff --git a/test/cookbooks/testdeps/metadata.rb b/test/cookbooks/testdeps/metadata.rb new file mode 100644 index 00000000..0d16b432 --- /dev/null +++ b/test/cookbooks/testdeps/metadata.rb @@ -0,0 +1,15 @@ +name 'testdeps' +maintainer 'The Authors' +maintainer_email 'you@example.com' +license 'All Rights Reserved' +description 'Installs/Configures testdeps' +version '0.1.0' +chef_version '>= 18.0' + +supports 'ubuntu', '>= 20.04' +supports 'redhat' + +depends 'lvm', '~> 6.1' # Needed for VG and LV management +depends 'vagrant', '>= 4.0.14' + +gem 'json', '>1.0.0' diff --git a/test/cookbooks/testdeps/recipes/default.rb b/test/cookbooks/testdeps/recipes/default.rb new file mode 100644 index 00000000..5e58a86e --- /dev/null +++ b/test/cookbooks/testdeps/recipes/default.rb @@ -0,0 +1 @@ +file '/tmp/testing.txt' diff --git a/test_chef_server/Berksfile.lock b/test_chef_server/Berksfile.lock index 95959712..c2101f02 100644 --- a/test_chef_server/Berksfile.lock +++ b/test_chef_server/Berksfile.lock @@ -7,7 +7,7 @@ GRAPH chef-ingredient (3.5.0) chef-server (5.6.0) chef-ingredient (>= 2.1.10) - line (4.5.13) + line (4.5.17) test_chef_server (0.1.0) chef-server (>= 0.0.0) line (>= 0.0.0) diff --git a/test_chef_server/recipes/chefapi.rb b/test_chef_server/recipes/chefapi.rb index af83645d..59b7814f 100644 --- a/test_chef_server/recipes/chefapi.rb +++ b/test_chef_server/recipes/chefapi.rb @@ -3,4 +3,9 @@ package 'git' -package 'golang' +package 'golang-1.21' + +link '/usr/bin/go' do + link_type :symbolic + to '/usr/lib/go-1.21/bin/go' +end diff --git a/test_chef_server/test/integration/default/inspec/cookbook_spec.rb b/test_chef_server/test/integration/default/inspec/cookbook_spec.rb index 2042174f..08052a53 100644 --- a/test_chef_server/test/integration/default/inspec/cookbook_spec.rb +++ b/test_chef_server/test/integration/default/inspec/cookbook_spec.rb @@ -6,6 +6,8 @@ its('stderr') { should_not match(/testbook/) } its('stderr') { should_not match(/sampbook/) } its('stderr') { should_not match(/Issue loading/) } + its('stderr') { should_not match(/Issue uploading/) } + its('stderr') { should_not match(/Issue finding/) } its('stdout') { should match(%r{^List initial cookbooks (?=.*sampbook => https://testhost/organizations/test/cookbooks/sampbook\n\s*\* 0.2.0)(?=.*testbook => https://testhost/organizations/test/cookbooks/testbook\n\s*\* 0.2.0).*EndInitialList}m) } # output from get cookbook is odd its('stdout') { should match(/^Get cookbook testbook/) } @@ -16,4 +18,6 @@ its('stdout') { should match(/^Delete testbook 0.1.0 /) } its('stdout') { should match(%r{^Final cookbook list sampbook => https://testhost/organizations/test/cookbooks/sampbook\n\s*\* 0.2.0}m) } its('stdout') { should match(%r{^Final cookbook versions sampbook sampbook => https://testhost/organizations/test/cookbooks/sampbook\n\s*\* 0.2.0\n\s*\* 0.1.0}m) } + its('stdout') { should match(/Uploaded cookbook name: testdeps-0\.1\.0/) } + its('stdout') { should match(/^Delete testdeps 0.1.0 /) } end diff --git a/testapi/bin/setup b/testapi/bin/setup index 4920473d..93dc1526 100755 --- a/testapi/bin/setup +++ b/testapi/bin/setup @@ -1 +1,4 @@ export GOPATH=/go:/go/src/github.com/go-chef/chef/testapi + +# Ensure we can locate the chef module +cd /go/src/github.com/go-chef/chef diff --git a/testapi/cookbook.go b/testapi/cookbook.go index f107b267..0e26c476 100644 --- a/testapi/cookbook.go +++ b/testapi/cookbook.go @@ -104,6 +104,46 @@ func Cookbook() { } fmt.Printf("Final cookbook versions sampbook %+v\n", sampbookversions) + // Upload a cookbook from a file path + _, err = client.Cookbooks.UploadFrom("/go/src/github.com/go-chef/chef/test/cookbooks/testdeps", false, false) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue uploading cookbook testdeps to Chef server:", err) + } + + uploadedCookbook, err := client.Cookbooks.GetVersion("testdeps", "0.1.0") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue finding uploaded cookbook testdeps on Chef server:", err) + } + + fmt.Printf("Uploaded cookbook name: %s\n", uploadedCookbook.Name) + + // Test API V2 upload + client.Auth.ServerApiVersion = "2" + + _, err = client.Cookbooks.UploadFrom("/go/src/github.com/go-chef/chef/test/cookbooks/testcomplex", false, false) + if err != nil { + fmt.Fprintln(os.Stderr, "Issue uploading cookbook testcomplex to Chef server with V2 API:", err) + } + + uploadedCookbook, err = client.Cookbooks.GetVersion("testcomplex", "1.2.3") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue finding uploaded cookbook testcomplex on Chef server:", err) + } + + fmt.Printf("Uploaded V2 cookbook name: %s\n", uploadedCookbook.Name) + + // Cleanup cookbook + err = client.Cookbooks.Delete("testdeps", "0.1.0") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting testdeps 0.1.0:", err) + } + fmt.Printf("Delete testdeps 0.1.0 %+v\n", err) + + err = client.Cookbooks.Delete("testcomplex", "1.2.3") + if err != nil { + fmt.Fprintln(os.Stderr, "Issue deleting testcomplex 1.2.3:", err) + } + fmt.Printf("Delete testcomplex 1.2.3 %+v\n", err) } func addSampleCookbooks() (err error) { diff --git a/testapi/policy.go b/testapi/policy.go index 77814618..9fee9924 100644 --- a/testapi/policy.go +++ b/testapi/policy.go @@ -3,8 +3,9 @@ package testapi import ( "fmt" - "github.com/go-chef/chef" "os" + + "github.com/go-chef/chef" ) // policy exercise the chef server api @@ -65,7 +66,7 @@ func firstPolicy(policyList chef.PoliciesGetResponse) (string, chef.Policy) { } func firstRevision(policy chef.Policy) string { - for key, _ := range policy.Revisions { + for key := range policy.Revisions { return key } return "" diff --git a/testapi/testapi.go b/testapi/testapi.go index 65c7c527..3ad19540 100644 --- a/testapi/testapi.go +++ b/testapi/testapi.go @@ -4,7 +4,6 @@ package testapi import ( "crypto/x509" "fmt" - "io/ioutil" "log" "os" "strconv" @@ -53,7 +52,7 @@ func buildClient(user string, keyfile string, baseurl string, skipssl bool, vers // clientKey reads the pem file containing the credentials needed to use the chef client. func clientKey(filepath string) string { - key, err := ioutil.ReadFile(filepath) + key, err := os.ReadFile(filepath) if err != nil { fmt.Fprintf(os.Stderr, "Couldn't read key.pem: %+v, %+v", filepath, err) os.Exit(1) @@ -70,7 +69,7 @@ func chefCerts() *x509.CertPool { certPool = x509.NewCertPool() } // Read in the cert file - certs, err := ioutil.ReadFile(localCertFile) + certs, err := os.ReadFile(localCertFile) if err != nil { log.Fatalf("Failed to append %q to RootCAs: %v", localCertFile, err) } diff --git a/testapi/testcase/testcase.go b/testapi/testcase/testcase.go index 6d0bf2f1..ccb9b87c 100644 --- a/testapi/testcase/testcase.go +++ b/testapi/testcase/testcase.go @@ -2,8 +2,8 @@ package main import ( "fmt" - "os" "github.com/go-chef/chef/testapi" + "os" ) var cases = map[string]func(){