Skip to content
This repository has been archived by the owner on Oct 9, 2023. It is now read-only.

Commit

Permalink
Pod Mutating Webhook & Secret Annotation Injector (#242)
Browse files Browse the repository at this point in the history
* Pod Mutating Webhook & Secret Annotation Injector

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Marshal the entire secret object instead

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* docs

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* cmd docs

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* refactor

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Unit tests

Signed-off-by: Haytham Abuelfutuh <[email protected]>

* Update pkg/utils/secrets/marshaler.go

Co-authored-by: Ketan Umare <[email protected]>
Signed-off-by: Haytham Abuelfutuh <[email protected]>

* introduce webhook in README

Signed-off-by: Haytham Abuelfutuh <[email protected]>

Co-authored-by: Ketan Umare <[email protected]>
  • Loading branch information
EngHabu and kumare3 authored Mar 22, 2021
1 parent 0d1eeab commit 979fabe
Show file tree
Hide file tree
Showing 33 changed files with 2,657 additions and 48 deletions.
71 changes: 61 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,41 @@ Flyte Propeller

Kubernetes operator to executes Flyte graphs natively on kubernetes

Components
==========

Propeller
---------
Propeller is a K8s native operator that executes Flyte workflows. Workflow Spec is written in Protobuf for
cross-compatibility.

Propeller Webhook
-----------------
A Mutating Webhook that can be optionally deployed to extend Flyte Propeller's functionality. It currently supports
enables injecting secrets into pods launched directly or indirectly through Flyte backend plugins.

kubectl-flyte
-------------
A Kubectl-plugin to interact with Flyte Workflow CRDs. It enables retrieving and rendering Flyte Workflows in CLI as
well as safely aborting running workflows.

Getting Started
===============
kubectl-flyte tool
------------------
kubectl-flyte is an command line tool that can be used as an extension to kubectl. It is a separate binary that is built from the propeller repo.
kubectl-flyte is an command line tool that can be used as an extension to kubectl. It is a separate binary that is built
from the propeller repo.

Install
-------
This command will install kubectl-flyte and flytepropeller to `~/go/bin`

```
$ make compile
```

You can also use [Krew](https://github.com/kubernetes-sigs/krew) to install the kubectl-flyte CLI:

```
$ kubectl krew install flyte
```
Expand Down Expand Up @@ -68,7 +89,8 @@ To retrieve all workflows in a namespace use the --namespace option, --namespace
Success: 19, Failed: 0, Running: 0, Waiting: 0
```

To retrieve a specific workflow, namespace can either be provided in the format namespace/name or using the --namespace argument
To retrieve a specific workflow, namespace can either be provided in the format namespace/name or using the --namespace
argument

```
$ kubectl-flyte get flytekit-development/flytekit-development-ff806e973581f4508bf1
Expand All @@ -89,48 +111,77 @@ To delete a specific workflow
$ kubectl-flyte delete --namespace flytekit-development flytekit-development-ff806e973581f4508bf1
```

To delete all completed workflows - they have to be either success/failed with a special isCompleted label set on them. The Label is set `here <https://github.com/flyteorg/flytepropeller/blob/master/pkg/controller/controller.go#L247>`
To delete all completed workflows - they have to be either success/failed with a special isCompleted label set on them.
The Label is set `here <https://github.com/flyteorg/flytepropeller/blob/master/pkg/controller/controller.go#L247>`

```
$ kubectl-flyte delete --namespace flytekit-development --all-completed
```

Running propeller locally
-------------------------
use the config.yaml in root found `here <https://github.com/flyteorg/flytepropeller/blob/master/config.yaml>`. Cd into this folder and then run
use the config.yaml in root found `here <https://github.com/flyteorg/flytepropeller/blob/master/config.yaml>`. Cd into
this folder and then run

```
$ flytepropeller --logtostderr
$ flytepropeller
```

Following dependencies need to be met

1. Blob store (you can forward minio port to localhost)
2. Admin Service endpoint (can be forwarded) OR *Disable* events to admin and launchplans
3. access to kubeconfig and kubeapi

Running webhook
---------------

API Server requires the webhook to serve traffic over SSL. To issue self-signed certs to be used for serving traffic,
use:

```
$ flytepropeller webhook init-certs
```

This will create a ca.crt, tls.crt and key.crt and store them to flyte-pod-webhook secret. If a secret of the same name
already exist, it'll not override it.

Starting the webhook can be done by running:

```
$ flytepropeller webhook
```

The secret should be mounted and accessible to this command. It'll then create a MutatingWebhookConfiguration object
with the details of the webhook and that registers the webhook with ApiServer.

Making changes to CRD
=====================
*Remember* changes to CRD should be carefully done, they should be backwards compatible or else you should use proper
operator versioning system. Once you do the changes, you have to follow the
following steps.
operator versioning system. Once you do the changes, you have to follow the following steps.

- ensure the propeller code is checked out in $GOPATH/github.com/flyteorg/flytepropeller
- Uncomment https://github.com/flyteorg/flytepropeller/blob/master/hack/tools.go#L5
-

```bash
go mod vendor
```

- Now generate the code

```bash
make op_code_generate
$make op_code_generate
```

**Why do we have to do this?**
Flytepropeller uses old way of writing Custom controllers for K8s. The k8s.io/code-generator only works in the GOPATH relative code path (sadly). So you have checkout the code in the right place.
Also, `go mod vendor` is needed to get code-generator in a discoverable path.
**Why do we have to do this?**
Flytepropeller uses old way of writing Custom controllers for K8s. The k8s.io/code-generator only works in the GOPATH
relative code path (sadly). So you have checkout the code in the right place. Also, `go mod vendor` is needed to get
code-generator in a discoverable path.

**TODO**

1. We may be able to avoid needing the old style go-path
2. Migrate to using controller runtime

222 changes: 222 additions & 0 deletions cmd/controller/cmd/init_certs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package cmd

import (
"bytes"
"context"
cryptorand "crypto/rand"

"github.com/flyteorg/flytestdlib/logger"
"k8s.io/apimachinery/pkg/api/errors"

"github.com/flyteorg/flytepropeller/pkg/controller/config"

corev1 "k8s.io/api/core/v1"
v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"

"github.com/flyteorg/flytepropeller/pkg/webhook"

"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"time"

"github.com/spf13/cobra"
)

const (
CaCertKey = "ca.crt"
ServerCertKey = "tls.crt"
ServerCertPrivateKey = "tls.key"
)

// initCertsCmd initializes x509 TLS Certificates and saves them to a secret.
var initCertsCmd = &cobra.Command{
Use: "init-certs",
Aliases: []string{"init-cert"},
Short: "Generates CA, Cert and cert key and saves them into a secret using the configured --webhook.secretName",
Long: `
K8s API Server Webhooks' Services are required to serve traffic over SSL. Ideally the SSL certificate is issued by a
known Certificate Authority that's already trusted by API Server (e.g. using Let's Encrypt Cluster Certificate Controller).
Otherwise, a self-issued certificate can be used to serve traffic as long as the CA for that certificate is stored in
the MutatingWebhookConfiguration object that registers the webhook with API Server.
init-certs generates 4096-bit X509 Certificates for Certificate Authority as well as a derived Server cert and its private
key. It serializes all of them in PEM format (base64 encoding-compatible) and stores them into a new kubernetes secret.
If a secret with the same name already exist, it'll not update it.
POD_NAMESPACE is an environment variable that can, optionally, be set on the Pod that runs init-certs command. If set,
the secret will be created in that namespace. It's critical that this command creates the secret in the right namespace
for the Webhook command to mount and read correctly.
`,
Example: "flytepropeller webhook init-certs",
RunE: func(cmd *cobra.Command, args []string) error {
return runCertsCmd(context.Background(), config.GetConfig(), webhook.GetConfig())
},
}

type webhookCerts struct {
// base64 Encoded CA Cert
CaPEM *bytes.Buffer
// base64 Encoded Server Cert
ServerPEM *bytes.Buffer
// base64 Encoded Server Cert Key
PrivateKeyPEM *bytes.Buffer
}

func init() {
webhookCmd.AddCommand(initCertsCmd)
}

func runCertsCmd(ctx context.Context, propellerCfg *config.Config, cfg *webhook.Config) error {
podNamespace, found := os.LookupEnv(PodNamespaceEnvVar)
if !found {
podNamespace = podDefaultNamespace
}

logger.Infof(ctx, "Issuing certs")
certs, err := createCerts(podNamespace)
if err != nil {
return err
}

kubeClient, _, err := getKubeConfig(ctx, propellerCfg)
if err != nil {
return err
}

logger.Infof(ctx, "Creating secret [%v] in Namespace [%v]", cfg.SecretName, podNamespace)
err = createWebhookSecret(ctx, podNamespace, cfg, certs, kubeClient.CoreV1().Secrets(podNamespace))
if err != nil {
return err
}

return nil
}

func createWebhookSecret(ctx context.Context, namespace string, cfg *webhook.Config, certs webhookCerts, secretsClient v1.SecretInterface) error {
isImmutable := true
_, err := secretsClient.Create(ctx, &corev1.Secret{
ObjectMeta: v12.ObjectMeta{
Name: cfg.SecretName,
Namespace: namespace,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
CaCertKey: certs.CaPEM.Bytes(),
ServerCertKey: certs.ServerPEM.Bytes(),
ServerCertPrivateKey: certs.PrivateKeyPEM.Bytes(),
},
Immutable: &isImmutable,
}, v12.CreateOptions{})

if errors.IsAlreadyExists(err) {
// TODO: Maybe get the secret and validate it has all the required keys?
logger.Infof(ctx, "A secret already exists with the same name. Ignoring creating secret.")
return nil
}

logger.Infof(ctx, "Created secret [%v]", cfg.SecretName)

return err
}

func createCerts(serviceNamespace string) (certs webhookCerts, err error) {
// CA config
caRequest := &x509.Certificate{
SerialNumber: big.NewInt(2021),
Subject: pkix.Name{
Organization: []string{"flyte.org"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}

// CA private key
caPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 4096)
if err != nil {
return webhookCerts{}, err
}

// Self signed CA certificate
caCert, err := x509.CreateCertificate(cryptorand.Reader, caRequest, caRequest, &caPrivateKey.PublicKey, caPrivateKey)
if err != nil {
return webhookCerts{}, err
}

// PEM encode CA cert
caPEM := new(bytes.Buffer)
err = pem.Encode(caPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: caCert,
})
if err != nil {
return webhookCerts{}, err
}

dnsNames := []string{"flyte-pod-webhook",
"flyte-pod-webhook." + serviceNamespace, "flyte-pod-webhook." + serviceNamespace + ".svc"}
commonName := "flyte-pod-webhook." + serviceNamespace + ".svc"

// server cert config
certRequest := &x509.Certificate{
DNSNames: dnsNames,
SerialNumber: big.NewInt(1658),
Subject: pkix.Name{
CommonName: commonName,
Organization: []string{"flyte.org"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}

// server private key
serverPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 4096)
if err != nil {
return webhookCerts{}, err
}

// sign the server cert
cert, err := x509.CreateCertificate(cryptorand.Reader, certRequest, caRequest, &serverPrivateKey.PublicKey, caPrivateKey)
if err != nil {
return webhookCerts{}, err
}

// PEM encode the server cert and key
serverCertPEM := new(bytes.Buffer)
err = pem.Encode(serverCertPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
})

if err != nil {
return webhookCerts{}, fmt.Errorf("failed to Encode CertPEM. Error: %w", err)
}

serverPrivKeyPEM := new(bytes.Buffer)
err = pem.Encode(serverPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(serverPrivateKey),
})

if err != nil {
return webhookCerts{}, fmt.Errorf("failed to Encode Cert Private Key. Error: %w", err)
}

return webhookCerts{
CaPEM: caPEM,
ServerPEM: serverCertPEM,
PrivateKeyPEM: serverPrivKeyPEM,
}, nil
}
9 changes: 6 additions & 3 deletions cmd/controller/cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Commands for FlytePropeller controller.
package cmd

import (
Expand Down Expand Up @@ -58,7 +59,7 @@ var rootCmd = &cobra.Command{
Short: "Operator for running Flyte Workflows",
Long: `Flyte Propeller runs a workflow to completion by recursing through the nodes,
handling their tasks to completion and propagating their status upstream.`,
PreRunE: initConfig,
PersistentPreRunE: initConfig,
Run: func(cmd *cobra.Command, args []string) {
executeRootCmd(config2.GetConfig())
},
Expand Down Expand Up @@ -93,12 +94,14 @@ func init() {
rootCmd.AddCommand(viper.GetConfigCommand())
}

func initConfig(_ *cobra.Command, _ []string) error {
func initConfig(cmd *cobra.Command, _ []string) error {
configAccessor = viper.NewAccessor(config.Options{
StrictMode: true,
StrictMode: false,
SearchPaths: []string{cfgFile},
})

configAccessor.InitializePflags(cmd.PersistentFlags())

err := configAccessor.UpdateConfig(context.TODO())
if err != nil {
return err
Expand Down
Loading

0 comments on commit 979fabe

Please sign in to comment.