Pass user authentication to Tiller

pull/1932/head
tamal 9 years ago
parent 401f8bcc18
commit 586a496fca

@ -34,4 +34,7 @@ message Info {
// Description is human-friendly "log entry" about this release.
string Description = 5;
// Username is the authenticated user who performed this release.
string Username = 6;
}

@ -64,7 +64,7 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command {
}
get.release = args[0]
if get.client == nil {
get.client = helm.NewClient(helm.Host(settings.TillerHost))
get.client = helm.NewClient(helm.Host(settings.TillerHost), helm.WithContext(loadAuthHeaders))
}
return get.run()
},

@ -55,7 +55,7 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command {
}
get.release = args[0]
if get.client == nil {
get.client = helm.NewClient(helm.Host(settings.TillerHost))
get.client = helm.NewClient(helm.Host(settings.TillerHost), helm.WithContext(loadAuthHeaders))
}
return get.run()
},

@ -17,6 +17,7 @@ limitations under the License.
package main
import (
"fmt"
"io"
"testing"
@ -29,7 +30,7 @@ func TestGetCmd(t *testing.T) {
name: "get with a release",
resp: releaseMock(&releaseOptions{name: "thomas-guide"}),
args: []string{"thomas-guide"},
expected: "REVISION: 1\nRELEASED: (.*)\nCHART: foo-0.1.0-beta.1\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nHOOKS:\n---\n# pre-install-hook\n" + mockHookTemplate + "\nMANIFEST:",
expected: fmt.Sprintf("REVISION: 1\nRELEASED: (.*)\nRELEASED BY: %s\nCHART: foo-0.1.0-beta.1\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nHOOKS:\n---\n# pre-install-hook\n"+mockHookTemplate+"\nMANIFEST:", username),
},
{
name: "get requires release name arg",

@ -17,6 +17,7 @@ limitations under the License.
package main // import "k8s.io/helm/cmd/helm"
import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
@ -25,8 +26,10 @@ import (
"strings"
"github.com/spf13/cobra"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
@ -279,6 +282,43 @@ func getInternalKubeClient(context string) (*rest.Config, internalclientset.Inte
return config, client, nil
}
func loadAuthHeaders(ctx context.Context) context.Context {
c, err := kube.GetConfig(kubeContext).ClientConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to extract authentication headers: %s\n", err)
os.Exit(1)
}
m := map[string]string{}
if c.AuthProvider != nil {
switch c.AuthProvider.Name {
case "gcp":
m[string(kube.Authorization)] = "Bearer " + c.AuthProvider.Config["access-token"]
case "oidc":
m[string(kube.Authorization)] = "Bearer " + c.AuthProvider.Config["id-token"]
default:
fmt.Fprintf(os.Stderr, "Unknown auth provider: %s\n", c.AuthProvider.Name)
os.Exit(1)
}
}
if len(c.BearerToken) != 0 {
m[string(kube.Authorization)] = "Bearer " + c.BearerToken
}
if len(c.Username) != 0 && len(c.Password) != 0 {
m[string(kube.Authorization)] = "Basic " + base64.StdEncoding.EncodeToString([]byte(c.Username+":"+c.Password))
}
md, _ := metadata.FromContext(ctx)
return metadata.NewContext(ctx, metadata.Join(md, metadata.New(m)))
}
// getKubeCmd is a convenience method for creating kubernetes cmd client
// for a given kubeconfig context
func getKubeCmd(context string) *kube.Client {
return kube.New(kube.GetConfig(context))
}
// ensureHelmClient returns a new helm client impl. if h is not nil.
func ensureHelmClient(h helm.Interface) helm.Interface {
if h != nil {
@ -288,7 +328,7 @@ func ensureHelmClient(h helm.Interface) helm.Interface {
}
func newClient() helm.Interface {
options := []helm.Option{helm.Host(settings.TillerHost)}
options := []helm.Option{helm.Host(settings.TillerHost), helm.WithContext(loadAuthHeaders)}
if tlsVerify || tlsEnable {
tlsopts := tlsutil.Options{KeyFile: tlsKeyFile, CertFile: tlsCertFile, InsecureSkipVerify: true}

@ -61,6 +61,8 @@ type releaseOptions struct {
namespace string
}
var username = "John"
func releaseMock(opts *releaseOptions) *release.Release {
date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0}
@ -104,6 +106,7 @@ func releaseMock(opts *releaseOptions) *release.Release {
LastDeployed: &date,
Status: &release.Status{Code: scode},
Description: "Release mock",
Username: username,
},
Chart: ch,
Config: &chart.Config{Raw: `name: "value"`},

@ -38,11 +38,11 @@ configures the maximum length of the revision list returned.
The historical release set is printed as a formatted table, e.g:
$ helm history angry-bird --max=4
REVISION UPDATED STATUS CHART DESCRIPTION
1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Initial install
2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Upgraded successfully
3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Rolled back to 2
4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 Upgraded successfully
REVISION UPDATED STATUS CHART DESCRIPTION RELEASED BY
1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Initial install x
2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Upgraded successfully y
3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Rolled back to 2 z
4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 Upgraded successfully x
`
type historyCmd struct {
@ -66,7 +66,7 @@ func newHistoryCmd(c helm.Interface, w io.Writer) *cobra.Command {
case len(args) == 0:
return errReleaseRequired
case his.helmc == nil:
his.helmc = helm.NewClient(helm.Host(settings.TillerHost))
his.helmc = helm.NewClient(helm.Host(settings.TillerHost), helm.WithContext(loadAuthHeaders))
}
his.rls = args[0]
return his.run()
@ -94,7 +94,7 @@ func (cmd *historyCmd) run() error {
func formatHistory(rls []*release.Release) string {
tbl := uitable.New()
tbl.MaxColWidth = 60
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "DESCRIPTION")
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "DESCRIPTION", "RELEASED BY")
for i := len(rls) - 1; i >= 0; i-- {
r := rls[i]
c := formatChartname(r.Chart)
@ -102,7 +102,8 @@ func formatHistory(rls []*release.Release) string {
s := r.Info.Status.Code.String()
v := r.Version
d := r.Info.Description
tbl.AddRow(v, t, s, c, d)
u := r.Info.Username
tbl.AddRow(v, t, s, c, d, u)
}
return tbl.String()
}

@ -18,6 +18,7 @@ package main
import (
"bytes"
"fmt"
"regexp"
"testing"
@ -50,7 +51,7 @@ func TestHistoryCmd(t *testing.T) {
mk("angry-bird", 2, rpb.Status_SUPERSEDED),
mk("angry-bird", 1, rpb.Status_SUPERSEDED),
},
xout: "REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\n",
xout: fmt.Sprintf("REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \tRELEASED BY\n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\t%s \n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\t%s \n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\t%s \n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\t%s \n", username, username, username, username),
},
{
cmds: "helm history --max=MAX RELEASE_NAME",
@ -60,7 +61,7 @@ func TestHistoryCmd(t *testing.T) {
mk("angry-bird", 4, rpb.Status_DEPLOYED),
mk("angry-bird", 3, rpb.Status_SUPERSEDED),
},
xout: "REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\n",
xout: fmt.Sprintf("REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \tRELEASED BY\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\t%s \n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\t%s \n", username, username),
},
}

@ -92,7 +92,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
list.filter = strings.Join(args, " ")
}
if list.client == nil {
list.client = helm.NewClient(helm.Host(settings.TillerHost))
list.client = helm.NewClient(helm.Host(settings.TillerHost), helm.WithContext(loadAuthHeaders))
}
return list.run()
},
@ -202,14 +202,15 @@ func (l *listCmd) statusCodes() []release.Status_Code {
func formatList(rels []*release.Release) string {
table := uitable.New()
table.MaxColWidth = 60
table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "NAMESPACE")
table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "NAMESPACE", "RELEASED BY")
for _, r := range rels {
c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version)
t := timeconv.String(r.Info.LastDeployed)
s := r.Info.Status.Code.String()
v := r.Version
n := r.Namespace
table.AddRow(r.Name, v, t, s, c, n)
u := r.Info.Username
table.AddRow(r.Name, v, t, s, c, n, u)
}
return table.String()
}

@ -18,6 +18,7 @@ package main
import (
"bytes"
"fmt"
"regexp"
"testing"
@ -45,7 +46,7 @@ func TestListCmd(t *testing.T) {
resp: []*release.Release{
releaseMock(&releaseOptions{name: "atlas"}),
},
expected: "NAME \tREVISION\tUPDATED \tSTATUS \tCHART \tNAMESPACE\natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\tdefault \n",
expected: fmt.Sprintf("NAME \tREVISION\tUPDATED \tSTATUS \tCHART \tNAMESPACE\tRELEASED BY\natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\tdefault \t%s \n", username),
},
{
name: "list, one deployed, one failed",

@ -29,6 +29,7 @@ import (
var printReleaseTemplate = `REVISION: {{.Release.Version}}
RELEASED: {{.ReleaseDate}}
RELEASED BY: {{.ReleasedBy}}
CHART: {{.Release.Chart.Metadata.Name}}-{{.Release.Chart.Metadata.Version}}
USER-SUPPLIED VALUES:
{{.Release.Config.Raw}}
@ -62,6 +63,7 @@ func printRelease(out io.Writer, rel *release.Release) error {
"Release": rel,
"ComputedValues": cfgStr,
"ReleaseDate": timeconv.Format(rel.Info.LastDeployed, time.ANSIC),
"ReleasedBy": rel.Info.Username,
}
return tpl(printReleaseTemplate, data, out)
}

@ -38,6 +38,7 @@ The status consists of:
- last deployment time
- k8s namespace in which the release lives
- state of the release (can be: UNKNOWN, DEPLOYED, DELETED, SUPERSEDED, FAILED or DELETING)
- name of the user who deployed the release
- list of resources that this release consists of, sorted by kind
- details on last test suite run, if applicable
- additional notes provided by the chart
@ -67,7 +68,7 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command {
}
status.release = args[0]
if status.client == nil {
status.client = helm.NewClient(helm.Host(settings.TillerHost))
status.client = helm.NewClient(helm.Host(settings.TillerHost), helm.WithContext(loadAuthHeaders))
}
return status.run()
},
@ -96,6 +97,7 @@ func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) {
}
fmt.Fprintf(out, "NAMESPACE: %s\n", res.Namespace)
fmt.Fprintf(out, "STATUS: %s\n", res.Info.Status.Code)
fmt.Fprintf(out, "RELEASED BY: %s\n", res.Info.Username)
fmt.Fprintf(out, "\n")
if len(res.Info.Status.Resources) > 0 {
re := regexp.MustCompile(" +")

@ -50,7 +50,7 @@ func TestStatusCmd(t *testing.T) {
{
name: "get status of a deployed release",
args: []string{"flummoxed-chickadee"},
expected: outputWithStatus("DEPLOYED\n\n"),
expected: outputWithStatus("DEPLOYED", username+"\n\n"),
rel: releaseMockWithStatus(&release.Status{
Code: release.Status_DEPLOYED,
}),
@ -58,7 +58,7 @@ func TestStatusCmd(t *testing.T) {
{
name: "get status of a deployed release with notes",
args: []string{"flummoxed-chickadee"},
expected: outputWithStatus("DEPLOYED\n\nNOTES:\nrelease notes\n"),
expected: outputWithStatus("DEPLOYED", username+"\n\nNOTES:\nrelease notes\n"),
rel: releaseMockWithStatus(&release.Status{
Code: release.Status_DEPLOYED,
Notes: "release notes",
@ -67,7 +67,7 @@ func TestStatusCmd(t *testing.T) {
{
name: "get status of a deployed release with resources",
args: []string{"flummoxed-chickadee"},
expected: outputWithStatus("DEPLOYED\n\nRESOURCES:\nresource A\nresource B\n\n"),
expected: outputWithStatus("DEPLOYED", username+"\n\nRESOURCES:\nresource A\nresource B\n\n"),
rel: releaseMockWithStatus(&release.Status{
Code: release.Status_DEPLOYED,
Resources: "resource A\nresource B\n",
@ -131,10 +131,11 @@ func TestStatusCmd(t *testing.T) {
}
}
func outputWithStatus(status string) string {
return fmt.Sprintf("LAST DEPLOYED: %s\nNAMESPACE: \nSTATUS: %s",
func outputWithStatus(status string, username string) string {
return fmt.Sprintf("LAST DEPLOYED: %s\nNAMESPACE: \nSTATUS: %s\nRELEASED BY: %s",
dateString,
status)
status,
username)
}
func releaseMockWithStatus(status *release.Status) *release.Release {
@ -144,6 +145,7 @@ func releaseMockWithStatus(status *release.Status) *release.Release {
FirstDeployed: &date,
LastDeployed: &date,
Status: status,
Username: username,
},
}
}

@ -141,7 +141,8 @@ func newLogger(prefix string) *log.Logger {
}
func start(c *cobra.Command, args []string) {
clientset, err := kube.New(nil).ClientSet()
client := kube.New(nil)
clientset, err := client.ClientSet()
if err != nil {
logger.Fatalf("Cannot initialize Kubernetes connection: %s", err)
}
@ -177,7 +178,7 @@ func start(c *cobra.Command, args []string) {
opts = append(opts, grpc.Creds(credentials.NewTLS(cfg)))
}
rootServer = tiller.NewServer(opts...)
rootServer = tiller.NewServer(client, opts...)
lstn, err := net.Listen("tcp", grpcAddr)
if err != nil {

@ -56,6 +56,9 @@ func (h *Client) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesRespo
req := &h.opts.listReq
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
@ -88,7 +91,9 @@ func (h *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...
req.DisableHooks = h.opts.disableHooks
req.ReuseName = h.opts.reuseName
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
@ -126,7 +131,9 @@ func (h *Client) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.Unins
req.Name = rlsName
req.DisableHooks = h.opts.disableHooks
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
@ -163,7 +170,9 @@ func (h *Client) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts
req.ResetValues = h.opts.resetValues
req.ReuseValues = h.opts.reuseValues
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
@ -188,7 +197,9 @@ func (h *Client) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, err
}
req := &rls.GetVersionRequest{}
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
@ -209,7 +220,9 @@ func (h *Client) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.R
req.DryRun = h.opts.dryRun
req.Name = rlsName
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
@ -226,7 +239,9 @@ func (h *Client) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetRe
req := &h.opts.statusReq
req.Name = rlsName
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
@ -243,7 +258,9 @@ func (h *Client) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.Get
req := &h.opts.contentReq
req.Name = rlsName
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err
@ -261,7 +278,9 @@ func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.Get
req := &h.opts.histReq
req.Name = rlsName
ctx := NewContext()
if h.opts.withContext != nil {
ctx = h.opts.withContext(ctx)
}
if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil {
return nil, err

@ -22,7 +22,6 @@ import (
"github.com/golang/protobuf/proto"
"golang.org/x/net/context"
"google.golang.org/grpc/metadata"
cpb "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release"
rls "k8s.io/helm/pkg/proto/hapi/services"
@ -68,6 +67,8 @@ type options struct {
contentReq rls.GetReleaseContentRequest
// release rollback options are applied directly to the rollback release request
rollbackReq rls.RollbackReleaseRequest
// withContext adds metadata to context before sending
withContext func(context.Context) context.Context
// before intercepts client calls before sending
before func(context.Context, proto.Message) error
// release history options are applied directly to the get release history request
@ -95,6 +96,14 @@ func WithTLS(cfg *tls.Config) Option {
}
}
// WithContext returns an option that allows adding metadata to context of a helm client rpc
// before being sent OTA to tiller.
func WithContext(fn func(context.Context) context.Context) Option {
return func(opts *options) {
opts.withContext = fn
}
}
// BeforeCall returns an option that allows intercepting a helm client rpc
// before being sent OTA to tiller. The intercepting function should return
// an error to indicate that the call should not proceed or nil otherwise.

@ -0,0 +1,28 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
//AuthHeader is key type for context
type AuthKey string
const (
Authorization AuthKey = "authorization"
UserInfo AuthKey = "k8s-user-info"
UserClient AuthKey = "k8s-user-client"
SystemClient AuthKey = "k8s-sys-client"
ImpersonateUser AuthKey = "k8s-impersonate-user"
)

@ -37,6 +37,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1"
@ -44,6 +45,7 @@ import (
batchinternal "k8s.io/kubernetes/pkg/apis/batch"
batch "k8s.io/kubernetes/pkg/apis/batch/v1"
"k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/authorization/internalversion"
conditions "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/kubectl"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
@ -75,6 +77,24 @@ func New(config clientcmd.ClientConfig) *Client {
// ResourceActorFunc performs an action on a single resource.
type ResourceActorFunc func(*resource.Info) error
// Discovery retrieves the DiscoveryClient
func (c *Client) Discovery() (discovery.DiscoveryInterface, error) {
client, err := c.ClientSet()
if err != nil {
return nil, err
}
return client.Discovery(), nil
}
// Authorization retrieves the AuthorizationInterface
func (c *Client) Authorization() (internalversion.AuthorizationInterface, error) {
client, err := c.ClientSet()
if err != nil {
return nil, err
}
return client.Authorization(), nil
}
// Create creates kubernetes resources from an io.reader
//
// Namespace will set the namespace

@ -23,6 +23,8 @@ type Info struct {
Deleted *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=deleted" json:"deleted,omitempty"`
// Description is human-friendly "log entry" about this release.
Description string `protobuf:"bytes,5,opt,name=Description" json:"Description,omitempty"`
// Username is the authenticated user who performed this release.
Username string `protobuf:"bytes,6,opt,name=Username" json:"Username,omitempty"`
}
func (m *Info) Reset() { *m = Info{} }
@ -65,6 +67,13 @@ func (m *Info) GetDescription() string {
return ""
}
func (m *Info) GetUsername() string {
if m != nil {
return m.Username
}
return ""
}
func init() {
proto.RegisterType((*Info)(nil), "hapi.release.Info")
}
@ -72,20 +81,21 @@ func init() {
func init() { proto.RegisterFile("hapi/release/info.proto", fileDescriptor1) }
var fileDescriptor1 = []byte{
// 235 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x8f, 0x31, 0x4f, 0xc3, 0x30,
0x10, 0x85, 0x95, 0x52, 0x5a, 0xd5, 0x6d, 0x19, 0x2c, 0x24, 0x42, 0x16, 0x22, 0xa6, 0x0e, 0xc8,
0x91, 0x80, 0x1d, 0x81, 0xba, 0xb0, 0x06, 0x26, 0x16, 0xe4, 0xe2, 0x73, 0xb1, 0xe4, 0xe6, 0x2c,
0xfb, 0x3a, 0xf0, 0x2f, 0xf8, 0xc9, 0xa8, 0xb6, 0x83, 0xd2, 0xa9, 0xab, 0xbf, 0xf7, 0x3e, 0xbf,
0x63, 0x57, 0xdf, 0xd2, 0x99, 0xc6, 0x83, 0x05, 0x19, 0xa0, 0x31, 0x9d, 0x46, 0xe1, 0x3c, 0x12,
0xf2, 0xc5, 0x01, 0x88, 0x0c, 0xaa, 0x9b, 0x2d, 0xe2, 0xd6, 0x42, 0x13, 0xd9, 0x66, 0xaf, 0x1b,
0x32, 0x3b, 0x08, 0x24, 0x77, 0x2e, 0xc5, 0xab, 0xeb, 0x23, 0x4f, 0x20, 0x49, 0xfb, 0x90, 0xd0,
0xed, 0xef, 0x88, 0x8d, 0x5f, 0x3b, 0x8d, 0xfc, 0x8e, 0x4d, 0x12, 0x28, 0x8b, 0xba, 0x58, 0xcd,
0xef, 0x2f, 0xc5, 0xf0, 0x0f, 0xf1, 0x16, 0x59, 0x9b, 0x33, 0xfc, 0x99, 0x5d, 0x68, 0xe3, 0x03,
0x7d, 0x2a, 0x70, 0x16, 0x7f, 0x40, 0x95, 0xa3, 0xd8, 0xaa, 0x44, 0xda, 0x22, 0xfa, 0x2d, 0xe2,
0xbd, 0xdf, 0xd2, 0x2e, 0x63, 0x63, 0x9d, 0x0b, 0xfc, 0x89, 0x2d, 0xad, 0x1c, 0x1a, 0xce, 0x4e,
0x1a, 0x16, 0x87, 0xc2, 0xbf, 0xe0, 0x91, 0x4d, 0x15, 0x58, 0x20, 0x50, 0xe5, 0xf8, 0x64, 0xb5,
0x8f, 0xf2, 0x9a, 0xcd, 0xd7, 0x10, 0xbe, 0xbc, 0x71, 0x64, 0xb0, 0x2b, 0xcf, 0xeb, 0x62, 0x35,
0x6b, 0x87, 0x4f, 0x2f, 0xb3, 0x8f, 0x69, 0xbe, 0x7a, 0x33, 0x89, 0xa6, 0x87, 0xbf, 0x00, 0x00,
0x00, 0xff, 0xff, 0x1a, 0x52, 0x8f, 0x9c, 0x89, 0x01, 0x00, 0x00,
// 249 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x90, 0x31, 0x4f, 0xc3, 0x30,
0x10, 0x85, 0x95, 0x52, 0x52, 0xea, 0xb6, 0x0c, 0x16, 0x12, 0x26, 0x0b, 0x11, 0x53, 0x07, 0xe4,
0x48, 0xc0, 0x8e, 0x40, 0x5d, 0x58, 0x03, 0x2c, 0x2c, 0xc8, 0x25, 0x97, 0x62, 0xc9, 0xc9, 0x59,
0xf6, 0x75, 0xe0, 0x3f, 0xf1, 0x23, 0x51, 0x1d, 0xa7, 0x0a, 0x53, 0xc6, 0xe4, 0x7b, 0xdf, 0xbb,
0x27, 0xb3, 0xcb, 0x6f, 0x65, 0x75, 0xe1, 0xc0, 0x80, 0xf2, 0x50, 0xe8, 0xb6, 0x46, 0x69, 0x1d,
0x12, 0xf2, 0xe5, 0x01, 0xc8, 0x08, 0xb2, 0xeb, 0x1d, 0xe2, 0xce, 0x40, 0x11, 0xd8, 0x76, 0x5f,
0x17, 0xa4, 0x1b, 0xf0, 0xa4, 0x1a, 0xdb, 0xc5, 0xb3, 0xab, 0x7f, 0x3d, 0x9e, 0x14, 0xed, 0x7d,
0x87, 0x6e, 0x7e, 0x27, 0x6c, 0xfa, 0xd2, 0xd6, 0xc8, 0x6f, 0x59, 0xda, 0x01, 0x91, 0xe4, 0xc9,
0x7a, 0x71, 0x77, 0x21, 0x87, 0x37, 0xe4, 0x6b, 0x60, 0x65, 0xcc, 0xf0, 0x27, 0x76, 0x5e, 0x6b,
0xe7, 0xe9, 0xb3, 0x02, 0x6b, 0xf0, 0x07, 0x2a, 0x31, 0x09, 0x56, 0x26, 0xbb, 0x2d, 0xb2, 0xdf,
0x22, 0xdf, 0xfa, 0x2d, 0xe5, 0x2a, 0x18, 0x9b, 0x28, 0xf0, 0x47, 0xb6, 0x32, 0x6a, 0xd8, 0x70,
0x32, 0xda, 0xb0, 0x3c, 0x08, 0xc7, 0x82, 0x07, 0x36, 0xab, 0xc0, 0x00, 0x41, 0x25, 0xa6, 0xa3,
0x6a, 0x1f, 0xe5, 0x39, 0x5b, 0x6c, 0xc0, 0x7f, 0x39, 0x6d, 0x49, 0x63, 0x2b, 0x4e, 0xf3, 0x64,
0x3d, 0x2f, 0x87, 0xbf, 0x78, 0xc6, 0xce, 0xde, 0x3d, 0xb8, 0x56, 0x35, 0x20, 0xd2, 0x80, 0x8f,
0xdf, 0xcf, 0xf3, 0x8f, 0x59, 0x7c, 0x91, 0x6d, 0x1a, 0xae, 0xdc, 0xff, 0x05, 0x00, 0x00, 0xff,
0xff, 0xf4, 0x9e, 0xd9, 0x9e, 0xa5, 0x01, 0x00, 0x00,
}

@ -117,12 +117,10 @@ type MockTestingEnvironment struct {
}
func newMockTestingEnvironment() *MockTestingEnvironment {
tEnv := mockTillerEnvironment()
return &MockTestingEnvironment{
Environment: &Environment{
Namespace: "default",
KubeClient: tEnv.KubeClient,
KubeClient: newPodSucceededKubeClient(),
Timeout: 5,
Stream: &mockStream{},
},

@ -293,7 +293,6 @@ func testEnvFixture() *Environment {
func mockTillerEnvironment() *tillerEnv.Environment {
e := tillerEnv.New()
e.Releases = storage.Init(driver.NewMemory())
e.KubeClient = newPodSucceededKubeClient()
return e
}

@ -44,6 +44,7 @@ func (s *ReleaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
}
return res, err
}
rel.Info.Username = getUserName(c)
s.Log("performing install for %s", req.Name)
res, err := s.performRelease(rel, req)

@ -39,6 +39,7 @@ func (s *ReleaseServer) RollbackRelease(c ctx.Context, req *services.RollbackRel
if err != nil {
return nil, err
}
targetRelease.Info.Username = getUserName(c)
s.Log("performing rollback of %s", req.Name)
res, err := s.performRollback(currentRelease, targetRelease, req)

@ -107,6 +107,7 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
rel.Info.Status.Code = release.Status_DELETED
rel.Info.Description = "Deletion complete"
rel.Info.Username = getUserName(c)
if req.Purge {
s.Log("purge requested for %s", req.Name)

@ -40,6 +40,7 @@ func (s *ReleaseServer) UpdateRelease(c ctx.Context, req *services.UpdateRelease
if err != nil {
return nil, err
}
updatedRelease.Info.Username = getUserName(c)
s.Log("performing update for %s", req.Name)
res, err := s.performUpdate(currentRelease, updatedRelease, req)

@ -17,15 +17,27 @@ limitations under the License.
package tiller
import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
goprom "github.com/grpc-ecosystem/go-grpc-prometheus"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
"k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/version"
)
@ -34,42 +46,96 @@ import (
var maxMsgSize = 1024 * 1024 * 20
// DefaultServerOpts returns the set of default grpc ServerOption's that Tiller requires.
func DefaultServerOpts() []grpc.ServerOption {
func DefaultServerOpts(sysCli *kube.Client) []grpc.ServerOption {
return []grpc.ServerOption{
grpc.MaxMsgSize(maxMsgSize),
grpc.UnaryInterceptor(newUnaryInterceptor()),
grpc.StreamInterceptor(newStreamInterceptor()),
grpc.UnaryInterceptor(newUnaryInterceptor(sysCli)),
grpc.StreamInterceptor(newStreamInterceptor(sysCli)),
}
}
// NewServer creates a new grpc server.
func NewServer(opts ...grpc.ServerOption) *grpc.Server {
return grpc.NewServer(append(DefaultServerOpts(), opts...)...)
func NewServer(sysCli *kube.Client, opts ...grpc.ServerOption) *grpc.Server {
return grpc.NewServer(append(DefaultServerOpts(sysCli), opts...)...)
}
func newUnaryInterceptor() grpc.UnaryServerInterceptor {
func authenticate(c context.Context, sysCli *kube.Client) (context.Context, error) {
md, ok := metadata.FromContext(c)
if !ok {
return nil, errors.New("Missing metadata in context.")
}
var err error
authHeader, ok := md[string(kube.Authorization)]
if !ok || len(authHeader) == 0 || authHeader[0] == "" {
c, err = checkClientCert(c, sysCli)
} else {
if strings.HasPrefix(authHeader[0], "Bearer ") {
c, err = checkBearerAuth(c, authHeader[0], sysCli)
} else if strings.HasPrefix(authHeader[0], "Basic ") {
c, err = checkBasicAuth(c, authHeader[0], sysCli)
} else {
return nil, errors.New("Unknown authorization scheme.")
}
}
return c, err
}
func newUnaryInterceptor(sysCli *kube.Client) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if err := checkClientVersion(ctx); err != nil {
err = checkClientVersion(ctx)
if err != nil {
// whitelist GetVersion() from the version check
if _, m := splitMethod(info.FullMethod); m != "GetVersion" {
log.Println(err)
return nil, err
}
}
ctx, err = authenticate(ctx, sysCli)
if err != nil {
log.Println(err)
return nil, err
}
return goprom.UnaryServerInterceptor(ctx, req, info, handler)
}
}
func newStreamInterceptor() grpc.StreamServerInterceptor {
func newStreamInterceptor(sysCli *kube.Client) grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if err := checkClientVersion(ss.Context()); err != nil {
ctx := ss.Context()
err := checkClientVersion(ctx)
if err != nil {
log.Println(err)
return err
}
return goprom.StreamServerInterceptor(srv, ss, info, handler)
ctx, err = authenticate(ctx, sysCli)
if err != nil {
log.Println(err)
return err
}
newStream := serverStreamWrapper{
ss: ss,
ctx: ctx,
}
return goprom.StreamServerInterceptor(srv, newStream, info, handler)
}
}
// serverStreamWrapper wraps original ServerStream but uses modified context.
// this modified context will be available inside handler()
type serverStreamWrapper struct {
ss grpc.ServerStream
ctx context.Context
}
func (w serverStreamWrapper) Context() context.Context { return w.ctx }
func (w serverStreamWrapper) RecvMsg(msg interface{}) error { return w.ss.RecvMsg(msg) }
func (w serverStreamWrapper) SendMsg(msg interface{}) error { return w.ss.SendMsg(msg) }
func (w serverStreamWrapper) SendHeader(md metadata.MD) error { return w.ss.SendHeader(md) }
func (w serverStreamWrapper) SetHeader(md metadata.MD) error { return w.ss.SetHeader(md) }
func (w serverStreamWrapper) SetTrailer(md metadata.MD) { w.ss.SetTrailer(md) }
func splitMethod(fullMethod string) (string, string) {
if frags := strings.Split(fullMethod, "/"); len(frags) == 3 {
return frags[1], frags[2]
@ -93,3 +159,173 @@ func checkClientVersion(ctx context.Context) error {
}
return nil
}
func checkBearerAuth(c context.Context, h string, sysCli *kube.Client) (context.Context, error) {
token := h[len("Bearer "):]
clientset, err := sysCli.ClientSet()
if err != nil {
return c, err
}
// verify token
tokenReq := &authenticationapi.TokenReview{
Spec: authenticationapi.TokenReviewSpec{
Token: token,
},
}
result, err := clientset.Authentication().TokenReviews().Create(tokenReq)
if err != nil && !apierrors.IsNotFound(err) {
return c, err
} else if err == nil && !result.Status.Authenticated {
return c, errors.New("Not authenticated")
}
syscfg, err := sysCli.ClientConfig()
if err != nil {
return c, err
}
usrcfg := &rest.Config{
Host: syscfg.Host,
APIPath: syscfg.APIPath,
Prefix: syscfg.Prefix,
BearerToken: token,
}
usrcfg.TLSClientConfig.CertData = syscfg.TLSClientConfig.CertData
c = context.WithValue(c, kube.UserInfo, &result.Status.User)
c = context.WithValue(c, kube.UserClient, kube.New(&wrapClientConfig{cfg: usrcfg}))
c = context.WithValue(c, kube.SystemClient, sysCli)
return c, nil
}
func checkBasicAuth(c context.Context, h string, sysCli *kube.Client) (context.Context, error) {
basicAuth, err := base64.StdEncoding.DecodeString(h[len("Basic "):])
if err != nil {
return c, err
}
username, password := getUserPasswordFromBasicAuth(string(basicAuth))
if len(username) == 0 || len(password) == 0 {
return c, errors.New("Missing username or password.")
}
syscfg, err := sysCli.ClientConfig()
if err != nil {
return c, err
}
usrcfg := &rest.Config{
Host: syscfg.Host,
APIPath: syscfg.APIPath,
Prefix: syscfg.Prefix,
Username: username,
Password: password,
}
usrcfg.TLSClientConfig.CertData = syscfg.TLSClientConfig.CertData
usrClient := kube.New(&wrapClientConfig{cfg: usrcfg})
clientset, err := usrClient.ClientSet()
if err != nil {
return c, err
}
// verify credentials
_, err = clientset.Discovery().ServerVersion()
if err != nil {
return c, err
}
c = context.WithValue(c, kube.UserInfo, &authenticationapi.UserInfo{
Username: username,
})
c = context.WithValue(c, kube.UserClient, usrClient)
c = context.WithValue(c, kube.SystemClient, sysCli)
return c, nil
}
func getUserPasswordFromBasicAuth(token string) (string, string) {
st := strings.SplitN(token, ":", 2)
if len(st) == 2 {
return st[0], st[1]
}
return "", ""
}
func checkClientCert(c context.Context, sysCli *kube.Client) (context.Context, error) {
// ref: https://github.com/grpc/grpc-go/issues/111#issuecomment-275820771
peer, ok := peer.FromContext(c)
if !ok {
return c, errors.New("No peer found!")
}
tlsInfo, ok := peer.AuthInfo.(credentials.TLSInfo)
if !ok {
return c, errors.New("No TLS credential found!")
}
if len(tlsInfo.State.VerifiedChains) == 0 || len(tlsInfo.State.VerifiedChains[0]) == 0 {
return c, errors.New("No verified client certificate found!")
}
crt := tlsInfo.State.VerifiedChains[0][0]
user := authenticationapi.UserInfo{
Username: crt.Subject.CommonName,
}
syscfg, err := sysCli.ClientConfig()
if err != nil {
return c, err
}
usrcfg := *syscfg
usrcfg.Impersonate.UserName = crt.Subject.CommonName
c = context.WithValue(c, kube.UserInfo, &user)
c = context.WithValue(c, kube.UserClient, kube.New(&wrapClientConfig{cfg: &usrcfg}))
c = context.WithValue(c, kube.SystemClient, sysCli)
c = context.WithValue(c, kube.ImpersonateUser, struct{}{})
return c, nil
}
// wrapClientConfig makes a config that wraps a kubeconfig
type wrapClientConfig struct {
cfg *rest.Config
}
var _ clientcmd.ClientConfig = wrapClientConfig{}
func (wrapClientConfig) RawConfig() (clientcmdapi.Config, error) {
return clientcmdapi.Config{}, fmt.Errorf("inCluster environment config doesn't support multiple clusters")
}
func (w wrapClientConfig) ClientConfig() (*rest.Config, error) {
return w.cfg, nil
}
func (wrapClientConfig) Namespace() (string, bool, error) {
// This way assumes you've set the POD_NAMESPACE environment variable using the downward API.
// This check has to be done first for backwards compatibility with the way InClusterConfig was originally set up
if ns := os.Getenv("POD_NAMESPACE"); ns != "" {
return ns, true, nil
}
// Fall back to the namespace associated with the service account token, if available
if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
if ns := strings.TrimSpace(string(data)); len(ns) > 0 {
return ns, true, nil
}
}
return "default", false, nil
}
func (wrapClientConfig) ConfigAccess() clientcmd.ConfigAccess {
return clientcmd.NewDefaultClientConfigLoadingRules()
}
func getUserName(c context.Context) string {
user := c.Value(kube.UserInfo)
if user == nil {
return ""
}
userInfo, ok := user.(*authenticationapi.UserInfo)
if !ok {
return ""
}
return userInfo.Username
}

Loading…
Cancel
Save