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  }