Skip to content

Commit

Permalink
Add function in translator to fetch secrets for Ingress
Browse files Browse the repository at this point in the history
  • Loading branch information
rramkumar1 committed May 13, 2020
1 parent 357db47 commit 86f2835
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 39 deletions.
55 changes: 16 additions & 39 deletions pkg/tls/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,70 +17,48 @@ limitations under the License.
package tls

import (
"context"
"fmt"

"k8s.io/klog"

api_v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/ingress-gce/pkg/loadbalancers"
"k8s.io/ingress-gce/pkg/translator"
)

// TlsLoader is the interface for loading the relevant TLSCerts for a given ingress.
type TlsLoader interface {
// Load loads the relevant TLSCerts based on ing.Spec.TLS
Load(ing *v1beta1.Ingress) ([]*loadbalancers.TLSCerts, error)
// Validate validates the given TLSCerts and returns an error if they are invalid.
Validate(certs *loadbalancers.TLSCerts) error
}

// TODO: Add better cert validation.
type noOPValidator struct{}

func (n *noOPValidator) Validate(certs *loadbalancers.TLSCerts) error {
return nil
}

// TLSCertsFromSecretsLoader loads TLS certs from kubernetes secrets.
type TLSCertsFromSecretsLoader struct {
noOPValidator
Client kubernetes.Interface
}

// Ensure that TLSCertsFromSecretsLoader implements TlsLoader interface.
var _ TlsLoader = &TLSCertsFromSecretsLoader{}

func (t *TLSCertsFromSecretsLoader) Load(ing *v1beta1.Ingress) ([]*loadbalancers.TLSCerts, error) {
if len(ing.Spec.TLS) == 0 {
return nil, nil
env, err := translator.NewEnv(ing, t.Client)
if err != nil {
return nil, fmt.Errorf("error initializing translator env: %v", err)
}

var certs []*loadbalancers.TLSCerts

for _, tlsSecret := range ing.Spec.TLS {
if tlsSecret.SecretName == "" {
continue
}
// TODO: Replace this for a secret watcher.
klog.V(3).Infof("Retrieving secret for ing %v with name %v", ing.Name, tlsSecret.SecretName)
secret, err := t.Client.CoreV1().Secrets(ing.Namespace).Get(context.TODO(), tlsSecret.SecretName, meta_v1.GetOptions{})
if err != nil {
return nil, err
}
cert, ok := secret.Data[api_v1.TLSCertKey]
if !ok {
return nil, fmt.Errorf("secret %v has no 'tls.crt'", tlsSecret.SecretName)
}
key, ok := secret.Data[api_v1.TLSPrivateKeyKey]
if !ok {
return nil, fmt.Errorf("secret %v has no 'tls.key'", tlsSecret.SecretName)
}
newCert := &loadbalancers.TLSCerts{Key: string(key), Cert: string(cert), Name: tlsSecret.SecretName,
CertHash: loadbalancers.GetCertHash(string(cert))}
if err := t.Validate(newCert); err != nil {
return nil, err
secrets, err := translator.Secrets(env)
if err != nil {
return nil, fmt.Errorf("error getting secrets for Ingress: %v", err)
}
for _, secret := range secrets {
cert := string(secret.Data[api_v1.TLSCertKey])
newCert := &loadbalancers.TLSCerts{
Key: string(secret.Data[api_v1.TLSPrivateKeyKey]),
Cert: string(cert),
Name: secret.Name,
CertHash: loadbalancers.GetCertHash(string(cert)),
}
certs = append(certs, newCert)
}
Expand All @@ -91,7 +69,6 @@ func (t *TLSCertsFromSecretsLoader) Load(ing *v1beta1.Ingress) ([]*loadbalancers

// FakeTLSSecretLoader fakes out TLS loading.
type FakeTLSSecretLoader struct {
noOPValidator
FakeCerts map[string]*loadbalancers.TLSCerts
}

Expand Down
59 changes: 59 additions & 0 deletions pkg/translator/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,76 @@ limitations under the License.
package translator

import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"

"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud"
"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta"
api_v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"

"k8s.io/ingress-gce/pkg/composite"
"k8s.io/ingress-gce/pkg/utils"
"k8s.io/ingress-gce/pkg/utils/namer"
)

// Env contains all k8s-style configuration needed to perform the translation.
type Env struct {
// MCI is the MCI we are translating.
Ing *v1beta1.Ingress
// SecretsMap contains a mapping from Secret name to the actual resource.
// It is assumed that the map contains resources from a single namespace.
SecretsMap map[string]*api_v1.Secret
}

// NewEnv returns an Env for the given Ingress.
func NewEnv(ing *v1beta1.Ingress, client kubernetes.Interface) (*Env, error) {
ret := &Env{Ing: ing, SecretsMap: make(map[string]*api_v1.Secret)}
secrets, err := client.CoreV1().Secrets(ing.Namespace).List(context.TODO(), meta_v1.ListOptions{})
if err != nil {
return nil, err
}
for _, s := range secrets.Items {
s := s
ret.SecretsMap[s.Name] = &s
}
return ret, nil
}

// Translator implements the mapping between an MCI and its corresponding GCE resources.
type Translator struct{}

// NewTranslator returns a new Translator.
func NewTranslator() *Translator {
return &Translator{}
}

// Secrets returns the Secrets from the environment which are specified in the Ingress.
func Secrets(env *Env) ([]*api_v1.Secret, error) {
var ret []*api_v1.Secret
spec := env.Ing.Spec
for _, tlsSpec := range spec.TLS {
secret, ok := env.SecretsMap[tlsSpec.SecretName]
if !ok {
return nil, fmt.Errorf("secret %q does not exist", tlsSpec.SecretName)
}
// Fail-fast if the user's secret does not have the proper fields specified.
if secret.Data[api_v1.TLSCertKey] == nil {
return nil, fmt.Errorf("secret %q does not specify cert as string data", tlsSpec.SecretName)
}
if secret.Data[api_v1.TLSPrivateKeyKey] == nil {
return nil, fmt.Errorf("secret %q does not specify private key as string data", tlsSpec.SecretName)
}
ret = append(ret, secret)
}

return ret, nil
}

// The gce api uses the name of a path rule to match a host rule.
const hostRulePrefix = "host"

Expand Down
146 changes: 146 additions & 0 deletions pkg/translator/translator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ limitations under the License.
package translator

import (
"context"
"testing"

"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta"
"github.com/google/go-cmp/cmp"
api_v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"

"k8s.io/ingress-gce/pkg/composite"
"k8s.io/ingress-gce/pkg/utils"
Expand Down Expand Up @@ -115,3 +120,144 @@ func testCompositeURLMap() *composite.UrlMap {
},
}
}

func TestSecrets(t *testing.T) {
secretsMap := map[string]*api_v1.Secret{
"first-secret": &api_v1.Secret{
ObjectMeta: meta_v1.ObjectMeta{
Name: "first-secret",
},
Data: map[string][]byte{
// TODO(rramkumar): Use real data here.
api_v1.TLSCertKey: []byte("cert"),
api_v1.TLSPrivateKeyKey: []byte("private key"),
},
},
"second-secret": &api_v1.Secret{
ObjectMeta: meta_v1.ObjectMeta{
Name: "second-secret",
},
Data: map[string][]byte{
api_v1.TLSCertKey: []byte("cert"),
api_v1.TLSPrivateKeyKey: []byte("private key"),
},
},
"third-secret": &api_v1.Secret{
ObjectMeta: meta_v1.ObjectMeta{
Name: "third-secret",
},
Data: map[string][]byte{
api_v1.TLSCertKey: []byte("cert"),
api_v1.TLSPrivateKeyKey: []byte("private key"),
},
},
"secret-no-cert": &api_v1.Secret{
ObjectMeta: meta_v1.ObjectMeta{
Name: "secret-no-cert",
},
Data: map[string][]byte{
api_v1.TLSPrivateKeyKey: []byte("private key"),
},
},
"secret-no-key": &api_v1.Secret{
ObjectMeta: meta_v1.ObjectMeta{
Name: "secret-no-key",
},
Data: map[string][]byte{
api_v1.TLSCertKey: []byte("private key"),
},
},
}

cases := []struct {
desc string
ing *v1beta1.Ingress
want []*api_v1.Secret
wantErr bool
}{
{
desc: "ingress-single-secret",
// TODO(rramkumar): Read Ingress spec from a file.
ing: &v1beta1.Ingress{
Spec: v1beta1.IngressSpec{
TLS: []v1beta1.IngressTLS{
{SecretName: "first-secret"},
},
},
},
want: []*api_v1.Secret{secretsMap["first-secret"]},
},
{
desc: "ingress-multi-secret",
ing: &v1beta1.Ingress{
Spec: v1beta1.IngressSpec{
TLS: []v1beta1.IngressTLS{
{SecretName: "first-secret"},
{SecretName: "second-secret"},
{SecretName: "third-secret"},
},
},
},
want: []*api_v1.Secret{secretsMap["first-secret"], secretsMap["second-secret"], secretsMap["third-secret"]},
},
{
desc: "mci-missing-secret",
ing: &v1beta1.Ingress{
Spec: v1beta1.IngressSpec{
TLS: []v1beta1.IngressTLS{
{SecretName: "does-not-exist-secret"},
},
},
},
wantErr: true,
},
{
desc: "mci-secret-empty-cert",
ing: &v1beta1.Ingress{
Spec: v1beta1.IngressSpec{
TLS: []v1beta1.IngressTLS{
{SecretName: "secret-no-cert"},
},
},
},
wantErr: true,
},
{
desc: "mci-secret-empty-priv-key",
ing: &v1beta1.Ingress{
Spec: v1beta1.IngressSpec{
TLS: []v1beta1.IngressTLS{
{SecretName: "secret-no-key"},
},
},
},
wantErr: true,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
kubeClient := fake.NewSimpleClientset()
for _, v := range secretsMap {
kubeClient.CoreV1().Secrets(tc.ing.Namespace).Create(context.TODO(), v, meta_v1.CreateOptions{})
}

env, err := NewEnv(tc.ing, kubeClient)
if err != nil {
t.Fatalf("NewEnv(): %v", err)
}

got, err := Secrets(env)
if tc.wantErr && err == nil {
t.Fatal("expected Secrets() to return an error")
}
if !tc.wantErr && err != nil {
t.Fatalf("Secrets(): %v", err)
}

if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("Got diff (-want +got):\n%s", diff)
}
})
}
}

0 comments on commit 86f2835

Please sign in to comment.