Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,25 @@ web/

If you don't specify a custom welcome page, ChartMuseum will serve the default one.

#### Artifact Hub

By setting the flag `--artifact-hub-repo-id <repo id>`, ChartMuseum will serve a `artifacthub-repo.yml` file with the
specified repo ID in the `repositoryID` field of the yaml file.

```yaml
repositoryID: The ID of the Artifact Hub repository where the packages will be published to (optional, but it enables verified publisher)
```

##### Multitenancy

For multitenancy setups, you can provide a key value pair to the flag in the format: `--artifact-hub-repo-id <repo>=<repo id>`

```bash
chartmuseum --storage local --storage-local-rootdir /tmp/ --depth 1 --artifact-hub-repo-id org1=<repo id> --artifact-hub-repo-id org2=<repo2 id>
```

The `artifacthub-repo.yml` file will then be served at `/org1/artifacthub-repo.yml` and `/org2/artifacthub-repo.yml`

## Original Logo

<sub>**_"Preserve your precious artifacts... in the cloud!"_**<sub>
Expand Down
2 changes: 2 additions & 0 deletions cmd/chartmuseum/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"

"github.com/chartmuseum/storage"

"helm.sh/chartmuseum/pkg/cache"
"helm.sh/chartmuseum/pkg/chartmuseum"
cm_logger "helm.sh/chartmuseum/pkg/chartmuseum/logger"
Expand Down Expand Up @@ -116,6 +117,7 @@ func cliHandler(c *cli.Context) {
Host: conf.GetString("listen.host"),
PerChartLimit: conf.GetInt("per-chart-limit"),
WebTemplatePath: conf.GetString("web-template-path"),
ArtifactHubRepoID: conf.GetStringMapString("artifact-hub-repo-id"),
}

server, err := newServer(options)
Expand Down
7 changes: 5 additions & 2 deletions pkg/chartmuseum/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"time"

"github.com/chartmuseum/storage"

"helm.sh/chartmuseum/pkg/cache"
cm_logger "helm.sh/chartmuseum/pkg/chartmuseum/logger"
cm_router "helm.sh/chartmuseum/pkg/chartmuseum/router"
Expand Down Expand Up @@ -70,6 +71,7 @@ type (
Host string
Version string
WebTemplatePath string
ArtifactHubRepoID map[string]string
// PerChartLimit allow museum server to keep max N version Charts
// And avoid swelling too large(if so , the index genertion will become slow)
PerChartLimit int
Expand Down Expand Up @@ -140,10 +142,11 @@ func NewServer(options ServerOptions) (Server, error) {
Version: options.Version,
CacheInterval: options.CacheInterval,
PerChartLimit: options.PerChartLimit,
ArtifactHubRepoID: options.ArtifactHubRepoID,
WebTemplatePath: options.WebTemplatePath,
// Deprecated options
// EnforceSemver2 - see https://github.com/helm/chartmuseum/issues/485 for more info
EnforceSemver2: options.EnforceSemver2,
WebTemplatePath: options.WebTemplatePath,
EnforceSemver2: options.EnforceSemver2,
})

return server, err
Expand Down
47 changes: 47 additions & 0 deletions pkg/chartmuseum/server/multitenant/artifacthub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright The Helm Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package multitenant

import (
"net/http"

"github.com/ghodss/yaml"

cm_logger "helm.sh/chartmuseum/pkg/chartmuseum/logger"
cm_repo "helm.sh/chartmuseum/pkg/repo"
)

const artifactHubFileContentType = "application/x-yaml"

func (server *MultiTenantServer) getArtifactHubYml(log cm_logger.LoggingFn, repo string) ([]byte, *HTTPError) {
if _, ok := server.ArtifactHubRepoID[repo]; !ok {
return nil, &HTTPError{http.StatusNotFound, "Artifact Hub repository ID not found"}
}
artifactHubFile := &cm_repo.ArtifactHubFile{
RepoID: server.ArtifactHubRepoID[repo],
}
log(cm_logger.DebugLevel, "Generating artifacthub-repo.yml file", "repo", repo)
rawArtifactHubFile, err := yaml.Marshal(&artifactHubFile)
if err != nil {
errStr := "failed to generate artifacthub-repo.yml file"
log(cm_logger.ErrorLevel, errStr,
"repo", repo,
)
return nil, &HTTPError{http.StatusInternalServerError, errStr}
}
return rawArtifactHubFile, nil
}
12 changes: 12 additions & 0 deletions pkg/chartmuseum/server/multitenant/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ func (server *MultiTenantServer) getIndexFileRequestHandler(c *gin.Context) {
c.Data(200, indexFileContentType, indexFile.Raw)
}

func (server *MultiTenantServer) getArtifactHubFileRequestHandler(c *gin.Context) {
repo := c.Param("repo")
log := server.Logger.ContextLoggingFn(c)
artifactHubFile, err := server.getArtifactHubYml(log, repo)
if err != nil {
c.JSON(err.Status, gin.H{"error": err.Message})
return
}

c.Data(200, artifactHubFileContentType, artifactHubFile)
}

func (server *MultiTenantServer) getStorageObjectRequestHandler(c *gin.Context) {
repo := c.Param("repo")
filename := c.Param("filename")
Expand Down
2 changes: 1 addition & 1 deletion pkg/chartmuseum/server/multitenant/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
cm_repo "helm.sh/chartmuseum/pkg/repo"
)

var (
const (
indexFileContentType = "application/x-yaml"
)

Expand Down
9 changes: 9 additions & 0 deletions pkg/chartmuseum/server/multitenant/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package multitenant

import (
cm_auth "github.com/chartmuseum/auth"

cm_router "helm.sh/chartmuseum/pkg/chartmuseum/router"
)

Expand All @@ -30,6 +31,10 @@ func (s *MultiTenantServer) Routes() []*cm_router.Route {
{"GET", "/health", s.getHealthCheckHandler, ""},
}

artifactHubRoutes := []*cm_router.Route{
{"GET", "/artifacthub-repo.yml", s.getArtifactHubFileRequestHandler, cm_auth.PullAction},
}

helmChartRepositoryRoutes := []*cm_router.Route{
{"GET", "/:repo/index.yaml", s.getIndexFileRequestHandler, cm_auth.PullAction},
{"GET", "/:repo/charts/:filename", s.getStorageObjectRequestHandler, cm_auth.PullAction},
Expand All @@ -50,6 +55,10 @@ func (s *MultiTenantServer) Routes() []*cm_router.Route {
routes = append(routes, serverInfoRoutes...)
routes = append(routes, helmChartRepositoryRoutes...)

if len(s.ArtifactHubRepoID) != 0 {
routes = append(routes, artifactHubRoutes...)
}

if s.WebTemplatePath != "" {
routes = append(routes, &cm_router.Route{
Method: "GET",
Expand Down
8 changes: 6 additions & 2 deletions pkg/chartmuseum/server/multitenant/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

cm_storage "github.com/chartmuseum/storage"
"github.com/gin-gonic/gin"

"helm.sh/chartmuseum/pkg/cache"
cm_logger "helm.sh/chartmuseum/pkg/chartmuseum/logger"
cm_router "helm.sh/chartmuseum/pkg/chartmuseum/router"
Expand Down Expand Up @@ -67,6 +68,7 @@ type (
CacheInterval time.Duration
EventChan chan event
ChartLimits *ObjectsPerChartLimit
ArtifactHubRepoID map[string]string
// Deprecated: see https://github.com/helm/chartmuseum/issues/485 for more info
EnforceSemver2 bool
WebTemplatePath string
Expand Down Expand Up @@ -98,9 +100,10 @@ type (
UseStatefiles bool
CacheInterval time.Duration
PerChartLimit int
ArtifactHubRepoID map[string]string
WebTemplatePath string
// Deprecated: see https://github.com/helm/chartmuseum/issues/485 for more info
EnforceSemver2 bool
WebTemplatePath string
EnforceSemver2 bool
}

tenantInternals struct {
Expand Down Expand Up @@ -159,6 +162,7 @@ func NewMultiTenantServer(options MultiTenantServerOptions) (*MultiTenantServer,
CacheInterval: options.CacheInterval,
ChartLimits: l,
WebTemplatePath: options.WebTemplatePath,
ArtifactHubRepoID: options.ArtifactHubRepoID,
}

if server.WebTemplatePath != "" {
Expand Down
97 changes: 77 additions & 20 deletions pkg/chartmuseum/server/multitenant/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"helm.sh/chartmuseum/pkg/repo"

"github.com/chartmuseum/storage"
"github.com/ghodss/yaml"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
)
Expand All @@ -53,26 +54,28 @@ var badTestProvfilePath = "../../../../testdata/badcharts/mybadchart/mybadchart-

type MultiTenantServerTestSuite struct {
suite.Suite
Depth0Server *MultiTenantServer
Depth1Server *MultiTenantServer
Depth2Server *MultiTenantServer
Depth3Server *MultiTenantServer
DisabledAPIServer *MultiTenantServer
DisabledDeleteServer *MultiTenantServer
OverwriteServer *MultiTenantServer
ForceOverwriteServer *MultiTenantServer
ChartURLServer *MultiTenantServer
MaxObjectsServer *MultiTenantServer
MaxUploadSizeServer *MultiTenantServer
Semver2Server *MultiTenantServer
PerChartLimitServer *MultiTenantServer
TempDirectory string
TestTarballFilename string
TestProvfileFilename string
StorageDirectory map[string]map[string][]string
LastCrashMessage string
LastPrinted string
LastExitCode int
Depth0Server *MultiTenantServer
Depth1Server *MultiTenantServer
Depth2Server *MultiTenantServer
Depth3Server *MultiTenantServer
DisabledAPIServer *MultiTenantServer
DisabledDeleteServer *MultiTenantServer
OverwriteServer *MultiTenantServer
ForceOverwriteServer *MultiTenantServer
ChartURLServer *MultiTenantServer
MaxObjectsServer *MultiTenantServer
MaxUploadSizeServer *MultiTenantServer
Semver2Server *MultiTenantServer
PerChartLimitServer *MultiTenantServer
ArtifactHubRepoIDServer *MultiTenantServer
TempDirectory string
TestTarballFilename string
TestProvfileFilename string
StorageDirectory map[string]map[string][]string
LastCrashMessage string
LastPrinted string
LastExitCode int
ArtifactHubIds map[string]string
}

func (suite *MultiTenantServerTestSuite) doRequest(stype string, method string, urlStr string, body io.Reader, contentType string, output ...*bytes.Buffer) gin.ResponseWriter {
Expand Down Expand Up @@ -113,6 +116,8 @@ func (suite *MultiTenantServerTestSuite) doRequest(stype string, method string,
suite.Semver2Server.Router.HandleContext(c)
case "per-chart-limit":
suite.PerChartLimitServer.Router.HandleContext(c)
case "artifacthub":
suite.ArtifactHubRepoIDServer.Router.HandleContext(c)
}

return c.Writer
Expand Down Expand Up @@ -206,6 +211,19 @@ func (suite *MultiTenantServerTestSuite) SetupSuite() {
},
}

// build the map of Artifact Hub Repo Ids from the StorageDirectory
// so we can test the /:repo/artifact-hub.yml route in TestRoutes
suite.ArtifactHubIds = map[string]string{"": "depth0"}
for depth1, v := range suite.StorageDirectory {
suite.ArtifactHubIds[depth1] = "depth1"
for depth2, v := range v {
suite.ArtifactHubIds[fmt.Sprintf("%s/%s", depth1, depth2)] = "depth2"
for _, depth3 := range v {
suite.ArtifactHubIds[fmt.Sprintf("%s/%s/%s", depth1, depth2, depth3)] = "depth3"
}
}
}

// Scaffold out test storage directory structure
for org, teams := range suite.StorageDirectory {
for team, repos := range teams {
Expand Down Expand Up @@ -237,6 +255,7 @@ func (suite *MultiTenantServerTestSuite) SetupSuite() {
ChartPostFormFieldName: "chart",
ProvPostFormFieldName: "prov",
IndexLimit: 1,
ArtifactHubRepoID: suite.ArtifactHubIds,
})
suite.NotNil(server)
suite.Nil(err, "no error creating new multitenant (depth=0) server")
Expand All @@ -256,6 +275,7 @@ func (suite *MultiTenantServerTestSuite) SetupSuite() {
EnableAPI: true,
ChartPostFormFieldName: "chart",
ProvPostFormFieldName: "prov",
ArtifactHubRepoID: suite.ArtifactHubIds,
})
suite.NotNil(server)
suite.Nil(err, "no error creating new multitenant (depth=1) server")
Expand All @@ -274,6 +294,7 @@ func (suite *MultiTenantServerTestSuite) SetupSuite() {
EnableAPI: true,
ChartPostFormFieldName: "chart",
ProvPostFormFieldName: "prov",
ArtifactHubRepoID: suite.ArtifactHubIds,
})
suite.NotNil(server)
suite.Nil(err, "no error creating new multitenant (depth=2) server")
Expand All @@ -292,6 +313,7 @@ func (suite *MultiTenantServerTestSuite) SetupSuite() {
EnableAPI: true,
ChartPostFormFieldName: "chart",
ProvPostFormFieldName: "prov",
ArtifactHubRepoID: suite.ArtifactHubIds,
})
suite.NotNil(server)
suite.Nil(err, "no error creating new multitenant (depth=3) server")
Expand Down Expand Up @@ -464,6 +486,26 @@ func (suite *MultiTenantServerTestSuite) SetupSuite() {
suite.NotNil(server)
suite.Nil(err, "no error creating new max upload size server")
suite.MaxUploadSizeServer = server

router = cm_router.NewRouter(cm_router.RouterOptions{
Logger: logger,
Depth: 0,
MaxUploadSize: 1, // intentionally small
})
server, err = NewMultiTenantServer(MultiTenantServerOptions{
Logger: logger,
Router: router,
StorageBackend: backend,
TimestampTolerance: time.Duration(0),
EnableAPI: true,
AllowOverwrite: true,
ChartPostFormFieldName: "chart",
ProvPostFormFieldName: "prov",
ArtifactHubRepoID: suite.ArtifactHubIds,
})
suite.NotNil(server)
suite.Nil(err, "no error creating new artifact hub repo id server")
suite.ArtifactHubRepoIDServer = server
}

func (suite *MultiTenantServerTestSuite) TearDownSuite() {
Expand Down Expand Up @@ -908,6 +950,17 @@ func (suite *MultiTenantServerTestSuite) TestMetrics() {
suite.True(strings.Contains(metrics, "chartmuseum_chart_versions_served_total{repo=\"b\"} 0"))
}

func (suite *MultiTenantServerTestSuite) TestArtifactHubRepoID() {
buffer := bytes.NewBufferString("")
res := suite.doRequest("artifacthub", "GET", "/artifacthub-repo.yml", nil, "", buffer)
suite.Equal(200, res.Status(), "200 GET /artifacthub-repo.yml")

artifactHubYmlString := buffer.Bytes()
artifactHubYmlFile := &repo.ArtifactHubFile{}
yaml.Unmarshal(artifactHubYmlString, artifactHubYmlFile)
suite.Equal(artifactHubYmlFile.RepoID, suite.ArtifactHubIds[""])
}

func (suite *MultiTenantServerTestSuite) TestRoutes() {
suite.testAllRoutes("", 0)
for org, teams := range suite.StorageDirectory {
Expand Down Expand Up @@ -945,6 +998,10 @@ func (suite *MultiTenantServerTestSuite) testAllRoutes(repo string, depth int) {
res = suite.doRequest(stype, "GET", fmt.Sprintf("%s/index.yaml", repoPrefix), nil, "")
suite.Equal(200, res.Status(), fmt.Sprintf("200 GET %s/index.yaml", repoPrefix))

// GET /:repo/artifacthub-repo.yaml
res = suite.doRequest(stype, "GET", fmt.Sprintf("%s/artifacthub-repo.yml", repoPrefix), nil, "")
suite.Equal(200, res.Status(), fmt.Sprintf("200 GET %s/artifacthub-repo.yml", repoPrefix))

// Issue #21
suite.NotEqual("", res.Header().Get("X-Request-Id"), "X-Request-Id header is present")
suite.Equal("", res.Header().Get("X-Blah-Blah-Blah"), "X-Blah-Blah-Blah header is not present")
Expand Down
Loading