Skip to content

Commit

Permalink
Add Helm 3 CreateRelease (#1399)
Browse files Browse the repository at this point in the history
Creating a release works, and it can be listed, but an error message is
displayed because 'GetRelease' does not exist yet.
  • Loading branch information
latiif authored and absoludity committed Dec 19, 2019
1 parent 4729203 commit 803e614
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 9 deletions.
54 changes: 53 additions & 1 deletion cmd/kubeops/internal/handler/handler.go
Expand Up @@ -4,9 +4,13 @@ import (
"net/http"

"github.com/kubeapps/common/response"
appRepo "github.com/kubeapps/kubeapps/cmd/apprepository-controller/pkg/client/clientset/versioned"
"github.com/kubeapps/kubeapps/pkg/agent"
"github.com/kubeapps/kubeapps/pkg/auth"
chartUtils "github.com/kubeapps/kubeapps/pkg/chart"
"github.com/kubeapps/kubeapps/pkg/handlerutil"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

const (
Expand All @@ -20,6 +24,16 @@ const (
// This approach practically eliminates that risk; it is much easier to use WithAgentConfig to create a handler guaranteed to use a valid agent config.
type dependentHandler func(cfg agent.Config, w http.ResponseWriter, req *http.Request, params handlerutil.Params)

func NewInClusterConfig(token string) (*rest.Config, error) {
config, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
config.BearerToken = token
config.BearerTokenFile = ""
return config, nil
}

// WithAgentConfig takes a dependentHandler and creates a regular (WithParams) handler that,
// for every request, will create an agent config for itself.
// Written in a curried fashion for convenient usage; see cmd/kubeops/main.go.
Expand All @@ -28,7 +42,25 @@ func WithAgentConfig(storageForDriver agent.StorageForDriver, options agent.Opti
return func(w http.ResponseWriter, req *http.Request, params handlerutil.Params) {
namespace := params[namespaceParam]
token := auth.ExtractToken(req.Header.Get(authHeader))
actionConfig, err := agent.NewActionConfig(storageForDriver, token, namespace)
restConfig, err := NewInClusterConfig(token)
if err != nil {
// TODO log details rather than return potentially sensitive details in error.
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
kubeClient, err := kubernetes.NewForConfig(restConfig)
if err != nil {
// TODO log details rather than return potentially sensitive details in error.
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
appRepoClient, err := appRepo.NewForConfig(restConfig)
if err != nil {
// TODO log details rather than return potentially sensitive details in error.
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
actionConfig, err := agent.NewActionConfig(storageForDriver, restConfig, kubeClient, namespace)
if err != nil {
// TODO log details rather than return potentially sensitive details in error.
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand All @@ -37,6 +69,7 @@ func WithAgentConfig(storageForDriver agent.StorageForDriver, options agent.Opti
cfg := agent.Config{
AgentOptions: options,
ActionConfig: actionConfig,
ChartClient: chartUtils.NewChartClient(kubeClient, appRepoClient, options.UserAgent),
}
f(cfg, w, req, params)
}
Expand All @@ -55,3 +88,22 @@ func ListReleases(cfg agent.Config, w http.ResponseWriter, req *http.Request, pa
func ListAllReleases(cfg agent.Config, w http.ResponseWriter, req *http.Request, _ handlerutil.Params) {
ListReleases(cfg, w, req, make(map[string]string))
}

func CreateRelease(cfg agent.Config, w http.ResponseWriter, req *http.Request, params handlerutil.Params) {
requireV1Support := false
chartDetails, chartMulti, err := handlerutil.ParseAndGetChart(req, cfg.ChartClient, requireV1Support)
if err != nil {
response.NewErrorResponse(handlerutil.ErrorCode(err), err.Error()).Write(w)
return
}
ch := chartMulti.Helm3Chart
releaseName := chartDetails.ReleaseName
namespace := params[namespaceParam]
valuesString := chartDetails.Values
release, err := agent.CreateRelease(cfg, releaseName, namespace, valuesString, ch)
if err != nil {
response.NewErrorResponse(handlerutil.ErrorCode(err), err.Error()).Write(w)
return
}
response.NewDataResponse(release).Write(w)
}
3 changes: 3 additions & 0 deletions cmd/kubeops/main.go
Expand Up @@ -68,6 +68,9 @@ func main() {
apiv1.Methods("GET").Path("/namespaces/{namespace}/releases").Handler(negroni.New(
negroni.Wrap(withAgentConfig(handler.ListReleases)),
))
apiv1.Methods("POST").Path("/namespaces/{namespace}/releases").Handler(negroni.New(
negroni.Wrap(withAgentConfig(handler.CreateRelease)),
))

// Chartsvc reverse proxy
authGate := auth.AuthGate()
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Expand Up @@ -25,7 +25,9 @@ require (
helm.sh/helm/v3 v3.0.0
k8s.io/api v0.0.0-20191016110408-35e52d86657a
k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8
k8s.io/cli-runtime v0.0.0-20191016114015-74ad18325ed5
k8s.io/client-go v0.0.0-20191016111102-bec269661e48
k8s.io/helm v2.16.0+incompatible
k8s.io/klog v1.0.0
sigs.k8s.io/yaml v1.1.0
)
64 changes: 56 additions & 8 deletions pkg/agent/agent.go
Expand Up @@ -4,15 +4,19 @@ import (
"errors"
"strconv"

chartUtils "github.com/kubeapps/kubeapps/pkg/chart"
"github.com/kubeapps/kubeapps/pkg/proxy"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog"
"sigs.k8s.io/yaml"
)

// StorageForDriver is a function type which returns a specific storage.
Expand Down Expand Up @@ -41,11 +45,13 @@ func StorageForMemory(_ string, _ *kubernetes.Clientset) *storage.Storage {
type Options struct {
ListLimit int
Timeout int64
UserAgent string
}

type Config struct {
ActionConfig *action.Configuration
AgentOptions Options
ChartClient chartUtils.Resolver
}

func ListReleases(actionConfig *action.Configuration, namespace string, listLimit int, status string) ([]proxy.AppOverview, error) {
Expand All @@ -68,23 +74,65 @@ func ListReleases(actionConfig *action.Configuration, namespace string, listLimi
return appOverviews, nil
}

func NewActionConfig(storageForDriver StorageForDriver, token, namespace string) (*action.Configuration, error) {
actionConfig := new(action.Configuration)
config, err := rest.InClusterConfig()
func CreateRelease(config Config, name, namespace, valueString string, ch *chart.Chart) (*release.Release, error) {
cmd := action.NewInstall(config.ActionConfig)
cmd.ReleaseName = name
cmd.Namespace = namespace
values, err := getValues([]byte(valueString))
if err != nil {
return nil, err
}
config.BearerToken = token
config.BearerTokenFile = ""
clientset, err := kubernetes.NewForConfig(config)
release, err := cmd.Run(ch, values)
if err != nil {
return nil, err
}
return release, nil
}

func NewActionConfig(storageForDriver StorageForDriver, config *rest.Config, clientset *kubernetes.Clientset, namespace string) (*action.Configuration, error) {
actionConfig := new(action.Configuration)
store := storageForDriver(namespace, clientset)
actionConfig.RESTClientGetter = nil // TODO replace nil with meaningful value
actionConfig.KubeClient = kube.New(nil) // TODO replace nil with meaningful value
restClientGetter := NewConfigFlagsFromCluster(namespace, config)
actionConfig.RESTClientGetter = restClientGetter
actionConfig.KubeClient = kube.New(restClientGetter)
actionConfig.Releases = store
actionConfig.Log = klog.Infof
return actionConfig, nil
}

// NewConfigFlagsFromCluster returns ConfigFlags with default values set from within cluster
func NewConfigFlagsFromCluster(namespace string, clusterConfig *rest.Config) *genericclioptions.ConfigFlags {
impersonateGroup := []string{}
insecure := false

// CertFile and KeyFile must be nil for the BearerToken to be used for authentication and authorization instead of the pod's service account.
return &genericclioptions.ConfigFlags{
Insecure: &insecure,
Timeout: stringptr("0"),
Namespace: stringptr(namespace),
APIServer: stringptr(clusterConfig.Host),
CAFile: stringptr(clusterConfig.CAFile),
BearerToken: stringptr(clusterConfig.BearerToken),
ImpersonateGroup: &impersonateGroup,
}
}

// Values is a type alias for values.yaml
type Values map[string]interface{}

func getValues(raw []byte) (Values, error) {
values := make(Values)
err := yaml.Unmarshal(raw, &values)
if err != nil {
return nil, err
}
return values, nil
}

func stringptr(val string) *string {
return &val
}

func ParseDriverType(raw string) (StorageForDriver, error) {
switch raw {
case "secret", "secrets":
Expand Down

0 comments on commit 803e614

Please sign in to comment.