pkg/k8scontext/secretstore.go (138 lines of code) (raw):
// -------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// --------------------------------------------------------------------------------------------
package k8scontext
import (
"bytes"
"context"
"os"
"os/exec"
"sync"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/controllererrors"
"github.com/Azure/application-gateway-kubernetes-ingress/pkg/utils"
)
// SecretsKeeper is the interface definition for secret store
type SecretsKeeper interface {
GetPfxCertificate(secretKey string) []byte
ConvertSecret(secretKey string, secret *v1.Secret) error
delete(secretKey string)
}
// SecretsStore maintains a cache of the deployment secrets.
type SecretsStore struct {
conversionSync sync.Mutex
Client kubernetes.Interface
Cache cache.ThreadSafeStore
}
// NewSecretStore creates a new SecretsKeeper object
func NewSecretStore(client kubernetes.Interface) SecretsKeeper {
return &SecretsStore{
Cache: cache.NewThreadSafeStore(cache.Indexers{}, cache.Indices{}),
Client: client,
}
}
// GetPfxCertificate returns the certificate for the given secret key.
func (s *SecretsStore) GetPfxCertificate(secretKey string) []byte {
if certInterface, exists := s.Cache.Get(secretKey); exists {
if cert, ok := certInterface.([]byte); ok {
return cert
}
}
if cert, err := s.GetFromCluster(secretKey); err == nil {
return cert
}
return nil
}
func (s *SecretsStore) GetFromCluster(secretKey string) ([]byte, error) {
secretNamespace, secretName, err := utils.ParseNamespacedName(secretKey)
if err != nil {
return nil, err
}
secret, err := s.Client.CoreV1().Secrets(secretNamespace).Get(context.Background(), secretName, metav1.GetOptions{})
if err != nil {
return nil, err
}
if err := s.ConvertSecret(secretKey, secret); err != nil {
return nil, err
}
certInterface, _ := s.Cache.Get(secretKey)
cert, _ := certInterface.([]byte)
return cert, nil
}
func (s *SecretsStore) delete(secretKey string) {
s.conversionSync.Lock()
defer s.conversionSync.Unlock()
s.Cache.Delete(secretKey)
}
// ConvertSecret converts a secret to a PKCS12.
func (s *SecretsStore) ConvertSecret(secretKey string, secret *v1.Secret) error {
s.conversionSync.Lock()
defer s.conversionSync.Unlock()
// check if this is a secret with the correct type
if secret.Type != v1.SecretTypeTLS {
return controllererrors.NewErrorf(
controllererrors.ErrorUnknownSecretType,
"secret [%v] is not type kubernetes.io/tls", secretKey,
)
}
if len(secret.Data[v1.TLSCertKey]) == 0 || len(secret.Data[v1.TLSPrivateKeyKey]) == 0 {
return controllererrors.NewErrorf(
controllererrors.ErrorMalformedSecret,
"secret [%v] is malformed, tls.key or tls.crt is not defined", secretKey,
)
}
tempfileCert, err := os.CreateTemp("", "appgw-ingress-cert")
if err != nil {
return controllererrors.NewErrorWithInnerErrorf(
controllererrors.ErrorCreatingFile,
err,
"unable to create temporary file for certificate conversion",
)
}
defer os.Remove(tempfileCert.Name())
tempfileKey, err := os.CreateTemp("", "appgw-ingress-key")
if err != nil {
return controllererrors.NewErrorWithInnerErrorf(
controllererrors.ErrorCreatingFile,
err,
"unable to create temporary file for certificate conversion",
)
}
defer os.Remove(tempfileKey.Name())
if err := writeFileDecode(secret.Data["tls.crt"], tempfileCert); err != nil {
return controllererrors.NewErrorWithInnerErrorf(
controllererrors.ErrorWritingToFile,
err,
"unable to write secret [%v].tls.crt to temporary file", secretKey,
)
}
if err := writeFileDecode(secret.Data["tls.key"], tempfileKey); err != nil {
return controllererrors.NewErrorWithInnerErrorf(
controllererrors.ErrorWritingToFile,
err,
"unable to write secret [%v].tls.key to temporary file", secretKey,
)
}
// both cert and key are in temp file now, call openssl
var cout, cerr bytes.Buffer
cmd := exec.Command("openssl", "pkcs12", "-export", "-in", tempfileCert.Name(), "-inkey", tempfileKey.Name(), "-password", "pass:msazure")
cmd.Stderr = &cerr
cmd.Stdout = &cout
// if openssl exited with an error or the output is empty, report error
if err := cmd.Run(); err != nil || len(cout.Bytes()) == 0 {
return controllererrors.NewErrorWithInnerErrorf(
controllererrors.ErrorExportingWithOpenSSL,
err,
"unable to export using openssl, error=[%v], stderr=[%v]", err, cerr.String(),
)
}
pfxCert := cout.Bytes()
// TODO i'm not sure if comparison against existing certificate can help
// us optimize by eliminating some events
_, exists := s.Cache.Get(secretKey)
if exists {
s.Cache.Update(secretKey, pfxCert)
} else {
s.Cache.Add(secretKey, pfxCert)
}
return nil
}
func writeFileDecode(data []byte, fileHandle *os.File) error {
if _, err := fileHandle.Write(data); err != nil {
return err
}
if err := fileHandle.Close(); err != nil {
return err
}
return nil
}