// Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package chef import ( "errors" "fmt" "io" "net/http" "net/url" "sort" "strings" "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/context" packages_module "code.gitea.io/gitea/modules/packages" chef_module "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" ) func apiError(ctx *context.Context, status int, obj any) { type Error struct { ErrorMessages []string `json:"error_messages"` } helper.LogAndProcessError(ctx, status, obj, func(message string) { ctx.JSON(status, Error{ ErrorMessages: []string{message}, }) }) } func PackagesUniverse(ctx *context.Context) { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeChef, IsInternal: util.OptionalBoolFalse, }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } pds, err := packages_model.GetPackageDescriptors(ctx, pvs) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } type VersionInfo struct { LocationType string `json:"location_type"` LocationPath string `json:"location_path"` DownloadURL string `json:"download_url"` Dependencies map[string]string `json:"dependencies"` } baseURL := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/chef/api/v1" universe := make(map[string]map[string]*VersionInfo) for _, pd := range pds { if _, ok := universe[pd.Package.Name]; !ok { universe[pd.Package.Name] = make(map[string]*VersionInfo) } universe[pd.Package.Name][pd.Version.Version] = &VersionInfo{ LocationType: "opscode", LocationPath: baseURL, DownloadURL: fmt.Sprintf("%s/cookbooks/%s/versions/%s/download", baseURL, url.PathEscape(pd.Package.Name), pd.Version.Version), Dependencies: pd.Metadata.(*chef_module.Metadata).Dependencies, } } ctx.JSON(http.StatusOK, universe) } // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_list.rb // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_search.rb func EnumeratePackages(ctx *context.Context) { opts := &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeChef, Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, IsInternal: util.OptionalBoolFalse, Paginator: db.NewAbsoluteListOptions( ctx.FormInt("start"), ctx.FormInt("items"), ), } switch strings.ToLower(ctx.FormTrim("order")) { case "recently_updated", "recently_added": opts.Sort = packages_model.SortCreatedDesc default: opts.Sort = packages_model.SortNameAsc } pvs, total, err := packages_model.SearchLatestVersions(ctx, opts) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } pds, err := packages_model.GetPackageDescriptors(ctx, pvs) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } type Item struct { CookbookName string `json:"cookbook_name"` CookbookMaintainer string `json:"cookbook_maintainer"` CookbookDescription string `json:"cookbook_description"` Cookbook string `json:"cookbook"` } type Result struct { Start int `json:"start"` Total int `json:"total"` Items []*Item `json:"items"` } baseURL := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/chef/api/v1/cookbooks/" items := make([]*Item, 0, len(pds)) for _, pd := range pds { metadata := pd.Metadata.(*chef_module.Metadata) items = append(items, &Item{ CookbookName: pd.Package.Name, CookbookMaintainer: metadata.Author, CookbookDescription: metadata.Description, Cookbook: baseURL + url.PathEscape(pd.Package.Name), }) } skip, _ := opts.Paginator.GetSkipTake() ctx.JSON(http.StatusOK, &Result{ Start: skip, Total: int(total), Items: items, }) } // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_show.rb func PackageMetadata(ctx *context.Context) { packageName := ctx.Params("name") pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } if len(pvs) == 0 { apiError(ctx, http.StatusNotFound, nil) return } pds, err := packages_model.GetPackageDescriptors(ctx, pvs) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } sort.Slice(pds, func(i, j int) bool { return pds[i].SemVer.LessThan(pds[j].SemVer) }) type Result struct { Name string `json:"name"` Maintainer string `json:"maintainer"` Description string `json:"description"` Category string `json:"category"` LatestVersion string `json:"latest_version"` SourceURL string `json:"source_url"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Deprecated bool `json:"deprecated"` Versions []string `json:"versions"` } baseURL := fmt.Sprintf("%sapi/packages/%s/chef/api/v1/cookbooks/%s/versions/", setting.AppURL, ctx.Package.Owner.Name, url.PathEscape(packageName)) versions := make([]string, 0, len(pds)) for _, pd := range pds { versions = append(versions, baseURL+pd.Version.Version) } latest := pds[len(pds)-1] metadata := latest.Metadata.(*chef_module.Metadata) ctx.JSON(http.StatusOK, &Result{ Name: latest.Package.Name, Maintainer: metadata.Author, Description: metadata.Description, LatestVersion: baseURL + latest.Version.Version, SourceURL: metadata.RepositoryURL, CreatedAt: latest.Version.CreatedUnix.AsLocalTime(), UpdatedAt: latest.Version.CreatedUnix.AsLocalTime(), Deprecated: false, Versions: versions, }) } // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_show.rb func PackageVersionMetadata(ctx *context.Context) { packageName := ctx.Params("name") packageVersion := strings.ReplaceAll(ctx.Params("version"), "_", ".") // Chef calls this endpoint with "_" instead of "."?! pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName, packageVersion) if err != nil { if err == packages_model.ErrPackageNotExist { apiError(ctx, http.StatusNotFound, err) return } apiError(ctx, http.StatusInternalServerError, err) return } pd, err := packages_model.GetPackageDescriptor(ctx, pv) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } type Result struct { Version string `json:"version"` TarballFileSize int64 `json:"tarball_file_size"` PublishedAt time.Time `json:"published_at"` Cookbook string `json:"cookbook"` File string `json:"file"` License string `json:"license"` Dependencies map[string]string `json:"dependencies"` } baseURL := fmt.Sprintf("%sapi/packages/%s/chef/api/v1/cookbooks/%s", setting.AppURL, ctx.Package.Owner.Name, url.PathEscape(pd.Package.Name)) metadata := pd.Metadata.(*chef_module.Metadata) ctx.JSON(http.StatusOK, &Result{ Version: pd.Version.Version, TarballFileSize: pd.Files[0].Blob.Size, PublishedAt: pd.Version.CreatedUnix.AsLocalTime(), Cookbook: baseURL, File: fmt.Sprintf("%s/versions/%s/download", baseURL, pd.Version.Version), License: metadata.License, Dependencies: metadata.Dependencies, }) } // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_share.rb func UploadPackage(ctx *context.Context) { file, _, err := ctx.Req.FormFile("tarball") if err != nil { apiError(ctx, http.StatusBadRequest, err) return } defer file.Close() buf, err := packages_module.CreateHashedBufferFromReader(file) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } defer buf.Close() pck, err := chef_module.ParsePackage(buf) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { apiError(ctx, http.StatusBadRequest, err) } else { apiError(ctx, http.StatusInternalServerError, err) } return } if _, err := buf.Seek(0, io.SeekStart); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } _, _, err = packages_service.CreatePackageAndAddFile( &packages_service.PackageCreationInfo{ PackageInfo: packages_service.PackageInfo{ Owner: ctx.Package.Owner, PackageType: packages_model.TypeChef, Name: pck.Name, Version: pck.Version, }, Creator: ctx.Doer, SemverCompatible: true, Metadata: pck.Metadata, }, &packages_service.PackageFileCreationInfo{ PackageFileInfo: packages_service.PackageFileInfo{ Filename: strings.ToLower(pck.Version + ".tar.gz"), }, Creator: ctx.Doer, Data: buf, IsLead: true, }, ) if err != nil { switch err { case packages_model.ErrDuplicatePackageVersion: apiError(ctx, http.StatusBadRequest, err) case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: apiError(ctx, http.StatusForbidden, err) default: apiError(ctx, http.StatusInternalServerError, err) } return } ctx.JSON(http.StatusCreated, make(map[any]any)) } // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_download.rb func DownloadPackage(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.Params("name"), ctx.Params("version")) if err != nil { if err == packages_model.ErrPackageNotExist { apiError(ctx, http.StatusNotFound, err) return } apiError(ctx, http.StatusInternalServerError, err) return } pd, err := packages_model.GetPackageDescriptor(ctx, pv) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } pf := pd.Files[0].File s, _, err := packages_service.GetPackageFileStream(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } defer s.Close() ctx.ServeContent(s, &context.ServeHeaderOptions{ Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), }) } // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb func DeletePackageVersion(ctx *context.Context) { packageName := ctx.Params("name") packageVersion := ctx.Params("version") err := packages_service.RemovePackageVersionByNameAndVersion( ctx.Doer, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, PackageType: packages_model.TypeChef, Name: packageName, Version: packageVersion, }, ) if err != nil { if err == packages_model.ErrPackageNotExist { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) } return } ctx.Status(http.StatusOK) } // https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb func DeletePackage(ctx *context.Context) { pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.Params("name")) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } if len(pvs) == 0 { apiError(ctx, http.StatusNotFound, err) return } for _, pv := range pvs { if err := packages_service.RemovePackageVersion(ctx.Doer, pv); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } } ctx.Status(http.StatusOK) }