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