Skip to content

Commit 197751d

Browse files
authored
Merge pull request kubernetes-sigs#1441 from christopherhein/export-certwatcher
🌱 Adding certwatcher as an external package
2 parents b5065bd + a93a723 commit 197751d

File tree

7 files changed

+343
-5
lines changed

7 files changed

+343
-5
lines changed

pkg/webhook/internal/certwatcher/certwatcher.go renamed to pkg/certwatcher/certwatcher.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2019 The Kubernetes Authors.
2+
Copyright 2021 The Kubernetes Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -31,7 +31,7 @@ var log = logf.RuntimeLog.WithName("certwatcher")
3131
// changes, it reads and parses both and calls an optional callback with the new
3232
// certificate.
3333
type CertWatcher struct {
34-
sync.Mutex
34+
sync.RWMutex
3535

3636
currentCert *tls.Certificate
3737
watcher *fsnotify.Watcher
@@ -64,8 +64,8 @@ func New(certPath, keyPath string) (*CertWatcher, error) {
6464

6565
// GetCertificate fetches the currently loaded certificate, which may be nil.
6666
func (cw *CertWatcher) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
67-
cw.Lock()
68-
defer cw.Unlock()
67+
cw.RLock()
68+
defer cw.RUnlock()
6969
return cw.currentCert, nil
7070
}
7171

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package certwatcher_test
18+
19+
import (
20+
"os"
21+
"testing"
22+
23+
. "github.com/onsi/ginkgo"
24+
. "github.com/onsi/gomega"
25+
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
26+
logf "sigs.k8s.io/controller-runtime/pkg/log"
27+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
28+
)
29+
30+
var (
31+
certPath = "testdata/tls.crt"
32+
keyPath = "testdata/tls.key"
33+
)
34+
35+
func TestSource(t *testing.T) {
36+
RegisterFailHandler(Fail)
37+
suiteName := "CertWatcher Suite"
38+
RunSpecsWithDefaultAndCustomReporters(t, suiteName, []Reporter{printer.NewlineReporter{}, printer.NewProwReporter(suiteName)})
39+
}
40+
41+
var _ = BeforeSuite(func(done Done) {
42+
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
43+
44+
close(done)
45+
}, 60)
46+
47+
var _ = AfterSuite(func(done Done) {
48+
for _, file := range []string{certPath, keyPath} {
49+
_ = os.Remove(file)
50+
}
51+
close(done)
52+
}, 60)

pkg/certwatcher/certwatcher_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package certwatcher_test
18+
19+
import (
20+
"context"
21+
"crypto/rand"
22+
"crypto/rsa"
23+
"crypto/x509"
24+
"crypto/x509/pkix"
25+
"encoding/pem"
26+
"math/big"
27+
"net"
28+
"os"
29+
"time"
30+
31+
. "github.com/onsi/ginkgo"
32+
. "github.com/onsi/gomega"
33+
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
34+
)
35+
36+
var _ = Describe("CertWatcher", func() {
37+
var _ = Describe("certwatcher New", func() {
38+
It("should errors without cert/key", func() {
39+
_, err := certwatcher.New("", "")
40+
Expect(err).ToNot(BeNil())
41+
})
42+
})
43+
44+
var _ = Describe("certwatcher Start", func() {
45+
var (
46+
ctx context.Context
47+
ctxCancel context.CancelFunc
48+
watcher *certwatcher.CertWatcher
49+
)
50+
51+
BeforeEach(func() {
52+
ctx, ctxCancel = context.WithCancel(context.Background())
53+
54+
err := writeCerts(certPath, keyPath, "127.0.0.1")
55+
Expect(err).To(BeNil())
56+
57+
Eventually(func() error {
58+
for _, file := range []string{certPath, keyPath} {
59+
_, err := os.ReadFile(file)
60+
if err != nil {
61+
return err
62+
}
63+
continue
64+
}
65+
66+
return nil
67+
}).Should(Succeed())
68+
69+
watcher, err = certwatcher.New(certPath, keyPath)
70+
Expect(err).To(BeNil())
71+
})
72+
73+
startWatcher := func() (done <-chan struct{}) {
74+
doneCh := make(chan struct{})
75+
go func() {
76+
defer GinkgoRecover()
77+
defer close(doneCh)
78+
Expect(watcher.Start(ctx)).To(Succeed())
79+
}()
80+
// wait till we read first cert
81+
Eventually(func() error {
82+
err := watcher.ReadCertificate()
83+
return err
84+
}).Should(Succeed())
85+
return doneCh
86+
}
87+
88+
It("should read the initial cert/key", func() {
89+
doneCh := startWatcher()
90+
91+
ctxCancel()
92+
Eventually(doneCh, "4s").Should(BeClosed())
93+
})
94+
95+
It("should reload currentCert when changed", func() {
96+
doneCh := startWatcher()
97+
98+
firstcert, _ := watcher.GetCertificate(nil)
99+
100+
err := writeCerts(certPath, keyPath, "192.168.0.1")
101+
Expect(err).To(BeNil())
102+
103+
Eventually(func() bool {
104+
secondcert, _ := watcher.GetCertificate(nil)
105+
first := firstcert.PrivateKey.(*rsa.PrivateKey)
106+
return first.Equal(secondcert.PrivateKey)
107+
}).ShouldNot(BeTrue())
108+
109+
ctxCancel()
110+
Eventually(doneCh, "4s").Should(BeClosed())
111+
})
112+
})
113+
})
114+
115+
func writeCerts(certPath, keyPath, ip string) error {
116+
var priv interface{}
117+
var err error
118+
priv, err = rsa.GenerateKey(rand.Reader, 2048)
119+
if err != nil {
120+
return err
121+
}
122+
123+
keyUsage := x509.KeyUsageDigitalSignature
124+
if _, isRSA := priv.(*rsa.PrivateKey); isRSA {
125+
keyUsage |= x509.KeyUsageKeyEncipherment
126+
}
127+
128+
var notBefore time.Time
129+
notBefore = time.Now()
130+
131+
notAfter := notBefore.Add(1 * time.Hour)
132+
133+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
134+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
135+
if err != nil {
136+
return err
137+
}
138+
139+
template := x509.Certificate{
140+
SerialNumber: serialNumber,
141+
Subject: pkix.Name{
142+
Organization: []string{"Kubernetes"},
143+
},
144+
NotBefore: notBefore,
145+
NotAfter: notAfter,
146+
147+
KeyUsage: keyUsage,
148+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
149+
BasicConstraintsValid: true,
150+
}
151+
152+
template.IPAddresses = append(template.IPAddresses, net.ParseIP(ip))
153+
154+
privkey := priv.(*rsa.PrivateKey)
155+
156+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privkey.PublicKey, priv)
157+
if err != nil {
158+
return err
159+
}
160+
161+
certOut, err := os.Create(certPath)
162+
if err != nil {
163+
return err
164+
}
165+
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
166+
return err
167+
}
168+
if err := certOut.Close(); err != nil {
169+
return err
170+
}
171+
172+
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
173+
if err != nil {
174+
return err
175+
}
176+
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
177+
if err != nil {
178+
return err
179+
}
180+
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
181+
return err
182+
}
183+
if err := keyOut.Close(); err != nil {
184+
return err
185+
}
186+
return nil
187+
}

pkg/certwatcher/doc.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/*
18+
Package certwatcher is a helper for reloading Certificates from disk to be used
19+
with tls servers. It provides a helper func `GetCertificate` which can be
20+
called from `tls.Config` and passed into your tls.Listener. For a detailed
21+
example server view pkg/webhook/server.go.
22+
*/
23+
package certwatcher

pkg/certwatcher/example_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package certwatcher_test
18+
19+
import (
20+
"context"
21+
"crypto/tls"
22+
"net/http"
23+
24+
ctrl "sigs.k8s.io/controller-runtime"
25+
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
26+
)
27+
28+
type sampleServer struct {
29+
}
30+
31+
func Example() {
32+
// Setup Context
33+
ctx := ctrl.SetupSignalHandler()
34+
35+
// Initialize a new cert watcher with cert/key pari
36+
watcher, err := certwatcher.New("ssl/tls.crt", "ssl/tls.key")
37+
if err != nil {
38+
panic(err)
39+
}
40+
41+
// Start goroutine with certwatcher running fsnotify against supplied certdir
42+
go func() {
43+
if err := watcher.Start(ctx); err != nil {
44+
panic(err)
45+
}
46+
}()
47+
48+
// Setup TLS listener using GetCertficate for fetching the cert when changes
49+
listener, err := tls.Listen("tcp", "localhost:9443", &tls.Config{
50+
GetCertificate: watcher.GetCertificate,
51+
})
52+
if err != nil {
53+
panic(err)
54+
}
55+
56+
// Initialize your tls server
57+
srv := &http.Server{
58+
Handler: &sampleServer{},
59+
}
60+
61+
// Start goroutine for handling server shutdown.
62+
go func() {
63+
<-ctx.Done()
64+
if err := srv.Shutdown(context.Background()); err != nil {
65+
panic(err)
66+
}
67+
}()
68+
69+
// Serve t
70+
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
71+
panic(err)
72+
}
73+
}
74+
75+
func (s *sampleServer) ServeHTTP(http.ResponseWriter, *http.Request) {
76+
}

pkg/certwatcher/testdata/.gitkeep

Whitespace-only changes.

pkg/webhook/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ import (
3131

3232
"github.com/prometheus/client_golang/prometheus"
3333
"github.com/prometheus/client_golang/prometheus/promhttp"
34+
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
3435
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
35-
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/certwatcher"
3636
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics"
3737
)
3838

0 commit comments

Comments
 (0)