Skip to content

Commit

Permalink
Add basic Helm 3 support (with ListReleases only) (#1359)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonAlling authored and absoludity committed Dec 10, 2019
1 parent a12eeb2 commit f6cd36c
Show file tree
Hide file tree
Showing 18 changed files with 1,144 additions and 116 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ GO_PACKAGES = ./...

default: all

all: kubeapps/dashboard kubeapps/apprepository-controller kubeapps/tiller-proxy
all: kubeapps/dashboard kubeapps/apprepository-controller kubeapps/tiller-proxy kubeapps/kubeops

# TODO(miguel) Create Makefiles per component
kubeapps/%:
Expand Down
7 changes: 7 additions & 0 deletions chart/kubeapps/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ Create name for the tiller-proxy based on the fullname
{{ template "kubeapps.fullname" . }}-internal-tiller-proxy
{{- end -}}

{{/*
Create name for kubeops based on the fullname
*/}}
{{- define "kubeapps.kubeops.fullname" -}}
{{ template "kubeapps.fullname" . }}-internal-kubeops
{{- end -}}

{{/*
Create name for the secrets related to an app repository
*/}}
Expand Down
4 changes: 2 additions & 2 deletions chart/kubeapps/templates/kubeapps-frontend-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ data:
{{- end }}
{{- if .Values.useHelm3 }}
# TODO proxy_pass for Helm 3
proxy_pass http://{{ template "kubeapps.kubeops.fullname" . }}:{{ .Values.kubeops.service.port }};
{{- else }}
proxy_pass http://{{ template "kubeapps.tiller-proxy.fullname" . }}:{{ .Values.tillerProxy.service.port }};
{{- end }}
Expand All @@ -76,7 +76,7 @@ data:
rewrite /api/tiller-deploy/(.*) /$1 break;
rewrite /api/tiller-deploy / break;
{{- if .Values.useHelm3 }}
# TODO proxy_pass for Helm 3
proxy_pass http://{{ template "kubeapps.kubeops.fullname" . }}:{{ .Values.kubeops.service.port }};
{{- else }}
proxy_pass http://{{ template "kubeapps.tiller-proxy.fullname" . }}:{{ .Values.tillerProxy.service.port }};
{{- end }}
Expand Down
63 changes: 63 additions & 0 deletions chart/kubeapps/templates/kubeops-deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{{- if .Values.useHelm3 -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ template "kubeapps.kubeops.fullname" . }}
labels:
app: {{ template "kubeapps.kubeops.fullname" . }}
chart: {{ template "kubeapps.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.kubeops.replicaCount }}
selector:
matchLabels:
app: {{ template "kubeapps.kubeops.fullname" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ template "kubeapps.kubeops.fullname" . }}
release: {{ .Release.Name }}
spec:
serviceAccountName: {{ template "kubeapps.kubeops.fullname" . }}
# Increase termination timeout to let remaining operations to finish before killing the pods
# This is because new releases/upgrades/deletions are synchronous operations
terminationGracePeriodSeconds: 300
{{- include "kubeapps.imagePullSecrets" . | indent 6 }}
containers:
- name: kubeops
image: {{ template "kubeapps.image" (list .Values.kubeops.image .Values.global) }}
command:
- /kubeops
args:
- --user-agent-comment=kubeapps/{{ .Chart.AppVersion }}
- --chartsvc-url=http://{{ template "kubeapps.chartsvc.fullname" . }}:{{ .Values.chartsvc.service.port }}
ports:
- name: http
containerPort: {{ .Values.kubeops.service.port }}
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
resources:
{{ toYaml .Values.kubeops.resources | indent 12 }}
{{- with .Values.kubeops.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- if .Values.securityContext.enabled }}
securityContext:
fsGroup: {{ .Values.securityContext.fsGroup }}
runAsUser: {{ .Values.securityContext.runAsUser }}
{{- end }}
{{- with .Values.kubeops.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.kubeops.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
{{- end }}{{/* matches useHelm3 */}}
21 changes: 21 additions & 0 deletions chart/kubeapps/templates/kubeops-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{- if .Values.useHelm3 -}}
apiVersion: v1
kind: Service
metadata:
name: {{ template "kubeapps.kubeops.fullname" . }}
labels:
app: {{ template "kubeapps.name" . }}
chart: {{ template "kubeapps.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
type: ClusterIP
ports:
- port: {{ .Values.kubeops.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app: {{ template "kubeapps.kubeops.fullname" . }}
release: {{ .Release.Name }}
{{- end }}{{/* matches useHelm3 */}}
11 changes: 11 additions & 0 deletions chart/kubeapps/templates/kubeops-serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- if .Values.useHelm3 -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ template "kubeapps.kubeops.fullname" . }}
labels:
app: {{ template "kubeapps.kubeops.fullname" . }}
chart: {{ template "kubeapps.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- end }}{{/* matches useHelm3 */}}
21 changes: 21 additions & 0 deletions chart/kubeapps/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,27 @@ hooks:
##
tolerations: {}

# Kubeops is an interface between the Kubeapps Dashboard and Helm 3/Kubernetes.
# Set useHelm3 to true to use Kubeops instead of Tiller Proxy.
kubeops:
replicaCount: 2
image:
registry: docker.io
repository: kubeapps/kubeops
tag: latest
service:
port: 8080
resources:
limits:
cpu: 250m
memory: 256Mi
requests:
cpu: 25m
memory: 32Mi
nodeSelector: {}
tolerations: []
affinity: {}

## Tiller Proxy is a secure REST API on top of Helm's Tiller component used to
## manage Helm chart releases in the cluster from Kubeapps. Set tillerProxy.host
## to configure a different Tiller host to use.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions cmd/kubeops/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# syntax = docker/dockerfile:experimental

FROM golang:1.13 as builder
WORKDIR /go/src/github.com/kubeapps/kubeapps
COPY go.mod go.sum ./
COPY vendor vendor
COPY pkg pkg
COPY cmd cmd
ARG VERSION
# With the trick below, Go's build cache is kept between builds.
# https://github.com/golang/go/issues/27719#issuecomment-514747274
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -installsuffix cgo -ldflags "-X main.version=$VERSION" ./cmd/kubeops

FROM scratch
COPY --from=builder /go/src/github.com/kubeapps/kubeapps/kubeops /kubeops
EXPOSE 8080
CMD ["/kubeops"]
51 changes: 51 additions & 0 deletions cmd/kubeops/internal/handler/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package handler

import (
"net/http"

"github.com/kubeapps/common/response"
"github.com/kubeapps/kubeapps/pkg/agent"
"github.com/kubeapps/kubeapps/pkg/auth"
"github.com/kubeapps/kubeapps/pkg/handlerutil"
)

const (
authHeader = "Authorization"
namespaceParam = "namespace"
)

// This type represents the fact that a regular handler cannot actually be created until we have access to the request,
// because a valid action config (and hence agent config) cannot be created until then.
// If the agent config were a "this" argument instead of an explicit argument, it would be easy to create a handler with a "zero" config.
// 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)

// 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.
func WithAgentConfig(driverType agent.DriverType, options agent.Options) func(f dependentHandler) handlerutil.WithParams {
return func(f dependentHandler) handlerutil.WithParams {
return func(w http.ResponseWriter, req *http.Request, params handlerutil.Params) {
namespace := params[namespaceParam]
token := auth.ExtractToken(req.Header.Get(authHeader))
cfg := agent.Config{
AgentOptions: options,
ActionConfig: agent.NewActionConfig(driverType, token, namespace),
}
f(cfg, w, req, params)
}
}
}

func ListReleases(cfg agent.Config, w http.ResponseWriter, req *http.Request, params handlerutil.Params) {
apps, err := agent.ListReleases(cfg.ActionConfig, params[namespaceParam], cfg.AgentOptions.ListLimit, req.URL.Query().Get("statuses"))
if err != nil {
response.NewErrorResponse(handlerutil.ErrorCode(err), err.Error()).Write(w)
return
}
response.NewDataResponse(apps).Write(w)
}

func ListAllReleases(cfg agent.Config, w http.ResponseWriter, req *http.Request, _ handlerutil.Params) {
ListReleases(cfg, w, req, make(map[string]string))
}
127 changes: 127 additions & 0 deletions cmd/kubeops/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package main

import (
"context"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"syscall"
"time"

"github.com/gorilla/mux"
"github.com/kubeapps/kubeapps/cmd/kubeops/internal/handler"
"github.com/kubeapps/kubeapps/pkg/agent"
"github.com/kubeapps/kubeapps/pkg/auth"
log "github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"github.com/urfave/negroni"
"k8s.io/helm/pkg/helm/environment"
)

const defaultHelmDriver agent.DriverType = agent.Secret

var (
settings environment.EnvSettings
chartsvcURL string
helmDriverArg string
userAgentComment string
listLimit int
timeout int64
)

func init() {
settings.AddFlags(pflag.CommandLine)
pflag.StringVar(&chartsvcURL, "chartsvc-url", "https://kubeapps-internal-chartsvc:8080", "URL to the internal chartsvc")
pflag.StringVar(&helmDriverArg, "helm-driver", "", "which Helm driver type to use")
pflag.IntVar(&listLimit, "list-max", 256, "maximum number of releases to fetch")
pflag.StringVar(&userAgentComment, "user-agent-comment", "", "UserAgent comment used during outbound requests")
// Default timeout from https://github.com/helm/helm/blob/b0b0accdfc84e154b3d48ec334cd5b4f9b345667/cmd/helm/install.go#L216
pflag.Int64Var(&timeout, "timeout", 300, "Timeout to perform release operations (install, upgrade, rollback, delete)")
}

func main() {
pflag.Parse()
settings.Init(pflag.CommandLine)

options := agent.Options{
ListLimit: listLimit,
Timeout: timeout,
}

// Will panic below if an invalid driver type is provided.
driverType := defaultHelmDriver
if helmDriverArg != "" {
driverType = agent.ParseDriverType(helmDriverArg)
}
withAgentConfig := handler.WithAgentConfig(driverType, options)
r := mux.NewRouter()

// Routes
// Auth not necessary here with Helm 3 because it's done by Kubernetes.
apiv1 := r.PathPrefix("/v1").Subrouter()
apiv1.Methods("GET").Path("/releases").Handler(negroni.New(
negroni.Wrap(withAgentConfig(handler.ListAllReleases)),
))
apiv1.Methods("GET").Path("/namespaces/{namespace}/releases").Handler(negroni.New(
negroni.Wrap(withAgentConfig(handler.ListReleases)),
))

// Chartsvc reverse proxy
authGate := auth.AuthGate()
parsedChartsvcURL, err := url.Parse(chartsvcURL)
if err != nil {
log.Fatalf("Unable to parse the chartsvc URL: %v", err)
}
chartsvcProxy := httputil.NewSingleHostReverseProxy(parsedChartsvcURL)
chartsvcPrefix := "/chartsvc"
chartsvcRouter := r.PathPrefix(chartsvcPrefix).Subrouter()
// Logos don't require authentication so bypass that step
chartsvcRouter.Methods("GET").Path("/v1/assets/{repo}/{id}/logo").Handler(negroni.New(
negroni.Wrap(http.StripPrefix(chartsvcPrefix, chartsvcProxy)),
))
chartsvcRouter.Methods("GET").Handler(negroni.New(
authGate,
negroni.Wrap(http.StripPrefix(chartsvcPrefix, chartsvcProxy)),
))

n := negroni.Classic()
n.UseHandler(r)

port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
addr := ":" + port

srv := &http.Server{
Addr: addr,
Handler: n,
}

go func() {
log.WithFields(log.Fields{"addr": addr}).Info("Started Kubeops")
err := srv.ListenAndServe()
if err != nil {
log.Info(err)
}
}()

// Catch SIGINT and SIGTERM
// Set up channel on which to send signal notifications.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
log.Debug("Set system to get notified on signals")
s := <-c
log.Infof("Received signal: %v. Waiting for existing requests to finish", s)
// Set a timeout value high enough to let k8s terminationGracePeriodSeconds to act
// accordingly and send a SIGKILL if needed
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3600)
defer cancel()
// Doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
srv.Shutdown(ctx)
log.Info("All requests have been served. Exiting")
os.Exit(0)
}
Loading

0 comments on commit f6cd36c

Please sign in to comment.