Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Support import of repositories for Helm v2 _and_ v3 #141

Merged
merged 4 commits into from
Dec 11, 2019
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
40 changes: 31 additions & 9 deletions cmd/helm-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ var (

listenAddr *string

versionedHelmRepositoryIndexes *[]string

enabledHelmVersions *[]string
defaultHelmVersion *string
)
Expand Down Expand Up @@ -120,11 +122,13 @@ func init() {
gitPollInterval = fs.Duration("git-poll-interval", 5*time.Minute, "period on which to poll git chart sources for changes")
gitDefaultRef = fs.String("git-default-ref", "master", "ref to clone chart from if ref is unspecified in a HelmRelease")

versionedHelmRepositoryIndexes = fs.StringSlice("helm-repository-import", nil, "Targeted version and the path of the Helm repository index to import, i.e. v3:/tmp/v3/index.yaml,v2:/tmp/v2/index.yaml")

enabledHelmVersions = fs.StringSlice("enabled-helm-versions", []string{v2.VERSION, v3.VERSION}, "Helm versions supported by this operator instance")
}

func main() {
// Explicitly initialize klog to enable stderr logging,
// explicitly initialize klog to enable stderr logging,
// and parse our own flags.
klog.InitFlags(nil)
fs.Parse(os.Args)
Expand All @@ -134,7 +138,7 @@ func main() {
os.Exit(0)
}

// Support enabling the Helm supported versions through an
// support enabling the Helm supported versions through an
// environment variable.
helmVersionEnv := getEnvAsSlice("HELM_VERSION", []string{})
if len(helmVersionEnv) > 0 && !fs.Changed("enabled-helm-versions") {
Expand Down Expand Up @@ -171,6 +175,7 @@ func main() {

mainLogger := log.With(logger, "component", "helm-operator")

// build Kubernetes clients
cfg, err := clientcmd.BuildConfigFromFlags(*master, *kubeconfig)
if err != nil {
mainLogger.Log("error", fmt.Sprintf("error building kubeconfig: %v", err))
Expand All @@ -189,11 +194,14 @@ func main() {
os.Exit(1)
}

// initialize versioned Helm clients
helmClients := &helm.Clients{}
for _, v := range *enabledHelmVersions {
versionedLogger := log.With(logger, "component", "helm", "version", v)

switch v {
case v2.VERSION:
helmClients.Add(v2.VERSION, v2.New(log.With(logger, "component", "helm", "version", "v2"), kubeClient, v2.TillerOptions{
helmClients.Add(v2.VERSION, v2.New(versionedLogger, kubeClient, v2.TillerOptions{
Host: *tillerIP,
Port: *tillerPort,
Namespace: *tillerNamespace,
Expand All @@ -205,14 +213,10 @@ func main() {
TLSHostname: *tillerTLSHostname,
}))
case v3.VERSION:
client := v3.New(log.With(logger, "component", "helm", "version", "v3"), cfg)
// TODO(hidde): remove hardcoded path
if err := client.(*v3.HelmV3).RepositoryImport("/var/fluxd/helm/repository/repositories.yaml"); err != nil {
mainLogger.Log("warning", "failed to import Helm chart repositories from path", "err", err)
}
client := v3.New(versionedLogger, cfg)
helmClients.Add(v3.VERSION, client)
default:
mainLogger.Log("error", fmt.Sprintf("%s is not a supported Helm version, ignoring...", v))
mainLogger.Log("error", fmt.Sprintf("unsupported Helm version: %s", v))
continue
}

Expand All @@ -222,6 +226,24 @@ func main() {
}
}

// import Helm chart repositories from provided indexes
for _, i := range *versionedHelmRepositoryIndexes {
parts := strings.Split(i, ":")
if len(parts) != 2 {
mainLogger.Log("error", fmt.Sprintf("invalid version/path pair: %s, expected format is [version]:[path]", i))
continue
}
v, p := parts[0], parts[1]
client, ok := helmClients.Load(v)
if !ok {
mainLogger.Log("error", fmt.Sprintf("no Helm client found for version: %s", v))
continue
}
if err := client.RepositoryImport(p); err != nil {
mainLogger.Log("error", fmt.Sprintf("failed to import Helm chart repositories for %s from %s: %v", v, p, err))
}
}

// setup shared informer for HelmReleases
nsOpt := ifinformers.WithNamespace(*namespace)
ifInformerFactory := ifinformers.NewSharedInformerFactoryWithOptions(ifClient, *chartsSyncInterval, nsOpt)
Expand Down
3 changes: 3 additions & 0 deletions docs/references/operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ take action accordingly.
| `--kubeconfig` | | Path to a kubeconfig. Only required if out-of-cluster.
| `--master` | | The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.
| `--allow-namespace` | | If set, this limits the scope to a single namespace. if not specified, all namespaces will be watched.
| **Helm options**
| `--enabled-helm-versions` | `v2,v3` | The Helm client versions supported by this operator instance
| `--helm-repository-import` | | Targeted version and the path of the Helm repository index to import, i.e. `v3:/tmp/v3/index.yaml,v2:/tmp/v2/index.yaml`
| **Tiller options**
| `--tiller-ip` | | Tiller IP address. Only required if out-of-cluster.
| `--tiller-port` | | Tiller port.
Expand Down
4 changes: 4 additions & 0 deletions pkg/helm/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ type Client interface {
History(releaseName string, opts HistoryOptions) ([]*Release, error)
Rollback(releaseName string, opts RollbackOptions) (*Release, error)
DependencyUpdate(chartPath string) error
RepositoryIndex() error
RepositoryAdd(name, url, username, password, certFile, keyFile, caFile string) error
RepositoryRemove(name string) error
RepositoryImport(path string) error
Uninstall(releaseName string, opts UninstallOptions) error
Version() string
}
21 changes: 18 additions & 3 deletions pkg/helm/v2/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package v2
import (
"errors"
"fmt"
"os"
"time"

"github.com/go-kit/kit/log"
Expand All @@ -11,13 +12,20 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
helmv2 "k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/helm/environment"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/tlsutil"

"github.com/fluxcd/helm-operator/pkg/helm"
)

const VERSION = "v2"

var (
repositoryConfig = helmHome().RepositoryFile()
repositoryCache = helmHome().Cache()
)

// TillerOptions holds configuration options for tiller
type TillerOptions struct {
Host string
Expand All @@ -31,7 +39,7 @@ type TillerOptions struct {
TLSHostname string
}

// HelmV2 provides access to the Client client, while adhering
// HelmV2 provides access to the Helm v2 client, while adhering
// to the generic Client interface
type HelmV2 struct {
client *helmv2.Client
Expand Down Expand Up @@ -97,11 +105,11 @@ func newHelmClient(kubeClient *kubernetes.Clientset, opts TillerOptions) (*helmv
if opts.TLSHostname != "" {
tlsopts.ServerName = opts.TLSHostname
}
tlscfg, err := tlsutil.ClientConfig(tlsopts)
tlsCfg, err := tlsutil.ClientConfig(tlsopts)
if err != nil {
return nil, "", err
}
options = append(options, helmv2.WithTLS(tlscfg))
options = append(options, helmv2.WithTLS(tlsCfg))
}

return helmv2.NewClient(options...), host, nil
Expand All @@ -128,3 +136,10 @@ func statusMessageErr(err error) error {
}
return err
}

func helmHome() helmpath.Home {
if v, ok := os.LookupEnv("HELM_HOME"); ok {
return helmpath.Home(v)
}
return helmpath.Home(environment.DefaultHelmHome)
}
142 changes: 142 additions & 0 deletions pkg/helm/v2/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package v2

import (
"os"
"sync"

"github.com/pkg/errors"

"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/repo"
)

var (
repositoryConfigLock sync.RWMutex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a good reason to have this as a global variable? Can't it be part of Helm2?

Copy link
Member Author

@hiddeco hiddeco Dec 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can theoretically be multiple versions of Helm2 while they all speak with the same repository index file (this is not completely true, as they can have different HELM_HOMEs and are thus working with a different file, but this is the simplest secure option).

getters = getter.Providers{{
Schemes: []string{"http", "https"},
New: func(URL, CertFile, KeyFile, CAFile string) (getter.Getter, error) {
return getter.NewHTTPGetter(URL, CertFile, KeyFile, CAFile)
},
}}
)

func (h *HelmV2) RepositoryIndex() error {
repositoryConfigLock.RLock()
defer repositoryConfigLock.RUnlock()

f, err := loadRepositoryConfig()
if err != nil {
return err
}

var wg sync.WaitGroup
for _, c := range f.Repositories {
r, err := repo.NewChartRepository(c, getters)
if err != nil {
return err
}
wg.Add(1)
go func(r *repo.ChartRepository) {
if err := r.DownloadIndexFile(repositoryCache); err != nil {
h.logger.Log("error", "unable to get an update from the chart repository", "url", r.Config.URL, "err", err)
} else {
h.logger.Log("info", "successfully got an update from the chart repository", "url", r.Config.URL)
}
wg.Done()
}(r)
}
wg.Wait()
return nil
}

func (h *HelmV2) RepositoryAdd(name, url, username, password, certFile, keyFile, caFile string) error {
repositoryConfigLock.Lock()
defer repositoryConfigLock.Unlock()

f, err := loadRepositoryConfig()
if err != nil {
return err
}

c := &repo.Entry{
Name: name,
URL: url,
Username: username,
Password: password,
CertFile: certFile,
KeyFile: keyFile,
CAFile: caFile,
}
f.Add(c)

if f.Has(name) {
return errors.New("chart repository with name %s already exists")
}

r, err := repo.NewChartRepository(c, getters)
if err != nil {
return err
}
if err = r.DownloadIndexFile(repositoryCache); err != nil {
return err
}

return f.WriteFile(repositoryConfig, 0644)
}

func (h *HelmV2) RepositoryRemove(name string) error {
repositoryConfigLock.Lock()
defer repositoryConfigLock.Unlock()

f, err := repo.LoadRepositoriesFile(repositoryConfig)
if err != nil {
return err
}
f.Remove(name)

return f.WriteFile(repositoryConfig, 0644)
}

func (h *HelmV2) RepositoryImport(path string) error {
s, err := repo.LoadRepositoriesFile(path)
if err != nil {
return err
}

repositoryConfigLock.Lock()
defer repositoryConfigLock.Unlock()

t, err := loadRepositoryConfig()
if err != nil {
return err
}

for _, c := range s.Repositories {
if t.Has(c.Name) {
h.logger.Log("error", "repository with name already exists", "name", c.Name, "url", c.URL)
continue
}
r, err := repo.NewChartRepository(c, getters)
if err != nil {
h.logger.Log("error", err, "name", c.Name, "url", c.URL)
continue
}
if err := r.DownloadIndexFile(repositoryCache); err != nil {
h.logger.Log("error", err, "name", c.Name, "url", c.URL)
continue
}

t.Add(c)
h.logger.Log("info", "successfully imported repository", "name", c.Name, "url", c.URL)
}

return t.WriteFile(repositoryConfig, 0644)
}

func loadRepositoryConfig() (*repo.RepoFile, error) {
r, err := repo.LoadRepositoriesFile(repositoryConfig)
if err != nil && !os.IsNotExist(errors.Cause(err)) {
return nil, err
}
return r, nil
}