...
 
Commits (2)
......@@ -16,9 +16,10 @@ type NginxSpec struct {
Image string `json:"image"`
// Reference to the nginx config object.
Config *ConfigRef `json:"configRef"`
// References to a secret containing tls certificate and key pairs.
// Certificates refers to a Secret containing one or more certificate-key
// pairs.
// +optional
TLSSecret *TLSSecret `json:"tlsSecret,omitempty"`
Certificates *TLSSecret `json:"certificates,omitempty"`
// Template used to configure the nginx pod.
// +optional
PodTemplate NginxPodTemplateSpec `json:"podTemplate,omitempty"`
......@@ -105,28 +106,30 @@ const (
ConfigKindInline = ConfigKind("Inline")
)
// TLSSecret is a reference to tls certificate and key pairs stored in a secret.
// TLSSecret is a reference to TLS certificate and key pairs stored in a Secret.
type TLSSecret struct {
// Name of the Secret holding the certificate and key.
// SecretName refers to the Secret holding the certificates and keys pairs.
SecretName string `json:"secretName"`
// Secret field that contains the key.
// Defaults to tls.key
// +optional
KeyField string `json:"keyField,omitempty"`
// Secret field that contains the certificate.
// Defaults to tls.crt
// Items maps the key and path where the certificate-key pairs should be
// mounted on nginx container.
Items []TLSSecretItem `json:"items"`
}
// TLSSecretItem maps each certificate and key pair against a key-value data
// from a Secret object.
type TLSSecretItem struct {
// CertificateField is the field name that contains the certificate.
CertificateField string `json:"certificateField"`
// CertificatePath holds the path where the certificate should be stored
// inside the nginx container. Defaults to same as CertificatedField.
// +optional
CertificateField string `json:"certificateField,omitempty"`
// Path where the key should be stored inside the nginx container.
// Relative to /etc/nginx/certs/.
// Defaults to <KeyName>
CertificatePath string `json:"certificatePath,omitempty"`
// KeyField is the field name that contains the key.
KeyField string `json:"keyField"`
// KeyPath holds the path where the key should be store inside the nginx
// container. Defaults to same as KeyField.
// +optional
KeyPath string `json:"keyPath,omitempty"`
// Path where the certificate should be stored inside the nginx container.
// Relative to /etc/nginx/certs/.
// Defaults to <CertificateName>
// +optional
CertificatePath string `json:"certificatePath,omitempty"`
}
// FilesRef is a reference to arbitrary files stored into a ConfigMap in the
......
......@@ -174,10 +174,10 @@ func (in *NginxSpec) DeepCopyInto(out *NginxSpec) {
*out = new(ConfigRef)
**out = **in
}
if in.TLSSecret != nil {
in, out := &in.TLSSecret, &out.TLSSecret
if in.Certificates != nil {
in, out := &in.Certificates, &out.Certificates
*out = new(TLSSecret)
**out = **in
(*in).DeepCopyInto(*out)
}
in.PodTemplate.DeepCopyInto(&out.PodTemplate)
if in.Service != nil {
......@@ -264,6 +264,11 @@ func (in *ServiceStatus) DeepCopy() *ServiceStatus {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSSecret) DeepCopyInto(out *TLSSecret) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TLSSecretItem, len(*in))
copy(*out, *in)
}
return
}
......@@ -276,3 +281,19 @@ func (in *TLSSecret) DeepCopy() *TLSSecret {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSSecretItem) DeepCopyInto(out *TLSSecretItem) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSecretItem.
func (in *TLSSecretItem) DeepCopy() *TLSSecretItem {
if in == nil {
return nil
}
out := new(TLSSecretItem)
in.DeepCopyInto(out)
return out
}
......@@ -67,7 +67,7 @@ func TestReconcileNginx_reconcileService(t *testing.T) {
Namespace: "default",
},
Spec: v1alpha1.NginxSpec{
TLSSecret: &v1alpha1.TLSSecret{},
Certificates: &v1alpha1.TLSSecret{},
},
Status: v1alpha1.NginxStatus{},
},
......
......@@ -99,7 +99,7 @@ func NewDeployment(n *v1alpha1.Nginx) (*appv1.Deployment, error) {
},
}
setupConfig(n.Spec.Config, &deployment)
setupTLS(n.Spec.TLSSecret, &deployment)
setupTLS(n.Spec.Certificates, &deployment)
setupExtraFiles(n.Spec.ExtraFiles, &deployment)
// This is done on the last step because n.Spec may have mutated during these methods
......@@ -161,7 +161,7 @@ func NewService(n *v1alpha1.Nginx) *corev1.Service {
Type: nginxService(n),
},
}
if n.Spec.TLSSecret != nil {
if n.Spec.Certificates != nil {
service.Spec.Ports = append(service.Spec.Ports, corev1.ServicePort{
Name: defaultHTTPSPortName,
Protocol: corev1.ProtocolTCP,
......@@ -283,20 +283,23 @@ func setupTLS(secret *v1alpha1.TLSSecret, dep *appv1.Deployment) {
MountPath: certMountPath,
})
secret.KeyField = valueOrDefault(secret.KeyField, "tls.key")
secret.CertificateField = valueOrDefault(secret.CertificateField, "tls.crt")
secret.KeyPath = valueOrDefault(secret.KeyPath, secret.KeyField)
secret.CertificatePath = valueOrDefault(secret.CertificatePath, secret.CertificateField)
var items []corev1.KeyToPath
for _, item := range secret.Items {
items = append(items, corev1.KeyToPath{
Key: item.CertificateField,
Path: valueOrDefault(item.CertificatePath, item.CertificateField),
}, corev1.KeyToPath{
Key: item.KeyField,
Path: valueOrDefault(item.KeyPath, item.KeyField),
})
}
dep.Spec.Template.Spec.Volumes = append(dep.Spec.Template.Spec.Volumes, corev1.Volume{
Name: "nginx-certs",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secret.SecretName,
Items: []corev1.KeyToPath{
{Key: secret.KeyField, Path: secret.KeyPath},
{Key: secret.CertificateField, Path: secret.CertificatePath},
},
Items: items,
},
},
})
......
......@@ -34,14 +34,18 @@ func nginxWithService() v1alpha1.Nginx {
return n
}
func nginxWithTLSSecret() v1alpha1.Nginx {
func nginxWithCertificate() v1alpha1.Nginx {
n := baseNginx()
n.Spec.TLSSecret = &v1alpha1.TLSSecret{
n.Spec.Certificates = &v1alpha1.TLSSecret{
SecretName: "my-secret",
KeyField: "key-field",
KeyPath: "key-path",
CertificateField: "cert-field",
CertificatePath: "cert-path",
Items: []v1alpha1.TLSSecretItem{
{
KeyField: "key-field",
KeyPath: "key-path",
CertificateField: "cert-field",
CertificatePath: "cert-path",
},
},
}
return n
}
......@@ -217,12 +221,16 @@ func Test_NewDeployment(t *testing.T) {
{
name: "with-tls",
nginxFn: func(n v1alpha1.Nginx) v1alpha1.Nginx {
n.Spec.TLSSecret = &v1alpha1.TLSSecret{
n.Spec.Certificates = &v1alpha1.TLSSecret{
SecretName: "my-secret",
KeyField: "key-field",
KeyPath: "key-path",
CertificateField: "cert-field",
CertificatePath: "cert-path",
Items: []v1alpha1.TLSSecretItem{
{
CertificateField: "cert-field",
CertificatePath: "cert-path",
KeyField: "key-field",
KeyPath: "key-path",
},
},
}
return n
},
......@@ -258,8 +266,8 @@ func Test_NewDeployment(t *testing.T) {
Secret: &corev1.SecretVolumeSource{
SecretName: "my-secret",
Items: []corev1.KeyToPath{
{Key: "key-field", Path: "key-path"},
{Key: "cert-field", Path: "cert-path"},
{Key: "key-field", Path: "key-path"},
},
},
},
......@@ -271,8 +279,74 @@ func Test_NewDeployment(t *testing.T) {
{
name: "with-tls-default-values",
nginxFn: func(n v1alpha1.Nginx) v1alpha1.Nginx {
n.Spec.TLSSecret = &v1alpha1.TLSSecret{
n.Spec.Certificates = &v1alpha1.TLSSecret{
SecretName: "my-secret",
Items: []v1alpha1.TLSSecretItem{
{
CertificateField: "cert.crt",
KeyField: "cert.key",
},
},
}
return n
},
deployFn: func(d appv1.Deployment) appv1.Deployment {
d.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{
{
Name: defaultHTTPPortName,
ContainerPort: defaultHTTPPort,
Protocol: corev1.ProtocolTCP,
},
{
Name: defaultHTTPSPortName,
ContainerPort: defaultHTTPSPort,
Protocol: corev1.ProtocolTCP,
},
}
d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
{Name: "nginx-certs", MountPath: "/etc/nginx/certs"},
}
d.Spec.Template.Spec.Containers[0].ReadinessProbe = &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/",
Port: intstr.FromString(defaultHTTPSPortName),
Scheme: corev1.URISchemeHTTPS,
},
},
}
d.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "nginx-certs",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "my-secret",
Items: []corev1.KeyToPath{
{Key: "cert.crt", Path: "cert.crt"},
{Key: "cert.key", Path: "cert.key"},
},
},
},
},
}
return d
},
},
{
name: "with-two-certificates",
nginxFn: func(n v1alpha1.Nginx) v1alpha1.Nginx {
n.Spec.Certificates = &v1alpha1.TLSSecret{
SecretName: "my-secret",
Items: []v1alpha1.TLSSecretItem{
{
CertificateField: "rsa.crt.pem",
KeyField: "rsa.key.pem",
},
{
CertificateField: "ecdsa.crt.pem",
KeyField: "ecdsa.key.pem",
},
},
}
return n
},
......@@ -308,8 +382,10 @@ func Test_NewDeployment(t *testing.T) {
Secret: &corev1.SecretVolumeSource{
SecretName: "my-secret",
Items: []corev1.KeyToPath{
{Key: "tls.key", Path: "tls.key"},
{Key: "tls.crt", Path: "tls.crt"},
{Key: "rsa.crt.pem", Path: "rsa.crt.pem"},
{Key: "rsa.key.pem", Path: "rsa.key.pem"},
{Key: "ecdsa.crt.pem", Path: "ecdsa.crt.pem"},
{Key: "ecdsa.key.pem", Path: "ecdsa.key.pem"},
},
},
},
......@@ -542,7 +618,7 @@ func TestNewService(t *testing.T) {
},
{
name: "with-tls",
nginx: nginxWithTLSSecret(),
nginx: nginxWithCertificate(),
want: &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
......
......@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tsuru/nginx-operator/pkg/apis/nginx/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
......@@ -24,6 +25,7 @@ func TestMain(m *testing.M) {
}
os.Exit(m.Run())
}
func Test_Operator(t *testing.T) {
cleanup, err := createNamespace(testingNamespace)
if err != nil {
......@@ -42,6 +44,60 @@ func Test_Operator(t *testing.T) {
assert.Equal(t, 2, len(nginx.Status.Pods))
assert.Equal(t, 1, len(nginx.Status.Services))
})
t.Run("with-certificates.yaml", func(t *testing.T) {
err = apply("testdata/with-certificates.yaml", testingNamespace)
require.NoError(t, err)
nginx, err := getReadyNginx("my-secured-nginx", 1, 1)
require.NoError(t, err)
require.NotNil(t, nginx)
assert.Equal(t, "nginx:alpine", nginx.Spec.Image)
defer func() {
err = delete("testdata/with-certificates.yaml", testingNamespace)
require.NoError(t, err)
}()
nginxService := corev1.Service{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"}}
err = get(&nginxService, "my-secured-nginx-service", nginx.Namespace)
require.NoError(t, err)
assert.NotNil(t, nginxService)
assert.Equal(t, int32(80), nginxService.Spec.Ports[0].Port)
assert.Equal(t, int32(443), nginxService.Spec.Ports[1].Port)
podName := nginx.Status.Pods[0].Name
err = waitPodBeAvailable(podName, testingNamespace)
require.NoError(t, err)
tests := []struct{
filename string
expectedSha256 string
}{
{
filename: "/etc/nginx/certs/rsa.crt",
expectedSha256: "f50457089e715bbc9d5a31a16cf53cc2f13a68333df71559bb5d06be2d2b8a63",
},
{
filename: "/etc/nginx/certs/rsa.key",
expectedSha256: "18580c25b2807b4c95502dd7051d414299e40d8d14024ad5d69c9915ec41e66e",
},
{
filename: "/etc/nginx/certs/custom_dir/custom_name.crt",
expectedSha256: "159af275ab3b22d9737617e51daca64efafb48287ecb3650661d2116cb4ef0c9",
},
{
filename: "/etc/nginx/certs/custom_dir/custom_name.key",
expectedSha256: "253b9795dcd80c493dcfade6b3dc5506fac1a38850abaa4e639fada5ea3dad5e",
},
}
for _, tt := range tests {
output, err := kubectl("exec", podName, "-n", testingNamespace, "--", "sha256sum", tt.filename)
require.NoError(t, err)
assert.Contains(t, string(output), tt.expectedSha256)
}
})
}
func getReadyNginx(name string, expectedPods int, expectedSvcs int) (*v1alpha1.Nginx, error) {
......@@ -62,3 +118,19 @@ func getReadyNginx(name string, expectedPods int, expectedSvcs int) (*v1alpha1.N
}
}
}
func waitPodBeAvailable(name, namespace string) error {
timeout := time.After(5 * time.Minute)
pingMessage := "PING"
for {
output, err := kubectl("exec", name, "-n", namespace, "--", "echo", "-n", pingMessage)
if err == nil && string(output) == pingMessage {
return nil
}
select {
case <-timeout:
return fmt.Errorf("Timeout waiting pod %q becomes available. Last output: %s. Last error: %v", name, string(output), err)
case <-time.After(100 * time.Millisecond):
}
}
}
apiVersion: v1
kind: Secret
metadata:
name: my-secured-nginx-certs
data:
rsa.crt: |-
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUI5VENDQVY2Z0F3SUJBZ0lSQUlEMmNIZVFN
Y2MvU2R2NXMyYjlCL0F3RFFZSktvWklodmNOQVFFTEJRQXcKRWpFUU1BNEdBMVVFQ2hNSFFXTnRa
U0JEYnpBZUZ3MHhPVEExTXpBeE9USXpNakJhRncweU1EQTFNamt4T1RJegpNakJhTUJJeEVEQU9C
Z05WQkFvVEIwRmpiV1VnUTI4d2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKCkFvR0JB
SzRYaWFjdDh0U1liS0ZQVzE3Qk9mWFpBUm9LWTVIdWNWZVdnN1lLK2pSSEJCelVlZy93SGFtUTJZ
aUUKRVVaM1NKZVVkUWRFaC9nT3hPajBsVmtZcjhKN2FxdzVQOGpFVFc1OGRHZ3BuQ0U3bXFQU2tq
UDJvbGdvSXZiZwplSHcwQ1NEdzBJMDQ2WUJ5czZLTGJDZ3pPS0tRZU4yczBXM1pueVhtMDUyYXpn
S3JBZ01CQUFHalN6QkpNQTRHCkExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJn
RUZCUWNEQVRBTUJnTlZIUk1CQWY4RUFqQUEKTUJRR0ExVWRFUVFOTUF1Q0NXeHZZMkZzYUc5emRE
QU5CZ2txaGtpRzl3MEJBUXNGQUFPQmdRQ2tHYWkvRlBBRgpXSmVsaGF3TjM4T29JZWdOQUJjZGM4
STV0c25MNjY2N3NSNWxyaDRTRmE1b1U5N09oWEVWSG0xT2Y3N1F4TzZQCkQ5dmsxNmVsQUp3UUNr
MzByQTJZR1ZFVFgxTytEZndER1FGempubG5pNE9lSzZjZmR1Nk5DUUxaOGt0NTRuZlcKUTNvdkVy
Yy9FbCtLNllZcmxjMjU5SmROTkhwMk02Nmpqdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
rsa.key: |-
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlDWFFJQkFBS0JnUUN1RjRtbkxmTFVt
R3loVDF0ZXdUbjEyUUVhQ21PUjduRlhsb08yQ3ZvMFJ3UWMxSG9QCjhCMnBrTm1JaEJGR2QwaVhs
SFVIUklmNERzVG85SlZaR0svQ2UycXNPVC9JeEUxdWZIUm9LWndoTzVxajBwSXoKOXFKWUtDTDI0
SGg4TkFrZzhOQ05PT21BY3JPaWkyd29Nemlpa0hqZHJORnQyWjhsNXRPZG1zNENxd0lEQVFBQgpB
b0dBTWtuRVRwRjhTcUw1QmlPNVBnYk02bEpUK2lTMEhHeG5PeWpVUlJxb3FiODExZW9lbEVvRkYz
MHJDVDgxCkNramNwRVNUQlpWclBXRnV5ZWdVMkJiRWRneGJnS2lYVEVHM05MOFZCZzR1SmcwZWZR
R2V3S1dUNkdObS9GSC8KcnplOEI2NDNyZTIxckNBUlYzV0ZONTRjNDRBQ2lqQkhFMW4rc3daMFFH
V0FUaEVDUVFEUHN5M1ZrTE90SjZxTApjK1dQUnFaYlMzNG5jZGZDS2grZGd6WFhBZVNlVkdRc3VX
SndmbTYxT0FRWXFnb1RLTG9aZXNTZlIzMW4rNzJQCk5uOEpXWVdqQWtFQTFwT2FuNnZ2QXBLMldO
d3pHWC9lN1lBSHhLSTNTSVF6MkVzOWh5dkxiTzV5Y1B6eHgyKzgKcFEzdHJpQ3h1Tmx1RWtZMlZR
czFhTURPVFZHMGJsNFBXUUpBSFlpbUFOTkZxVjZWa3FUVlJLMVFKSFUwcUJrNQpDK1AvZ045U2Za
TklWZmM4RTF2OUVtLzRBQTdLRGxQSGdQZnVsb2J0aEpTZTBYVnJlL3pNemFDTUx3SkJBTlI1CmZw
QURrWDJMeDNLQVFUMUx5SFNHNWFwSk10TGt6ZTJzdTNWZzRXT3hUYk1pVlV1MkVpZWlmaXVCaGxw
VE8xSnYKSWhmZUxIblpDYSs0YXlFcWxLa0NRUURDZTdUeFJRdzhJTk9BZWZrNGRSTU91NHM0SmM0
YjZHTVJodGJydTB4bgo5R1Z5bUNXa2dwT0EvcUZzWE4yVTREd0kxMTlrL2hOZjJQdjAxaDM4VVZW
MgotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
ecdsa.crt: |-
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJiRENDQVJPZ0F3SUJBZ0lRSE9PYUM0ZGRJ
RVJ1T2JPYXhEdU0wakFLQmdncWhrak9QUVFEQWpBU01SQXcKRGdZRFZRUUtFd2RCWTIxbElFTnZN
QjRYRFRFNU1EVXpNREU1TWpVeU0xb1hEVEl3TURVeU9URTVNalV5TTFvdwpFakVRTUE0R0ExVUVD
aE1IUVdOdFpTQkRiekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSU1oCnB2MWFG
QnhNcTNVelc2c0VLYXJ3YU10dlJRczRSQXZrL2FuTUx5U3IwOWJFa05GKzNjN3c3UDZ0NlJ5d0o4
MnIKUkt2UWg4TjNIalZGWHpXYmp5NmpTekJKTUE0R0ExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhT
VUVEREFLQmdncgpCZ0VGQlFjREFUQU1CZ05WSFJNQkFmOEVBakFBTUJRR0ExVWRFUVFOTUF1Q0NX
eHZZMkZzYUc5emREQUtCZ2dxCmhrak9QUVFEQWdOSEFEQkVBaUJTUXVXWVlJNlRTdmZWdHl4eHIv
UjlkT2piajFVUUNBMWU1S2RoTUxmS2lRSWcKWS9aN1oxa0VUczBpOXRUeE4zNEpidDMwN29vTm55
WnFLVnlHbmZyMm1Xbz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
ecdsa.key: |-
LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUhhL3BuY1BCZzdqNkdaNi95
S1MxRDNLR2dibXVtT0k2ZjE2ZElFRmxjNXVvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFZ3lHbS9W
b1VIRXlyZFROYnF3UXBxdkJveTI5RkN6aEVDK1Q5cWN3dkpLdlQxc1NRMFg3ZAp6dkRzL3EzcEhM
QW56YXRFcTlDSHczY2VOVVZmTlp1UExnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
---
apiVersion: nginx.tsuru.io/v1alpha1
kind: Nginx
metadata:
name: my-secured-nginx
spec:
image: nginx:alpine
certificates:
secretName: my-secured-nginx-certs
items:
- certificateField: rsa.crt
keyField: rsa.key
- certificateField: ecdsa.crt
certificatePath: custom_dir/custom_name.crt
keyField: ecdsa.key
keyPath: custom_dir/custom_name.key