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 }
// 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 }
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) } }
// 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 }