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