Skip to content

Commit

Permalink
Support for proxying Juju client through k8s
Browse files Browse the repository at this point in the history
When Juju clients want to talk with Juju controllers inside of k8s they
require the controller to be publicly accessible. This commit introduces
a change allowing clients to port-forward to the controller when a
route able connection is not possible.

Main features of the PR:
- k8s tunnel and proxier for the k8s api
- Start of a wider proxying framework for developing other types of
  proxy's for Juju controller connections
- Changes to the users controller.yaml file describing the proxy to use
  when making connections.
  • Loading branch information
tlm committed Jan 18, 2021
1 parent daa4214 commit 4a8250d
Show file tree
Hide file tree
Showing 34 changed files with 1,372 additions and 40 deletions.
1 change: 1 addition & 0 deletions agent/agentbootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ func InitializeState(
if err != nil {
return nil, errors.Trace(err)
}
cloudSpec.IsControllerCloud = true

provider, err := args.Provider(cloudSpec.Type)
if err != nil {
Expand Down
7 changes: 4 additions & 3 deletions agent/agentbootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,10 @@ LXC_BRIDGE="ignored"`[1:])
envProvider.CheckCall(c, 2, "Open", environs.OpenParams{
ControllerUUID: controllerCfg.ControllerUUID(),
Cloud: environscloudspec.CloudSpec{
Type: "dummy",
Name: "dummy",
Region: "dummy-region",
Type: "dummy",
Name: "dummy",
Region: "dummy-region",
IsControllerCloud: true,
},
Config: expectedCalledCfg,
})
Expand Down
26 changes: 26 additions & 0 deletions api/apiclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/juju/juju/charmstore"

"github.com/juju/juju/core/network"
jujuproxy "github.com/juju/juju/proxy"
"github.com/juju/juju/rpc"
"github.com/juju/juju/rpc/jsoncodec"
"github.com/juju/juju/utils/proxy"
Expand Down Expand Up @@ -158,6 +159,11 @@ type state struct {
// bakeryClient holds the client that will be used to
// authorize macaroon based login requests.
bakeryClient *httpbakery.Client

// proxy is the proxy used for this connection when not nil. If's expected
// the proxy has already been started when placing in this var. This struct
// will take the responsibility of closing the proxy.
proxy jujuproxy.Proxier
}

// RedirectError is returned from Open when the controller
Expand Down Expand Up @@ -211,6 +217,23 @@ func Open(info *Info, opts DialOpts) (Connection, error) {
defer cancel()
dialCtx = ctx1
}

if info.Proxier != nil {
if err := info.Proxier.Start(); err != nil {
return nil, errors.Annotate(err, "starting proxy for api connection")
}

switch p := info.Proxier.(type) {
case jujuproxy.TunnelProxier:
info.Addrs = []string{
fmt.Sprintf("%s:%s", p.Host(), p.Port()),
}
default:
info.Proxier.Stop()
return nil, errors.New("unknown proxier provided")
}
}

dialResult, err := dialAPI(dialCtx, info, opts)
if err != nil {
return nil, errors.Trace(err)
Expand Down Expand Up @@ -1241,6 +1264,9 @@ func (s *state) Close() error {
close(s.closed)
}
<-s.broken
if s.proxy != nil {
s.proxy.Stop()
}
return err
}

Expand Down
5 changes: 5 additions & 0 deletions api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/juju/juju/api/uniter"
"github.com/juju/juju/api/upgrader"
"github.com/juju/juju/core/network"
"github.com/juju/juju/proxy"
"github.com/juju/juju/rpc/jsoncodec"
)

Expand Down Expand Up @@ -80,6 +81,10 @@ type Info struct {
// Nonce holds the nonce used when provisioning the machine. Used
// only by the machine agent.
Nonce string `yaml:",omitempty"`

// Proxier describes a proxier to use to for establing an API connection
// A nil proxier means that it will not be used.
Proxier proxy.Proxier
}

// Ports returns the unique ports for the api addresses.
Expand Down
55 changes: 54 additions & 1 deletion caas/kubernetes/provider/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
agenttools "github.com/juju/juju/agent/tools"
"github.com/juju/juju/caas"
"github.com/juju/juju/caas/kubernetes/provider/constants"
k8sproxy "github.com/juju/juju/caas/kubernetes/provider/proxy"
k8sutils "github.com/juju/juju/caas/kubernetes/provider/utils"
providerutils "github.com/juju/juju/caas/kubernetes/provider/utils"
"github.com/juju/juju/cloud"
Expand All @@ -47,6 +48,8 @@ const (

// ControllerServiceFQDNTemplate is the FQDN of the controller service using the cluster DNS.
ControllerServiceFQDNTemplate = "controller-service.controller-%s.svc.cluster.local"

proxyResourceName = "proxy"
)

var (
Expand Down Expand Up @@ -251,8 +254,12 @@ func newcontrollerStack(
return cs, nil
}

func getBootstrapResourceName(stackName string, name string) string {
return stackName + "-" + strings.Replace(name, ".", "-", -1)
}

func (c *controllerStack) getResourceName(name string) string {
return c.stackName + "-" + strings.Replace(name, ".", "-", -1)
return getBootstrapResourceName(c.stackName, name)
}

func (c *controllerStack) getControllerSecret() (secret *core.Secret, err error) {
Expand Down Expand Up @@ -354,6 +361,11 @@ func (c *controllerStack) Deploy() (err error) {
return bootstrap.Cancelled()
}

// create the proxy resources for services of type cluster ip
if err = c.createControllerProxy(); err != nil {
return errors.Annotate(err, "creating controller service proxy for controller")
}

// create shared-secret secret for controller pod.
if err = c.createControllerSecretSharedSecret(); err != nil {
return errors.Annotate(err, "creating shared-secret secret for controller")
Expand Down Expand Up @@ -450,6 +462,47 @@ func (c *controllerStack) getControllerSvcSpec(cloudType string, cfg *podcfg.Boo
return spec, nil
}

func (c *controllerStack) createControllerProxy() error {
if c.pcfg.Bootstrap.IgnoreProxy {
return nil
}

// Lets first take a look at what will be deployed for a service.
// If the service type is clusterip then we will setup the proxy

cloudType, _, _ := cloud.SplitHostCloudRegion(c.pcfg.Bootstrap.ControllerCloud.HostCloudRegion)
controllerSvcSpec, err := c.getControllerSvcSpec(cloudType, c.pcfg.Bootstrap)
if err != nil {
return errors.Trace(err)
}

if controllerSvcSpec.ServiceType != core.ServiceTypeClusterIP {
// Not a cluster ip service so we don't need to setup a k8s proxy
return nil
}

k8sClient := c.broker.client()

remotePort := intstr.FromInt(c.portAPIServer)
config := k8sproxy.ControllerProxyConfig{
Name: c.getResourceName(proxyResourceName),
Namespace: c.broker.GetCurrentNamespace(),
RemotePort: remotePort.String(),
TargetService: c.resourceNameService,
}

err = k8sproxy.CreateControllerProxy(
config,
c.stackLabels,
k8sClient.CoreV1().ConfigMaps(c.broker.GetCurrentNamespace()),
k8sClient.RbacV1().Roles(c.broker.GetCurrentNamespace()),
k8sClient.RbacV1().RoleBindings(c.broker.GetCurrentNamespace()),
k8sClient.CoreV1().ServiceAccounts(c.broker.GetCurrentNamespace()),
)

return errors.Trace(err)
}

func (c *controllerStack) createControllerService() error {
svcName := c.resourceNameService

Expand Down
1 change: 1 addition & 0 deletions caas/kubernetes/provider/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ func (s *bootstrapSuite) TestBootstrap(c *gc.C) {
SHA256: "deadbeef",
Size: 999,
}
s.pcfg.Bootstrap.IgnoreProxy = true

controllerStacker := s.controllerStackerGetter()

Expand Down
19 changes: 19 additions & 0 deletions caas/kubernetes/provider/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package provider

import (
k8sproxy "github.com/juju/juju/caas/kubernetes/provider/proxy"
"github.com/juju/juju/proxy"
)

func (k *kubernetesClient) ConnectionProxyInfo() (proxy.Proxier, error) {
return k8sproxy.GetControllerProxy(
getBootstrapResourceName(JujuControllerStackName, proxyResourceName),
k.k8sCfgUnlocked.Host,
k.client().CoreV1().ConfigMaps(k.GetCurrentNamespace()),
k.client().CoreV1().ServiceAccounts(k.GetCurrentNamespace()),
k.client().CoreV1().Secrets(k.GetCurrentNamespace()),
)
}
9 changes: 9 additions & 0 deletions caas/kubernetes/provider/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ package provider
import (
k8slabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/klog/v2"

"github.com/juju/juju/caas"
"github.com/juju/juju/caas/kubernetes/provider/constants"
)

type klogWriter func([]byte) (int, error)

const volBindModeWaitFirstConsumer = "WaitForFirstConsumer"

var (
Expand All @@ -28,6 +31,8 @@ var (
)

func init() {
klog.SetLogger(newKlogAdapter())

caas.RegisterContainerProvider(constants.CAASProviderType, providerInstance)

// k8sCloudCheckers is a collection of k8s node selector requirement definitions
Expand Down Expand Up @@ -144,3 +149,7 @@ func compileLifecycleModelTeardownSelector() k8slabels.Selector {
}},
)
}

func (k klogWriter) Write(p []byte) (n int, err error) {
return k(p)
}
56 changes: 56 additions & 0 deletions caas/kubernetes/provider/klog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2020 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package provider

import (
"github.com/go-logr/logr"
"github.com/juju/loggo"
)

// klogAdapter is an adapter for Kubernetes logger onto juju loggo. We use this
// to suppress logging from client-go and force it through juju logging methods
type klogAdapter struct {
loggo.Logger
}

// newKlogAdapter creates a new klog adapter to juju loggo
func newKlogAdapter() *klogAdapter {
return &klogAdapter{
Logger: loggo.GetLogger("juju.kubernetes.klog"),
}
}

// Enabled see https://pkg.go.dev/github.com/go-logr/logr#Logger
func (k *klogAdapter) Enabled() bool {
return true
}

// Error see https://pkg.go.dev/github.com/go-logr/logr#Logger
func (k *klogAdapter) Error(err error, msg string, keysAndValues ...interface{}) {
if err != nil {
k.Logger.Debugf(msg+": "+err.Error(), keysAndValues...)
} else {
k.Logger.Debugf(msg, keysAndValues...)
}
}

// Info see https://pkg.go.dev/github.com/go-logr/logr#Logger
func (k *klogAdapter) Info(msg string, keysAndValues ...interface{}) {
k.Logger.Infof(msg, keysAndValues...)
}

// V see https://pkg.go.dev/github.com/go-logr/logr#Logger
func (k *klogAdapter) V(level int) logr.Logger {
return k
}

// WithValues see https://pkg.go.dev/github.com/go-logr/logr#Logger
func (k *klogAdapter) WithValues(keysAndValues ...interface{}) logr.Logger {
return k
}

// WithName see https://pkg.go.dev/github.com/go-logr/logr#Logger
func (k *klogAdapter) WithName(name string) logr.Logger {
return k
}
83 changes: 83 additions & 0 deletions caas/kubernetes/provider/proxy/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package proxy

import (
"context"
"encoding/json"
"fmt"

"github.com/juju/errors"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
core "k8s.io/client-go/kubernetes/typed/core/v1"
)

const (
serviceAccountSecretCADataKey = "ca.crt"
serviceAccountSecretNamespaceKey = "namespace"
serviceAccountSecretTokenKey = "token"
)

func GetControllerProxy(
name,
apiHost string,
configI core.ConfigMapInterface,
saI core.ServiceAccountInterface,
secretI core.SecretInterface,
) (*Proxier, error) {
cm, err := configI.Get(context.TODO(), name, meta.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil, errors.NotFoundf("controller proxy config %s", name)
} else if err != nil {
return nil, errors.Trace(err)
}

config := ControllerProxyConfig{}
if err := json.Unmarshal([]byte(cm.Data[ProxyConfigMapKey]), &config); err != nil {
return nil, errors.Trace(err)
}

sa, err := saI.Get(context.TODO(), config.Name, meta.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil, errors.NotFoundf("controller proxy service account for %s", name)
} else if err != nil {
return nil, errors.Trace(err)
}

if secLen := len(sa.Secrets); secLen < 1 || secLen > 1 {
return nil, fmt.Errorf("unsupported number of service account secrets: %d", secLen)
}

sec, err := secretI.Get(context.TODO(), sa.Secrets[0].Name, meta.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil, fmt.Errorf("could not get proxy service account secret: %s", sa.Secrets[0].Name)
} else if err != nil {
return nil, errors.Trace(err)
}

proxierConfig := ProxierConfig{
APIHost: apiHost,
CAData: string(sec.Data[serviceAccountSecretCADataKey]),
Namespace: config.Namespace,
RemotePort: config.RemotePort,
Service: config.TargetService,
ServiceAccountToken: string(sec.Data[serviceAccountSecretTokenKey]),
}

return NewProxier(proxierConfig), nil
}

func HasControllerProxy(
name string,
configI core.ConfigMapInterface,
) (bool, error) {
_, err := configI.Get(context.TODO(), name, meta.GetOptions{})
if k8serrors.IsNotFound(err) {
return false, nil
} else if err != nil {
return false, errors.Trace(err)
}
return true, nil
}
Loading

0 comments on commit 4a8250d

Please sign in to comment.