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  }