diff --git a/cmd/helm-operator/main.go b/cmd/helm-operator/main.go index c72f70953..387d9cc2a 100644 --- a/cmd/helm-operator/main.go +++ b/cmd/helm-operator/main.go @@ -67,6 +67,8 @@ var ( listenAddr *string + versionedHelmRepositoryIndexes *[]string + enabledHelmVersions *[]string defaultHelmVersion *string ) @@ -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) @@ -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") { @@ -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)) @@ -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, @@ -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 } @@ -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) diff --git a/docs/references/operator.md b/docs/references/operator.md index 42bba36ea..0f537836a 100644 --- a/docs/references/operator.md +++ b/docs/references/operator.md @@ -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. diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 46460c401..30aa19d54 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -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 } diff --git a/pkg/helm/v2/helm.go b/pkg/helm/v2/helm.go index c7043b11c..86820e6a5 100644 --- a/pkg/helm/v2/helm.go +++ b/pkg/helm/v2/helm.go @@ -3,6 +3,7 @@ package v2 import ( "errors" "fmt" + "os" "time" "github.com/go-kit/kit/log" @@ -11,6 +12,8 @@ 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" @@ -18,6 +21,11 @@ import ( const VERSION = "v2" +var ( + repositoryConfig = helmHome().RepositoryFile() + repositoryCache = helmHome().Cache() +) + // TillerOptions holds configuration options for tiller type TillerOptions struct { Host string @@ -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 @@ -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 @@ -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) +} diff --git a/pkg/helm/v2/repository.go b/pkg/helm/v2/repository.go new file mode 100644 index 000000000..b0616f1c3 --- /dev/null +++ b/pkg/helm/v2/repository.go @@ -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 + 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 +}