Skip to content

Commit b57293f

Browse files
committed
certsync+installerpod: Use atomicdir.Sync
1 parent 4bebc96 commit b57293f

File tree

3 files changed

+189
-88
lines changed

3 files changed

+189
-88
lines changed

pkg/operator/staticpod/certsyncpod/certsync_controller.go

Lines changed: 23 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package certsyncpod
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"path/filepath"
78
"reflect"
@@ -17,8 +18,8 @@ import (
1718

1819
"github.com/openshift/library-go/pkg/controller/factory"
1920
"github.com/openshift/library-go/pkg/operator/events"
20-
"github.com/openshift/library-go/pkg/operator/staticpod"
2121
"github.com/openshift/library-go/pkg/operator/staticpod/controller/installer"
22+
"github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir"
2223
)
2324

2425
type CertSyncController struct {
@@ -115,7 +116,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte
115116

116117
contentDir := getConfigMapDir(c.destinationDir, cm.Name)
117118

118-
data := map[string]string{}
119+
data := make(map[string]string, len(configMap.Data))
119120
for filename := range configMap.Data {
120121
fullFilename := filepath.Join(contentDir, filename)
121122

@@ -152,27 +153,11 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte
152153
continue
153154
}
154155

155-
klog.Infof("Creating directory %q ...", contentDir)
156-
if err := os.MkdirAll(contentDir, 0755); err != nil && !os.IsExist(err) {
157-
c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed creating directory for configmap: %s/%s: %v", configMap.Namespace, configMap.Name, err)
158-
errors = append(errors, err)
159-
continue
156+
files := make(map[string][]byte, len(configMap.Data))
157+
for k, v := range configMap.Data {
158+
files[k] = []byte(v)
160159
}
161-
for filename, content := range configMap.Data {
162-
fullFilename := filepath.Join(contentDir, filename)
163-
// if the existing is the same, do nothing
164-
if reflect.DeepEqual(data[fullFilename], content) {
165-
continue
166-
}
167-
168-
klog.Infof("Writing configmap manifest %q ...", fullFilename)
169-
if err := staticpod.WriteFileAtomic([]byte(content), 0644, fullFilename); err != nil {
170-
c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed writing file for configmap: %s/%s: %v", configMap.Namespace, configMap.Name, err)
171-
errors = append(errors, err)
172-
continue
173-
}
174-
}
175-
c.eventRecorder.Eventf("CertificateUpdated", "Wrote updated configmap: %s/%s", configMap.Namespace, configMap.Name)
160+
errors = append(errors, syncDirectory(c.eventRecorder, "configmap", configMap.ObjectMeta, contentDir, files, 0644))
176161
}
177162

178163
klog.Infof("Syncing secrets: %v", c.secrets)
@@ -220,7 +205,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte
220205

221206
contentDir := getSecretDir(c.destinationDir, s.Name)
222207

223-
data := map[string][]byte{}
208+
data := make(map[string][]byte, len(secret.Data))
224209
for filename := range secret.Data {
225210
fullFilename := filepath.Join(contentDir, filename)
226211

@@ -257,29 +242,21 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte
257242
continue
258243
}
259244

260-
klog.Infof("Creating directory %q ...", contentDir)
261-
if err := os.MkdirAll(contentDir, 0755); err != nil && !os.IsExist(err) {
262-
c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed creating directory for secret: %s/%s: %v", secret.Namespace, secret.Name, err)
263-
errors = append(errors, err)
264-
continue
265-
}
266-
for filename, content := range secret.Data {
267-
// TODO fix permissions
268-
fullFilename := filepath.Join(contentDir, filename)
269-
// if the existing is the same, do nothing
270-
if reflect.DeepEqual(data[fullFilename], content) {
271-
continue
272-
}
273-
274-
klog.Infof("Writing secret manifest %q ...", fullFilename)
275-
if err := staticpod.WriteFileAtomic(content, 0600, fullFilename); err != nil {
276-
c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed writing file for secret: %s/%s: %v", secret.Namespace, secret.Name, err)
277-
errors = append(errors, err)
278-
continue
279-
}
280-
}
281-
c.eventRecorder.Eventf("CertificateUpdated", "Wrote updated secret: %s/%s", secret.Namespace, secret.Name)
245+
errors = append(errors, syncDirectory(c.eventRecorder, "secret", secret.ObjectMeta, contentDir, data, 0600))
282246
}
283-
284247
return utilerrors.NewAggregate(errors)
285248
}
249+
250+
func syncDirectory(
251+
eventRecorder events.Recorder,
252+
typeName string, o metav1.ObjectMeta,
253+
targetDir string, files map[string][]byte, filePerm os.FileMode,
254+
) error {
255+
if err := atomicdir.Sync(targetDir, files, filePerm); err != nil {
256+
err = fmt.Errorf("failed to sync %s %s/%s (directory %q): %w", typeName, o.Name, o.Namespace, targetDir, err)
257+
eventRecorder.Warning("CertificateUpdateFailed", err.Error())
258+
return err
259+
}
260+
eventRecorder.Eventf("CertificateUpdated", "Wrote updated %s: %s/%s", typeName, o.Namespace, o.Name)
261+
return nil
262+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
//go:build linux
2+
3+
package certsyncpod
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"crypto/ecdsa"
9+
"crypto/elliptic"
10+
"crypto/rand"
11+
"crypto/x509"
12+
"crypto/x509/pkix"
13+
"encoding/pem"
14+
"math/big"
15+
"os"
16+
"path/filepath"
17+
"sync"
18+
"testing"
19+
"time"
20+
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
"k8s.io/apimachinery/pkg/util/wait"
23+
"k8s.io/apiserver/pkg/server/dynamiccertificates"
24+
25+
"github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir"
26+
)
27+
28+
// TestDynamicCertificates makes sure the receiving side of certificate synchronization works as expected.
29+
// It reads and watches the certificates being synchronized in the same way as e.g. kube-apiserver,
30+
// the very same libraries are being used.
31+
func TestDynamicCertificates(t *testing.T) {
32+
const typeName = "secret"
33+
om := metav1.ObjectMeta{
34+
Namespace: "openshift-kube-apiserver",
35+
Name: "s1",
36+
}
37+
38+
// Generate all necessary keypairs.
39+
tlsCert, tlsKey := generateKeypair(t)
40+
tlsCertUpdated, tlsKeyUpdated := generateKeypair(t)
41+
42+
// Write the keypair into a secret directory.
43+
secretDir := filepath.Join(t.TempDir(), "secrets", om.Name)
44+
certFile := filepath.Join(secretDir, "tls.crt")
45+
keyFile := filepath.Join(secretDir, "tls.key")
46+
47+
if err := os.MkdirAll(secretDir, 0700); err != nil {
48+
t.Fatalf("Failed to create secret directory %q: %v", secretDir, err)
49+
}
50+
if err := os.WriteFile(certFile, tlsCert, 0600); err != nil {
51+
t.Fatalf("Failed to write TLS certificate into %q: %v", certFile, err)
52+
}
53+
if err := os.WriteFile(keyFile, tlsKey, 0600); err != nil {
54+
t.Fatalf("Failed to write TLS key into %q: %v", keyFile, err)
55+
}
56+
57+
// Start the watcher.
58+
// This reads the keypair synchronously so the initial state is loaded here.
59+
dc, err := dynamiccertificates.NewDynamicServingContentFromFiles("localhost TLS", certFile, keyFile)
60+
if err != nil {
61+
t.Fatalf("Failed to init dynamic certificate: %v", err)
62+
}
63+
64+
// Check the initial keypair is loaded.
65+
cert, key := dc.CurrentCertKeyContent()
66+
if !bytes.Equal(cert, tlsCert) || !bytes.Equal(key, tlsKey) {
67+
t.Fatal("Unexpected initial keypair loaded")
68+
}
69+
70+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
71+
var wg sync.WaitGroup
72+
wg.Add(1)
73+
go func() {
74+
defer wg.Done()
75+
dc.Run(ctx, 1)
76+
}()
77+
defer wg.Wait()
78+
defer cancel()
79+
80+
// Poll until update detected.
81+
files := map[string][]byte{
82+
"tls.crt": tlsCertUpdated,
83+
"tls.key": tlsKeyUpdated,
84+
}
85+
err = wait.PollUntilContextCancel(ctx, 250*time.Millisecond, true, func(ctx context.Context) (bool, error) {
86+
// Replace the secret directory.
87+
if err := atomicdir.Sync(secretDir, files, 0600); err != nil {
88+
t.Errorf("Failed to write files: %v", err)
89+
return false, err
90+
}
91+
92+
// Check the loaded content matches.
93+
// This is most probably updated based on write in a previous Poll invocation.
94+
cert, key := dc.CurrentCertKeyContent()
95+
return bytes.Equal(cert, tlsCertUpdated) && bytes.Equal(key, tlsKeyUpdated), nil
96+
})
97+
if err != nil {
98+
t.Fatalf("Failed to wait for dynamic certificate: %v", err)
99+
}
100+
}
101+
102+
// generateKeypair returns (cert, key).
103+
func generateKeypair(t *testing.T) ([]byte, []byte) {
104+
t.Helper()
105+
106+
privateKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
107+
if err != nil {
108+
t.Fatalf("Failed to generate TLS key: %v", err)
109+
}
110+
111+
notBefore := time.Now()
112+
notAfter := notBefore.Add(1 * time.Hour)
113+
114+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
115+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
116+
if err != nil {
117+
t.Fatalf("Failed to generate serial number for TLS keypair: %v", err)
118+
}
119+
120+
template := x509.Certificate{
121+
SerialNumber: serialNumber,
122+
Subject: pkix.Name{
123+
Organization: []string{"Example Org"},
124+
},
125+
NotBefore: notBefore,
126+
NotAfter: notAfter,
127+
KeyUsage: x509.KeyUsageDigitalSignature,
128+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
129+
BasicConstraintsValid: true,
130+
DNSNames: []string{"example.com"},
131+
}
132+
133+
publicKeyBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
134+
if err != nil {
135+
t.Fatalf("Failed to create TLS certificate: %v", err)
136+
}
137+
138+
var certOut bytes.Buffer
139+
if err := pem.Encode(&certOut, &pem.Block{Type: "CERTIFICATE", Bytes: publicKeyBytes}); err != nil {
140+
t.Fatalf("Failed to write certificate PEM: %v", err)
141+
}
142+
143+
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
144+
if err != nil {
145+
t.Fatalf("Unable to marshal private key: %v", err)
146+
}
147+
148+
var keyOut bytes.Buffer
149+
if err := pem.Encode(&keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes}); err != nil {
150+
t.Fatalf("Failed to write certificate PEM: %v", err)
151+
}
152+
153+
return certOut.Bytes(), keyOut.Bytes()
154+
}

pkg/operator/staticpod/installerpod/cmd.go

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,28 @@ import (
1010
"strings"
1111
"time"
1212

13-
"k8s.io/utils/clock"
14-
15-
"k8s.io/apimachinery/pkg/util/wait"
16-
1713
"github.com/blang/semver/v4"
1814
"github.com/davecgh/go-spew/spew"
1915
"github.com/spf13/cobra"
2016
"github.com/spf13/pflag"
21-
"k8s.io/klog/v2"
2217

2318
corev1 "k8s.io/api/core/v1"
2419
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2520
"k8s.io/apimachinery/pkg/util/sets"
2621
"k8s.io/apimachinery/pkg/util/uuid"
22+
"k8s.io/apimachinery/pkg/util/wait"
2723
"k8s.io/apiserver/pkg/server"
2824
"k8s.io/client-go/kubernetes"
2925
"k8s.io/client-go/rest"
26+
"k8s.io/klog/v2"
27+
"k8s.io/utils/clock"
3028

3129
"github.com/openshift/library-go/pkg/config/client"
3230
"github.com/openshift/library-go/pkg/operator/events"
3331
"github.com/openshift/library-go/pkg/operator/resource/resourceread"
3432
"github.com/openshift/library-go/pkg/operator/resource/retry"
35-
"github.com/openshift/library-go/pkg/operator/staticpod"
3633
"github.com/openshift/library-go/pkg/operator/staticpod/internal"
34+
"github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir"
3735
"github.com/openshift/library-go/pkg/operator/staticpod/internal/flock"
3836
)
3937

@@ -259,14 +257,8 @@ func (o *InstallOptions) copySecretsAndConfigMaps(ctx context.Context, resourceD
259257
secretBaseName = o.prefixFor(secret.Name)
260258
}
261259
contentDir := path.Join(resourceDir, "secrets", secretBaseName)
262-
klog.Infof("Creating directory %q ...", contentDir)
263-
if err := os.MkdirAll(contentDir, 0755); err != nil {
264-
return err
265-
}
266-
for filename, content := range secret.Data {
267-
if err := writeSecret(content, path.Join(contentDir, filename)); err != nil {
268-
return err
269-
}
260+
if err := atomicdir.Sync(contentDir, secret.Data, 0600); err != nil {
261+
return fmt.Errorf("failed to sync secret %s/%s (directory %q): %w", secret.Namespace, secret.Name, contentDir, err)
270262
}
271263
}
272264
for _, configmap := range configs {
@@ -275,17 +267,15 @@ func (o *InstallOptions) copySecretsAndConfigMaps(ctx context.Context, resourceD
275267
configMapBaseName = o.prefixFor(configmap.Name)
276268
}
277269
contentDir := path.Join(resourceDir, "configmaps", configMapBaseName)
278-
klog.Infof("Creating directory %q ...", contentDir)
279-
if err := os.MkdirAll(contentDir, 0755); err != nil {
280-
return err
270+
271+
files := make(map[string][]byte, len(configmap.Data))
272+
for k, v := range configmap.Data {
273+
files[k] = []byte(v)
281274
}
282-
for filename, content := range configmap.Data {
283-
if err := writeConfig([]byte(content), path.Join(contentDir, filename)); err != nil {
284-
return err
285-
}
275+
if err := atomicdir.Sync(contentDir, files, 0600); err != nil {
276+
return fmt.Errorf("failed to sync configmap %s/%s (directory %q): %w", configmap.Namespace, configmap.Name, contentDir, err)
286277
}
287278
}
288-
289279
return nil
290280
}
291281

@@ -625,23 +615,3 @@ func (o *InstallOptions) writePod(rawPodBytes []byte, manifestFileName, resource
625615
}
626616
return nil
627617
}
628-
629-
func writeConfig(content []byte, fullFilename string) error {
630-
klog.Infof("Writing config file %q ...", fullFilename)
631-
632-
filePerms := os.FileMode(0600)
633-
if strings.HasSuffix(fullFilename, ".sh") {
634-
filePerms = 0755
635-
}
636-
return staticpod.WriteFileAtomic(content, filePerms, fullFilename)
637-
}
638-
639-
func writeSecret(content []byte, fullFilename string) error {
640-
klog.Infof("Writing secret manifest %q ...", fullFilename)
641-
642-
filePerms := os.FileMode(0600)
643-
if strings.HasSuffix(fullFilename, ".sh") {
644-
filePerms = 0700
645-
}
646-
return staticpod.WriteFileAtomic(content, filePerms, fullFilename)
647-
}

0 commit comments

Comments
 (0)