You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

chef.go 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package chef
  4. import (
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "net/url"
  10. "sort"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/models/db"
  14. packages_model "code.gitea.io/gitea/models/packages"
  15. "code.gitea.io/gitea/modules/context"
  16. packages_module "code.gitea.io/gitea/modules/packages"
  17. chef_module "code.gitea.io/gitea/modules/packages/chef"
  18. "code.gitea.io/gitea/modules/setting"
  19. "code.gitea.io/gitea/modules/util"
  20. "code.gitea.io/gitea/routers/api/packages/helper"
  21. packages_service "code.gitea.io/gitea/services/packages"
  22. )
  23. func apiError(ctx *context.Context, status int, obj any) {
  24. type Error struct {
  25. ErrorMessages []string `json:"error_messages"`
  26. }
  27. helper.LogAndProcessError(ctx, status, obj, func(message string) {
  28. ctx.JSON(status, Error{
  29. ErrorMessages: []string{message},
  30. })
  31. })
  32. }
  33. func PackagesUniverse(ctx *context.Context) {
  34. pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
  35. OwnerID: ctx.Package.Owner.ID,
  36. Type: packages_model.TypeChef,
  37. IsInternal: util.OptionalBoolFalse,
  38. })
  39. if err != nil {
  40. apiError(ctx, http.StatusInternalServerError, err)
  41. return
  42. }
  43. pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
  44. if err != nil {
  45. apiError(ctx, http.StatusInternalServerError, err)
  46. return
  47. }
  48. type VersionInfo struct {
  49. LocationType string `json:"location_type"`
  50. LocationPath string `json:"location_path"`
  51. DownloadURL string `json:"download_url"`
  52. Dependencies map[string]string `json:"dependencies"`
  53. }
  54. baseURL := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/chef/api/v1"
  55. universe := make(map[string]map[string]*VersionInfo)
  56. for _, pd := range pds {
  57. if _, ok := universe[pd.Package.Name]; !ok {
  58. universe[pd.Package.Name] = make(map[string]*VersionInfo)
  59. }
  60. universe[pd.Package.Name][pd.Version.Version] = &VersionInfo{
  61. LocationType: "opscode",
  62. LocationPath: baseURL,
  63. DownloadURL: fmt.Sprintf("%s/cookbooks/%s/versions/%s/download", baseURL, url.PathEscape(pd.Package.Name), pd.Version.Version),
  64. Dependencies: pd.Metadata.(*chef_module.Metadata).Dependencies,
  65. }
  66. }
  67. ctx.JSON(http.StatusOK, universe)
  68. }
  69. // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_list.rb
  70. // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_search.rb
  71. func EnumeratePackages(ctx *context.Context) {
  72. opts := &packages_model.PackageSearchOptions{
  73. OwnerID: ctx.Package.Owner.ID,
  74. Type: packages_model.TypeChef,
  75. Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
  76. IsInternal: util.OptionalBoolFalse,
  77. Paginator: db.NewAbsoluteListOptions(
  78. ctx.FormInt("start"),
  79. ctx.FormInt("items"),
  80. ),
  81. }
  82. switch strings.ToLower(ctx.FormTrim("order")) {
  83. case "recently_updated", "recently_added":
  84. opts.Sort = packages_model.SortCreatedDesc
  85. default:
  86. opts.Sort = packages_model.SortNameAsc
  87. }
  88. pvs, total, err := packages_model.SearchLatestVersions(ctx, opts)
  89. if err != nil {
  90. apiError(ctx, http.StatusInternalServerError, err)
  91. return
  92. }
  93. pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
  94. if err != nil {
  95. apiError(ctx, http.StatusInternalServerError, err)
  96. return
  97. }
  98. type Item struct {
  99. CookbookName string `json:"cookbook_name"`
  100. CookbookMaintainer string `json:"cookbook_maintainer"`
  101. CookbookDescription string `json:"cookbook_description"`
  102. Cookbook string `json:"cookbook"`
  103. }
  104. type Result struct {
  105. Start int `json:"start"`
  106. Total int `json:"total"`
  107. Items []*Item `json:"items"`
  108. }
  109. baseURL := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/chef/api/v1/cookbooks/"
  110. items := make([]*Item, 0, len(pds))
  111. for _, pd := range pds {
  112. metadata := pd.Metadata.(*chef_module.Metadata)
  113. items = append(items, &Item{
  114. CookbookName: pd.Package.Name,
  115. CookbookMaintainer: metadata.Author,
  116. CookbookDescription: metadata.Description,
  117. Cookbook: baseURL + url.PathEscape(pd.Package.Name),
  118. })
  119. }
  120. skip, _ := opts.Paginator.GetSkipTake()
  121. ctx.JSON(http.StatusOK, &Result{
  122. Start: skip,
  123. Total: int(total),
  124. Items: items,
  125. })
  126. }
  127. // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_show.rb
  128. func PackageMetadata(ctx *context.Context) {
  129. packageName := ctx.Params("name")
  130. pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName)
  131. if err != nil {
  132. apiError(ctx, http.StatusInternalServerError, err)
  133. return
  134. }
  135. if len(pvs) == 0 {
  136. apiError(ctx, http.StatusNotFound, nil)
  137. return
  138. }
  139. pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
  140. if err != nil {
  141. apiError(ctx, http.StatusInternalServerError, err)
  142. return
  143. }
  144. sort.Slice(pds, func(i, j int) bool {
  145. return pds[i].SemVer.LessThan(pds[j].SemVer)
  146. })
  147. type Result struct {
  148. Name string `json:"name"`
  149. Maintainer string `json:"maintainer"`
  150. Description string `json:"description"`
  151. Category string `json:"category"`
  152. LatestVersion string `json:"latest_version"`
  153. SourceURL string `json:"source_url"`
  154. CreatedAt time.Time `json:"created_at"`
  155. UpdatedAt time.Time `json:"updated_at"`
  156. Deprecated bool `json:"deprecated"`
  157. Versions []string `json:"versions"`
  158. }
  159. baseURL := fmt.Sprintf("%sapi/packages/%s/chef/api/v1/cookbooks/%s/versions/", setting.AppURL, ctx.Package.Owner.Name, url.PathEscape(packageName))
  160. versions := make([]string, 0, len(pds))
  161. for _, pd := range pds {
  162. versions = append(versions, baseURL+pd.Version.Version)
  163. }
  164. latest := pds[len(pds)-1]
  165. metadata := latest.Metadata.(*chef_module.Metadata)
  166. ctx.JSON(http.StatusOK, &Result{
  167. Name: latest.Package.Name,
  168. Maintainer: metadata.Author,
  169. Description: metadata.Description,
  170. LatestVersion: baseURL + latest.Version.Version,
  171. SourceURL: metadata.RepositoryURL,
  172. CreatedAt: latest.Version.CreatedUnix.AsLocalTime(),
  173. UpdatedAt: latest.Version.CreatedUnix.AsLocalTime(),
  174. Deprecated: false,
  175. Versions: versions,
  176. })
  177. }
  178. // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_show.rb
  179. func PackageVersionMetadata(ctx *context.Context) {
  180. packageName := ctx.Params("name")
  181. packageVersion := strings.ReplaceAll(ctx.Params("version"), "_", ".") // Chef calls this endpoint with "_" instead of "."?!
  182. pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName, packageVersion)
  183. if err != nil {
  184. if err == packages_model.ErrPackageNotExist {
  185. apiError(ctx, http.StatusNotFound, err)
  186. return
  187. }
  188. apiError(ctx, http.StatusInternalServerError, err)
  189. return
  190. }
  191. pd, err := packages_model.GetPackageDescriptor(ctx, pv)
  192. if err != nil {
  193. apiError(ctx, http.StatusInternalServerError, err)
  194. return
  195. }
  196. type Result struct {
  197. Version string `json:"version"`
  198. TarballFileSize int64 `json:"tarball_file_size"`
  199. PublishedAt time.Time `json:"published_at"`
  200. Cookbook string `json:"cookbook"`
  201. File string `json:"file"`
  202. License string `json:"license"`
  203. Dependencies map[string]string `json:"dependencies"`
  204. }
  205. baseURL := fmt.Sprintf("%sapi/packages/%s/chef/api/v1/cookbooks/%s", setting.AppURL, ctx.Package.Owner.Name, url.PathEscape(pd.Package.Name))
  206. metadata := pd.Metadata.(*chef_module.Metadata)
  207. ctx.JSON(http.StatusOK, &Result{
  208. Version: pd.Version.Version,
  209. TarballFileSize: pd.Files[0].Blob.Size,
  210. PublishedAt: pd.Version.CreatedUnix.AsLocalTime(),
  211. Cookbook: baseURL,
  212. File: fmt.Sprintf("%s/versions/%s/download", baseURL, pd.Version.Version),
  213. License: metadata.License,
  214. Dependencies: metadata.Dependencies,
  215. })
  216. }
  217. // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_share.rb
  218. func UploadPackage(ctx *context.Context) {
  219. file, _, err := ctx.Req.FormFile("tarball")
  220. if err != nil {
  221. apiError(ctx, http.StatusBadRequest, err)
  222. return
  223. }
  224. defer file.Close()
  225. buf, err := packages_module.CreateHashedBufferFromReader(file)
  226. if err != nil {
  227. apiError(ctx, http.StatusInternalServerError, err)
  228. return
  229. }
  230. defer buf.Close()
  231. pck, err := chef_module.ParsePackage(buf)
  232. if err != nil {
  233. if errors.Is(err, util.ErrInvalidArgument) {
  234. apiError(ctx, http.StatusBadRequest, err)
  235. } else {
  236. apiError(ctx, http.StatusInternalServerError, err)
  237. }
  238. return
  239. }
  240. if _, err := buf.Seek(0, io.SeekStart); err != nil {
  241. apiError(ctx, http.StatusInternalServerError, err)
  242. return
  243. }
  244. _, _, err = packages_service.CreatePackageAndAddFile(
  245. ctx,
  246. &packages_service.PackageCreationInfo{
  247. PackageInfo: packages_service.PackageInfo{
  248. Owner: ctx.Package.Owner,
  249. PackageType: packages_model.TypeChef,
  250. Name: pck.Name,
  251. Version: pck.Version,
  252. },
  253. Creator: ctx.Doer,
  254. SemverCompatible: true,
  255. Metadata: pck.Metadata,
  256. },
  257. &packages_service.PackageFileCreationInfo{
  258. PackageFileInfo: packages_service.PackageFileInfo{
  259. Filename: strings.ToLower(pck.Version + ".tar.gz"),
  260. },
  261. Creator: ctx.Doer,
  262. Data: buf,
  263. IsLead: true,
  264. },
  265. )
  266. if err != nil {
  267. switch err {
  268. case packages_model.ErrDuplicatePackageVersion:
  269. apiError(ctx, http.StatusBadRequest, err)
  270. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  271. apiError(ctx, http.StatusForbidden, err)
  272. default:
  273. apiError(ctx, http.StatusInternalServerError, err)
  274. }
  275. return
  276. }
  277. ctx.JSON(http.StatusCreated, make(map[any]any))
  278. }
  279. // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_download.rb
  280. func DownloadPackage(ctx *context.Context) {
  281. pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.Params("name"), ctx.Params("version"))
  282. if err != nil {
  283. if err == packages_model.ErrPackageNotExist {
  284. apiError(ctx, http.StatusNotFound, err)
  285. return
  286. }
  287. apiError(ctx, http.StatusInternalServerError, err)
  288. return
  289. }
  290. pd, err := packages_model.GetPackageDescriptor(ctx, pv)
  291. if err != nil {
  292. apiError(ctx, http.StatusInternalServerError, err)
  293. return
  294. }
  295. pf := pd.Files[0].File
  296. s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
  297. if err != nil {
  298. apiError(ctx, http.StatusInternalServerError, err)
  299. return
  300. }
  301. helper.ServePackageFile(ctx, s, u, pf)
  302. }
  303. // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb
  304. func DeletePackageVersion(ctx *context.Context) {
  305. packageName := ctx.Params("name")
  306. packageVersion := ctx.Params("version")
  307. err := packages_service.RemovePackageVersionByNameAndVersion(
  308. ctx,
  309. ctx.Doer,
  310. &packages_service.PackageInfo{
  311. Owner: ctx.Package.Owner,
  312. PackageType: packages_model.TypeChef,
  313. Name: packageName,
  314. Version: packageVersion,
  315. },
  316. )
  317. if err != nil {
  318. if err == packages_model.ErrPackageNotExist {
  319. apiError(ctx, http.StatusNotFound, err)
  320. } else {
  321. apiError(ctx, http.StatusInternalServerError, err)
  322. }
  323. return
  324. }
  325. ctx.Status(http.StatusOK)
  326. }
  327. // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb
  328. func DeletePackage(ctx *context.Context) {
  329. pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.Params("name"))
  330. if err != nil {
  331. apiError(ctx, http.StatusInternalServerError, err)
  332. return
  333. }
  334. if len(pvs) == 0 {
  335. apiError(ctx, http.StatusNotFound, err)
  336. return
  337. }
  338. for _, pv := range pvs {
  339. if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
  340. apiError(ctx, http.StatusInternalServerError, err)
  341. return
  342. }
  343. }
  344. ctx.Status(http.StatusOK)
  345. }