mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-31 03:45:17 +00:00
Merge branch 'master' of https://github.com/nirmata/kube-policy
This commit is contained in:
commit
a9843b2f55
14 changed files with 756 additions and 87 deletions
17
config/config.go
Normal file
17
config/config.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
// These constants MUST be equal to the corresponding names in service definition in definitions/install.yaml
|
||||
WebhookServiceNamespace = "kube-system"
|
||||
WebhookServiceName = "kube-policy-svc"
|
||||
|
||||
WebhookConfigName = "nirmata-kube-policy-webhook-cfg"
|
||||
MutationWebhookName = "webhook.nirmata.kube-policy"
|
||||
)
|
||||
|
||||
var (
|
||||
WebhookServicePath = "/mutate"
|
||||
WebhookConfigLabels = map[string]string {
|
||||
"app": "kube-policy",
|
||||
}
|
||||
)
|
17
constants/constants.go
Normal file
17
constants/constants.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package constants
|
||||
|
||||
const (
|
||||
// These constants MUST be equal to the corresponding names in service definition in definitions/install.yaml
|
||||
WebhookServiceNamespace = "kube-system"
|
||||
WebhookServiceName = "kube-policy-svc"
|
||||
|
||||
WebhookConfigName = "nirmata-kube-policy-webhook-cfg"
|
||||
MutationWebhookName = "webhook.nirmata.kube-policy"
|
||||
)
|
||||
|
||||
var (
|
||||
WebhookServicePath = "/mutate"
|
||||
WebhookConfigLabels = map[string]string {
|
||||
"app": "kube-policy",
|
||||
}
|
||||
)
|
|
@ -145,3 +145,60 @@ spec:
|
|||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: kube-system
|
||||
name: kube-policy-svc
|
||||
labels:
|
||||
app: kube-policy
|
||||
spec:
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 443
|
||||
selector:
|
||||
app: kube-policy
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: kube-policy-service-account
|
||||
namespace: kube-system
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
metadata:
|
||||
name: kube-policy-admin
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: kube-policy-service-account
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: kube-system
|
||||
name: kube-policy-deployment
|
||||
labels:
|
||||
app: kube-policy
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: kube-policy
|
||||
spec:
|
||||
serviceAccountName: kube-policy-service-account
|
||||
containers:
|
||||
- name: kube-policy
|
||||
image: nirmata/kube-policy:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 443
|
||||
securityContext:
|
||||
privileged: true
|
||||
|
|
76
init.go
Normal file
76
init.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"github.com/nirmata/kube-policy/config"
|
||||
"github.com/nirmata/kube-policy/kubeclient"
|
||||
"github.com/nirmata/kube-policy/utils"
|
||||
|
||||
rest "k8s.io/client-go/rest"
|
||||
clientcmd "k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
func createClientConfig(kubeconfig string) (*rest.Config, error) {
|
||||
if kubeconfig == "" {
|
||||
log.Printf("Using in-cluster configuration")
|
||||
return rest.InClusterConfig()
|
||||
} else {
|
||||
log.Printf("Using configuration from '%s'", kubeconfig)
|
||||
return clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
}
|
||||
}
|
||||
|
||||
func readTlsPairFromFiles(certFile, keyFile string) *utils.TlsPemPair {
|
||||
if certFile == "" || keyFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
certContent, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
log.Printf("Unable to read file with TLS certificate: path - %s, error - %v", certFile, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
keyContent, err := ioutil.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
log.Printf("Unable to read file with TLS private key: path - %s, error - %v", keyFile, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &utils.TlsPemPair{
|
||||
Certificate: certContent,
|
||||
PrivateKey: keyContent,
|
||||
}
|
||||
}
|
||||
|
||||
// Loads or creates PEM private key and TLS certificate for webhook server
|
||||
// Returns struct with key/certificate pair
|
||||
func initTlsPemsPair(configuration *rest.Config, client *kubeclient.KubeClient) (*utils.TlsPemPair, error) {
|
||||
apiServerUrl, err := url.Parse(configuration.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certProps := utils.TlsCertificateProps{
|
||||
Service: config.WebhookServiceName,
|
||||
Namespace: config.WebhookServiceNamespace,
|
||||
ApiServerHost: apiServerUrl.Hostname(),
|
||||
}
|
||||
|
||||
tlsPair := client.ReadTlsPair(certProps)
|
||||
if utils.IsTlsPairShouldBeUpdated(tlsPair) {
|
||||
log.Printf("Generating new key/certificate pair for TLS")
|
||||
tlsPair, err = client.GenerateTlsPemPair(certProps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = client.WriteTlsPair(certProps, tlsPair)
|
||||
if err != nil {
|
||||
log.Printf("Unable to save TLS pair to the cluster: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return tlsPair, nil
|
||||
}
|
181
kubeclient/certificates.go
Normal file
181
kubeclient/certificates.go
Normal file
|
@ -0,0 +1,181 @@
|
|||
package kubeclient
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nirmata/kube-policy/utils"
|
||||
|
||||
certificates "k8s.io/api/certificates/v1beta1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Issues TLS certificate for webhook server using given PEM private key
|
||||
// Returns signed and approved TLS certificate in PEM format
|
||||
func (kc *KubeClient) GenerateTlsPemPair(props utils.TlsCertificateProps) (*utils.TlsPemPair, error) {
|
||||
privateKey, err := utils.TlsGeneratePrivateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certRequest, err := utils.TlsCertificateGenerateRequest(privateKey, props)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Unable to create certificate request: %v", err))
|
||||
}
|
||||
|
||||
certRequest, err = kc.submitAndApproveCertificateRequest(certRequest)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Unable to submit and approve certificate request: %v", err))
|
||||
}
|
||||
|
||||
tlsCert, err := kc.fetchCertificateFromRequest(certRequest, 10)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Unable to fetch certificate from request: %v", err))
|
||||
}
|
||||
|
||||
return &utils.TlsPemPair{
|
||||
Certificate: tlsCert,
|
||||
PrivateKey: utils.TlsPrivateKeyToPem(privateKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Submits and approves certificate request, returns request which need to be fetched
|
||||
func (kc *KubeClient) submitAndApproveCertificateRequest(req *certificates.CertificateSigningRequest) (*certificates.CertificateSigningRequest, error) {
|
||||
certClient := kc.client.CertificatesV1beta1().CertificateSigningRequests()
|
||||
|
||||
csrList, err := certClient.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Unable to list existing certificate requests: %v", err))
|
||||
}
|
||||
|
||||
for _, csr := range csrList.Items {
|
||||
if csr.ObjectMeta.Name == req.ObjectMeta.Name {
|
||||
err := certClient.Delete(csr.ObjectMeta.Name, defaultDeleteOptions())
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Unable to delete existing certificate request: %v", err))
|
||||
}
|
||||
kc.logger.Printf("Old certificate request is deleted")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
res, err := certClient.Create(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kc.logger.Printf("Certificate request %s is created", req.ObjectMeta.Name)
|
||||
|
||||
res.Status.Conditions = append(res.Status.Conditions, certificates.CertificateSigningRequestCondition{
|
||||
Type: certificates.CertificateApproved,
|
||||
Reason: "NKP-Approve",
|
||||
Message: "This CSR was approved by Nirmata kube-policy controller",
|
||||
})
|
||||
res, err = certClient.UpdateApproval(res)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Unable to approve certificate request: %v", err))
|
||||
}
|
||||
kc.logger.Printf("Certificate request %s is approved", res.ObjectMeta.Name)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
const certificateFetchWaitInterval time.Duration = 200 * time.Millisecond
|
||||
|
||||
// Fetches certificate from given request. Tries to obtain certificate for maxWaitSeconds
|
||||
func (kc *KubeClient) fetchCertificateFromRequest(req *certificates.CertificateSigningRequest, maxWaitSeconds uint8) ([]byte, error) {
|
||||
// TODO: react of SIGINT and SIGTERM
|
||||
timeStart := time.Now()
|
||||
certClient := kc.client.CertificatesV1beta1().CertificateSigningRequests()
|
||||
for time.Now().Sub(timeStart) < time.Duration(maxWaitSeconds)*time.Second {
|
||||
r, err := certClient.Get(req.ObjectMeta.Name, defaultGetOptions())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.Status.Certificate != nil {
|
||||
return r.Status.Certificate, nil
|
||||
}
|
||||
|
||||
for _, condition := range r.Status.Conditions {
|
||||
if condition.Type == certificates.CertificateDenied {
|
||||
return nil, errors.New(condition.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("Cerificate fetch timeout is reached: %d seconds", maxWaitSeconds))
|
||||
}
|
||||
|
||||
const privateKeyField string = "privateKey"
|
||||
const certificateField string = "certificate"
|
||||
|
||||
// Reads the pair of TLS certificate and key from the specified secret.
|
||||
func (kc *KubeClient) ReadTlsPair(props utils.TlsCertificateProps) *utils.TlsPemPair {
|
||||
name := generateSecretName(props)
|
||||
secret, err := kc.client.CoreV1().Secrets(props.Namespace).Get(name, defaultGetOptions())
|
||||
if err != nil {
|
||||
kc.logger.Printf("Unable to get secret %s/%s: %s", props.Namespace, name, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
pemPair := utils.TlsPemPair{
|
||||
Certificate: secret.Data[certificateField],
|
||||
PrivateKey: secret.Data[privateKeyField],
|
||||
}
|
||||
if len(pemPair.Certificate) == 0 {
|
||||
kc.logger.Printf("TLS Certificate not found in secret %s/%s", props.Namespace, name)
|
||||
return nil
|
||||
}
|
||||
if len(pemPair.PrivateKey) == 0 {
|
||||
kc.logger.Printf("TLS PrivateKey not found in secret %s/%s", props.Namespace, name)
|
||||
return nil
|
||||
}
|
||||
return &pemPair
|
||||
}
|
||||
|
||||
// Writes the pair of TLS certificate and key to the specified secret.
|
||||
// Updates existing secret or creates new one.
|
||||
func (kc *KubeClient) WriteTlsPair(props utils.TlsCertificateProps, pemPair *utils.TlsPemPair) error {
|
||||
name := generateSecretName(props)
|
||||
secret, err := kc.client.CoreV1().Secrets(props.Namespace).Get(name, defaultGetOptions())
|
||||
|
||||
if err == nil { // Update existing secret
|
||||
if secret.Data == nil {
|
||||
secret.Data = make(map[string][]byte)
|
||||
}
|
||||
secret.Data[certificateField] = pemPair.Certificate
|
||||
secret.Data[privateKeyField] = pemPair.PrivateKey
|
||||
|
||||
secret, err = kc.client.CoreV1().Secrets(props.Namespace).Update(secret)
|
||||
if err == nil {
|
||||
kc.logger.Printf("Secret %s is updated", name)
|
||||
}
|
||||
|
||||
} else { // Create new secret
|
||||
secret = &v1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: props.Namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
certificateField: pemPair.Certificate,
|
||||
privateKeyField: pemPair.PrivateKey,
|
||||
},
|
||||
}
|
||||
|
||||
secret, err = kc.client.CoreV1().Secrets(props.Namespace).Create(secret)
|
||||
if err == nil {
|
||||
kc.logger.Printf("Secret %s is created", name)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func generateSecretName(props utils.TlsCertificateProps) string {
|
||||
return utils.GenerateInClusterServiceName(props) + ".kube-policy-tls-pair"
|
||||
}
|
|
@ -22,7 +22,7 @@ type KubeClient struct {
|
|||
// Checks parameters and creates new instance of KubeClient
|
||||
func NewKubeClient(config *rest.Config, logger *log.Logger) (*KubeClient, error) {
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stdout, "Policy Controller: ", log.LstdFlags|log.Lshortfile)
|
||||
logger = log.New(os.Stdout, "Kubernetes client: ", log.LstdFlags|log.Lshortfile)
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
|
@ -113,6 +113,13 @@ func defaultGetOptions() metav1.GetOptions {
|
|||
}
|
||||
}
|
||||
|
||||
func defaultDeleteOptions() *metav1.DeleteOptions {
|
||||
var deletePeriod int64 = 0
|
||||
return &metav1.DeleteOptions{
|
||||
GracePeriodSeconds: &deletePeriod,
|
||||
}
|
||||
}
|
||||
|
||||
const namespaceCreationMaxWaitTime time.Duration = 30 * time.Second
|
||||
const namespaceCreationWaitInterval time.Duration = 100 * time.Millisecond
|
||||
|
||||
|
|
54
main.go
54
main.go
|
@ -2,51 +2,36 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/nirmata/kube-policy/controller"
|
||||
"github.com/nirmata/kube-policy/kubeclient"
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
"github.com/nirmata/kube-policy/server"
|
||||
|
||||
rest "k8s.io/client-go/rest"
|
||||
clientcmd "k8s.io/client-go/tools/clientcmd"
|
||||
signals "k8s.io/sample-controller/pkg/signals"
|
||||
)
|
||||
|
||||
var (
|
||||
masterURL string
|
||||
kubeconfig string
|
||||
cert string
|
||||
key string
|
||||
)
|
||||
|
||||
func createClientConfig(masterURL, kubeconfig string) (*rest.Config, error) {
|
||||
// TODO: make possible to create config within a cluster with proper rights
|
||||
config, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if cert == "" || key == "" {
|
||||
log.Fatal("TLS certificate or/and key is not set")
|
||||
}
|
||||
|
||||
clientConfig, err := createClientConfig(masterURL, kubeconfig)
|
||||
clientConfig, err := createClientConfig(kubeconfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error building kubeconfig: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = webhooks.RegisterMutationWebhook(clientConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error registering mutation webhook server: %v\n", err)
|
||||
}
|
||||
|
||||
controller, err := controller.NewPolicyController(clientConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating PolicyController! Error: %s\n", err)
|
||||
return
|
||||
log.Fatalf("Error creating PolicyController: %s\n", err)
|
||||
}
|
||||
|
||||
kubeclient, err := kubeclient.NewKubeClient(clientConfig, nil)
|
||||
|
@ -54,13 +39,21 @@ func main() {
|
|||
log.Fatalf("Error creating kubeclient: %v\n", err)
|
||||
}
|
||||
|
||||
tlsPair := readTlsPairFromFiles(cert, key)
|
||||
if tlsPair != nil {
|
||||
log.Print("Using given TLS key/certificate pair")
|
||||
} else {
|
||||
tlsPair, err = initTlsPemsPair(clientConfig, kubeclient)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
serverConfig := server.WebhookServerConfig{
|
||||
CertFile: cert,
|
||||
KeyFile: key,
|
||||
TlsPemPair: tlsPair,
|
||||
Controller: controller,
|
||||
Kubeclient: kubeclient,
|
||||
}
|
||||
|
||||
server, err := server.NewWebhookServer(serverConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create webhook server: %v\n", err)
|
||||
|
@ -71,19 +64,18 @@ func main() {
|
|||
controller.Run(stopCh)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error running PolicyController! Error: %s\n", err)
|
||||
return
|
||||
log.Fatalf("Error running PolicyController: %s\n", err)
|
||||
}
|
||||
|
||||
fmt.Println("Policy Controller has started")
|
||||
log.Println("Policy Controller has started")
|
||||
<-stopCh
|
||||
server.Stop()
|
||||
fmt.Println("Policy Controller has stopped")
|
||||
log.Println("Policy Controller has stopped")
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
|
||||
flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
|
||||
flag.StringVar(&cert, "cert", "", "TLS certificate used in connection with cluster.")
|
||||
flag.StringVar(&key, "key", "", "Key, used in TLS connection.")
|
||||
flag.Parse()
|
||||
}
|
||||
|
|
|
@ -12,9 +12,12 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
controller "github.com/nirmata/kube-policy/controller"
|
||||
kubeclient "github.com/nirmata/kube-policy/kubeclient"
|
||||
webhooks "github.com/nirmata/kube-policy/webhooks"
|
||||
"github.com/nirmata/kube-policy/config"
|
||||
"github.com/nirmata/kube-policy/controller"
|
||||
"github.com/nirmata/kube-policy/kubeclient"
|
||||
"github.com/nirmata/kube-policy/utils"
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
|
@ -30,31 +33,30 @@ type WebhookServer struct {
|
|||
// Configuration struct for WebhookServer used in NewWebhookServer
|
||||
// Controller and Kubeclient should be initialized and valid
|
||||
type WebhookServerConfig struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
TlsPemPair *utils.TlsPemPair
|
||||
Controller *controller.PolicyController
|
||||
Kubeclient *kubeclient.KubeClient
|
||||
}
|
||||
|
||||
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
|
||||
// Policy Controller and Kubernetes Client should be initialized in configuration
|
||||
func NewWebhookServer(config WebhookServerConfig, logger *log.Logger) (*WebhookServer, error) {
|
||||
func NewWebhookServer(configuration WebhookServerConfig, logger *log.Logger) (*WebhookServer, error) {
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stdout, "HTTPS Server: ", log.LstdFlags|log.Lshortfile)
|
||||
}
|
||||
|
||||
if config.Controller == nil || config.Kubeclient == nil {
|
||||
return nil, errors.New("WebHook server requires initialized Policy Controller and Kubernetes Client")
|
||||
if configuration.TlsPemPair == nil || configuration.Controller == nil || configuration.Kubeclient == nil {
|
||||
return nil, errors.New("WebhookServerConfig is not initialized properly")
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
pair, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
|
||||
pair, err := tls.X509KeyPair(configuration.TlsPemPair.Certificate, configuration.TlsPemPair.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{pair}
|
||||
|
||||
mw, err := webhooks.NewMutationWebhook(config.Kubeclient, config.Controller, logger)
|
||||
mw, err := webhooks.NewMutationWebhook(configuration.Kubeclient, configuration.Controller, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -65,7 +67,7 @@ func NewWebhookServer(config WebhookServerConfig, logger *log.Logger) (*WebhookS
|
|||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/mutate", ws.serve)
|
||||
mux.HandleFunc(config.WebhookServicePath, ws.serve)
|
||||
|
||||
ws.server = http.Server{
|
||||
Addr: ":443", // Listen on port for HTTPS requests
|
||||
|
@ -81,7 +83,7 @@ func NewWebhookServer(config WebhookServerConfig, logger *log.Logger) (*WebhookS
|
|||
|
||||
// Main server endpoint for all requests
|
||||
func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/mutate" {
|
||||
if r.URL.Path == config.WebhookServicePath {
|
||||
admissionReview := ws.parseAdmissionReview(r, w)
|
||||
if admissionReview == nil {
|
||||
return
|
||||
|
|
141
utils/certificates_utils.go
Normal file
141
utils/certificates_utils.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
certificates "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Properties of TLS certificate which should be issued for webhook server
|
||||
type TlsCertificateProps struct {
|
||||
Service string
|
||||
Namespace string
|
||||
ApiServerHost string
|
||||
}
|
||||
|
||||
// The pair of TLS certificate corresponding private key, both in PEM format
|
||||
type TlsPemPair struct {
|
||||
Certificate []byte
|
||||
PrivateKey []byte
|
||||
}
|
||||
|
||||
// Generates RSA private key
|
||||
func TlsGeneratePrivateKey() (*rsa.PrivateKey, error) {
|
||||
return rsa.GenerateKey(rand.Reader, 2048)
|
||||
}
|
||||
|
||||
// Creates PEM block from private key object
|
||||
func TlsPrivateKeyToPem(rsaKey *rsa.PrivateKey) []byte {
|
||||
privateKey := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(rsaKey),
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(privateKey)
|
||||
}
|
||||
|
||||
// Creates PEM block from raw certificate request
|
||||
func TlsCertificateRequestToPem(csrRaw []byte) []byte {
|
||||
csrBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrRaw,
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(csrBlock)
|
||||
}
|
||||
|
||||
// Generates raw certificate signing request
|
||||
func TlsCertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertificateProps) (*certificates.CertificateSigningRequest, error) {
|
||||
dnsNames := make([]string, 3)
|
||||
dnsNames[0] = props.Service
|
||||
dnsNames[1] = props.Service + "." + props.Namespace
|
||||
// The full service name is the CommonName for the certificate
|
||||
commonName := GenerateInClusterServiceName(props)
|
||||
dnsNames[2] = commonName
|
||||
|
||||
var ips []net.IP
|
||||
apiServerIp := net.ParseIP(props.ApiServerHost)
|
||||
if apiServerIp != nil {
|
||||
ips = append(ips, apiServerIp)
|
||||
} else {
|
||||
dnsNames = append(dnsNames, props.ApiServerHost)
|
||||
}
|
||||
|
||||
csrTemplate := x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: props.Service, //commonName,
|
||||
},
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
DNSNames: dnsNames,
|
||||
IPAddresses: ips,
|
||||
}
|
||||
|
||||
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &certificates.CertificateSigningRequest{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "certificates.k8s.io/v1beta1",
|
||||
Kind: "CertificateSigningRequest",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: props.Service + "." + props.Namespace + ".cert-request",
|
||||
},
|
||||
Spec: certificates.CertificateSigningRequestSpec{
|
||||
Request: TlsCertificateRequestToPem(csrBytes),
|
||||
Groups: []string{"system:masters", "system:authenticated"},
|
||||
Usages: []certificates.KeyUsage{
|
||||
certificates.UsageDigitalSignature,
|
||||
certificates.UsageKeyEncipherment,
|
||||
certificates.UsageServerAuth,
|
||||
certificates.UsageClientAuth,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// The generated service name should be the common name for TLS certificate
|
||||
func GenerateInClusterServiceName(props TlsCertificateProps) string {
|
||||
return props.Service + "." + props.Namespace + ".svc"
|
||||
}
|
||||
|
||||
//Gets NotAfter property from raw certificate
|
||||
func TlsCertificateGetExpirationDate(certData []byte) (*time.Time, error) {
|
||||
block, _ := pem.Decode(certData)
|
||||
if block == nil {
|
||||
return nil, errors.New("Failed to decode PEM")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to parse certificate: %v" + err.Error())
|
||||
}
|
||||
return &cert.NotAfter, nil
|
||||
}
|
||||
|
||||
// The certificate is valid for a year, but we update it earlier to avoid using
|
||||
// an expired certificate in a controller that has been running for a long time
|
||||
const timeReserveBeforeCertificateExpiration time.Duration = time.Hour * 24 * 30 * 6 // About half a year
|
||||
|
||||
func IsTlsPairShouldBeUpdated(tlsPair *TlsPemPair) bool {
|
||||
if tlsPair == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
expirationDate, err := TlsCertificateGetExpirationDate(tlsPair.Certificate)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return expirationDate.Sub(time.Now()) < timeReserveBeforeCertificateExpiration
|
||||
}
|
|
@ -41,42 +41,32 @@ func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest) *v1beta1.Ad
|
|||
|
||||
var allPatches []PatchBytes
|
||||
for _, policy := range policies {
|
||||
patchingSets := getPolicyPatchingSets(policy)
|
||||
mw.logger.Printf("Applying policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
||||
|
||||
for ruleIdx, rule := range policy.Spec.Rules {
|
||||
err := rule.Validate()
|
||||
policyPatches, err := mw.applyPolicyRules(request, policy)
|
||||
if err != nil {
|
||||
mw.logger.Printf("Invalid rule detected: #%d in policy %s", ruleIdx, policy.ObjectMeta.Name)
|
||||
continue
|
||||
mw.controller.LogPolicyError(policy.Name, err.Error())
|
||||
|
||||
errStr := fmt.Sprintf("Unable to apply policy %s: %v", policy.Name, err)
|
||||
mw.logger.Printf("Denying the request because of error: %s", errStr)
|
||||
return mw.denyResourceCreation(errStr)
|
||||
}
|
||||
|
||||
mw.logger.Printf("Applying policy %s, rule #%d", policy.ObjectMeta.Name, ruleIdx)
|
||||
rulePatches, err := mw.applyRule(request, rule, patchingSets)
|
||||
if err != nil {
|
||||
return mw.denyResourceCreation(policy.Name, fmt.Sprintf("Unable to apply rule #%d: %s", ruleIdx, err))
|
||||
}
|
||||
if len(policyPatches) > 0 {
|
||||
meta := parseMetadataFromObject(request.Object.Raw)
|
||||
namespace := parseNamespaceFromMetadata(meta)
|
||||
name := parseNameFromMetadata(meta)
|
||||
mw.controller.LogPolicyInfo(policy.Name, fmt.Sprintf("Applied to %s %s/%s", request.Kind.Kind, namespace, name))
|
||||
|
||||
rulePatchesProcessed, err := ProcessPatches(rulePatches, request.Object.Raw, patchingSets)
|
||||
if err != nil {
|
||||
return mw.denyResourceCreation(policy.Name, fmt.Sprintf("Unable to apply rule #%d: %s", ruleIdx, err))
|
||||
}
|
||||
|
||||
if rulePatches != nil {
|
||||
allPatches = append(allPatches, rulePatchesProcessed...)
|
||||
mw.logger.Printf("Prepared %d patches", len(rulePatchesProcessed))
|
||||
} else {
|
||||
mw.logger.Print("No patches prepared")
|
||||
}
|
||||
allPatches = append(allPatches, policyPatches...)
|
||||
}
|
||||
}
|
||||
|
||||
patchType := v1beta1.PatchTypeJSONPatch
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
Patch: JoinPatches(allPatches),
|
||||
PatchType: func() *v1beta1.PatchType {
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
return &pt
|
||||
}(),
|
||||
PatchType: &patchType,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,20 +79,43 @@ func getPolicyPatchingSets(policy types.Policy) PatchingSets {
|
|||
return PatchingSetsDefault
|
||||
}
|
||||
|
||||
// Applies all rule to the created object and returns list of JSON patches.
|
||||
// Applies all policy rules to the created object and returns list of processed JSON patches.
|
||||
// May return nil patches if it is not necessary to create patches for requested object.
|
||||
// Returns error ONLY in case when creation of resource should be denied.
|
||||
func (mw *MutationWebhook) applyRule(request *v1beta1.AdmissionRequest, rule types.PolicyRule, errorBehavior PatchingSets) ([]types.PolicyPatch, error) {
|
||||
func (mw *MutationWebhook) applyPolicyRules(request *v1beta1.AdmissionRequest, policy types.Policy) ([]PatchBytes, error) {
|
||||
patchingSets := getPolicyPatchingSets(policy)
|
||||
var policyPatches []PatchBytes
|
||||
|
||||
for ruleIdx, rule := range policy.Spec.Rules {
|
||||
err := rule.Validate()
|
||||
if err != nil {
|
||||
mw.logger.Printf("Invalid rule detected: #%d in policy %s", ruleIdx, policy.ObjectMeta.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if !IsRuleApplicableToRequest(rule.Resource, request) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err := mw.applyRuleGenerators(request, rule)
|
||||
if err != nil && errorBehavior == PatchingSetsStopOnError {
|
||||
return nil, err
|
||||
} else {
|
||||
return rule.Patches, nil
|
||||
err = mw.applyRuleGenerators(request, rule)
|
||||
if err != nil && patchingSets == PatchingSetsStopOnError {
|
||||
return nil, errors.New(fmt.Sprintf("Failed to apply generators from rule #%d: %s", ruleIdx, err))
|
||||
}
|
||||
|
||||
rulePatchesProcessed, err := ProcessPatches(rule.Patches, request.Object.Raw, patchingSets)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Failed to process patches from rule #%d: %s", ruleIdx, err))
|
||||
}
|
||||
|
||||
if rulePatchesProcessed != nil {
|
||||
policyPatches = append(policyPatches, rulePatchesProcessed...)
|
||||
mw.logger.Printf("Rule %d: prepared %d patches", ruleIdx, len(rulePatchesProcessed))
|
||||
} else {
|
||||
mw.logger.Print("Rule %d: no patches prepared")
|
||||
}
|
||||
}
|
||||
|
||||
return policyPatches, nil
|
||||
}
|
||||
|
||||
// Applies "configMapGenerator" and "secretGenerator" described in PolicyRule
|
||||
|
@ -121,7 +134,7 @@ func (mw *MutationWebhook) applyRuleGenerators(request *v1beta1.AdmissionRequest
|
|||
return nil
|
||||
}
|
||||
|
||||
// Creates resourceKind (ConfigMap or Secret) with parameters specified in generator in cluster specified in request
|
||||
// Creates resourceKind (ConfigMap or Secret) with parameters specified in generator in cluster specified in request.
|
||||
func (mw *MutationWebhook) applyConfigGenerator(generator *types.PolicyConfigGenerator, namespace string, configKind string) error {
|
||||
if generator == nil {
|
||||
return nil
|
||||
|
@ -149,10 +162,7 @@ func (mw *MutationWebhook) applyConfigGenerator(generator *types.PolicyConfigGen
|
|||
}
|
||||
|
||||
// Forms AdmissionResponse with denial of resource creation and error message
|
||||
func (mw *MutationWebhook) denyResourceCreation(policyName, errStr string) *v1beta1.AdmissionResponse {
|
||||
mw.logger.Printf("Denying the request because of error: %s", errStr)
|
||||
mw.controller.LogPolicyError(policyName, errStr)
|
||||
|
||||
func (mw *MutationWebhook) denyResourceCreation(errStr string) *v1beta1.AdmissionResponse {
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: errStr,
|
||||
|
|
82
webhooks/registration.go
Normal file
82
webhooks/registration.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/nirmata/kube-policy/config"
|
||||
|
||||
adm "k8s.io/api/admissionregistration/v1beta1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
admreg "k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1"
|
||||
rest "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
func RegisterMutationWebhook(config *rest.Config) error {
|
||||
registrationClient, err := admreg.NewForConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = registrationClient.MutatingWebhookConfigurations().Create(constructWebhookConfig(config))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func constructWebhookConfig(configuration *rest.Config) *adm.MutatingWebhookConfiguration {
|
||||
return &adm.MutatingWebhookConfiguration{
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: config.WebhookConfigName,
|
||||
Labels: config.WebhookConfigLabels,
|
||||
},
|
||||
Webhooks: []adm.Webhook{
|
||||
adm.Webhook{
|
||||
Name: config.MutationWebhookName,
|
||||
ClientConfig: adm.WebhookClientConfig{
|
||||
Service: &adm.ServiceReference{
|
||||
Namespace: config.WebhookServiceNamespace,
|
||||
Name: config.WebhookServiceName,
|
||||
Path: &config.WebhookServicePath,
|
||||
},
|
||||
CABundle: ExtractCA(configuration),
|
||||
},
|
||||
Rules: []adm.RuleWithOperations{
|
||||
adm.RuleWithOperations{
|
||||
Operations: []adm.OperationType{
|
||||
adm.Create,
|
||||
},
|
||||
Rule: adm.Rule{
|
||||
APIGroups: []string{
|
||||
"*",
|
||||
},
|
||||
APIVersions: []string{
|
||||
"*",
|
||||
},
|
||||
Resources: []string{
|
||||
"*/*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractCA(config *rest.Config) (result []byte) {
|
||||
fileName := config.TLSClientConfig.CAFile
|
||||
|
||||
if fileName != "" {
|
||||
result, err := ioutil.ReadFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
} else {
|
||||
return config.TLSClientConfig.CAData
|
||||
}
|
||||
}
|
66
webhooks/registration_test.go
Normal file
66
webhooks/registration_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package webhooks_test
|
||||
|
||||
import (
|
||||
"gotest.tools/assert"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"bytes"
|
||||
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
|
||||
rest "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
func TestExtractCA_EmptyBundle(t *testing.T) {
|
||||
CAFile := "resources/CAFile"
|
||||
|
||||
config := &rest.Config {
|
||||
TLSClientConfig: rest.TLSClientConfig {
|
||||
CAData: nil,
|
||||
CAFile: CAFile,
|
||||
},
|
||||
}
|
||||
|
||||
expected, err := ioutil.ReadFile(CAFile)
|
||||
assert.Assert(t, err == nil)
|
||||
actual := webhooks.ExtractCA(config)
|
||||
assert.Assert(t, bytes.Equal(expected, actual))
|
||||
}
|
||||
|
||||
func TestExtractCA_EmptyCAFile(t *testing.T) {
|
||||
CABundle := []byte(`LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1ETXhPVEUwTURjd05Gb1hEVEk1TURNeE5qRTBNRGN3TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTStQClVLVmExcm9tQndOZzdqNnBBSGo5TDQ4RVJpdEplRzRXM1pUYmNMNWNKbnVTQmFsc1h1TWpQTGZmbUV1VEZIdVAKenRqUlBEUHcreEg1d3VTWFF2U0tIaXF2VE1pUm9DSlJFa09sQXpIa1dQM0VrdnUzNzRqZDVGV3Q3NEhnRk91cApIZ1ZwdUxPblczK2NDVE5iQ3VkeDFMVldRbGgwQzJKbm1Lam5uS1YrTkxzNFJVaVk1dk91ekpuNHl6QldLRjM2CmJLZ3ZDOVpMWlFSM3dZcnJNZWllYzBnWVY2VlJtaGgxSjRDV3V1UWd0ckM2d2NJanFWZFdEUlJyNHFMdEtDcDIKQVNIZmNieitwcEdHblJ5Z2FzcWNJdnpiNUVwV3NIRGtHRStUUW5WQ0JmTmsxN0NEOTZBQ1pmRWVybzEvWE16MgpRbzZvcUE0dnF5ZkdWWVU5RVZFQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFNWFVpUVJpdUc4cGdzcHMrZTdGZWdCdEJOZEcKZlFUdHVLRWFUZ0U0RjQwamJ3UmdrN25DTHlsSHgvRG04aVRRQmsyWjR4WnNuY0huRys4SkwrckRLdlJBSE5iVQpsYnpReXA1V3FwdjdPcThwZ01wU0o5bTdVY3BGZmRVZkorNW43aXFnTGdMb3lhNmtRVTR2Rk0yTE1rWjI5NVpxCmVId0hnREo5Z3IwWGNyOWM1L2tRdkxFc2Z2WU5QZVhuamNyWXlDb2JNcVduSElxeVd3cHM1VTJOaGgraXhSZEIKbzRRL3RJS04xOU93WGZBaVc5SENhNzZMb3ZXaUhPU2UxVnFzK1h1N1A5ckx4eW1vQm91aFcxVmZ0bUo5Qy9vTAp3cFVuNnlXRCttY0tkZ3J5QTFjTWJ4Q281bUd6YTNLaFk1QTd5eDQ1cThkSEIzTWU4d0FCam1wWEs0ST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=`)
|
||||
|
||||
config := &rest.Config {
|
||||
TLSClientConfig: rest.TLSClientConfig {
|
||||
CAData: CABundle,
|
||||
CAFile: "",
|
||||
},
|
||||
}
|
||||
|
||||
actual := webhooks.ExtractCA(config)
|
||||
assert.Assert(t, bytes.Equal(CABundle, actual))
|
||||
}
|
||||
|
||||
func TestExtractCA_EmptyConfig(t *testing.T) {
|
||||
config := &rest.Config {
|
||||
TLSClientConfig: rest.TLSClientConfig {
|
||||
CAData: nil,
|
||||
CAFile: "",
|
||||
},
|
||||
}
|
||||
|
||||
actual := webhooks.ExtractCA(config)
|
||||
assert.Assert(t, actual == nil)
|
||||
}
|
||||
|
||||
func TestExtractCA_InvalidFile(t *testing.T) {
|
||||
config := &rest.Config {
|
||||
TLSClientConfig: rest.TLSClientConfig {
|
||||
CAData: nil,
|
||||
CAFile: "somenonexistingfile",
|
||||
},
|
||||
}
|
||||
|
||||
actual := webhooks.ExtractCA(config)
|
||||
assert.Assert(t, actual == nil)
|
||||
}
|
14
webhooks/resources/CAFile
Normal file
14
webhooks/resources/CAFile
Normal file
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
V2VsY29tZSB0byBUaGUgUnVzdCBQcm9ncmFtbWluZyBMYW5ndWFnZSwgY
|
||||
W4gaW50cm9kdWN0b3J5IGJvb2sgYWJvdXQgUnVzdC4gVGhlIFJ1c3QgcH
|
||||
JvZ3JhbW1pbmcgbGFuZ3VhZ2UgaGVscHMgeW91IHdyaXRlIGZhc3Rlciw
|
||||
gbW9yZSByZWxpYWJsZSBzb2Z0d2FyZS4gSGlnaC1sZXZlbCBlcmdvbm9t
|
||||
aWNzIGFuZCBsb3ctbGV2ZWwgY29udHJvbCBhcmUgb2Z0ZW4gYXQgb2Rkc
|
||||
yBpbiBwcm9ncmFtbWluZyBsYW5ndWFnZSBkZXNpZ247IFJ1c3QgY2hhbG
|
||||
xlbmdlcyB0aGF0IGNvbmZsaWN0LiBUaHJvdWdoIGJhbGFuY2luZyBwb3d
|
||||
lcmZ1bCB0ZWNobmljYWwgY2FwYWNpdHkgYW5kIGEgZ3JlYXQgZGV2ZWxv
|
||||
cGVyIGV4cGVyaWVuY2UsIFJ1c3QgZ2l2ZXMgeW91IHRoZSBvcHRpb24gd
|
||||
G8gY29udHJvbCBsb3ctbGV2ZWwgZGV0YWlscyAoc3VjaCBhcyBtZW1vcn
|
||||
kgdXNhZ2UpIHdpdGhvdXQgYWxsIHRoZSBoYXNzbGUgdHJhZGl0aW9uYWx
|
||||
seSBhc3NvY2lhdGVkIHdpdGggc3VjaCBjb250cm9sLgyzmqp31l8rqr1==
|
||||
-----END CERTIFICATE-----
|
|
@ -31,3 +31,10 @@ func parseNameFromMetadata(meta map[string]interface{}) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseNamespaceFromMetadata(meta map[string]interface{}) string {
|
||||
if namespace, ok := meta["namespace"].(string); ok {
|
||||
return namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue