controllers: test HelmRepository TLS auth

This commit is contained in:
Hidde Beydals 2020-04-17 10:35:16 +02:00
parent 1cc6464b73
commit 3c70c8d333
14 changed files with 269 additions and 1 deletions

View File

@ -82,6 +82,10 @@ func (r *HelmRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err
syncedRepo, err := r.sync(*repository.DeepCopy()) syncedRepo, err := r.sync(*repository.DeepCopy())
if err != nil { if err != nil {
log.Error(err, "Helm repository sync failed") log.Error(err, "Helm repository sync failed")
if err := r.Status().Update(ctx, &syncedRepo); err != nil {
log.Error(err, "unable to update HelmRepository status")
}
return ctrl.Result{Requeue: true}, err
} }
// update status // update status
@ -89,7 +93,6 @@ func (r *HelmRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err
log.Error(err, "unable to update HelmRepository status") log.Error(err, "unable to update HelmRepository status")
return ctrl.Result{Requeue: true}, err return ctrl.Result{Requeue: true}, err
} }
log.Info("Helm repository sync succeeded", "msg", sourcev1.HelmRepositoryReadyMessage(syncedRepo)) log.Info("Helm repository sync succeeded", "msg", sourcev1.HelmRepositoryReadyMessage(syncedRepo))
// requeue repository // requeue repository

View File

@ -1,3 +1,19 @@
/*
Copyright 2020 The Flux CD contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers package controllers
import ( import (
@ -200,4 +216,85 @@ var _ = Describe("HelmRepositoryReconciler", func() {
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
}) })
}) })
It("Authenticates when TLS credentials are provided", func() {
helmServer, err = testserver.NewTempHelmServer()
Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(helmServer.Root())
defer helmServer.Stop()
err = helmServer.StartTLS(examplePublicKey, examplePrivateKey, exampleCA)
Expect(err).NotTo(HaveOccurred())
Expect(helmServer.PackageChart(path.Join("testdata/helmchart"))).Should(Succeed())
Expect(helmServer.GenerateIndex()).Should(Succeed())
secretKey := types.NamespacedName{
Name: "helmrepository-auth-" + randStringRunes(5),
Namespace: namespace.Name,
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretKey.Name,
Namespace: secretKey.Namespace,
},
Data: map[string][]byte{},
}
Expect(k8sClient.Create(context.Background(), secret)).Should(Succeed())
key := types.NamespacedName{
Name: "helmrepository-sample-" + randStringRunes(5),
Namespace: namespace.Name,
}
created := &sourcev1.HelmRepository{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: sourcev1.HelmRepositorySpec{
URL: helmServer.URL(),
SecretRef: &corev1.LocalObjectReference{
Name: secretKey.Name,
},
Interval: metav1.Duration{Duration: interval},
},
}
Expect(k8sClient.Create(context.Background(), created)).Should(Succeed())
By("Expecting unknown authority error")
Eventually(func() bool {
got := &sourcev1.HelmRepository{}
_ = k8sClient.Get(context.Background(), key, got)
for _, c := range got.Status.Conditions {
if c.Reason == sourcev1.IndexationFailedReason &&
strings.Contains(c.Message, "certificate signed by unknown authority") {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())
By("Expecting missing field error")
secret.Data["certFile"] = examplePublicKey
secret.Data["keyFile"] = examplePrivateKey
Expect(k8sClient.Update(context.Background(), secret)).Should(Succeed())
Eventually(func() bool {
got := &sourcev1.HelmRepository{}
_ = k8sClient.Get(context.Background(), key, got)
for _, c := range got.Status.Conditions {
if c.Reason == sourcev1.AuthenticationFailedReason {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())
By("Expecting artifact")
secret.Data["caFile"] = exampleCA
Expect(k8sClient.Update(context.Background(), secret)).Should(Succeed())
Eventually(func() bool {
got := &sourcev1.HelmRepository{}
_ = k8sClient.Get(context.Background(), key, got)
return got.Status.Artifact != nil
}, timeout, interval).Should(BeTrue())
})
}) })

View File

@ -49,6 +49,10 @@ var k8sManager ctrl.Manager
var testEnv *envtest.Environment var testEnv *envtest.Environment
var storage *Storage var storage *Storage
var examplePublicKey []byte
var examplePrivateKey []byte
var exampleCA []byte
func TestAPIs(t *testing.T) { func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail) RegisterFailHandler(Fail)
@ -88,6 +92,8 @@ var _ = BeforeSuite(func(done Done) {
// +kubebuilder:scaffold:scheme // +kubebuilder:scaffold:scheme
Expect(loadExampleKeys()).To(Succeed())
tmpStoragePath, err := ioutil.TempDir("", "helmrepository") tmpStoragePath, err := ioutil.TempDir("", "helmrepository")
Expect(err).NotTo(HaveOccurred(), "failed to create tmp storage dir") Expect(err).NotTo(HaveOccurred(), "failed to create tmp storage dir")
@ -136,6 +142,19 @@ func init() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
} }
func loadExampleKeys() (err error) {
examplePublicKey, err = ioutil.ReadFile(filepath.Join("testdata/certs/server.pem"))
if err != nil {
return err
}
examplePrivateKey, err = ioutil.ReadFile(filepath.Join("testdata/certs/server-key.pem"))
if err != nil {
return err
}
exampleCA, err = ioutil.ReadFile(filepath.Join("testdata/certs/ca.pem"))
return err
}
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890") var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
func randStringRunes(n int) string { func randStringRunes(n int) string {

30
controllers/testdata/certs/Makefile vendored Normal file
View File

@ -0,0 +1,30 @@
# Copyright 2020 The Flux CD contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
all: server-key.pem
ca-key.pem: ca-csr.json
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
ca.pem: ca-key.pem
ca.csr: ca-key.pem
server-key.pem: server-csr.json ca-config.json ca-key.pem
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=web-servers \
server-csr.json | cfssljson -bare server
sever.pem: server-key.pem
server.csr: server-key.pem

View File

@ -0,0 +1,18 @@
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"web-servers": {
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
],
"expiry": "87600h"
}
}
}
}

View File

@ -0,0 +1,9 @@
{
"CN": "example.com CA",
"hosts": [
"127.0.0.1",
"localhost",
"example.com",
"www.example.com"
]
}

5
controllers/testdata/certs/ca-key.pem vendored Normal file
View File

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIOH/u9dMcpVcZ0+X9Fc78dCTj8SHuXawhLjhu/ej64WToAoGCCqGSM49
AwEHoUQDQgAEruH/kPxtX3cyYR2G7TYmxLq6AHyzo/NGXc9XjGzdJutE2SQzn37H
dvSJbH+Lvqo9ik0uiJVRVdCYD1j7gNszGA==
-----END EC PRIVATE KEY-----

9
controllers/testdata/certs/ca.csr vendored Normal file
View File

@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBIDCBxgIBADAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr
RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxigSzBJBgkqhkiG9w0BCQ4x
PDA6MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFt
cGxlLmNvbYcEfwAAATAKBggqhkjOPQQDAgNJADBGAiEAkw85nyLhJssyCYsaFvRU
EErhu66xHPJug/nG50uV5OoCIQCUorrflOSxfChPeCe4xfwcPv7FpcCYbKVYtGzz
b34Wow==
-----END CERTIFICATE REQUEST-----

11
controllers/testdata/certs/ca.pem vendored Normal file
View File

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw
GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw
NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr
RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE
AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ
4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O
1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb
OD8EjjCMY69RMO0=
-----END CERTIFICATE-----

View File

@ -0,0 +1,9 @@
{
"CN": "example.com",
"hosts": [
"127.0.0.1",
"localhost",
"example.com",
"www.example.com"
]
}

View File

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKQbEXV6nljOHMmPrWVWQ+JrAE5wsbE9iMhfY7wlJgXOoAoGCCqGSM49
AwEHoUQDQgAE+53oBGlrvVUTelSGYji8GNHVhVg8jOs1PeeLuXCIZjQmctHLFEq3
fE+mGxCL93MtpYzlwIWBf0m7pEGQre6bzg==
-----END EC PRIVATE KEY-----

8
controllers/testdata/certs/server.csr vendored Normal file
View File

@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBHDCBwwIBADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR
yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86gSzBJBgkqhkiG9w0BCQ4xPDA6
MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxl
LmNvbYcEfwAAATAKBggqhkjOPQQDAgNIADBFAiB5A6wvQ5x6g/zhiyn+wLzXsOaB
Gb/F25p/zTHHQqZbkwIhAPUgWzy/2bs6eZEi97bSlaRdmrqHwqT842t5sEwGyXNV
-----END CERTIFICATE REQUEST-----

13
controllers/testdata/certs/server.pem vendored Normal file
View File

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB7TCCAZKgAwIBAgIUB+17B8PU05wVTzRHLeG+S+ybZK4wCgYIKoZIzj0EAwIw
GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMzAw
NDE1MDgxODAwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR
yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86jgbowgbcwDgYDVR0PAQH/BAQD
AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA
MB0GA1UdDgQWBBTM8HS5EIlVMBYv/300jN8PEArUgDAfBgNVHSMEGDAWgBQGyUiU
1QEZiMAqjsnIYTwZ4yp5wzA4BgNVHREEMTAvgglsb2NhbGhvc3SCC2V4YW1wbGUu
Y29tgg93d3cuZXhhbXBsZS5jb22HBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhAOgB
5W82FEgiTTOmsNRekkK5jUPbj4D4eHtb2/BI7ph4AiEA2AxHASIFBdv5b7Qf5prb
bdNmUCzAvVuCAKuMjg2OPrE=
-----END CERTIFICATE-----

View File

@ -1,6 +1,8 @@
package testserver package testserver
import ( import (
"crypto/tls"
"crypto/x509"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -48,6 +50,36 @@ func (s *HTTP) Start() {
})) }))
} }
func (s *HTTP) StartTLS(cert, key, ca []byte) error {
s.server = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler := http.FileServer(http.Dir(s.docroot))
if s.middleware != nil {
s.middleware(handler).ServeHTTP(w, r)
return
}
handler.ServeHTTP(w, r)
}))
config := tls.Config{}
keyPair, err := tls.X509KeyPair(cert, key)
if err != nil {
return err
}
config.Certificates = []tls.Certificate{keyPair}
cp := x509.NewCertPool()
cp.AppendCertsFromPEM(ca)
config.RootCAs = cp
config.BuildNameToCertificate()
config.ServerName = "example.com"
s.server.TLS = &config
s.server.StartTLS()
return nil
}
func (s *HTTP) Stop() { func (s *HTTP) Stop() {
if s.server != nil { if s.server != nil {
s.server.Close() s.server.Close()