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  }