github.com/go-chef/chef@v0.30.1/cookbook.go (about) 1 package chef 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 "strings" 10 ) 11 12 const metaRbName = "metadata.rb" 13 const metaJsonName = "metadata.json" 14 15 type metaFunc func(s []string, m *CookbookMeta) error 16 17 var metaRegistry map[string]metaFunc 18 19 // CookbookService is the service for interacting with chef server cookbooks endpoint 20 type CookbookService struct { 21 client *Client 22 } 23 24 // CookbookItem represents a object of cookbook file data 25 type CookbookItem struct { 26 Url string `json:"url,omitempty"` 27 Path string `json:"path,omitempty"` 28 Name string `json:"name,omitempty"` 29 Checksum string `json:"checksum,omitempty"` 30 Specificity string `json:"specificity,omitempty"` 31 } 32 33 // CookbookListResult is the summary info returned by chef-api when listing 34 // http://docs.opscode.com/api_chef_server.html#cookbooks 35 type CookbookListResult map[string]CookbookVersions 36 37 // CookbookRecipesResult is the summary info returned by chef-api when listing 38 // http://docs.opscode.com/api_chef_server.html#cookbooks-recipes 39 type CookbookRecipesResult []string 40 41 // CookbookVersions is the data container returned from the chef server when listing all cookbooks 42 type CookbookVersions struct { 43 Url string `json:"url,omitempty"` 44 Versions []CookbookVersion `json:"versions,omitempty"` 45 } 46 47 // CookbookVersion is the data for a specific cookbook version 48 type CookbookVersion struct { 49 Url string `json:"url,omitempty"` 50 Version string `json:"version,omitempty"` 51 } 52 53 // CookbookMeta represents a Golang version of cookbook metadata 54 type CookbookMeta struct { 55 Name string `json:"name,omitempty"` 56 Version string `json:"version,omitempty"` 57 Description string `json:"description,omitempty"` 58 LongDescription string `json:"long_description,omitempty"` 59 Maintainer string `json:"maintainer,omitempty"` 60 MaintainerEmail string `json:"maintainer_email,omitempty"` 61 License string `json:"license,omitempty"` 62 Platforms map[string]interface{} `json:"platforms,omitempty"` 63 Depends map[string]string `json:"dependencies,omitempty"` 64 Reccomends map[string]string `json:"recommendations,omitempty"` 65 Suggests map[string]string `json:"suggestions,omitempty"` 66 Conflicts map[string]string `json:"conflicting,omitempty"` 67 Provides map[string]interface{} `json:"providing,omitempty"` 68 Replaces map[string]string `json:"replacing,omitempty"` 69 Attributes map[string]interface{} `json:"attributes,omitempty"` // this has a format as well that could be typed, but blargh https://github.com/lob/chef/blob/master/cookbooks/apache2/metadata.json 70 Groupings map[string]interface{} `json:"groupings,omitempty"` // never actually seen this used.. looks like it should be map[string]map[string]string, but not sure http://docs.opscode.com/essentials_cookbook_metadata.html 71 Recipes map[string]string `json:"recipes,omitempty"` 72 SourceUrl string `json:"source_url"` 73 IssueUrl string `json:"issues_url"` 74 ChefVersion string 75 OhaiVersion string 76 Gems [][]string `json:"gems"` 77 EagerLoadLibraries bool `json:"eager_load_libraries"` 78 Privacy bool `json:"privacy"` 79 } 80 81 // CookbookAccess represents the permissions on a Cookbook 82 type CookbookAccess struct { 83 Read bool `json:"read,omitempty"` 84 Create bool `json:"create,omitempty"` 85 Grant bool `json:"grant,omitempty"` 86 Update bool `json:"update,omitempty"` 87 Delete bool `json:"delete,omitempty"` 88 } 89 90 // Cookbook represents the native Go version of the deserialized api cookbook 91 type Cookbook struct { 92 CookbookName string `json:"cookbook_name"` 93 Name string `json:"name"` 94 Version string `json:"version,omitempty"` 95 ChefType string `json:"chef_type,omitempty"` 96 Frozen bool `json:"frozen?,omitempty"` 97 JsonClass string `json:"json_class,omitempty"` 98 Files []CookbookItem `json:"files,omitempty"` 99 Templates []CookbookItem `json:"templates,omitempty"` 100 Attributes []CookbookItem `json:"attributes,omitempty"` 101 Recipes []CookbookItem `json:"recipes,omitempty"` 102 Definitions []CookbookItem `json:"definitions,omitempty"` 103 Libraries []CookbookItem `json:"libraries,omitempty"` 104 Providers []CookbookItem `json:"providers,omitempty"` 105 Resources []CookbookItem `json:"resources,omitempty"` 106 RootFiles []CookbookItem `json:"root_files,omitempty"` 107 Metadata CookbookMeta `json:"metadata,omitempty"` 108 Access CookbookAccess `json:"access,omitempty"` 109 } 110 111 // String makes CookbookListResult implement the string result 112 func (c CookbookListResult) String() (out string) { 113 for k, v := range c { 114 out += fmt.Sprintf("%s => %s\n", k, v.Url) 115 for _, i := range v.Versions { 116 out += fmt.Sprintf(" * %s\n", i.Version) 117 } 118 } 119 return out 120 } 121 122 // versionParams assembles a querystring for the chef api's num_versions 123 // This is used to restrict the number of versions returned in the reponse 124 func versionParams(path, numVersions string) string { 125 if numVersions == "0" { 126 numVersions = "all" 127 } 128 129 // need to optionally add numVersion args to the request 130 if len(numVersions) > 0 { 131 path = fmt.Sprintf("%s?num_versions=%s", path, numVersions) 132 } 133 return path 134 } 135 136 // Get retruns a CookbookVersion for a specific cookbook 137 // 138 // GET /cookbooks/name 139 func (c *CookbookService) Get(name string) (data CookbookVersion, err error) { 140 path := fmt.Sprintf("cookbooks/%s", name) 141 err = c.client.magicRequestDecoder("GET", path, nil, &data) 142 return 143 } 144 145 // GetAvailable returns the versions of a coookbook available on a server 146 func (c *CookbookService) GetAvailableVersions(name, numVersions string) (data CookbookListResult, err error) { 147 path := versionParams(fmt.Sprintf("cookbooks/%s", name), numVersions) 148 err = c.client.magicRequestDecoder("GET", path, nil, &data) 149 return 150 } 151 152 // GetVersion fetches a specific version of a cookbooks data from the server api 153 // 154 // GET /cookbook/foo/1.2.3 155 // GET /cookbook/foo/_latest 156 // Chef API docs: https://docs.chef.io/api_chef_server.html#cookbooks-name-version 157 func (c *CookbookService) GetVersion(name, version string) (data Cookbook, err error) { 158 url := fmt.Sprintf("cookbooks/%s/%s", name, version) 159 err = c.client.magicRequestDecoder("GET", url, nil, &data) 160 return 161 } 162 163 // ListVersions lists the cookbooks available on the server limited to numVersions 164 // 165 // Chef API docs: https://docs.chef.io/api_chef_server.html#cookbooks-name 166 func (c *CookbookService) ListAvailableVersions(numVersions string) (data CookbookListResult, err error) { 167 path := versionParams("cookbooks", numVersions) 168 err = c.client.magicRequestDecoder("GET", path, nil, &data) 169 return 170 } 171 172 // ListAllRecipes lists the names of all recipes in the most recent cookbook versions 173 // 174 // Chef API docs: https://docs.chef.io/api_chef_server.html#cookbooks-recipes 175 func (c *CookbookService) ListAllRecipes() (data CookbookRecipesResult, err error) { 176 path := "cookbooks/_recipes" 177 err = c.client.magicRequestDecoder("GET", path, nil, &data) 178 return 179 } 180 181 // List returns a CookbookListResult with the latest versions of cookbooks available on the server 182 func (c *CookbookService) List() (CookbookListResult, error) { 183 return c.ListAvailableVersions("") 184 } 185 186 // DeleteVersion removes a version of a cook from a server 187 func (c *CookbookService) Delete(name, version string) (err error) { 188 path := fmt.Sprintf("cookbooks/%s/%s", name, version) 189 err = c.client.magicRequestDecoder("DELETE", path, nil, nil) 190 return 191 } 192 func ReadMetaData(path string) (m CookbookMeta, err error) { 193 fileName := filepath.Join(path, metaJsonName) 194 jsonType := true 195 if !isFileExists(fileName) { 196 jsonType = false 197 fileName = filepath.Join(path, metaRbName) 198 199 } 200 file, err := os.ReadFile(fileName) 201 if err != nil { 202 fmt.Println(err.Error()) 203 os.Exit(1) 204 } 205 if jsonType { 206 return NewMetaDataFromJson(file) 207 } else { 208 return NewMetaData(string(file)) 209 } 210 211 } 212 func trimQuotes(s string) string { 213 if len(s) >= 2 { 214 if c := s[len(s)-1]; s[0] == c && (c == '"' || c == '\'') { 215 return s[1 : len(s)-1] 216 } 217 } 218 return s 219 } 220 func getKeyValue(str string) (string, []string) { 221 c := strings.Split(str, " ") 222 if len(c) == 0 { 223 return "", nil 224 } 225 return strings.TrimSpace(c[0]), c[1:] 226 } 227 func isFileExists(name string) bool { 228 if _, err := os.Stat(name); errors.Is(err, os.ErrNotExist) { 229 return false 230 } 231 return true 232 } 233 234 func clearWhiteSpace(s []string) (result []string) { 235 for _, i := range s { 236 if len(i) > 0 { 237 result = append(result, i) 238 } 239 } 240 return result 241 } 242 243 func NewMetaData(data string) (m CookbookMeta, err error) { 244 linesData := strings.Split(data, "\n") 245 if len(linesData) < 3 { 246 return m, errors.New("not much info") 247 } 248 m.Depends = make(map[string]string, 1) 249 m.Platforms = make(map[string]interface{}, 1) 250 for _, i := range linesData { 251 key, value := getKeyValue(strings.TrimSpace(i)) 252 if fn, ok := metaRegistry[key]; ok { 253 err = fn(value, &m) 254 if err != nil { 255 return 256 } 257 } 258 } 259 return m, err 260 } 261 262 func NewMetaDataFromJson(data []byte) (m CookbookMeta, err error) { 263 err = json.Unmarshal(data, &m) 264 return m, err 265 } 266 267 func StringParserForMeta(s []string) string { 268 str := strings.Join(s, " ") 269 return trimQuotes(strings.TrimSpace(str)) 270 } 271 func metaNameParser(s []string, m *CookbookMeta) error { 272 m.Name = StringParserForMeta(s) 273 return nil 274 } 275 func metaMaintainerParser(s []string, m *CookbookMeta) error { 276 m.Maintainer = StringParserForMeta(s) 277 return nil 278 } 279 func metaMaintainerMailParser(s []string, m *CookbookMeta) error { 280 m.MaintainerEmail = StringParserForMeta(s) 281 return nil 282 } 283 func metaLicenseParser(s []string, m *CookbookMeta) error { 284 m.License = StringParserForMeta(s) 285 return nil 286 } 287 func metaDescriptionParser(s []string, m *CookbookMeta) error { 288 m.Description = StringParserForMeta(s) 289 return nil 290 } 291 func metaLongDescriptionParser(s []string, m *CookbookMeta) error { 292 m.LongDescription = StringParserForMeta(s) 293 return nil 294 } 295 func metaIssueUrlParser(s []string, m *CookbookMeta) error { 296 m.IssueUrl = StringParserForMeta(s) 297 return nil 298 } 299 func metaSourceUrlParser(s []string, m *CookbookMeta) error { 300 m.SourceUrl = StringParserForMeta(s) 301 return nil 302 } 303 func metaGemParser(s []string, m *CookbookMeta) error { 304 m.Gems = append(m.Gems, s) 305 return nil 306 } 307 308 func metaVersionParser(s []string, m *CookbookMeta) error { 309 m.Version = StringParserForMeta(s) 310 return nil 311 } 312 func metaOhaiVersionParser(s []string, m *CookbookMeta) error { 313 m.OhaiVersion = StringParserForMeta(s) 314 return nil 315 } 316 func metaChefVersionParser(s []string, m *CookbookMeta) error { 317 m.ChefVersion = StringParserForMeta(s) 318 return nil 319 } 320 func metaPrivacyParser(s []string, m *CookbookMeta) error { 321 if s[0] == "true" { 322 m.Privacy = true 323 } 324 return nil 325 } 326 func metaSupportsParser(s []string, m *CookbookMeta) error { 327 s = clearWhiteSpace(s) 328 switch len(s) { 329 case 1: 330 if s[0] != "os" { 331 m.Platforms[strings.TrimSpace(s[0])] = ">= 0.0.0" 332 } 333 case 2: 334 m.Platforms[strings.TrimSpace(s[0])] = s[1] 335 case 3: 336 v := trimQuotes(s[1] + " " + s[2]) 337 m.Platforms[strings.TrimSpace(s[0])] = v 338 339 } 340 if len(s) > 3 { 341 return errors.New(`<<~OBSOLETED 342 The dependency specification syntax you are using is no longer valid. You may not 343 specify more than one version constraint for a particular cookbook. 344 Consult https://docs.chef.io/config_rb_metadata/ for the updated syntax.`) 345 } 346 return nil 347 } 348 func metaDependsParser(s []string, m *CookbookMeta) error { 349 s = clearWhiteSpace(s) 350 switch len(s) { 351 case 1: 352 m.Depends[strings.TrimSpace(s[0])] = ">= 0.0.0" 353 case 2: 354 m.Depends[strings.TrimSpace(s[0])] = s[1] 355 356 case 3: 357 v := trimQuotes(s[1] + " " + s[2]) 358 m.Depends[strings.TrimSpace(s[0])] = v 359 360 } 361 if len(s) > 3 { 362 return errors.New(`<<~OBSOLETED 363 The dependency specification syntax you are using is no longer valid. You may not 364 specify more than one version constraint for a particular cookbook. 365 Consult https://docs.chef.io/config_rb_metadata/ for the updated syntax.`) 366 } 367 return nil 368 } 369 370 func metaSupportsRubyParser(s []string, m *CookbookMeta) error { 371 if len(s) > 1 { 372 for _, i := range s { 373 switch i { 374 case ").each": 375 continue 376 case "do": 377 continue 378 case "|os|": 379 continue 380 default: 381 m.Platforms[strings.TrimSpace(s[0])] = ">= 0.0.0" 382 } 383 } 384 } 385 return nil 386 } 387 func init() { 388 metaRegistry = make(map[string]metaFunc, 15) 389 metaRegistry["name"] = metaNameParser 390 metaRegistry["maintainer"] = metaMaintainerParser 391 metaRegistry["maintainer_email"] = metaMaintainerMailParser 392 metaRegistry["license"] = metaLicenseParser 393 metaRegistry["description"] = metaDescriptionParser 394 metaRegistry["long_description"] = metaLongDescriptionParser 395 metaRegistry["source_url"] = metaSourceUrlParser 396 metaRegistry["issues_url"] = metaIssueUrlParser 397 metaRegistry["platforms"] = metaSupportsParser 398 metaRegistry["supports"] = metaSupportsParser 399 metaRegistry["%w("] = metaSupportsRubyParser 400 metaRegistry["privacy"] = metaPrivacyParser 401 metaRegistry["depends"] = metaDependsParser 402 metaRegistry["version"] = metaVersionParser 403 metaRegistry["chef_version"] = metaChefVersionParser 404 metaRegistry["ohai_version"] = metaOhaiVersionParser 405 metaRegistry["gem"] = metaGemParser 406 407 }