mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
403 lines
12 KiB
Go
403 lines
12 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package chef
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
packages_model "github.com/hanzoai/git/models/packages"
|
|
"github.com/hanzoai/git/modules/optional"
|
|
packages_module "github.com/hanzoai/git/modules/packages"
|
|
chef_module "github.com/hanzoai/git/modules/packages/chef"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/util"
|
|
"github.com/hanzoai/git/routers/api/packages/helper"
|
|
"github.com/hanzoai/git/services/context"
|
|
packages_service "github.com/hanzoai/git/services/packages"
|
|
)
|
|
|
|
func apiError(ctx *context.Context, status int, obj any) {
|
|
type Error struct {
|
|
ErrorMessages []string `json:"error_messages"`
|
|
}
|
|
|
|
message := helper.ProcessErrorForUser(ctx, status, obj)
|
|
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: optional.Some(false),
|
|
})
|
|
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.PackageRegistryURL(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: optional.Some(false),
|
|
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.PackageRegistryURL(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.PathParam("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("%s/api/v1/cookbooks/%s/versions/", setting.PackageRegistryURL(ctx.Package.Owner.Name, "chef"), 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.PathParam("name")
|
|
packageVersion := strings.ReplaceAll(ctx.PathParam("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 errors.Is(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("%s/api/v1/cookbooks/%s", setting.PackageRegistryURL(ctx.Package.Owner.Name, "chef"), 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(
|
|
ctx,
|
|
&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.StatusConflict, 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.PathParam("name"), ctx.PathParam("version"))
|
|
if err != nil {
|
|
if errors.Is(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, u, _, err := packages_service.OpenFileForDownload(ctx, pf, ctx.Req.Method)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
helper.ServePackageFile(ctx, s, u, pf)
|
|
}
|
|
|
|
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb
|
|
func DeletePackageVersion(ctx *context.Context) {
|
|
packageName := ctx.PathParam("name")
|
|
packageVersion := ctx.PathParam("version")
|
|
|
|
err := packages_service.RemovePackageVersionByNameAndVersion(
|
|
ctx,
|
|
ctx.Doer,
|
|
&packages_service.PackageInfo{
|
|
Owner: ctx.Package.Owner,
|
|
PackageType: packages_model.TypeChef,
|
|
Name: packageName,
|
|
Version: packageVersion,
|
|
},
|
|
)
|
|
if err != nil {
|
|
if errors.Is(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.PathParam("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, ctx.Doer, pv); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.Status(http.StatusOK)
|
|
}
|