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 00000000000..9c43b08bf42
--- /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 78288a9e42f..837c8434ae4 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 28b7cb88270..9ab4ac900cf 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 5565bf5b895..0b92d85a3d4 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 1e4209930f5..10f2184bc9c 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 1d7a8f22e86..d9f4872dca4 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 603e925e32a..9d9ce09b1c6 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 00000000000..fcf5d2f7622
--- /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 3249260f80a..fbdc40f37fb 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 373bd86d9f7..bdb535492bb 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 00000000000..9517448ca68
--- /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 5662ed2c402..21bf0c49ea1 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 00000000000..5ab50dd29eb
--- /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 f0251b95eb1..b5fdc739d7c 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 00000000000..ae0643a35a9
--- /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 b445e8e2f83..f3aa19c3192 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 114a108feb9..373a97407b5 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 00000000000..a85f7c4850f
--- /dev/null
+++ b/templates/package/content/helm.tmpl
@@ -0,0 +1,57 @@
+{{if eq .PackageDescriptor.Package.Type "helm"}}
+
helm repo add gitea {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/helm
+helm repo update
helm install {{.PackageDescriptor.Package.Name}} gitea/{{.PackageDescriptor.Package.Name}}
{{.i18n.Tr "packages.dependency.id"}} | +{{.i18n.Tr "packages.dependency.version"}} | +
---|---|
{{.Name}} | +{{.Version}} | +