mirror of
https://github.com/argoproj/argo-cd
synced 2026-05-24 09:50:08 +00:00
246 lines
5.8 KiB
Go
246 lines
5.8 KiB
Go
package helm
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver"
|
|
executil "github.com/argoproj/gitops-engine/pkg/utils/exec"
|
|
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
|
log "github.com/sirupsen/logrus"
|
|
"gopkg.in/yaml.v2"
|
|
|
|
"github.com/argoproj/argo-cd/util"
|
|
)
|
|
|
|
var (
|
|
globalLock = util.NewKeyLock()
|
|
)
|
|
|
|
type Creds struct {
|
|
Username string
|
|
Password string
|
|
CAPath string
|
|
CertData []byte
|
|
KeyData []byte
|
|
InsecureSkipVerify bool
|
|
}
|
|
|
|
type Client interface {
|
|
CleanChartCache(chart string, version *semver.Version) error
|
|
ExtractChart(chart string, version *semver.Version) (string, io.Closer, error)
|
|
GetIndex() (*Index, error)
|
|
}
|
|
|
|
func NewClient(repoURL string, creds Creds) Client {
|
|
return NewClientWithLock(repoURL, creds, globalLock)
|
|
}
|
|
|
|
func NewClientWithLock(repoURL string, creds Creds, repoLock *util.KeyLock) Client {
|
|
return &nativeHelmChart{
|
|
repoURL: repoURL,
|
|
creds: creds,
|
|
repoPath: filepath.Join(os.TempDir(), strings.Replace(repoURL, "/", "_", -1)),
|
|
repoLock: repoLock,
|
|
}
|
|
}
|
|
|
|
type nativeHelmChart struct {
|
|
repoPath string
|
|
repoURL string
|
|
creds Creds
|
|
repoLock *util.KeyLock
|
|
}
|
|
|
|
func fileExist(filePath string) (bool, error) {
|
|
if _, err := os.Stat(filePath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
} else {
|
|
return false, err
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (c *nativeHelmChart) ensureHelmChartRepoPath() error {
|
|
c.repoLock.Lock(c.repoPath)
|
|
defer c.repoLock.Unlock(c.repoPath)
|
|
|
|
err := os.Mkdir(c.repoPath, 0700)
|
|
if err != nil && !os.IsExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *nativeHelmChart) CleanChartCache(chart string, version *semver.Version) error {
|
|
return os.RemoveAll(c.getChartPath(chart, version))
|
|
}
|
|
|
|
func (c *nativeHelmChart) ExtractChart(chart string, version *semver.Version) (string, io.Closer, error) {
|
|
err := c.ensureHelmChartRepoPath()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
chartPath := c.getChartPath(chart, version)
|
|
|
|
c.repoLock.Lock(chartPath)
|
|
defer c.repoLock.Unlock(chartPath)
|
|
|
|
exists, err := fileExist(chartPath)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
if !exists {
|
|
// always use Helm V3 since we don't have chart content to determine correct Helm version
|
|
helmCmd, err := NewCmdWithVersion(c.repoPath, HelmV3)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
defer helmCmd.Close()
|
|
|
|
_, err = helmCmd.Init()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// (1) because `helm fetch` downloads an arbitrary file name, we download to an empty temp directory
|
|
tempDest, err := ioutil.TempDir("", "helm")
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
defer func() { _ = os.RemoveAll(tempDest) }()
|
|
_, err = helmCmd.Fetch(c.repoURL, chart, version.String(), tempDest, c.creds)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
// (2) then we assume that the only file downloaded into the directory is the tgz file
|
|
// and we move that to where we want it
|
|
infos, err := ioutil.ReadDir(tempDest)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
if len(infos) != 1 {
|
|
return "", nil, fmt.Errorf("expected 1 file, found %v", len(infos))
|
|
}
|
|
err = os.Rename(filepath.Join(tempDest, infos[0].Name()), chartPath)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
// untar helm chart into throw away temp directory which should be deleted as soon as no longer needed
|
|
tempDir, err := ioutil.TempDir("", "helm")
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
cmd := exec.Command("tar", "-zxvf", chartPath)
|
|
cmd.Dir = tempDir
|
|
_, err = executil.Run(cmd)
|
|
if err != nil {
|
|
_ = os.RemoveAll(tempDir)
|
|
return "", nil, err
|
|
}
|
|
return path.Join(tempDir, chart), io.NewCloser(func() error {
|
|
return os.RemoveAll(tempDir)
|
|
}), nil
|
|
}
|
|
|
|
func (c *nativeHelmChart) GetIndex() (*Index, error) {
|
|
start := time.Now()
|
|
|
|
data, err := c.loadRepoIndex()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
index := &Index{}
|
|
err = yaml.NewDecoder(bytes.NewBuffer(data)).Decode(index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.WithFields(log.Fields{"seconds": time.Since(start).Seconds()}).Info("took to get index")
|
|
|
|
return index, nil
|
|
}
|
|
|
|
func (c *nativeHelmChart) loadRepoIndex() ([]byte, error) {
|
|
repoURL, err := url.Parse(c.repoURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repoURL.Path = path.Join(repoURL.Path, "index.yaml")
|
|
|
|
req, err := http.NewRequest("GET", repoURL.String(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if c.creds.Username != "" || c.creds.Password != "" {
|
|
// only basic supported
|
|
req.SetBasicAuth(c.creds.Username, c.creds.Password)
|
|
}
|
|
|
|
tlsConf, err := newTLSConfig(c.creds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tr := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSClientConfig: tlsConf,
|
|
}
|
|
client := http.Client{Transport: tr}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, errors.New("failed to get index: " + resp.Status)
|
|
}
|
|
return ioutil.ReadAll(resp.Body)
|
|
}
|
|
|
|
func newTLSConfig(creds Creds) (*tls.Config, error) {
|
|
tlsConfig := &tls.Config{InsecureSkipVerify: creds.InsecureSkipVerify}
|
|
|
|
if creds.CAPath != "" {
|
|
caData, err := ioutil.ReadFile(creds.CAPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caData)
|
|
tlsConfig.RootCAs = caCertPool
|
|
}
|
|
|
|
// If a client cert & key is provided then configure TLS config accordingly.
|
|
if len(creds.CertData) > 0 && len(creds.KeyData) > 0 {
|
|
cert, err := tls.X509KeyPair(creds.CertData, creds.KeyData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
|
}
|
|
// nolint:staticcheck
|
|
tlsConfig.BuildNameToCertificate()
|
|
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
func (c *nativeHelmChart) getChartPath(chart string, version *semver.Version) string {
|
|
return path.Join(c.repoPath, fmt.Sprintf("%s-%v.tgz", chart, version))
|
|
}
|