code.gitea.io/gitea@v1.21.7/routers/api/packages/helm/helm.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package helm
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  	"time"
    14  
    15  	packages_model "code.gitea.io/gitea/models/packages"
    16  	"code.gitea.io/gitea/modules/context"
    17  	"code.gitea.io/gitea/modules/json"
    18  	"code.gitea.io/gitea/modules/log"
    19  	packages_module "code.gitea.io/gitea/modules/packages"
    20  	helm_module "code.gitea.io/gitea/modules/packages/helm"
    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  	"gopkg.in/yaml.v3"
    27  )
    28  
    29  func apiError(ctx *context.Context, status int, obj any) {
    30  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    31  		type Error struct {
    32  			Error string `json:"error"`
    33  		}
    34  		ctx.JSON(status, Error{
    35  			Error: message,
    36  		})
    37  	})
    38  }
    39  
    40  // Index generates the Helm charts index
    41  func Index(ctx *context.Context) {
    42  	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
    43  		OwnerID:    ctx.Package.Owner.ID,
    44  		Type:       packages_model.TypeHelm,
    45  		IsInternal: util.OptionalBoolFalse,
    46  	})
    47  	if err != nil {
    48  		apiError(ctx, http.StatusInternalServerError, err)
    49  		return
    50  	}
    51  
    52  	baseURL := setting.AppURL + "api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm"
    53  
    54  	type ChartVersion struct {
    55  		helm_module.Metadata `yaml:",inline"`
    56  		URLs                 []string  `yaml:"urls"`
    57  		Created              time.Time `yaml:"created,omitempty"`
    58  		Removed              bool      `yaml:"removed,omitempty"`
    59  		Digest               string    `yaml:"digest,omitempty"`
    60  	}
    61  
    62  	type ServerInfo struct {
    63  		ContextPath string `yaml:"contextPath,omitempty"`
    64  	}
    65  
    66  	type Index struct {
    67  		APIVersion string                     `yaml:"apiVersion"`
    68  		Entries    map[string][]*ChartVersion `yaml:"entries"`
    69  		Generated  time.Time                  `yaml:"generated,omitempty"`
    70  		ServerInfo *ServerInfo                `yaml:"serverInfo,omitempty"`
    71  	}
    72  
    73  	entries := make(map[string][]*ChartVersion)
    74  	for _, pv := range pvs {
    75  		metadata := &helm_module.Metadata{}
    76  		if err := json.Unmarshal([]byte(pv.MetadataJSON), &metadata); err != nil {
    77  			apiError(ctx, http.StatusInternalServerError, err)
    78  			return
    79  		}
    80  
    81  		entries[metadata.Name] = append(entries[metadata.Name], &ChartVersion{
    82  			Metadata: *metadata,
    83  			Created:  pv.CreatedUnix.AsTime(),
    84  			URLs:     []string{fmt.Sprintf("%s/%s", baseURL, url.PathEscape(createFilename(metadata)))},
    85  		})
    86  	}
    87  
    88  	ctx.Resp.WriteHeader(http.StatusOK)
    89  	if err := yaml.NewEncoder(ctx.Resp).Encode(&Index{
    90  		APIVersion: "v1",
    91  		Entries:    entries,
    92  		Generated:  time.Now(),
    93  		ServerInfo: &ServerInfo{
    94  			ContextPath: setting.AppSubURL + "/api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm",
    95  		},
    96  	}); err != nil {
    97  		log.Error("YAML encode failed: %v", err)
    98  	}
    99  }
   100  
   101  // DownloadPackageFile serves the content of a package
   102  func DownloadPackageFile(ctx *context.Context) {
   103  	filename := ctx.Params("filename")
   104  
   105  	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
   106  		OwnerID: ctx.Package.Owner.ID,
   107  		Type:    packages_model.TypeHelm,
   108  		Name: packages_model.SearchValue{
   109  			ExactMatch: true,
   110  			Value:      ctx.Params("package"),
   111  		},
   112  		HasFileWithName: filename,
   113  		IsInternal:      util.OptionalBoolFalse,
   114  	})
   115  	if err != nil {
   116  		apiError(ctx, http.StatusInternalServerError, err)
   117  		return
   118  	}
   119  	if len(pvs) != 1 {
   120  		apiError(ctx, http.StatusNotFound, nil)
   121  		return
   122  	}
   123  
   124  	s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
   125  		ctx,
   126  		pvs[0],
   127  		&packages_service.PackageFileInfo{
   128  			Filename: filename,
   129  		},
   130  	)
   131  	if err != nil {
   132  		if err == packages_model.ErrPackageFileNotExist {
   133  			apiError(ctx, http.StatusNotFound, err)
   134  			return
   135  		}
   136  		apiError(ctx, http.StatusInternalServerError, err)
   137  		return
   138  	}
   139  
   140  	helper.ServePackageFile(ctx, s, u, pf)
   141  }
   142  
   143  // UploadPackage creates a new package
   144  func UploadPackage(ctx *context.Context) {
   145  	upload, needToClose, err := ctx.UploadStream()
   146  	if err != nil {
   147  		apiError(ctx, http.StatusInternalServerError, err)
   148  		return
   149  	}
   150  	if needToClose {
   151  		defer upload.Close()
   152  	}
   153  
   154  	buf, err := packages_module.CreateHashedBufferFromReader(upload)
   155  	if err != nil {
   156  		apiError(ctx, http.StatusInternalServerError, err)
   157  		return
   158  	}
   159  	defer buf.Close()
   160  
   161  	metadata, err := helm_module.ParseChartArchive(buf)
   162  	if err != nil {
   163  		if errors.Is(err, util.ErrInvalidArgument) {
   164  			apiError(ctx, http.StatusBadRequest, err)
   165  		} else {
   166  			apiError(ctx, http.StatusInternalServerError, err)
   167  		}
   168  		return
   169  	}
   170  
   171  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   172  		apiError(ctx, http.StatusInternalServerError, err)
   173  		return
   174  	}
   175  
   176  	_, _, err = packages_service.CreatePackageOrAddFileToExisting(
   177  		ctx,
   178  		&packages_service.PackageCreationInfo{
   179  			PackageInfo: packages_service.PackageInfo{
   180  				Owner:       ctx.Package.Owner,
   181  				PackageType: packages_model.TypeHelm,
   182  				Name:        metadata.Name,
   183  				Version:     metadata.Version,
   184  			},
   185  			SemverCompatible: true,
   186  			Creator:          ctx.Doer,
   187  			Metadata:         metadata,
   188  		},
   189  		&packages_service.PackageFileCreationInfo{
   190  			PackageFileInfo: packages_service.PackageFileInfo{
   191  				Filename: createFilename(metadata),
   192  			},
   193  			Creator:           ctx.Doer,
   194  			Data:              buf,
   195  			IsLead:            true,
   196  			OverwriteExisting: true,
   197  		},
   198  	)
   199  	if err != nil {
   200  		switch err {
   201  		case packages_model.ErrDuplicatePackageVersion:
   202  			apiError(ctx, http.StatusConflict, err)
   203  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   204  			apiError(ctx, http.StatusForbidden, err)
   205  		default:
   206  			apiError(ctx, http.StatusInternalServerError, err)
   207  		}
   208  		return
   209  	}
   210  
   211  	ctx.Status(http.StatusCreated)
   212  }
   213  
   214  func createFilename(metadata *helm_module.Metadata) string {
   215  	return strings.ToLower(fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version))
   216  }