mirror of https://github.com/helm/helm
Helm 3: initial registry support (#5243)
* initial registry support Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * fix dependency mess Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * add extra chart command output Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * sanitize registry path (windows fix) Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * store all sha256 blobs in same dir Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * switch to use chartutil.SaveDir Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * populate chart command long descriptions Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * remove test cache dir in teardown Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * add long description of chart export Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com> * clean up table rows code Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>pull/5280/head
parent
612d3a9f27
commit
a32f8ebb37
@ -0,0 +1,55 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/pkg/action"
|
||||
)
|
||||
|
||||
const chartHelp = `
|
||||
This command consists of multiple subcommands to interact with charts and registries.
|
||||
|
||||
It can be used to push, pull, tag, list, or remove Helm charts.
|
||||
Example usage:
|
||||
$ helm chart pull [URL]
|
||||
`
|
||||
|
||||
func newChartCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "chart",
|
||||
Short: "push, pull, tag, or remove Helm charts",
|
||||
Long: chartHelp,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newChartListCmd(cfg, out),
|
||||
newChartExportCmd(cfg, out),
|
||||
newChartPullCmd(cfg, out),
|
||||
newChartPushCmd(cfg, out),
|
||||
newChartRemoveCmd(cfg, out),
|
||||
newChartSaveCmd(cfg, out),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TODO remove once WARN lines removed from oras or containerd
|
||||
func init() {
|
||||
logrus.SetLevel(logrus.ErrorLevel)
|
||||
}
|
@ -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 main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/cmd/helm/require"
|
||||
"k8s.io/helm/pkg/action"
|
||||
)
|
||||
|
||||
const chartExportDesc = `
|
||||
Export a chart stored in local registry cache.
|
||||
|
||||
This will create a new directory with the name of
|
||||
the chart, in a format that developers can modify
|
||||
and check into source control if desired.
|
||||
`
|
||||
|
||||
func newChartExportCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "export [ref]",
|
||||
Short: "export a chart to directory",
|
||||
Long: chartExportDesc,
|
||||
Args: require.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ref := args[0]
|
||||
return action.NewChartExport(cfg).Run(out, ref)
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/pkg/action"
|
||||
)
|
||||
|
||||
const chartListDesc = `
|
||||
List all charts in the local registry cache.
|
||||
|
||||
Charts are sorted by ref name, alphabetically.
|
||||
`
|
||||
|
||||
func newChartListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "list all saved charts",
|
||||
Long: chartListDesc,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return action.NewChartList(cfg).Run(out)
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/cmd/helm/require"
|
||||
"k8s.io/helm/pkg/action"
|
||||
)
|
||||
|
||||
const chartPullDesc = `
|
||||
Download a chart from a remote registry.
|
||||
|
||||
This will store the chart in the local registry cache to be used later.
|
||||
`
|
||||
|
||||
func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "pull [ref]",
|
||||
Short: "pull a chart from remote",
|
||||
Long: chartPullDesc,
|
||||
Args: require.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ref := args[0]
|
||||
return action.NewChartPull(cfg).Run(out, ref)
|
||||
},
|
||||
}
|
||||
}
|
@ -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 main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/cmd/helm/require"
|
||||
"k8s.io/helm/pkg/action"
|
||||
)
|
||||
|
||||
const chartPushDesc = `
|
||||
Upload a chart to a remote registry.
|
||||
|
||||
Note: the ref must already exist in the local registry cache.
|
||||
|
||||
Must first run "helm chart save" or "helm chart pull".
|
||||
`
|
||||
|
||||
func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "push [ref]",
|
||||
Short: "push a chart to remote",
|
||||
Long: chartPushDesc,
|
||||
Args: require.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ref := args[0]
|
||||
return action.NewChartPush(cfg).Run(out, ref)
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/cmd/helm/require"
|
||||
"k8s.io/helm/pkg/action"
|
||||
)
|
||||
|
||||
const chartRemoveDesc = `
|
||||
Remove a chart from the local registry cache.
|
||||
|
||||
Note: the chart content will still exist in the cache,
|
||||
but it will no longer appear in "helm chart list".
|
||||
|
||||
To remove all unlinked content, please run "helm chart prune". (TODO)
|
||||
`
|
||||
|
||||
func newChartRemoveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "remove [ref]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "remove a chart",
|
||||
Long: chartRemoveDesc,
|
||||
Args: require.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ref := args[0]
|
||||
return action.NewChartRemove(cfg).Run(out, ref)
|
||||
},
|
||||
}
|
||||
}
|
@ -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 main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/cmd/helm/require"
|
||||
"k8s.io/helm/pkg/action"
|
||||
)
|
||||
|
||||
const chartSaveDesc = `
|
||||
Store a copy of chart in local registry cache.
|
||||
|
||||
Note: modifying the chart after this operation will
|
||||
not change the item as it exists in the cache.
|
||||
`
|
||||
|
||||
func newChartSaveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "save [path] [ref]",
|
||||
Short: "save a chart directory",
|
||||
Long: chartSaveDesc,
|
||||
Args: require.MinimumNArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
path := args[0]
|
||||
ref := args[1]
|
||||
return action.NewChartSave(cfg).Run(out, path, ref)
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
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 action
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/registry"
|
||||
)
|
||||
|
||||
// ChartExport performs a chart export operation.
|
||||
type ChartExport struct {
|
||||
cfg *Configuration
|
||||
}
|
||||
|
||||
// NewChartExport creates a new ChartExport object with the given configuration.
|
||||
func NewChartExport(cfg *Configuration) *ChartExport {
|
||||
return &ChartExport{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the chart export operation
|
||||
func (a *ChartExport) Run(out io.Writer, ref string) error {
|
||||
r, err := registry.ParseReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := a.cfg.RegistryClient.LoadChart(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the chart to local directory
|
||||
// TODO: make destination dir configurable
|
||||
err = chartutil.SaveDir(ch, ".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Exported to %s/\n", ch.Metadata.Name)
|
||||
return nil
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
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 action
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// ChartList performs a chart list operation.
|
||||
type ChartList struct {
|
||||
cfg *Configuration
|
||||
}
|
||||
|
||||
// NewChartList creates a new ChartList object with the given configuration.
|
||||
func NewChartList(cfg *Configuration) *ChartList {
|
||||
return &ChartList{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the chart list operation
|
||||
func (a *ChartList) Run(out io.Writer) error {
|
||||
return a.cfg.RegistryClient.PrintChartTable()
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 action
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/helm/pkg/registry"
|
||||
)
|
||||
|
||||
// ChartPull performs a chart pull operation.
|
||||
type ChartPull struct {
|
||||
cfg *Configuration
|
||||
}
|
||||
|
||||
// NewChartPull creates a new ChartPull object with the given configuration.
|
||||
func NewChartPull(cfg *Configuration) *ChartPull {
|
||||
return &ChartPull{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the chart pull operation
|
||||
func (a *ChartPull) Run(out io.Writer, ref string) error {
|
||||
r, err := registry.ParseReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.cfg.RegistryClient.PullChart(r)
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 action
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/helm/pkg/registry"
|
||||
)
|
||||
|
||||
// ChartPush performs a chart push operation.
|
||||
type ChartPush struct {
|
||||
cfg *Configuration
|
||||
}
|
||||
|
||||
// NewChartPush creates a new ChartPush object with the given configuration.
|
||||
func NewChartPush(cfg *Configuration) *ChartPush {
|
||||
return &ChartPush{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the chart push operation
|
||||
func (a *ChartPush) Run(out io.Writer, ref string) error {
|
||||
r, err := registry.ParseReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.cfg.RegistryClient.PushChart(r)
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 action
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/helm/pkg/registry"
|
||||
)
|
||||
|
||||
// ChartRemove performs a chart remove operation.
|
||||
type ChartRemove struct {
|
||||
cfg *Configuration
|
||||
}
|
||||
|
||||
// NewChartRemove creates a new ChartRemove object with the given configuration.
|
||||
func NewChartRemove(cfg *Configuration) *ChartRemove {
|
||||
return &ChartRemove{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the chart remove operation
|
||||
func (a *ChartRemove) Run(out io.Writer, ref string) error {
|
||||
r, err := registry.ParseReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.cfg.RegistryClient.RemoveChart(r)
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
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 action
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"k8s.io/helm/pkg/chart/loader"
|
||||
"k8s.io/helm/pkg/registry"
|
||||
)
|
||||
|
||||
// ChartSave performs a chart save operation.
|
||||
type ChartSave struct {
|
||||
cfg *Configuration
|
||||
}
|
||||
|
||||
// NewChartSave creates a new ChartSave object with the given configuration.
|
||||
func NewChartSave(cfg *Configuration) *ChartSave {
|
||||
return &ChartSave{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the chart save operation
|
||||
func (a *ChartSave) Run(out io.Writer, path string, ref string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := loader.LoadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := registry.ParseReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.cfg.RegistryClient.SaveChart(ch, r)
|
||||
}
|
@ -0,0 +1,493 @@
|
||||
/*
|
||||
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 registry // import "k8s.io/helm/pkg/registry"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
orascontent "github.com/deislabs/oras/pkg/content"
|
||||
"github.com/docker/go-units"
|
||||
checksum "github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"k8s.io/helm/pkg/chart"
|
||||
"k8s.io/helm/pkg/chart/loader"
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
)
|
||||
|
||||
var (
|
||||
tableHeaders = []string{"name", "version", "digest", "size", "created"}
|
||||
)
|
||||
|
||||
type (
|
||||
filesystemCache struct {
|
||||
out io.Writer
|
||||
rootDir string
|
||||
store *orascontent.Memorystore
|
||||
}
|
||||
)
|
||||
|
||||
func (cache *filesystemCache) LayersToChart(layers []ocispec.Descriptor) (*chart.Chart, error) {
|
||||
metaLayer, contentLayer, err := extractLayers(layers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name, version, err := extractChartNameVersionFromLayer(contentLayer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Obtain raw chart meta content (json)
|
||||
_, metaJSONRaw, ok := cache.store.Get(metaLayer)
|
||||
if !ok {
|
||||
return nil, errors.New("error retrieving meta layer")
|
||||
}
|
||||
|
||||
// Construct chart metadata object
|
||||
metadata := chart.Metadata{}
|
||||
err = json.Unmarshal(metaJSONRaw, &metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata.Name = name
|
||||
metadata.Version = version
|
||||
|
||||
// Obtain raw chart content
|
||||
_, contentRaw, ok := cache.store.Get(contentLayer)
|
||||
if !ok {
|
||||
return nil, errors.New("error retrieving meta layer")
|
||||
}
|
||||
|
||||
// Construct chart object and attach metadata
|
||||
ch, err := loader.LoadArchive(bytes.NewBuffer(contentRaw))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch.Metadata = &metadata
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (cache *filesystemCache) ChartToLayers(ch *chart.Chart) ([]ocispec.Descriptor, error) {
|
||||
|
||||
// extract/separate the name and version from other metadata
|
||||
if ch.Metadata == nil {
|
||||
return nil, errors.New("chart does not contain metadata")
|
||||
}
|
||||
name := ch.Metadata.Name
|
||||
version := ch.Metadata.Version
|
||||
|
||||
// Create meta layer, clear name and version from Chart.yaml and convert to json
|
||||
ch.Metadata.Name = ""
|
||||
ch.Metadata.Version = ""
|
||||
metaJSONRaw, err := json.Marshal(ch.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaMediaType, metaJSONRaw)
|
||||
|
||||
// Create content layer
|
||||
// TODO: something better than this hack. Currently needed for chartutil.Save()
|
||||
// If metadata does not contain Name or Version, an error is returned
|
||||
// such as "no chart name specified (Chart.yaml)"
|
||||
ch.Metadata = &chart.Metadata{Name: "-", Version: "-"}
|
||||
destDir := mkdir(filepath.Join(cache.rootDir, "blobs", ".build"))
|
||||
tmpFile, err := chartutil.Save(ch, destDir)
|
||||
defer os.Remove(tmpFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save")
|
||||
}
|
||||
contentRaw, err := ioutil.ReadFile(tmpFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentMediaType, contentRaw)
|
||||
|
||||
// Set annotations
|
||||
contentLayer.Annotations[HelmChartNameAnnotation] = name
|
||||
contentLayer.Annotations[HelmChartVersionAnnotation] = version
|
||||
|
||||
layers := []ocispec.Descriptor{metaLayer, contentLayer}
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func (cache *filesystemCache) LoadReference(ref *Reference) ([]ocispec.Descriptor, error) {
|
||||
tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Locator), "tags", tagOrDefault(ref.Object))
|
||||
|
||||
// add meta layer
|
||||
metaJSONRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "meta"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaMediaType, metaJSONRaw)
|
||||
|
||||
// add content layer
|
||||
contentRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "content"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentMediaType, contentRaw)
|
||||
|
||||
// set annotations on content layer (chart name and version)
|
||||
err = setLayerAnnotationsFromChartLink(contentLayer, filepath.Join(tagDir, "chart"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
printChartSummary(cache.out, metaLayer, contentLayer)
|
||||
layers := []ocispec.Descriptor{metaLayer, contentLayer}
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func (cache *filesystemCache) StoreReference(ref *Reference, layers []ocispec.Descriptor) (bool, error) {
|
||||
tag := tagOrDefault(ref.Object)
|
||||
tagDir := mkdir(filepath.Join(cache.rootDir, "refs", escape(ref.Locator), "tags", tag))
|
||||
|
||||
// Retrieve just the meta and content layers
|
||||
metaLayer, contentLayer, err := extractLayers(layers)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Extract chart name and version
|
||||
name, version, err := extractChartNameVersionFromLayer(contentLayer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Create chart file
|
||||
chartPath, err := createChartFile(filepath.Join(cache.rootDir, "charts"), name, version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Create chart symlink
|
||||
err = createSymlink(chartPath, filepath.Join(tagDir, "chart"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Save meta blob
|
||||
metaExists, metaPath := digestPath(filepath.Join(cache.rootDir, "blobs"), metaLayer.Digest)
|
||||
if !metaExists {
|
||||
fmt.Fprintf(cache.out, "%s: Saving meta (%s)\n",
|
||||
shortDigest(metaLayer.Digest.Hex()), byteCountBinary(metaLayer.Size))
|
||||
_, metaJSONRaw, ok := cache.store.Get(metaLayer)
|
||||
if !ok {
|
||||
return false, errors.New("error retrieving meta layer")
|
||||
}
|
||||
err = writeFile(metaPath, metaJSONRaw)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create meta symlink
|
||||
err = createSymlink(metaPath, filepath.Join(tagDir, "meta"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Save content blob
|
||||
contentExists, contentPath := digestPath(filepath.Join(cache.rootDir, "blobs"), contentLayer.Digest)
|
||||
if !contentExists {
|
||||
fmt.Fprintf(cache.out, "%s: Saving content (%s)\n",
|
||||
shortDigest(contentLayer.Digest.Hex()), byteCountBinary(contentLayer.Size))
|
||||
_, contentRaw, ok := cache.store.Get(contentLayer)
|
||||
if !ok {
|
||||
return false, errors.New("error retrieving content layer")
|
||||
}
|
||||
err = writeFile(contentPath, contentRaw)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create content symlink
|
||||
err = createSymlink(contentPath, filepath.Join(tagDir, "content"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
printChartSummary(cache.out, metaLayer, contentLayer)
|
||||
return metaExists && contentExists, nil
|
||||
}
|
||||
|
||||
func (cache *filesystemCache) DeleteReference(ref *Reference) error {
|
||||
tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Locator), "tags", tagOrDefault(ref.Object))
|
||||
if _, err := os.Stat(tagDir); os.IsNotExist(err) {
|
||||
return errors.New("ref not found")
|
||||
}
|
||||
return os.RemoveAll(tagDir)
|
||||
}
|
||||
|
||||
func (cache *filesystemCache) TableRows() ([][]interface{}, error) {
|
||||
return getRefsSorted(filepath.Join(cache.rootDir, "refs"))
|
||||
}
|
||||
|
||||
// escape sanitizes a registry URL to remove characters such as ":"
|
||||
// which are illegal on windows
|
||||
func escape(s string) string {
|
||||
return strings.Replace(s, ":", "_", -1)
|
||||
}
|
||||
|
||||
// escape reverses escape
|
||||
func unescape(s string) string {
|
||||
return strings.Replace(s, "_", ":", -1)
|
||||
}
|
||||
|
||||
// printChartSummary prints details about a chart layers
|
||||
func printChartSummary(out io.Writer, metaLayer ocispec.Descriptor, contentLayer ocispec.Descriptor) {
|
||||
fmt.Fprintf(out, "Name: %s\n", contentLayer.Annotations[HelmChartNameAnnotation])
|
||||
fmt.Fprintf(out, "Version: %s\n", contentLayer.Annotations[HelmChartVersionAnnotation])
|
||||
fmt.Fprintf(out, "Meta: %s\n", metaLayer.Digest)
|
||||
fmt.Fprintf(out, "Content: %s\n", contentLayer.Digest)
|
||||
}
|
||||
|
||||
// fileExists determines if a file exists
|
||||
func fileExists(path string) bool {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// mkdir will create a directory (no error check) and return the path
|
||||
func mkdir(dir string) string {
|
||||
os.MkdirAll(dir, 0755)
|
||||
return dir
|
||||
}
|
||||
|
||||
// createSymlink creates a symbolic link, deleting existing one if exists
|
||||
func createSymlink(src string, dest string) error {
|
||||
os.Remove(dest)
|
||||
err := os.Symlink(src, dest)
|
||||
return err
|
||||
}
|
||||
|
||||
// getSymlinkDestContent returns the file contents of a symlink's destination
|
||||
func getSymlinkDestContent(linkPath string) ([]byte, error) {
|
||||
src, err := os.Readlink(linkPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.ReadFile(src)
|
||||
}
|
||||
|
||||
// setLayerAnnotationsFromChartLink will set chart name/version annotations on a layer
|
||||
// based on the path of the chart link destination
|
||||
func setLayerAnnotationsFromChartLink(layer ocispec.Descriptor, chartLinkPath string) error {
|
||||
src, err := os.Readlink(chartLinkPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// example path: /some/path/charts/mychart/versions/1.2.0
|
||||
chartName := filepath.Base(filepath.Dir(filepath.Dir(src)))
|
||||
chartVersion := filepath.Base(src)
|
||||
layer.Annotations[HelmChartNameAnnotation] = chartName
|
||||
layer.Annotations[HelmChartVersionAnnotation] = chartVersion
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractLayers obtains the meta and content layers from a list of layers
|
||||
func extractLayers(layers []ocispec.Descriptor) (ocispec.Descriptor, ocispec.Descriptor, error) {
|
||||
var metaLayer, contentLayer ocispec.Descriptor
|
||||
|
||||
if len(layers) != 2 {
|
||||
return metaLayer, contentLayer, errors.New("manifest does not contain exactly 2 layers")
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
switch layer.MediaType {
|
||||
case HelmChartMetaMediaType:
|
||||
metaLayer = layer
|
||||
case HelmChartContentMediaType:
|
||||
contentLayer = layer
|
||||
}
|
||||
}
|
||||
|
||||
if metaLayer.Size == 0 {
|
||||
return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart meta layer")
|
||||
}
|
||||
|
||||
if contentLayer.Size == 0 {
|
||||
return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart content layer")
|
||||
}
|
||||
|
||||
return metaLayer, contentLayer, nil
|
||||
}
|
||||
|
||||
// extractChartNameVersionFromLayer retrieves the chart name and version from layer annotations
|
||||
func extractChartNameVersionFromLayer(layer ocispec.Descriptor) (string, string, error) {
|
||||
name, ok := layer.Annotations[HelmChartNameAnnotation]
|
||||
if !ok {
|
||||
return "", "", errors.New("could not find chart name in annotations")
|
||||
}
|
||||
version, ok := layer.Annotations[HelmChartVersionAnnotation]
|
||||
if !ok {
|
||||
return "", "", errors.New("could not find chart version in annotations")
|
||||
}
|
||||
return name, version, nil
|
||||
}
|
||||
|
||||
// createChartFile creates a file under "<chartsdir>" dir which is linked to by ref
|
||||
func createChartFile(chartsRootDir string, name string, version string) (string, error) {
|
||||
chartPathDir := filepath.Join(chartsRootDir, name, "versions")
|
||||
chartPath := filepath.Join(chartPathDir, version)
|
||||
if _, err := os.Stat(chartPath); err != nil && os.IsNotExist(err) {
|
||||
os.MkdirAll(chartPathDir, 0755)
|
||||
err := ioutil.WriteFile(chartPath, []byte("-"), 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return chartPath, nil
|
||||
}
|
||||
|
||||
// digestPath returns the path to addressable content, and whether the file exists
|
||||
func digestPath(rootDir string, digest checksum.Digest) (bool, string) {
|
||||
path := filepath.Join(rootDir, "sha256", digest.Hex())
|
||||
exists := fileExists(path)
|
||||
return exists, path
|
||||
}
|
||||
|
||||
// writeFile creates a path, ensuring parent directory
|
||||
func writeFile(path string, c []byte) error {
|
||||
os.MkdirAll(filepath.Dir(path), 0755)
|
||||
return ioutil.WriteFile(path, c, 0644)
|
||||
}
|
||||
|
||||
// byteCountBinary produces a human-readable file size
|
||||
func byteCountBinary(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// tagOrDefault returns the tag if present, if not the default tag
|
||||
func tagOrDefault(tag string) string {
|
||||
if tag != "" {
|
||||
return tag
|
||||
}
|
||||
return HelmChartDefaultTag
|
||||
}
|
||||
|
||||
// shortDigest returns first 7 characters of a sha256 digest
|
||||
func shortDigest(digest string) string {
|
||||
if len(digest) == 64 {
|
||||
return digest[:7]
|
||||
}
|
||||
return digest
|
||||
}
|
||||
|
||||
// getRefsSorted returns a map of all refs stored in a refsRootDir
|
||||
func getRefsSorted(refsRootDir string) ([][]interface{}, error) {
|
||||
refsMap := map[string]map[string]string{}
|
||||
|
||||
// Walk the storage dir, check for symlinks under "refs" dir pointing to valid files in "blobs/" and "charts/"
|
||||
err := filepath.Walk(refsRootDir, func(path string, fileInfo os.FileInfo, fileError error) error {
|
||||
|
||||
// Check if this file is a symlink
|
||||
linkPath, err := os.Readlink(path)
|
||||
if err == nil {
|
||||
destFileInfo, err := os.Stat(linkPath)
|
||||
if err == nil {
|
||||
tagDir := filepath.Dir(path)
|
||||
|
||||
// Determine the ref
|
||||
locator := unescape(strings.TrimLeft(
|
||||
strings.TrimPrefix(filepath.Dir(filepath.Dir(tagDir)), refsRootDir), "/\\"))
|
||||
object := filepath.Base(tagDir)
|
||||
ref := fmt.Sprintf("%s:%s", locator, object)
|
||||
|
||||
// Init hashmap entry if does not exist
|
||||
if _, ok := refsMap[ref]; !ok {
|
||||
refsMap[ref] = map[string]string{}
|
||||
}
|
||||
|
||||
// Add data to entry based on file name (symlink name)
|
||||
base := filepath.Base(path)
|
||||
switch base {
|
||||
case "chart":
|
||||
refsMap[ref]["name"] = filepath.Base(filepath.Dir(filepath.Dir(linkPath)))
|
||||
refsMap[ref]["version"] = destFileInfo.Name()
|
||||
case "content":
|
||||
|
||||
// Make sure the filename looks like a sha256 digest (64 chars)
|
||||
digest := destFileInfo.Name()
|
||||
if len(digest) == 64 {
|
||||
refsMap[ref]["digest"] = shortDigest(digest)
|
||||
refsMap[ref]["size"] = byteCountBinary(destFileInfo.Size())
|
||||
refsMap[ref]["created"] = units.HumanDuration(time.Now().UTC().Sub(destFileInfo.ModTime()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Filter out any refs that are incomplete (do not have all required fields)
|
||||
for k, ref := range refsMap {
|
||||
allKeysFound := true
|
||||
for _, v := range tableHeaders {
|
||||
if _, ok := ref[v]; !ok {
|
||||
allKeysFound = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allKeysFound {
|
||||
delete(refsMap, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort and convert to format expected by uitable
|
||||
refs := make([][]interface{}, len(refsMap))
|
||||
keys := make([]string, 0, len(refsMap))
|
||||
for key := range refsMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for i, key := range keys {
|
||||
refs[i] = make([]interface{}, len(tableHeaders)+1)
|
||||
refs[i][0] = key
|
||||
ref := refsMap[key]
|
||||
for j, k := range tableHeaders {
|
||||
refs[i][j+1] = ref[k]
|
||||
}
|
||||
}
|
||||
|
||||
return refs, err
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
/*
|
||||
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 registry // import "k8s.io/helm/pkg/registry"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
orascontent "github.com/deislabs/oras/pkg/content"
|
||||
"github.com/deislabs/oras/pkg/oras"
|
||||
"github.com/gosuri/uitable"
|
||||
|
||||
"k8s.io/helm/pkg/chart"
|
||||
)
|
||||
|
||||
type (
|
||||
// ClientOptions is used to construct a new client
|
||||
ClientOptions struct {
|
||||
Out io.Writer
|
||||
Resolver Resolver
|
||||
CacheRootDir string
|
||||
}
|
||||
|
||||
// Client works with OCI-compliant registries and local Helm chart cache
|
||||
Client struct {
|
||||
out io.Writer
|
||||
resolver Resolver
|
||||
cache *filesystemCache // TODO: something more robust
|
||||
}
|
||||
)
|
||||
|
||||
// NewClient returns a new registry client with config
|
||||
func NewClient(options *ClientOptions) *Client {
|
||||
return &Client{
|
||||
out: options.Out,
|
||||
resolver: options.Resolver,
|
||||
cache: &filesystemCache{
|
||||
out: options.Out,
|
||||
rootDir: options.CacheRootDir,
|
||||
store: orascontent.NewMemoryStore(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PushChart uploads a chart to a registry
|
||||
func (c *Client) PushChart(ref *Reference) error {
|
||||
c.setDefaultTag(ref)
|
||||
fmt.Fprintf(c.out, "The push refers to repository [%s]\n", ref.Locator)
|
||||
layers, err := c.cache.LoadReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = oras.Push(context.Background(), c.resolver, ref.String(), c.cache.store, layers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var totalSize int64
|
||||
for _, layer := range layers {
|
||||
totalSize += layer.Size
|
||||
}
|
||||
fmt.Fprintf(c.out,
|
||||
"%s: pushed to remote (%d layers, %s total)\n", ref.Object, len(layers), byteCountBinary(totalSize))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PullChart downloads a chart from a registry
|
||||
func (c *Client) PullChart(ref *Reference) error {
|
||||
c.setDefaultTag(ref)
|
||||
fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Object, ref.Locator)
|
||||
layers, err := oras.Pull(context.Background(), c.resolver, ref.String(), c.cache.store, KnownMediaTypes()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exists, err := c.cache.StoreReference(ref, layers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s:%s\n", ref.Locator, ref.Object)
|
||||
} else {
|
||||
fmt.Fprintf(c.out, "Status: Chart is up to date for %s:%s\n", ref.Locator, ref.Object)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveChart stores a copy of chart in local cache
|
||||
func (c *Client) SaveChart(ch *chart.Chart, ref *Reference) error {
|
||||
c.setDefaultTag(ref)
|
||||
layers, err := c.cache.ChartToLayers(ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.cache.StoreReference(ref, layers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.out, "%s: saved\n", ref.Object)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadChart retrieves a chart object by reference
|
||||
func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) {
|
||||
c.setDefaultTag(ref)
|
||||
layers, err := c.cache.LoadReference(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch, err := c.cache.LayersToChart(layers)
|
||||
return ch, err
|
||||
}
|
||||
|
||||
// RemoveChart deletes a locally saved chart
|
||||
func (c *Client) RemoveChart(ref *Reference) error {
|
||||
c.setDefaultTag(ref)
|
||||
err := c.cache.DeleteReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.out, "%s: removed\n", ref.Object)
|
||||
return err
|
||||
}
|
||||
|
||||
// PrintChartTable prints a list of locally stored charts
|
||||
func (c *Client) PrintChartTable() error {
|
||||
table := uitable.New()
|
||||
table.MaxColWidth = 60
|
||||
table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED")
|
||||
rows, err := c.cache.TableRows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
table.AddRow(row...)
|
||||
}
|
||||
fmt.Fprintln(c.out, table.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) setDefaultTag(ref *Reference) {
|
||||
if ref.Object == "" {
|
||||
ref.Object = HelmChartDefaultTag
|
||||
fmt.Fprintf(c.out, "Using default tag: %s\n", HelmChartDefaultTag)
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
/*
|
||||
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 registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"k8s.io/helm/pkg/chart"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/remotes/docker"
|
||||
"github.com/docker/distribution/configuration"
|
||||
"github.com/docker/distribution/registry"
|
||||
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
var (
|
||||
testCacheRootDir = "helm-registry-test"
|
||||
)
|
||||
|
||||
type RegistryClientTestSuite struct {
|
||||
suite.Suite
|
||||
Out io.Writer
|
||||
DockerRegistryHost string
|
||||
CacheRootDir string
|
||||
RegistryClient *Client
|
||||
}
|
||||
|
||||
func (suite *RegistryClientTestSuite) SetupSuite() {
|
||||
suite.CacheRootDir = testCacheRootDir
|
||||
|
||||
// Init test client
|
||||
var out bytes.Buffer
|
||||
suite.Out = &out
|
||||
suite.RegistryClient = NewClient(&ClientOptions{
|
||||
Out: suite.Out,
|
||||
Resolver: Resolver{
|
||||
Resolver: docker.NewResolver(docker.ResolverOptions{}),
|
||||
},
|
||||
CacheRootDir: suite.CacheRootDir,
|
||||
})
|
||||
|
||||
// Registry config
|
||||
config := &configuration.Configuration{}
|
||||
port, err := getFreePort()
|
||||
if err != nil {
|
||||
suite.Nil(err, "no error finding free port for test registry")
|
||||
}
|
||||
suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port)
|
||||
config.HTTP.Addr = fmt.Sprintf(":%d", port)
|
||||
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
|
||||
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
|
||||
dockerRegistry, err := registry.NewRegistry(context.Background(), config)
|
||||
suite.Nil(err, "no error creating test registry")
|
||||
|
||||
// Start Docker registry
|
||||
go dockerRegistry.ListenAndServe()
|
||||
}
|
||||
|
||||
func (suite *RegistryClientTestSuite) TearDownSuite() {
|
||||
os.RemoveAll(suite.CacheRootDir)
|
||||
}
|
||||
|
||||
func (suite *RegistryClientTestSuite) Test_0_SaveChart() {
|
||||
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
|
||||
// empty chart
|
||||
err = suite.RegistryClient.SaveChart(&chart.Chart{}, ref)
|
||||
suite.NotNil(err)
|
||||
|
||||
// valid chart
|
||||
ch := &chart.Chart{}
|
||||
ch.Metadata = &chart.Metadata{
|
||||
Name: "testchart",
|
||||
Version: "1.2.3",
|
||||
}
|
||||
err = suite.RegistryClient.SaveChart(ch, ref)
|
||||
suite.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *RegistryClientTestSuite) Test_1_LoadChart() {
|
||||
|
||||
// non-existent ref
|
||||
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
ch, err := suite.RegistryClient.LoadChart(ref)
|
||||
suite.NotNil(err)
|
||||
|
||||
// existing ref
|
||||
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
ch, err = suite.RegistryClient.LoadChart(ref)
|
||||
suite.Nil(err)
|
||||
suite.Equal("testchart", ch.Metadata.Name)
|
||||
suite.Equal("1.2.3", ch.Metadata.Version)
|
||||
}
|
||||
|
||||
func (suite *RegistryClientTestSuite) Test_2_PushChart() {
|
||||
|
||||
// non-existent ref
|
||||
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
err = suite.RegistryClient.PushChart(ref)
|
||||
suite.NotNil(err)
|
||||
|
||||
// existing ref
|
||||
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
err = suite.RegistryClient.PushChart(ref)
|
||||
suite.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *RegistryClientTestSuite) Test_3_PullChart() {
|
||||
|
||||
// non-existent ref
|
||||
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
err = suite.RegistryClient.PullChart(ref)
|
||||
suite.NotNil(err)
|
||||
|
||||
// existing ref
|
||||
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
err = suite.RegistryClient.PullChart(ref)
|
||||
suite.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *RegistryClientTestSuite) Test_4_PrintChartTable() {
|
||||
err := suite.RegistryClient.PrintChartTable()
|
||||
suite.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *RegistryClientTestSuite) Test_5_RemoveChart() {
|
||||
|
||||
// non-existent ref
|
||||
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
err = suite.RegistryClient.RemoveChart(ref)
|
||||
suite.NotNil(err)
|
||||
|
||||
// existing ref
|
||||
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
|
||||
suite.Nil(err)
|
||||
err = suite.RegistryClient.RemoveChart(ref)
|
||||
suite.Nil(err)
|
||||
}
|
||||
|
||||
func TestRegistryClientTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(RegistryClientTestSuite))
|
||||
}
|
||||
|
||||
// borrowed from https://github.com/phayes/freeport
|
||||
func getFreePort() (int, error) {
|
||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
l, err := net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer l.Close()
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
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 registry // import "k8s.io/helm/pkg/registry"
|
||||
|
||||
const (
|
||||
// HelmChartDefaultTag is the default tag used when storing a chart reference with no tag
|
||||
HelmChartDefaultTag = "latest"
|
||||
|
||||
// HelmChartMetaMediaType is the reserved media type for Helm chart metadata
|
||||
HelmChartMetaMediaType = "application/vnd.cncf.helm.chart.meta.v1+json"
|
||||
|
||||
// HelmChartContentMediaType is the reserved media type for Helm chart package content
|
||||
HelmChartContentMediaType = "application/vnd.cncf.helm.chart.content.v1+tar"
|
||||
|
||||
// HelmChartMetaFileName is the reserved file name for Helm chart metadata
|
||||
HelmChartMetaFileName = "chart-meta.json"
|
||||
|
||||
// HelmChartContentFileName is the reserved file name for Helm chart package content
|
||||
HelmChartContentFileName = "chart-content.tgz"
|
||||
|
||||
// HelmChartNameAnnotation is the reserved annotation key for Helm chart name
|
||||
HelmChartNameAnnotation = "sh.helm.chart.name"
|
||||
|
||||
// HelmChartVersionAnnotation is the reserved annotation key for Helm chart version
|
||||
HelmChartVersionAnnotation = "sh.helm.chart.version"
|
||||
)
|
||||
|
||||
// KnownMediaTypes returns a list of layer mediaTypes that the Helm client knows about
|
||||
func KnownMediaTypes() []string {
|
||||
return []string{
|
||||
HelmChartMetaMediaType,
|
||||
HelmChartContentMediaType,
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
knownMediaTypes := KnownMediaTypes()
|
||||
assert.Contains(t, knownMediaTypes, HelmChartMetaMediaType)
|
||||
assert.Contains(t, knownMediaTypes, HelmChartContentMediaType)
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
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 registry // import "k8s.io/helm/pkg/registry"
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/reference"
|
||||
)
|
||||
|
||||
type (
|
||||
// Reference defines the main components of a reference specification
|
||||
Reference struct {
|
||||
*reference.Spec
|
||||
}
|
||||
)
|
||||
|
||||
// ParseReference converts a string to a Reference
|
||||
func ParseReference(s string) (*Reference, error) {
|
||||
spec, err := reference.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ref := Reference{&spec}
|
||||
return &ref, nil
|
||||
}
|
||||
|
||||
// Repo returns a reference's repo minus the hostname
|
||||
func (ref *Reference) Repo() string {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(ref.Locator, ref.Hostname()), "/")
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
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 registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReference(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
// bad ref
|
||||
s := ""
|
||||
_, err := ParseReference(s)
|
||||
is.Error(err)
|
||||
|
||||
// good refs
|
||||
s = "localhost:5000/mychart:latest"
|
||||
ref, err := ParseReference(s)
|
||||
is.NoError(err)
|
||||
is.Equal("localhost:5000", ref.Hostname())
|
||||
is.Equal("mychart", ref.Repo())
|
||||
is.Equal("localhost:5000/mychart", ref.Locator)
|
||||
is.Equal("latest", ref.Object)
|
||||
|
||||
s = "my.host.com/my/nested/repo:1.2.3"
|
||||
ref, err = ParseReference(s)
|
||||
is.NoError(err)
|
||||
is.Equal("my.host.com", ref.Hostname())
|
||||
is.Equal("my/nested/repo", ref.Repo())
|
||||
is.Equal("my.host.com/my/nested/repo", ref.Locator)
|
||||
is.Equal("1.2.3", ref.Object)
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 registry // import "k8s.io/helm/pkg/registry"
|
||||
|
||||
import (
|
||||
"github.com/containerd/containerd/remotes"
|
||||
)
|
||||
|
||||
type (
|
||||
// Resolver provides remotes based on a locator
|
||||
Resolver struct {
|
||||
remotes.Resolver
|
||||
}
|
||||
)
|
Loading…
Reference in new issue