diff --git a/controllers/helmrepository_controller.go b/controllers/helmrepository_controller.go index d646106c..6d9eb994 100644 --- a/controllers/helmrepository_controller.go +++ b/controllers/helmrepository_controller.go @@ -82,6 +82,10 @@ func (r *HelmRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err syncedRepo, err := r.sync(*repository.DeepCopy()) if err != nil { 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 @@ -89,7 +93,6 @@ func (r *HelmRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err log.Error(err, "unable to update HelmRepository status") return ctrl.Result{Requeue: true}, err } - log.Info("Helm repository sync succeeded", "msg", sourcev1.HelmRepositoryReadyMessage(syncedRepo)) // requeue repository diff --git a/controllers/helmrepository_controller_test.go b/controllers/helmrepository_controller_test.go index a3250d13..03b56e42 100644 --- a/controllers/helmrepository_controller_test.go +++ b/controllers/helmrepository_controller_test.go @@ -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 import ( @@ -200,4 +216,85 @@ var _ = Describe("HelmRepositoryReconciler", func() { }, 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()) + }) }) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 0750f9ed..4fa3bfd6 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -49,6 +49,10 @@ var k8sManager ctrl.Manager var testEnv *envtest.Environment var storage *Storage +var examplePublicKey []byte +var examplePrivateKey []byte +var exampleCA []byte + func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -88,6 +92,8 @@ var _ = BeforeSuite(func(done Done) { // +kubebuilder:scaffold:scheme + Expect(loadExampleKeys()).To(Succeed()) + tmpStoragePath, err := ioutil.TempDir("", "helmrepository") Expect(err).NotTo(HaveOccurred(), "failed to create tmp storage dir") @@ -136,6 +142,19 @@ func init() { 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") func randStringRunes(n int) string { diff --git a/controllers/testdata/certs/Makefile b/controllers/testdata/certs/Makefile new file mode 100644 index 00000000..39bd5424 --- /dev/null +++ b/controllers/testdata/certs/Makefile @@ -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 diff --git a/controllers/testdata/certs/ca-config.json b/controllers/testdata/certs/ca-config.json new file mode 100644 index 00000000..91c0644c --- /dev/null +++ b/controllers/testdata/certs/ca-config.json @@ -0,0 +1,18 @@ +{ + "signing": { + "default": { + "expiry": "87600h" + }, + "profiles": { + "web-servers": { + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ], + "expiry": "87600h" + } + } + } +} diff --git a/controllers/testdata/certs/ca-csr.json b/controllers/testdata/certs/ca-csr.json new file mode 100644 index 00000000..941277bb --- /dev/null +++ b/controllers/testdata/certs/ca-csr.json @@ -0,0 +1,9 @@ +{ + "CN": "example.com CA", + "hosts": [ + "127.0.0.1", + "localhost", + "example.com", + "www.example.com" + ] +} diff --git a/controllers/testdata/certs/ca-key.pem b/controllers/testdata/certs/ca-key.pem new file mode 100644 index 00000000..b69de5ab --- /dev/null +++ b/controllers/testdata/certs/ca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIOH/u9dMcpVcZ0+X9Fc78dCTj8SHuXawhLjhu/ej64WToAoGCCqGSM49 +AwEHoUQDQgAEruH/kPxtX3cyYR2G7TYmxLq6AHyzo/NGXc9XjGzdJutE2SQzn37H +dvSJbH+Lvqo9ik0uiJVRVdCYD1j7gNszGA== +-----END EC PRIVATE KEY----- diff --git a/controllers/testdata/certs/ca.csr b/controllers/testdata/certs/ca.csr new file mode 100644 index 00000000..baa8aeb2 --- /dev/null +++ b/controllers/testdata/certs/ca.csr @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBIDCBxgIBADAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr +RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxigSzBJBgkqhkiG9w0BCQ4x +PDA6MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFt +cGxlLmNvbYcEfwAAATAKBggqhkjOPQQDAgNJADBGAiEAkw85nyLhJssyCYsaFvRU +EErhu66xHPJug/nG50uV5OoCIQCUorrflOSxfChPeCe4xfwcPv7FpcCYbKVYtGzz +b34Wow== +-----END CERTIFICATE REQUEST----- diff --git a/controllers/testdata/certs/ca.pem b/controllers/testdata/certs/ca.pem new file mode 100644 index 00000000..080bd24e --- /dev/null +++ b/controllers/testdata/certs/ca.pem @@ -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----- diff --git a/controllers/testdata/certs/server-csr.json b/controllers/testdata/certs/server-csr.json new file mode 100644 index 00000000..0baf1160 --- /dev/null +++ b/controllers/testdata/certs/server-csr.json @@ -0,0 +1,9 @@ +{ + "CN": "example.com", + "hosts": [ + "127.0.0.1", + "localhost", + "example.com", + "www.example.com" + ] +} diff --git a/controllers/testdata/certs/server-key.pem b/controllers/testdata/certs/server-key.pem new file mode 100644 index 00000000..5054ff39 --- /dev/null +++ b/controllers/testdata/certs/server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKQbEXV6nljOHMmPrWVWQ+JrAE5wsbE9iMhfY7wlJgXOoAoGCCqGSM49 +AwEHoUQDQgAE+53oBGlrvVUTelSGYji8GNHVhVg8jOs1PeeLuXCIZjQmctHLFEq3 +fE+mGxCL93MtpYzlwIWBf0m7pEGQre6bzg== +-----END EC PRIVATE KEY----- diff --git a/controllers/testdata/certs/server.csr b/controllers/testdata/certs/server.csr new file mode 100644 index 00000000..5caf7b39 --- /dev/null +++ b/controllers/testdata/certs/server.csr @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBHDCBwwIBADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR +yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86gSzBJBgkqhkiG9w0BCQ4xPDA6 +MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxl +LmNvbYcEfwAAATAKBggqhkjOPQQDAgNIADBFAiB5A6wvQ5x6g/zhiyn+wLzXsOaB +Gb/F25p/zTHHQqZbkwIhAPUgWzy/2bs6eZEi97bSlaRdmrqHwqT842t5sEwGyXNV +-----END CERTIFICATE REQUEST----- diff --git a/controllers/testdata/certs/server.pem b/controllers/testdata/certs/server.pem new file mode 100644 index 00000000..11c655a0 --- /dev/null +++ b/controllers/testdata/certs/server.pem @@ -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----- diff --git a/internal/testserver/http.go b/internal/testserver/http.go index 91944d61..865820f4 100644 --- a/internal/testserver/http.go +++ b/internal/testserver/http.go @@ -1,6 +1,8 @@ package testserver import ( + "crypto/tls" + "crypto/x509" "io/ioutil" "net/http" "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() { if s.server != nil { s.server.Close()