mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-02-14 18:09:51 +00:00
* added/fixed helm chart images/dependencies features * added helm chart images/dependencies features to sync/manifests * more fixes for helm chart images/dependencies features * fixed tests for incorrect referenced images * fixed sync for helm chart images/dependencies * added helm chart image annotations and registry/platform features * updated ordering of experimental * added more parsing types for helm images/dependencies * a few more remove artifacts updates --------- Signed-off-by: Zack Brady <zackbrady123@gmail.com>
309 lines
7.2 KiB
Go
309 lines
7.2 KiB
Go
package chart
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
gv1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/partial"
|
|
gtypes "github.com/google/go-containerregistry/pkg/v1/types"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"hauler.dev/go/hauler/pkg/artifacts"
|
|
"hauler.dev/go/hauler/pkg/log"
|
|
"helm.sh/helm/v3/pkg/action"
|
|
"helm.sh/helm/v3/pkg/chart"
|
|
"helm.sh/helm/v3/pkg/chart/loader"
|
|
"helm.sh/helm/v3/pkg/cli"
|
|
"helm.sh/helm/v3/pkg/registry"
|
|
|
|
"hauler.dev/go/hauler/pkg/consts"
|
|
"hauler.dev/go/hauler/pkg/layer"
|
|
)
|
|
|
|
var (
|
|
_ artifacts.OCI = (*Chart)(nil)
|
|
settings = cli.New()
|
|
)
|
|
|
|
// chart implements the oci interface for chart api objects... api spec values are stored into the name, repo, and version fields
|
|
type Chart struct {
|
|
path string
|
|
annotations map[string]string
|
|
}
|
|
|
|
// newchart is a helper method that returns newlocalchart or newremotechart depending on chart contents
|
|
func NewChart(name string, opts *action.ChartPathOptions) (*Chart, error) {
|
|
chartRef := name
|
|
actionConfig := new(action.Configuration)
|
|
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), log.NewLogger(os.Stdout).Debugf); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client := action.NewInstall(actionConfig)
|
|
client.ChartPathOptions.Version = opts.Version
|
|
|
|
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
|
|
client.InsecureSkipTLSverify, client.PlainHTTP)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("missing registry client: %w", err)
|
|
}
|
|
|
|
client.SetRegistryClient(registryClient)
|
|
if registry.IsOCI(opts.RepoURL) {
|
|
chartRef = opts.RepoURL + "/" + name
|
|
} else if isUrl(opts.RepoURL) { // oci protocol registers as a valid url
|
|
client.ChartPathOptions.RepoURL = opts.RepoURL
|
|
} else { // handles cases like grafana and loki
|
|
chartRef = opts.RepoURL + "/" + name
|
|
}
|
|
|
|
// suppress helm downloader oci logs (stdout/stderr)
|
|
oldStdout := os.Stdout
|
|
oldStderr := os.Stderr
|
|
rOut, wOut, _ := os.Pipe()
|
|
rErr, wErr, _ := os.Pipe()
|
|
os.Stdout = wOut
|
|
os.Stderr = wErr
|
|
|
|
chartPath, err := client.ChartPathOptions.LocateChart(chartRef, settings)
|
|
|
|
wOut.Close()
|
|
wErr.Close()
|
|
os.Stdout = oldStdout
|
|
os.Stderr = oldStderr
|
|
_, _ = io.Copy(io.Discard, rOut)
|
|
_, _ = io.Copy(io.Discard, rErr)
|
|
rOut.Close()
|
|
rErr.Close()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Chart{
|
|
path: chartPath,
|
|
}, err
|
|
}
|
|
|
|
func (h *Chart) MediaType() string {
|
|
return consts.OCIManifestSchema1
|
|
}
|
|
|
|
func (h *Chart) Manifest() (*gv1.Manifest, error) {
|
|
cfgDesc, err := h.configDescriptor()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var layerDescs []gv1.Descriptor
|
|
ls, err := h.Layers()
|
|
for _, l := range ls {
|
|
desc, err := partial.Descriptor(l)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layerDescs = append(layerDescs, *desc)
|
|
}
|
|
|
|
return &gv1.Manifest{
|
|
SchemaVersion: 2,
|
|
MediaType: gtypes.MediaType(h.MediaType()),
|
|
Config: cfgDesc,
|
|
Layers: layerDescs,
|
|
Annotations: h.annotations,
|
|
}, nil
|
|
}
|
|
|
|
func (h *Chart) RawConfig() ([]byte, error) {
|
|
ch, err := loader.Load(h.path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(ch.Metadata)
|
|
}
|
|
|
|
func (h *Chart) configDescriptor() (gv1.Descriptor, error) {
|
|
data, err := h.RawConfig()
|
|
if err != nil {
|
|
return gv1.Descriptor{}, err
|
|
}
|
|
|
|
hash, size, err := gv1.SHA256(bytes.NewBuffer(data))
|
|
if err != nil {
|
|
return gv1.Descriptor{}, err
|
|
}
|
|
|
|
return gv1.Descriptor{
|
|
MediaType: consts.ChartConfigMediaType,
|
|
Size: size,
|
|
Digest: hash,
|
|
}, nil
|
|
}
|
|
|
|
func (h *Chart) Load() (*chart.Chart, error) {
|
|
return loader.Load(h.path)
|
|
}
|
|
|
|
func (h *Chart) Layers() ([]gv1.Layer, error) {
|
|
chartDataLayer, err := h.chartData()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return []gv1.Layer{
|
|
chartDataLayer,
|
|
// TODO: Add provenance
|
|
}, nil
|
|
}
|
|
|
|
func (h *Chart) RawChartData() ([]byte, error) {
|
|
return os.ReadFile(h.path)
|
|
}
|
|
|
|
// chartdata loads the chart contents into memory and returns a NopCloser for the contents
|
|
// normally we avoid loading into memory, but charts sizes are strictly capped at ~1MB
|
|
func (h *Chart) chartData() (gv1.Layer, error) {
|
|
info, err := os.Stat(h.path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var chartdata []byte
|
|
if info.IsDir() {
|
|
buf := &bytes.Buffer{}
|
|
gw := gzip.NewWriter(buf)
|
|
tw := tar.NewWriter(gw)
|
|
|
|
if err := filepath.WalkDir(h.path, func(path string, d fs.DirEntry, err error) error {
|
|
fi, err := d.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
header, err := tar.FileInfoHeader(fi, fi.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rel, err := filepath.Rel(filepath.Dir(h.path), path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header.Name = rel
|
|
|
|
if err := tw.WriteHeader(header); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !d.IsDir() {
|
|
data, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(tw, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tw.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := gw.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
chartdata = buf.Bytes()
|
|
|
|
} else {
|
|
data, err := os.ReadFile(h.path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chartdata = data
|
|
}
|
|
|
|
annotations := make(map[string]string)
|
|
annotations[ocispec.AnnotationTitle] = filepath.Base(h.path)
|
|
|
|
opener := func() layer.Opener {
|
|
return func() (io.ReadCloser, error) {
|
|
return io.NopCloser(bytes.NewBuffer(chartdata)), nil
|
|
}
|
|
}
|
|
chartDataLayer, err := layer.FromOpener(opener(),
|
|
layer.WithMediaType(consts.ChartLayerMediaType),
|
|
layer.WithAnnotations(annotations))
|
|
|
|
return chartDataLayer, err
|
|
}
|
|
func isUrl(name string) bool {
|
|
_, err := url.ParseRequestURI(name)
|
|
return err == nil
|
|
}
|
|
|
|
func newRegistryClient(certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool) (*registry.Client, error) {
|
|
if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSverify {
|
|
registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return registryClient, nil
|
|
}
|
|
registryClient, err := newDefaultRegistryClient(plainHTTP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return registryClient, nil
|
|
}
|
|
|
|
func newDefaultRegistryClient(plainHTTP bool) (*registry.Client, error) {
|
|
opts := []registry.ClientOption{
|
|
registry.ClientOptDebug(settings.Debug),
|
|
registry.ClientOptEnableCache(true),
|
|
registry.ClientOptWriter(io.Discard),
|
|
registry.ClientOptCredentialsFile(settings.RegistryConfig),
|
|
}
|
|
if plainHTTP {
|
|
opts = append(opts, registry.ClientOptPlainHTTP())
|
|
}
|
|
|
|
// create a new registry client
|
|
registryClient, err := registry.NewClient(opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return registryClient, nil
|
|
}
|
|
|
|
func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*registry.Client, error) {
|
|
// create a new registry client
|
|
registryClient, err := registry.NewRegistryClientWithTLS(
|
|
io.Discard,
|
|
certFile, keyFile, caFile,
|
|
insecureSkipTLSverify,
|
|
settings.RegistryConfig,
|
|
settings.Debug,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return registryClient, nil
|
|
}
|
|
|
|
// path returns the local filesystem path to the chart archive or directory
|
|
func (h *Chart) Path() string {
|
|
return h.path
|
|
}
|