// Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package helm import ( "errors" "fmt" "io" "net/http" "net/url" "strings" "time" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" helm_module "code.gitea.io/gitea/modules/packages/helm" "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" "gopkg.in/yaml.v3" ) func apiError(ctx *context.Context, status int, obj any) { helper.LogAndProcessError(ctx, status, obj, func(message string) { type Error struct { Error string `json:"error"` } ctx.JSON(status, Error{ Error: message, }) }) } // Index generates the Helm charts index func Index(ctx *context.Context) { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeHelm, IsInternal: util.OptionalBoolFalse, }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } baseURL := setting.AppURL + "api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm" type ChartVersion struct { helm_module.Metadata `yaml:",inline"` URLs []string `yaml:"urls"` Created time.Time `yaml:"created,omitempty"` Removed bool `yaml:"removed,omitempty"` Digest string `yaml:"digest,omitempty"` } type ServerInfo struct { ContextPath string `yaml:"contextPath,omitempty"` } type Index struct { APIVersion string `yaml:"apiVersion"` Entries map[string][]*ChartVersion `yaml:"entries"` Generated time.Time `yaml:"generated,omitempty"` ServerInfo *ServerInfo `yaml:"serverInfo,omitempty"` } entries := make(map[string][]*ChartVersion) for _, pv := range pvs { metadata := &helm_module.Metadata{} if err := json.Unmarshal([]byte(pv.MetadataJSON), &metadata); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } entries[metadata.Name] = append(entries[metadata.Name], &ChartVersion{ Metadata: *metadata, Created: pv.CreatedUnix.AsTime(), URLs: []string{fmt.Sprintf("%s/%s", baseURL, url.PathEscape(createFilename(metadata)))}, }) } ctx.Resp.WriteHeader(http.StatusOK) if err := yaml.NewEncoder(ctx.Resp).Encode(&Index{ APIVersion: "v1", Entries: entries, Generated: time.Now(), ServerInfo: &ServerInfo{ ContextPath: setting.AppSubURL + "/api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm", }, }); err != nil { log.Error("YAML encode failed: %v", err) } } // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { filename := ctx.Params("filename") pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeHelm, Name: packages_model.SearchValue{ ExactMatch: true, Value: ctx.Params("package"), }, HasFileWithName: filename, IsInternal: util.OptionalBoolFalse, }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } if len(pvs) != 1 { apiError(ctx, http.StatusNotFound, nil) return } s, pf, err := packages_service.GetFileStreamByPackageVersion( ctx, pvs[0], &packages_service.PackageFileInfo{ Filename: filename, }, ) if err != nil { if err == packages_model.ErrPackageFileNotExist { apiError(ctx, http.StatusNotFound, err) return } apiError(ctx, http.StatusInternalServerError, err) return } defer s.Close() ctx.ServeContent(s, &context.ServeHeaderOptions{ Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), }) } // UploadPackage creates a new package func UploadPackage(ctx *context.Context) { upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } if needToClose { defer upload.Close() } buf, err := packages_module.CreateHashedBufferFromReader(upload) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } defer buf.Close() metadata, err := helm_module.ParseChartArchive(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.CreatePackageOrAddFileToExisting( &packages_service.PackageCreationInfo{ PackageInfo: packages_service.PackageInfo{ Owner: ctx.Package.Owner, PackageType: packages_model.TypeHelm, Name: metadata.Name, Version: metadata.Version, }, SemverCompatible: true, Creator: ctx.Doer, Metadata: metadata, }, &packages_service.PackageFileCreationInfo{ PackageFileInfo: packages_service.PackageFileInfo{ Filename: createFilename(metadata), }, Creator: ctx.Doer, Data: buf, IsLead: true, OverwriteExisting: 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.Status(http.StatusCreated) } func createFilename(metadata *helm_module.Metadata) string { return strings.ToLower(fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)) }