diff --git a/docs/content/doc/packages/helm.en-us.md b/docs/content/doc/packages/helm.en-us.md new file mode 100644 index 000000000..9c43b08bf --- /dev/null +++ b/docs/content/doc/packages/helm.en-us.md @@ -0,0 +1,67 @@ +--- +date: "2022-04-14T00:00:00+00:00" +title: "Helm Chart Registry" +slug: "packages/helm" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Helm" + weight: 50 + identifier: "helm" +--- + +# Helm Chart Registry + +Publish [Helm](https://helm.sh/) charts for your user or organization. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Helm Chart registry use a simple HTTP client like `curl` or the [`helm cm-push`](https://github.com/chartmuseum/helm-push/) plugin. + +## Publish a package + +Publish a package by running the following command: + +```shell +curl --user {username}:{password} -X POST --upload-file ./{chart_file}.tgz https://gitea.example.com/api/packages/{owner}/helm/api/charts +``` + +or with the `helm cm-push` plugin: + +```shell +helm repo add --username {username} --password {password} {repo} https://gitea.example.com/api/packages/{owner}/helm +helm cm-push ./{chart_file}.tgz {repo} +``` + +| Parameter | Description | +| ------------ | ----------- | +| `username` | Your Gitea username. | +| `password` | Your Gitea password or a personal access token. | +| `repo` | The name for the repository. | +| `chart_file` | The Helm Chart archive. | +| `owner` | The owner of the package. | + +## Install a package + +To install a Helm char from the registry, execute the following command: + +```shell +helm repo add --username {username} --password {password} {repo} https://gitea.example.com/api/packages/{owner}/helm +helm repo update +helm install {name} {repo}/{chart} +``` + +| Parameter | Description | +| ---------- | ----------- | +| `username` | Your Gitea username. | +| `password` | Your Gitea password or a personal access token. | +| `repo` | The name for the repository. | +| `owner` | The owner of the package. | +| `name` | The local name. | +| `chart` | The name Helm Chart. | diff --git a/docs/content/doc/packages/maven.en-us.md b/docs/content/doc/packages/maven.en-us.md index 78288a9e4..837c8434a 100644 --- a/docs/content/doc/packages/maven.en-us.md +++ b/docs/content/doc/packages/maven.en-us.md @@ -8,7 +8,7 @@ menu: sidebar: parent: "packages" name: "Maven" - weight: 50 + weight: 60 identifier: "maven" --- diff --git a/docs/content/doc/packages/npm.en-us.md b/docs/content/doc/packages/npm.en-us.md index 28b7cb882..9ab4ac900 100644 --- a/docs/content/doc/packages/npm.en-us.md +++ b/docs/content/doc/packages/npm.en-us.md @@ -8,7 +8,7 @@ menu: sidebar: parent: "packages" name: "npm" - weight: 60 + weight: 70 identifier: "npm" --- diff --git a/docs/content/doc/packages/nuget.en-us.md b/docs/content/doc/packages/nuget.en-us.md index 5565bf5b8..0b92d85a3 100644 --- a/docs/content/doc/packages/nuget.en-us.md +++ b/docs/content/doc/packages/nuget.en-us.md @@ -8,7 +8,7 @@ menu: sidebar: parent: "packages" name: "NuGet" - weight: 70 + weight: 80 identifier: "nuget" --- diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md index 1e4209930..10f2184bc 100644 --- a/docs/content/doc/packages/overview.en-us.md +++ b/docs/content/doc/packages/overview.en-us.md @@ -30,6 +30,7 @@ The following package managers are currently supported: | [Conan]({{< relref "doc/packages/conan.en-us.md" >}}) | C++ | `conan` | | [Container]({{< relref "doc/packages/container.en-us.md" >}}) | - | any OCI compliant client | | [Generic]({{< relref "doc/packages/generic.en-us.md" >}}) | - | any HTTP client | +| [Helm]({{< relref "doc/packages/helm.en-us.md" >}}) | - | any HTTP client, `cm-push` | | [Maven]({{< relref "doc/packages/maven.en-us.md" >}}) | Java | `mvn`, `gradle` | | [npm]({{< relref "doc/packages/npm.en-us.md" >}}) | JavaScript | `npm`, `yarn` | | [NuGet]({{< relref "doc/packages/nuget.en-us.md" >}}) | .NET | `nuget` | diff --git a/docs/content/doc/packages/pypi.en-us.md b/docs/content/doc/packages/pypi.en-us.md index 1d7a8f22e..d9f4872dc 100644 --- a/docs/content/doc/packages/pypi.en-us.md +++ b/docs/content/doc/packages/pypi.en-us.md @@ -8,7 +8,7 @@ menu: sidebar: parent: "packages" name: "PyPI" - weight: 80 + weight: 90 identifier: "pypi" --- diff --git a/docs/content/doc/packages/rubygems.en-us.md b/docs/content/doc/packages/rubygems.en-us.md index 603e925e3..9d9ce09b1 100644 --- a/docs/content/doc/packages/rubygems.en-us.md +++ b/docs/content/doc/packages/rubygems.en-us.md @@ -8,7 +8,7 @@ menu: sidebar: parent: "packages" name: "RubyGems" - weight: 90 + weight: 100 identifier: "rubygems" --- diff --git a/integrations/api_packages_helm_test.go b/integrations/api_packages_helm_test.go new file mode 100644 index 000000000..fcf5d2f76 --- /dev/null +++ b/integrations/api_packages_helm_test.go @@ -0,0 +1,166 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "net/http" + "testing" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + helm_module "code.gitea.io/gitea/modules/packages/helm" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" +) + +func TestPackageHelm(t *testing.T) { + defer prepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + + packageName := "test-chart" + packageVersion := "1.0.3" + packageAuthor := "KN4CK3R" + packageDescription := "Gitea Test Package" + + filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) + + chartContent := `apiVersion: v2 +description: ` + packageDescription + ` +name: ` + packageName + ` +type: application +version: ` + packageVersion + ` +maintainers: +- name: ` + packageAuthor + ` +dependencies: +- name: dep1 + repository: https://example.com/ + version: 1.0.0` + + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + archive := tar.NewWriter(zw) + archive.WriteHeader(&tar.Header{ + Name: fmt.Sprintf("%s/Chart.yaml", packageName), + Mode: 0o600, + Size: int64(len(chartContent)), + }) + archive.Write([]byte(chartContent)) + archive.Close() + zw.Close() + content := buf.Bytes() + + url := fmt.Sprintf("/api/packages/%s/helm", user.Name) + + t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + + uploadURL := url + "/api/charts" + + req := NewRequestWithBody(t, "POST", uploadURL, bytes.NewReader(content)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeHelm) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.NotNil(t, pd.SemVer) + assert.IsType(t, &helm_module.Metadata{}, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, filename, pfs[0].Name) + assert.True(t, pfs[0].IsLead) + + pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID) + assert.NoError(t, err) + assert.Equal(t, int64(len(content)), pb.Size) + + req = NewRequestWithBody(t, "POST", uploadURL, bytes.NewReader(content)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + }) + + t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + + checkDownloadCount := func(count int64) { + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeHelm) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + assert.Equal(t, count, pvs[0].DownloadCount) + } + + checkDownloadCount(0) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", url, filename)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, content, resp.Body.Bytes()) + + checkDownloadCount(1) + }) + + t.Run("Index", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/index.yaml", url)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + 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"` + } + + var result Index + assert.NoError(t, yaml.NewDecoder(resp.Body).Decode(&result)) + assert.NotEmpty(t, result.Entries) + assert.Contains(t, result.Entries, packageName) + + cvs := result.Entries[packageName] + assert.Len(t, cvs, 1) + + cv := cvs[0] + assert.Equal(t, packageName, cv.Name) + assert.Equal(t, packageVersion, cv.Version) + assert.Equal(t, packageDescription, cv.Description) + assert.Len(t, cv.Maintainers, 1) + assert.Equal(t, packageAuthor, cv.Maintainers[0].Name) + assert.Len(t, cv.Dependencies, 1) + assert.ElementsMatch(t, []string{fmt.Sprintf("%s%s/%s", setting.AppURL, url[1:], filename)}, cv.URLs) + + assert.Equal(t, url, result.ServerInfo.ContextPath) + }) +} diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index 3249260f8..fbdc40f37 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/packages/composer" "code.gitea.io/gitea/modules/packages/conan" "code.gitea.io/gitea/modules/packages/container" + "code.gitea.io/gitea/modules/packages/helm" "code.gitea.io/gitea/modules/packages/maven" "code.gitea.io/gitea/modules/packages/npm" "code.gitea.io/gitea/modules/packages/nuget" @@ -129,6 +130,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc metadata = &container.Metadata{} case TypeGeneric: // generic packages have no metadata + case TypeHelm: + metadata = &helm.Metadata{} case TypeNuGet: metadata = &nuget.Metadata{} case TypeNpm: diff --git a/models/packages/package.go b/models/packages/package.go index 373bd86d9..bdb535492 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -35,9 +35,10 @@ const ( TypeConan Type = "conan" TypeContainer Type = "container" TypeGeneric Type = "generic" - TypeNuGet Type = "nuget" - TypeNpm Type = "npm" + TypeHelm Type = "helm" TypeMaven Type = "maven" + TypeNpm Type = "npm" + TypeNuGet Type = "nuget" TypePyPI Type = "pypi" TypeRubyGems Type = "rubygems" ) @@ -53,12 +54,14 @@ func (pt Type) Name() string { return "Container" case TypeGeneric: return "Generic" - case TypeNuGet: - return "NuGet" - case TypeNpm: - return "npm" + case TypeHelm: + return "Helm" case TypeMaven: return "Maven" + case TypeNpm: + return "npm" + case TypeNuGet: + return "NuGet" case TypePyPI: return "PyPI" case TypeRubyGems: @@ -78,12 +81,14 @@ func (pt Type) SVGName() string { return "octicon-container" case TypeGeneric: return "octicon-package" - case TypeNuGet: - return "gitea-nuget" - case TypeNpm: - return "gitea-npm" + case TypeHelm: + return "gitea-helm" case TypeMaven: return "gitea-maven" + case TypeNpm: + return "gitea-npm" + case TypeNuGet: + return "gitea-nuget" case TypePyPI: return "gitea-python" case TypeRubyGems: diff --git a/modules/packages/helm/metadata.go b/modules/packages/helm/metadata.go new file mode 100644 index 000000000..9517448ca --- /dev/null +++ b/modules/packages/helm/metadata.go @@ -0,0 +1,131 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package helm + +import ( + "archive/tar" + "compress/gzip" + "errors" + "io" + "strings" + + "code.gitea.io/gitea/modules/validation" + + "github.com/hashicorp/go-version" + "gopkg.in/yaml.v2" +) + +var ( + // ErrMissingChartFile indicates a missing Chart.yaml file + ErrMissingChartFile = errors.New("Chart.yaml file is missing") + // ErrInvalidName indicates an invalid package name + ErrInvalidName = errors.New("package name is invalid") + // ErrInvalidVersion indicates an invalid package version + ErrInvalidVersion = errors.New("package version is invalid") + // ErrInvalidChart indicates an invalid chart + ErrInvalidChart = errors.New("chart is invalid") +) + +// Metadata for a Chart file. This models the structure of a Chart.yaml file. +type Metadata struct { + APIVersion string `json:"api_version" yaml:"apiVersion"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Name string `json:"name" yaml:"name"` + Version string `json:"version" yaml:"version"` + AppVersion string `json:"app_version,omitempty" yaml:"appVersion,omitempty"` + Home string `json:"home,omitempty" yaml:"home,omitempty"` + Sources []string `json:"sources,omitempty" yaml:"sources,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty"` + Maintainers []*Maintainer `json:"maintainers,omitempty" yaml:"maintainers,omitempty"` + Icon string `json:"icon,omitempty" yaml:"icon,omitempty"` + Condition string `json:"condition,omitempty" yaml:"condition,omitempty"` + Tags string `json:"tags,omitempty" yaml:"tags,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` + KubeVersion string `json:"kube_version,omitempty" yaml:"kubeVersion,omitempty"` + Dependencies []*Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"` +} + +type Maintainer struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Email string `json:"email,omitempty" yaml:"email,omitempty"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` +} + +type Dependency struct { + Name string `json:"name" yaml:"name"` + Version string `json:"version,omitempty" yaml:"version,omitempty"` + Repository string `json:"repository" yaml:"repository"` + Condition string `json:"condition,omitempty" yaml:"condition,omitempty"` + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` + Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + ImportValues []interface{} `json:"import_values,omitempty" yaml:"import-values,omitempty"` + Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` +} + +// ParseChartArchive parses the metadata of a Helm archive +func ParseChartArchive(r io.Reader) (*Metadata, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + if hd.FileInfo().Name() == "Chart.yaml" { + if strings.Count(hd.Name, "/") != 1 { + continue + } + + return ParseChartFile(tr) + } + } + + return nil, ErrMissingChartFile +} + +// ParseChartFile parses a Chart.yaml file to retrieve the metadata of a Helm chart +func ParseChartFile(r io.Reader) (*Metadata, error) { + var metadata *Metadata + if err := yaml.NewDecoder(r).Decode(&metadata); err != nil { + return nil, err + } + + if metadata.APIVersion == "" { + return nil, ErrInvalidChart + } + + if metadata.Type != "" && metadata.Type != "application" && metadata.Type != "library" { + return nil, ErrInvalidChart + } + + if metadata.Name == "" { + return nil, ErrInvalidName + } + + if _, err := version.NewSemver(metadata.Version); err != nil { + return nil, ErrInvalidVersion + } + + if !validation.IsValidURL(metadata.Home) { + metadata.Home = "" + } + + return metadata, nil +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 5662ed2c4..21bf0c49e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3051,6 +3051,9 @@ container.labels.key = Key container.labels.value = Value generic.download = Download package from the command line: generic.documentation = For more information on the generic registry, see the documentation. +helm.registry = Setup this registry from the command line: +helm.install = To install the package, run the following command: +helm.documentation = For more information on the Helm registry, see the documentation. maven.registry = Setup this registry in your project pom.xml file: maven.install = To use the package include the following in the dependencies block in the pom.xml file: maven.install2 = Run via command line: diff --git a/public/img/svg/gitea-helm.svg b/public/img/svg/gitea-helm.svg new file mode 100644 index 000000000..5ab50dd29 --- /dev/null +++ b/public/img/svg/gitea-helm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index f0251b95e..b5fdc739d 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/routers/api/packages/conan" "code.gitea.io/gitea/routers/api/packages/container" "code.gitea.io/gitea/routers/api/packages/generic" + "code.gitea.io/gitea/routers/api/packages/helm" "code.gitea.io/gitea/routers/api/packages/maven" "code.gitea.io/gitea/routers/api/packages/npm" "code.gitea.io/gitea/routers/api/packages/nuget" @@ -162,6 +163,11 @@ func Routes() *web.Route { }, reqPackageAccess(perm.AccessModeWrite)) }) }) + r.Group("/helm", func() { + r.Get("/index.yaml", helm.Index) + r.Get("/{filename}", helm.DownloadPackageFile) + r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage) + }) r.Group("/maven", func() { r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile) r.Get("/*", maven.DownloadPackageFile) diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go new file mode 100644 index 000000000..ae0643a35 --- /dev/null +++ b/routers/api/packages/helm/helm.go @@ -0,0 +1,205 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package helm + +import ( + "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/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" + + "gopkg.in/yaml.v2" +) + +func apiError(ctx *context.Context, status int, obj interface{}) { + 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, + }) + 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, + }) + 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.ServeStream(s, pf.Name) +} + +// 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, 32*1024*1024) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + metadata, err := helm_module.ParseChartArchive(buf) + if err != nil { + apiError(ctx, http.StatusBadRequest, 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), + }, + Data: buf, + IsLead: true, + OverwriteExisting: true, + }, + ) + if err != nil { + if err == packages_model.ErrDuplicatePackageVersion { + apiError(ctx, http.StatusConflict, err) + return + } + 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)) +} diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index b445e8e2f..f3aa19c31 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) { // in: query // description: package type filter // type: string - // enum: [composer, conan, generic, maven, npm, nuget, pypi, rubygems] + // enum: [composer, conan, container, generic, helm, maven, npm, nuget, pypi, rubygems] // - name: q // in: query // description: name filter diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 114a108fe..373a97407 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -17,6 +17,7 @@ + diff --git a/templates/package/content/helm.tmpl b/templates/package/content/helm.tmpl new file mode 100644 index 000000000..a85f7c485 --- /dev/null +++ b/templates/package/content/helm.tmpl @@ -0,0 +1,57 @@ +{{if eq .PackageDescriptor.Package.Type "helm"}} +

{{.i18n.Tr "packages.installation"}}

+
+
+
+ +
helm repo add gitea {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/helm
+helm repo update
+
+
+ +
helm install {{.PackageDescriptor.Package.Name}} gitea/{{.PackageDescriptor.Package.Name}}
+
+
+ +
+
+
+ + {{if .PackageDescriptor.Metadata.Description}} +

{{.i18n.Tr "packages.about"}}

+
+ {{.PackageDescriptor.Metadata.Description}} +
+ {{end}} + + {{if .PackageDescriptor.Metadata.Dependencies}} +

{{.i18n.Tr "packages.dependencies"}}

+
+ + + + + + + + + {{range .PackageDescriptor.Metadata.Dependencies}} + + + + + {{end}} + +
{{.i18n.Tr "packages.dependency.id"}}{{.i18n.Tr "packages.dependency.version"}}
{{.Name}}{{.Version}}
+
+ {{end}} + + {{if .PackageDescriptor.Metadata.Keywords}} +

{{.i18n.Tr "packages.keywords"}}

+
+ {{range .PackageDescriptor.Metadata.Keywords}} + {{.}} + {{end}} +
+ {{end}} +{{end}} diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl index 16347d1b6..bc714e5c9 100644 --- a/templates/package/content/npm.tmpl +++ b/templates/package/content/npm.tmpl @@ -45,7 +45,7 @@ {{end}} - {{if or .PackageDescriptor.Metadata.Keywords}} + {{if .PackageDescriptor.Metadata.Keywords}}

{{.i18n.Tr "packages.keywords"}}

{{range .PackageDescriptor.Metadata.Keywords}} diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl new file mode 100644 index 000000000..7c97c6358 --- /dev/null +++ b/templates/package/metadata/helm.tmpl @@ -0,0 +1,4 @@ +{{if eq .PackageDescriptor.Package.Type "helm"}} + {{range .PackageDescriptor.Metadata.Maintainers}}
{{svg "octicon-person" 16 "mr-3"}} {{.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.Home}}
{{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "packages.details.project_site"}}
{{end}} +{{end}} diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 9216e6b9d..0b0f71283 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -10,6 +10,7 @@ + diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 1b1c5d50c..bb96da341 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -23,9 +23,10 @@ {{template "package/content/conan" .}} {{template "package/content/container" .}} {{template "package/content/generic" .}} - {{template "package/content/nuget" .}} - {{template "package/content/npm" .}} + {{template "package/content/helm" .}} {{template "package/content/maven" .}} + {{template "package/content/npm" .}} + {{template "package/content/nuget" .}} {{template "package/content/pypi" .}} {{template "package/content/rubygems" .}}
@@ -43,9 +44,10 @@ {{template "package/metadata/conan" .}} {{template "package/metadata/container" .}} {{template "package/metadata/generic" .}} - {{template "package/metadata/nuget" .}} - {{template "package/metadata/npm" .}} + {{template "package/metadata/helm" .}} {{template "package/metadata/maven" .}} + {{template "package/metadata/npm" .}} + {{template "package/metadata/nuget" .}} {{template "package/metadata/pypi" .}} {{template "package/metadata/rubygems" .}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4bf3874ba..e35ba9890 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1904,7 +1904,9 @@ "enum": [ "composer", "conan", + "container", "generic", + "helm", "maven", "npm", "nuget", diff --git a/web_src/svg/gitea-helm.svg b/web_src/svg/gitea-helm.svg new file mode 100644 index 000000000..120996559 --- /dev/null +++ b/web_src/svg/gitea-helm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file