Skip to content
This repository has been archived by the owner on Jul 22, 2019. It is now read-only.

Commit

Permalink
adds support for fetching charts with http authentication (#23)
Browse files Browse the repository at this point in the history
* adds support for fetching charts with http authentication

* support for HTTP Authorization header based authentication

* address review comments

* address review comments

* fix resolveURL

* move `netClient` to controller object

* more test coverage

* resolveChartURL expects repo index url

* address more review comments
  • Loading branch information
Sameer Naik authored and anguslees committed Apr 25, 2018
1 parent 1b4e29b commit 8a1b29f
Show file tree
Hide file tree
Showing 200 changed files with 16,535 additions and 25 deletions.
165 changes: 141 additions & 24 deletions cmd/controller/controller.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
package main

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"

"github.com/ghodss/yaml"
"google.golang.org/grpc"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/repo"

Expand All @@ -27,19 +32,27 @@ import (
)

const (
defaultRepoURL = "https://kubernetes-charts.storage.googleapis.com"
maxRetries = 5
defaultNamespace = metav1.NamespaceSystem
defaultRepoURL = "https://kubernetes-charts.storage.googleapis.com"
defaultTimeoutSeconds = 180
maxRetries = 5
)

// Controller is a cache.Controller for acting on Helm CRD objects
type Controller struct {
queue workqueue.RateLimitingInterface
informer cache.SharedIndexInformer
kubeClient kubernetes.Interface
helmClient *helm.Client
netClient *http.Client
}

type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}

// NewController creates a Controller
func NewController(clientset helmClientset.Interface) cache.Controller {
func NewController(clientset helmClientset.Interface, kubeClient kubernetes.Interface) cache.Controller {
lw := cache.NewListWatchFromClient(clientset.HelmV1().RESTClient(), "helmreleases", metav1.NamespaceAll, fields.Everything())

queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
Expand Down Expand Up @@ -77,7 +90,11 @@ func NewController(clientset helmClientset.Interface) cache.Controller {
return &Controller{
informer: informer,
queue: queue,
kubeClient: kubeClient,
helmClient: helm.NewClient(helm.Host(settings.TillerHost)),
netClient: &http.Client{
Timeout: time.Second * defaultTimeoutSeconds,
},
}
}

Expand Down Expand Up @@ -155,6 +172,86 @@ func (c *Controller) processNextItem() bool {
return true
}

func fetchUrl(netClient httpClient, reqURL, authHeader string) (*http.Response, error) {
req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
return nil, err
}

if len(authHeader) > 0 {
req.Header.Set("Authorization", authHeader)
}
return netClient.Do(req)
}

func fetchRepoIndex(netClient httpClient, repoURL string, authHeader string) (*repo.IndexFile, error) {
parsedURL, err := url.ParseRequestURI(repoURL)
if err != nil {
return nil, err
}

res, err := fetchUrl(netClient, parsedURL.String(), authHeader)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}

if res.StatusCode != http.StatusOK {
return nil, errors.New("repo index request failed")
}

body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}

index := &repo.IndexFile{}
err = yaml.Unmarshal(body, index)
if err != nil {
return index, err
}
index.SortEntries()
return index, nil
}

func findChartInRepoIndex(repoIndex *repo.IndexFile, chartName, chartVersion string) (string, error) {
errMsg := fmt.Sprintf("chart %q", chartName)
if chartVersion != "" {
errMsg = fmt.Sprintf("%s version %q", errMsg, chartVersion)
}
cv, err := repoIndex.Get(chartName, chartVersion)
if err != nil {
return "", fmt.Errorf("%s not found in repository", errMsg)
}

if len(cv.URLs) == 0 {
return "", fmt.Errorf("%s has no downloadable URLs", errMsg)
}
return cv.URLs[0], nil
}

func fetchChart(netClient httpClient, chartURL, authHeader string) (*chart.Chart, error) {
res, err := fetchUrl(netClient, chartURL, authHeader)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}

if res.StatusCode != http.StatusOK {
return nil, errors.New("chart download request failed")
}

body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return chartutil.LoadArchive(bytes.NewReader(body))
}

func releaseName(ns, name string) string {
return fmt.Sprintf("%s-%s", ns, name)
}
Expand All @@ -165,6 +262,19 @@ func isNotFound(err error) bool {
return strings.Contains(grpc.ErrorDesc(err), "not found")
}

//
func resolveChartURL(index, chart string) (string, error) {
indexURL, err := url.Parse(strings.TrimSpace(index))
if err != nil {
return "", err
}
chartURL, err := indexURL.Parse(strings.TrimSpace(chart))
if err != nil {
return "", err
}
return chartURL.String(), nil
}

func (c *Controller) updateRelease(key string) error {
obj, exists, err := c.informer.GetIndexer().GetByKey(key)
if err != nil {
Expand All @@ -190,38 +300,45 @@ func (c *Controller) updateRelease(key string) error {

helmObj := obj.(*helmCrdV1.HelmRelease)

// FIXME: make configurable
keyring := "/keyring/pubring.gpg"

dl := downloader.ChartDownloader{
HelmHome: settings.Home,
Out: os.Stdout,
Keyring: keyring,
Getters: getter.All(settings),
Verify: downloader.VerifyNever, // FIXME
}

repoURL := helmObj.Spec.RepoURL
if repoURL == "" {
// FIXME: Make configurable
repoURL = defaultRepoURL
}
repoURL = strings.TrimSuffix(strings.TrimSpace(repoURL), "/") + "/index.yaml"

authHeader := ""
if helmObj.Spec.Auth.Header != nil {
namespace := os.Getenv("POD_NAMESPACE")
if namespace == "" {
namespace = defaultNamespace
}

secret, err := c.kubeClient.Core().Secrets(namespace).Get(helmObj.Spec.Auth.Header.SecretKeyRef.Name, metav1.GetOptions{})
if err != nil {
return err
}
authHeader = string(secret.Data[helmObj.Spec.Auth.Header.SecretKeyRef.Key])
}

certFile := ""
keyFile := ""
caFile := ""
chartURL, err := repo.FindChartInRepoURL(repoURL, helmObj.Spec.ChartName, helmObj.Spec.Version, certFile, keyFile, caFile, getter.All(settings))
log.Printf("Downloading repo %s index...", repoURL)
repoIndex, err := fetchRepoIndex(c.netClient, repoURL, authHeader)
if err != nil {
return err
}

log.Printf("Downloading %s ...", chartURL)
fname, _, err := dl.DownloadTo(chartURL, helmObj.Spec.Version, settings.Home.Archive())
chartURL, err := findChartInRepoIndex(repoIndex, helmObj.Spec.ChartName, helmObj.Spec.Version)
if err != nil {
return err
}

chartURL, err = resolveChartURL(repoURL, chartURL)
if err != nil {
return err
}
log.Printf("Downloaded %s to %s", chartURL, fname)
chartRequested, err := chartutil.LoadFile(fname) // fixme: just download to ram buf

log.Printf("Downloading %s ...", chartURL)
chartRequested, err := fetchChart(c.netClient, chartURL, authHeader)
if err != nil {
return err
}
Expand Down
50 changes: 50 additions & 0 deletions cmd/controller/controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"testing"

"github.com/arschles/assert"
)

// TODO: add more tests

func Test_resolveChartURL(t *testing.T) {
tests := []struct {
name string
baseURL string
chartURL string
wantedURL string
}{
{
"absolute url",
"http://www.google.com",
"http://charts.example.com/repo/wordpress-0.1.0.tgz",
"http://charts.example.com/repo/wordpress-0.1.0.tgz",
},
{
"relative, repo url",
"http://charts.example.com/repo/",
"wordpress-0.1.0.tgz",
"http://charts.example.com/repo/wordpress-0.1.0.tgz",
},
{
"relative, repo index url",
"http://charts.example.com/repo/index.yaml",
"wordpress-0.1.0.tgz",
"http://charts.example.com/repo/wordpress-0.1.0.tgz",
},
{
"relative, repo url - no trailing slash",
"http://charts.example.com/repo",
"wordpress-0.1.0.tgz",
"http://charts.example.com/wordpress-0.1.0.tgz",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
chartURL, err := resolveChartURL(tt.baseURL, tt.chartURL)
assert.NoErr(t, err)
assert.Equal(t, chartURL, tt.wantedURL, "url")
})
}
}
8 changes: 7 additions & 1 deletion cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"syscall"

"github.com/spf13/pflag"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/helm/pkg/helm/environment"

Expand All @@ -26,12 +27,17 @@ func main2() error {
return err
}

kubeClient, err := kubernetes.NewForConfig(config)
if err != nil {
return err
}

clientset, err := helmClientset.NewForConfig(config)
if err != nil {
return err
}

controller := NewController(clientset)
controller := NewController(clientset, kubeClient)

stop := make(chan struct{})
defer close(stop)
Expand Down
13 changes: 13 additions & 0 deletions pkg/apis/helm.bitnami.com/v1/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand All @@ -24,10 +25,22 @@ type HelmReleaseSpec struct {
ChartName string `json:"chartName,omitempty"`
// Version is the chart version
Version string `json:"version,omitempty"`
// Auth is the authentication
Auth HelmReleaseAuth `json:"auth,omitempty"`
// Values is a string containing (unparsed) YAML values
Values string `json:"values,omitempty"`
}

type HelmReleaseAuth struct {
// Header is header based Authorization
Header *HelmReleaseAuthHeader `json:"header,omitempty"`
}

type HelmReleaseAuthHeader struct {
// Selects a key of a secret in the pod's namespace
SecretKeyRef corev1.SecretKeySelector `json:"secretKeyRef,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// HelmReleaseList is a list of HelmRelease resources
Expand Down
Loading

0 comments on commit 8a1b29f

Please sign in to comment.