Ejemplo n.º 1
0
func extractVerNums(cbVersion string) (maj, min, patch int64, err util.Gerror) {
	if _, err = util.ValidateAsVersion(cbVersion); err != nil {
		return 0, 0, 0, err
	}
	nums := strings.Split(cbVersion, ".")
	if len(nums) < 2 && len(nums) > 3 {
		err = util.Errorf("incorrect number of numbers in version string '%s': %d", cbVersion, len(nums))
		return 0, 0, 0, err
	}
	var vt int64
	var nerr error
	vt, nerr = strconv.ParseInt(nums[0], 0, 64)
	if nerr != nil {
		err = util.Errorf(nerr.Error())
		return 0, 0, 0, err
	}
	maj = vt
	vt, nerr = strconv.ParseInt(nums[1], 0, 64)
	if nerr != nil {
		err = util.Errorf(nerr.Error())
		return 0, 0, 0, err
	}
	min = vt
	if len(nums) == 3 {
		vt, nerr = strconv.ParseInt(nums[2], 0, 64)
		if nerr != nil {
			err = util.Errorf(nerr.Error())
			return 0, 0, 0, err
		}
		patch = vt
	} else {
		patch = 0
	}
	return maj, min, patch, nil
}
Ejemplo n.º 2
0
// UpdateVersion updates a specific version of a cookbook.
func (cbv *CookbookVersion) UpdateVersion(cbvData map[string]interface{}, force string) util.Gerror {
	/* Allow force to update a frozen cookbook */
	if cbv.IsFrozen == true && force != "true" {
		err := util.Errorf("The cookbook %s at version %s is frozen. Use the 'force' option to override.", cbv.CookbookName, cbv.Version)
		err.SetStatus(http.StatusConflict)
		return err
	}

	fhashes := cbv.fileHashes()

	_, nerr := util.ValidateAsString(cbvData["cookbook_name"])
	if nerr != nil {
		if nerr.Error() == "Field 'name' missing" {
			nerr = util.Errorf("Field 'cookbook_name' missing")
		} else {
			nerr = util.Errorf("Field 'cookbook_name' invalid")
		}
		return nerr
	}

	/* Validation, validation, all is validation. */
	validElements := []string{"cookbook_name", "name", "version", "json_class", "chef_type", "definitions", "libraries", "attributes", "recipes", "providers", "resources", "templates", "root_files", "files", "frozen?", "metadata", "force"}
ValidElem:
	for k := range cbvData {
		for _, i := range validElements {
			if k == i {
				continue ValidElem
			}
		}
		err := util.Errorf("Invalid key %s in request body", k)
		return err
	}

	var verr util.Gerror
	cbvData["chef_type"], verr = util.ValidateAsFieldString(cbvData["chef_type"])
	if verr != nil {
		if verr.Error() == "Field 'name' nil" {
			cbvData["chef_type"] = cbv.ChefType
		} else {
			verr = util.Errorf("Field 'chef_type' invalid")
			return verr
		}
	} else {
		// Wait, what was I doing here?
		// if !util.ValidateEnvName(cbvData["chef_type"].(string)) {
		if cbvData["chef_type"].(string) != "cookbook_version" {
			verr = util.Errorf("Field 'chef_type' invalid")
			return verr
		}
	}

	cbvData["json_class"], verr = util.ValidateAsFieldString(cbvData["json_class"])
	if verr != nil {
		if verr.Error() == "Field 'name' nil" {
			cbvData["json_class"] = cbv.JSONClass
		} else {
			verr = util.Errorf("Field 'json_class' invalid")
			return verr
		}
	} else {
		if cbvData["json_class"].(string) != "Chef::CookbookVersion" {
			verr = util.Errorf("Field 'json_class' invalid")
			return verr
		}
	}

	cbvData["version"], verr = util.ValidateAsVersion(cbvData["version"])
	if verr != nil {
		verr = util.Errorf("Field 'version' invalid")
		return verr
	}
	if cbvData["version"].(string) == "0.0.0" && cbv.Version != "" {
		cbvData["version"] = cbv.Version
	}

	divs := []string{"definitions", "libraries", "attributes", "recipes", "providers", "resources", "templates", "root_files", "files"}
	for _, d := range divs {
		cbvData[d], verr = util.ValidateCookbookDivision(d, cbvData[d])
		if verr != nil {
			return verr
		}
	}
	cbvData["metadata"], verr = util.ValidateCookbookMetadata(cbvData["metadata"])
	if verr != nil {
		return verr
	}

	cbvData["frozen?"], verr = util.ValidateAsBool(cbvData["frozen?"])
	if verr != nil {
		return verr
	}

	/* Basic sanity checking */
	if cbvData["cookbook_name"].(string) != cbv.CookbookName {
		err := util.Errorf("Field 'cookbook_name' invalid")
		return err
	}
	if cbvData["name"].(string) != cbv.Name {
		err := util.Errorf("Field 'name' invalid")
		return err
	}
	if cbvData["version"].(string) != cbv.Version && cbvData["version"] != "0.0.0" {
		err := util.Errorf("Field 'version' invalid")
		return err
	}

	/* Update the data */
	/* With these next two, should we test for existence before setting? */
	cbv.ChefType = cbvData["chef_type"].(string)
	cbv.JSONClass = cbvData["json_class"].(string)
	cbv.Definitions = convertToCookbookDiv(cbvData["definitions"])
	cbv.Libraries = convertToCookbookDiv(cbvData["libraries"])
	cbv.Attributes = convertToCookbookDiv(cbvData["attributes"])
	cbv.Recipes = cbvData["recipes"].([]map[string]interface{})
	cbv.Providers = convertToCookbookDiv(cbvData["providers"])
	cbv.Resources = convertToCookbookDiv(cbvData["resources"])
	cbv.Templates = convertToCookbookDiv(cbvData["templates"])
	cbv.RootFiles = convertToCookbookDiv(cbvData["root_files"])
	cbv.Files = convertToCookbookDiv(cbvData["files"])
	if cbv.IsFrozen != true {
		cbv.IsFrozen = cbvData["frozen?"].(bool)
	}
	cbv.Metadata = cbvData["metadata"].(map[string]interface{})

	/* If we're using SQL, update this version in the DB. */
	if config.UsingDB() {
		if err := cbv.updateCookbookVersionSQL(); err != nil {
			return err
		}
	}

	/* Clean cookbook hashes */
	if len(fhashes) > 0 {
		// Get our parent. Bravely assuming that if it exists we exist.
		cbook, _ := Get(cbv.CookbookName)
		cbook.Versions[cbv.Version] = cbv
		cbook.deleteHashes(fhashes)
	}

	return nil
}
Ejemplo n.º 3
0
func cookbookHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	pathArray := splitPath(r.URL.Path)
	cookbookResponse := make(map[string]interface{})

	opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID"))
	if oerr != nil {
		jsonErrorReport(w, r, oerr.Error(), oerr.Status())
		return
	}

	var numResults string
	r.ParseForm()
	if nrs, found := r.Form["num_versions"]; found {
		if len(nrs) < 0 {
			jsonErrorReport(w, r, "invalid num_versions", http.StatusBadRequest)
			return
		}
		numResults = nrs[0]
		err := util.ValidateNumVersions(numResults)
		if err != nil {
			jsonErrorReport(w, r, err.Error(), err.Status())
			return
		}
	}
	force := ""
	if f, fok := r.Form["force"]; fok {
		if len(f) > 0 {
			force = f[0]
		}
	}

	pathArrayLen := len(pathArray)

	/* 1 and 2 length path arrays only support GET */
	if pathArrayLen < 3 && r.Method != "GET" {
		jsonErrorReport(w, r, "Bad request.", http.StatusMethodNotAllowed)
		return
	} else if pathArrayLen < 3 && opUser.IsValidator() {
		jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden)
		return
	}

	/* chef-pedant is happier when checking if a validator can do something
	 * surprisingly late in the game. It wants the perm checks to be
	 * checked after the method for the end point is checked out as
	 * something it's going to handle, so, for instance, issuing a DELETE
	 * against an endpoint where that isn't allowed should respond with a
	 * 405, rather than a 403, which also makes sense in areas where
	 * validators shouldn't be able to do anything. *shrugs*
	 */

	if pathArrayLen == 1 || (pathArrayLen == 2 && pathArray[1] == "") {
		/* list all cookbooks */
		cookbookResponse = cookbook.CookbookLister(numResults)
	} else if pathArrayLen == 2 {
		/* info about a cookbook and all its versions */
		cookbookName := pathArray[1]
		/* Undocumented behavior - a cookbook name of _latest gets a
		 * list of the latest versions of all the cookbooks, and _recipe
		 * gets the recipes of the latest cookbooks. */
		if cookbookName == "_latest" {
			cookbookResponse = cookbook.CookbookLatest()
		} else if cookbookName == "_recipes" {
			rlist, nerr := cookbook.CookbookRecipes()
			if nerr != nil {
				jsonErrorReport(w, r, nerr.Error(), nerr.Status())
				return
			}
			enc := json.NewEncoder(w)
			if err := enc.Encode(&rlist); err != nil {
				jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError)
			}
			return
		} else {
			cb, err := cookbook.Get(cookbookName)
			if err != nil {
				jsonErrorReport(w, r, err.Error(), http.StatusNotFound)
				return
			}
			/* Strange thing here. The API docs say if num_versions
			 * is not specified to return one cookbook, yet the
			 * spec indicates that if it's not set that all
			 * cookbooks should be returned. Most places *except
			 * here* that's the case, so it can't be changed in
			 * infoHashBase. Explicitly set numResults to all
			 * here. */
			if numResults == "" {
				numResults = "all"
			}
			cookbookResponse[cookbookName] = cb.InfoHash(numResults)
		}
	} else if pathArrayLen == 3 {
		/* get information about or manipulate a specific cookbook
		 * version */
		cookbookName := pathArray[1]
		var cookbookVersion string
		var vererr util.Gerror
		opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID"))
		if oerr != nil {
			jsonErrorReport(w, r, oerr.Error(), oerr.Status())
			return
		}
		if r.Method == "GET" && pathArray[2] == "_latest" { // might be other special vers
			cookbookVersion = pathArray[2]
		} else {
			cookbookVersion, vererr = util.ValidateAsVersion(pathArray[2])
			if vererr != nil {
				vererr := util.Errorf("Invalid cookbook version '%s'.", pathArray[2])
				jsonErrorReport(w, r, vererr.Error(), vererr.Status())
				return
			}
		}
		switch r.Method {
		case "DELETE", "GET":
			if opUser.IsValidator() {
				jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden)
				return
			}
			cb, err := cookbook.Get(cookbookName)
			if err != nil {
				if err.Status() == http.StatusNotFound {
					msg := fmt.Sprintf("Cannot find a cookbook named %s with version %s", cookbookName, cookbookVersion)
					jsonErrorReport(w, r, msg, err.Status())
				} else {
					jsonErrorReport(w, r, err.Error(), err.Status())
				}
				return
			}
			cbv, err := cb.GetVersion(cookbookVersion)
			if err != nil {
				jsonErrorReport(w, r, err.Error(), http.StatusNotFound)
				return
			}
			if r.Method == "DELETE" {
				if !opUser.IsAdmin() {
					jsonErrorReport(w, r, "You are not allowed to take this action.", http.StatusForbidden)
					return
				}
				err := cb.DeleteVersion(cookbookVersion)
				if err != nil {
					jsonErrorReport(w, r, err.Error(), err.Status())
					return
				}
				if lerr := loginfo.LogEvent(opUser, cbv, "delete"); lerr != nil {
					jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError)
					return
				}
				/* If all versions are gone, remove the
				 * cookbook - seems to be the desired
				 * behavior. */
				if cb.NumVersions() == 0 {
					if cerr := cb.Delete(); cerr != nil {
						jsonErrorReport(w, r, cerr.Error(), http.StatusInternalServerError)
						return
					}
				}
			} else {
				/* Special JSON rendition of the
				 * cookbook with some but not all of
				 * the fields. */
				cookbookResponse = cbv.ToJSON(r.Method)
				/* Sometimes, but not always, chef needs
				 * empty slices of maps for these
				 * values. Arrrgh. */
				/* Doing it this way is absolutely
				 * insane. However, webui really wants
				 * this information, while chef-pedant
				 * absolutely does NOT want it there.
				 * knife seems happy without it as well.
				 * Until such time that this gets
				 * resolved in a non-crazy manner, for
				 * this only send that info back if it's
				 * a webui request. */
				if rs := r.Header.Get("X-Ops-Request-Source"); rs == "web" {
					chkDiv := []string{"definitions", "libraries", "attributes", "providers", "resources", "templates", "root_files", "files"}
					for _, cd := range chkDiv {
						if cookbookResponse[cd] == nil {
							cookbookResponse[cd] = make([]map[string]interface{}, 0)
						}
					}
				}
			}
		case "PUT":
			if !opUser.IsAdmin() {
				jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden)
				return
			}
			cbvData, jerr := parseObjJSON(r.Body)
			if jerr != nil {
				jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest)
				return
			}
			/* First, see if the cookbook already exists, &
			 * if not create it. Second, see if this
			 * specific version of the cookbook exists. If
			 * so, update it, otherwise, create it and set
			 * the latest version as needed. */
			cb, err := cookbook.Get(cookbookName)
			if err != nil {
				cb, err = cookbook.New(cookbookName)
				if err != nil {
					jsonErrorReport(w, r, err.Error(), err.Status())
					return
				}
				/* save it so we get the id with mysql
				 * for creating versions & such */
				serr := cb.Save()
				if serr != nil {
					jsonErrorReport(w, r, serr.Error(), http.StatusInternalServerError)
					return
				}
				if lerr := loginfo.LogEvent(opUser, cb, "create"); lerr != nil {
					jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError)
					return
				}
			}
			cbv, err := cb.GetVersion(cookbookVersion)

			/* Does the cookbook_name in the URL and what's
			 * in the body match? */
			switch t := cbvData["cookbook_name"].(type) {
			case string:
				/* Only send this particular
				 * error if the cookbook version
				 * hasn't been created yet.
				 * Instead we want a slightly
				 * different version later. */
				if t != cookbookName && cbv == nil {
					terr := util.Errorf("Field 'name' invalid")
					jsonErrorReport(w, r, terr.Error(), terr.Status())
					return
				}
			default:
				// rather unlikely, I think, to
				// be able to get here past the
				// cookbook get. Punk out and
				// don't do anything

			}
			if err != nil {
				var nerr util.Gerror
				cbv, nerr = cb.NewVersion(cookbookVersion, cbvData)
				if nerr != nil {
					// If the new version failed to
					// take, and there aren't any
					// other versions of the cookbook
					// it needs to be deleted.
					if cb.NumVersions() == 0 {
						cb.Delete()
					}
					jsonErrorReport(w, r, nerr.Error(), nerr.Status())
					return
				}
				if lerr := loginfo.LogEvent(opUser, cbv, "create"); lerr != nil {
					jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError)
					return
				}
				w.WriteHeader(http.StatusCreated)
			} else {
				err := cbv.UpdateVersion(cbvData, force)
				if err != nil {
					jsonErrorReport(w, r, err.Error(), err.Status())
					return
				}
				gerr := cb.Save()
				if gerr != nil {
					jsonErrorReport(w, r, gerr.Error(), http.StatusInternalServerError)
					return
				}
				if lerr := loginfo.LogEvent(opUser, cbv, "modify"); lerr != nil {
					jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError)
					return
				}
			}
			/* API docs are wrong. The docs claim that this
			 * should have no response body, but in fact it
			 * wants some (not all) of the cookbook version
			 * data. */
			cookbookResponse = cbv.ToJSON(r.Method)
		default:
			jsonErrorReport(w, r, "Unrecognized method", http.StatusMethodNotAllowed)
			return
		}
	} else {
		/* Say what? Bad request. */
		jsonErrorReport(w, r, "Bad request", http.StatusBadRequest)
		return
	}

	enc := json.NewEncoder(w)
	if err := enc.Encode(&cookbookResponse); err != nil {
		jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError)
	}
}
Ejemplo n.º 4
0
// UpdateFromJSON updates an existing environment from JSON uploaded to the
// server.
func (e *ChefEnvironment) UpdateFromJSON(jsonEnv map[string]interface{}) util.Gerror {
	if e.Name != jsonEnv["name"].(string) {
		err := util.Errorf("Environment name %s and %s from JSON do not match", e.Name, jsonEnv["name"].(string))
		return err
	} else if e.Name == "_default" {
		err := util.Errorf("The '_default' environment cannot be modified.")
		err.SetStatus(http.StatusMethodNotAllowed)
		return err
	}

	/* Validations */
	validElements := []string{"name", "chef_type", "json_class", "description", "default_attributes", "override_attributes", "cookbook_versions"}
ValidElem:
	for k := range jsonEnv {
		for _, i := range validElements {
			if k == i {
				continue ValidElem
			}
		}
		err := util.Errorf("Invalid key %s in request body", k)
		return err
	}

	var verr util.Gerror

	attrs := []string{"default_attributes", "override_attributes"}
	for _, a := range attrs {
		jsonEnv[a], verr = util.ValidateAttributes(a, jsonEnv[a])
		if verr != nil {
			return verr
		}
	}

	jsonEnv["json_class"], verr = util.ValidateAsFieldString(jsonEnv["json_class"])
	if verr != nil {
		if verr.Error() == "Field 'name' nil" {
			jsonEnv["json_class"] = e.JSONClass
		} else {
			return verr
		}
	} else {
		if jsonEnv["json_class"].(string) != "Chef::Environment" {
			verr = util.Errorf("Field 'json_class' invalid")
			return verr
		}
	}

	jsonEnv["chef_type"], verr = util.ValidateAsFieldString(jsonEnv["chef_type"])
	if verr != nil {
		if verr.Error() == "Field 'name' nil" {
			jsonEnv["chef_type"] = e.ChefType
		} else {
			return verr
		}
	} else {
		if jsonEnv["chef_type"].(string) != "environment" {
			verr = util.Errorf("Field 'chef_type' invalid")
			return verr
		}
	}

	jsonEnv["cookbook_versions"], verr = util.ValidateAttributes("cookbook_versions", jsonEnv["cookbook_versions"])
	if verr != nil {
		return verr
	}
	for k, v := range jsonEnv["cookbook_versions"].(map[string]interface{}) {
		if !util.ValidateEnvName(k) || k == "" {
			merr := util.Errorf("Cookbook name %s invalid", k)
			merr.SetStatus(http.StatusBadRequest)
			return merr
		}

		if v == nil {
			verr = util.Errorf("Invalid version number")
			return verr
		}
		_, verr = util.ValidateAsConstraint(v)
		if verr != nil {
			/* try validating as a version */
			v, verr = util.ValidateAsVersion(v)
			if verr != nil {
				return verr
			}
		}
	}

	jsonEnv["description"], verr = util.ValidateAsString(jsonEnv["description"])
	if verr != nil {
		if verr.Error() == "Field 'name' missing" {
			jsonEnv["description"] = ""
		} else {
			return verr
		}
	}

	e.ChefType = jsonEnv["chef_type"].(string)
	e.JSONClass = jsonEnv["json_class"].(string)
	e.Description = jsonEnv["description"].(string)
	e.Default = jsonEnv["default_attributes"].(map[string]interface{})
	e.Override = jsonEnv["override_attributes"].(map[string]interface{})
	/* clear out, then loop over the cookbook versions */
	e.CookbookVersions = make(map[string]string, len(jsonEnv["cookbook_versions"].(map[string]interface{})))
	for c, v := range jsonEnv["cookbook_versions"].(map[string]interface{}) {
		e.CookbookVersions[c] = v.(string)
	}

	return nil
}