feat: kubernetes basic integration
This commit is contained in:
31
internal/integration/integration.go
Normal file
31
internal/integration/integration.go
Normal file
@ -0,0 +1,31 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Integration interface {
|
||||
Integration()
|
||||
}
|
||||
|
||||
type OnStartup interface {
|
||||
Integration
|
||||
OnStartup(ctx context.Context) error
|
||||
}
|
||||
|
||||
func RunOnStartup(ctx context.Context, integrations []Integration) error {
|
||||
for _, it := range integrations {
|
||||
onStartup, ok := it.(OnStartup)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := onStartup.OnStartup(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
194
internal/integration/kubernetes/integration.go
Normal file
194
internal/integration/kubernetes/integration.go
Normal file
@ -0,0 +1,194 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
|
||||
writerTokenSubject = "bouncer-admin-kubernetes-writer"
|
||||
readerTokenSubject = "bouncer-admin-kubernetes-reader"
|
||||
)
|
||||
|
||||
type Integration struct {
|
||||
Options *Options
|
||||
}
|
||||
|
||||
// Integration implements integration.OnStartup.
|
||||
func (i *Integration) Integration() {}
|
||||
|
||||
// OnStartup implements integration.OnStartup.
|
||||
func (i *Integration) OnStartup(ctx context.Context) error {
|
||||
locker := i.Options.Locker
|
||||
timeout := i.Options.LockTimeout
|
||||
err := locker.WithLock(ctx, "bouncer-kubernetes-onstartup", timeout, func(ctx context.Context) error {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if i.Options.WriterTokenSecret != "" {
|
||||
if err := i.upsertTokenSecret(ctx, client, i.Options.WriterTokenSecretNamespace, i.Options.WriterTokenSecret, writerTokenSubject, jwt.RoleWriter); err != nil {
|
||||
return errors.Wrap(err, "could not upsert writer token secret")
|
||||
}
|
||||
}
|
||||
|
||||
if i.Options.ReaderTokenSecret != "" {
|
||||
if err := i.upsertTokenSecret(ctx, client, i.Options.ReaderTokenSecretNamespace, i.Options.ReaderTokenSecret, readerTokenSubject, jwt.RoleReader); err != nil {
|
||||
return errors.Wrap(err, "could not upsert reader token secret")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
annotationPublicKey = "bouncer.cadoles.com/public-key"
|
||||
)
|
||||
|
||||
func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.Clientset, namespace string, name string, subject string, role jwt.Role) error {
|
||||
if namespace == "" {
|
||||
defaultNamespace, err := i.getCurrentNamespace()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx,
|
||||
logger.F("secretNamespace", namespace),
|
||||
logger.F("secretName", name),
|
||||
logger.F("tokenRole", role),
|
||||
logger.F("tokenSubject", subject),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "generating new token")
|
||||
|
||||
key, err := jwk.LoadOrGenerate(i.Options.PrivateKey, jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
publicKey, err := key.PublicKey()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
publicKeyThumbprint, err := publicKey.Thumbprint(crypto.SHA256)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
publicKeyHash := fmt.Sprintf("%x", publicKeyThumbprint)
|
||||
|
||||
alreadyExists := true
|
||||
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serr.IsNotFound(err) {
|
||||
alreadyExists = false
|
||||
} else {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if !alreadyExists {
|
||||
token, err := jwt.GenerateToken(ctx, key, i.Options.Issuer, subject, role)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
secret := &v1.Secret{
|
||||
Type: v1.SecretTypeOpaque,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Annotations: map[string]string{
|
||||
annotationPublicKey: publicKeyHash,
|
||||
},
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"token": token,
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info(ctx, "creating token secret")
|
||||
|
||||
if _, err := client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
existingPublicKeyHash, exists := secret.Annotations[annotationPublicKey]
|
||||
if !exists || publicKeyHash != existingPublicKeyHash {
|
||||
token, err := jwt.GenerateToken(ctx, key, i.Options.Issuer, subject, role)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
secret.StringData = map[string]string{
|
||||
"token": token,
|
||||
}
|
||||
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
secret.Annotations[annotationPublicKey] = publicKeyHash
|
||||
|
||||
logger.Info(ctx, "updating token secret")
|
||||
|
||||
if _, err := client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
logger.Info(ctx, "key did not changed, doing nothing")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Integration) getCurrentNamespace() (string, error) {
|
||||
namespace, err := os.ReadFile(namespaceFile)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not retrieve current namespace")
|
||||
}
|
||||
|
||||
return string(namespace), nil
|
||||
}
|
||||
|
||||
func NewIntegration(funcs ...OptionFunc) *Integration {
|
||||
opts := NewOptions(funcs...)
|
||||
|
||||
return &Integration{
|
||||
Options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ integration.OnStartup = &Integration{}
|
||||
)
|
87
internal/integration/kubernetes/options.go
Normal file
87
internal/integration/kubernetes/options.go
Normal file
@ -0,0 +1,87 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock/memory"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
WriterTokenSecret string
|
||||
WriterTokenSecretNamespace string
|
||||
ReaderTokenSecret string
|
||||
ReaderTokenSecretNamespace string
|
||||
PrivateKey string
|
||||
Issuer string
|
||||
Locker lock.Locker
|
||||
LockTimeout time.Duration
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
WriterTokenSecret: "",
|
||||
WriterTokenSecretNamespace: "",
|
||||
ReaderTokenSecret: "",
|
||||
ReaderTokenSecretNamespace: "",
|
||||
PrivateKey: "",
|
||||
Issuer: "",
|
||||
Locker: memory.NewLocker(),
|
||||
LockTimeout: 30 * time.Second,
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithWriterTokenSecret(secretName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.WriterTokenSecret = secretName
|
||||
}
|
||||
}
|
||||
|
||||
func WithWriterTokenSecretNamespace(namespace string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.WriterTokenSecretNamespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithReaderTokenSecret(secretName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.ReaderTokenSecret = secretName
|
||||
}
|
||||
}
|
||||
|
||||
func WithReaderTokenSecretNamespace(namespace string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.ReaderTokenSecretNamespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithPrivateKey(privateKeyFile string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.PrivateKey = privateKeyFile
|
||||
}
|
||||
}
|
||||
|
||||
func WithIssuer(issuer string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Issuer = issuer
|
||||
}
|
||||
}
|
||||
|
||||
func WithLocker(locker lock.Locker) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Locker = locker
|
||||
}
|
||||
}
|
||||
|
||||
func WithLockTimeout(timeout time.Duration) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.LockTimeout = timeout
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user