Skip to content

Commit 2e36109

Browse files
committed
add issuance resumption integration tests
Signed-off-by: James Munnelly <[email protected]>
1 parent cf77048 commit 2e36109

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
Copyright 2023 The cert-manager 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 integration
18+
19+
import (
20+
"context"
21+
"crypto"
22+
"crypto/x509"
23+
"os"
24+
"reflect"
25+
"testing"
26+
"time"
27+
28+
cmclient "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned"
29+
"github.com/container-storage-interface/spec/lib/go/csi"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/types"
32+
fakeclock "k8s.io/utils/clock/testing"
33+
34+
"github.com/cert-manager/csi-lib/manager"
35+
"github.com/cert-manager/csi-lib/metadata"
36+
"github.com/cert-manager/csi-lib/storage"
37+
testdriver "github.com/cert-manager/csi-lib/test/driver"
38+
testutil "github.com/cert-manager/csi-lib/test/util"
39+
)
40+
41+
func testResumesExistingRequest(t *testing.T, issueBeforeCall bool) {
42+
store := storage.NewMemoryFS()
43+
ns := "certificaterequest-namespace"
44+
clock := fakeclock.NewFakeClock(time.Now())
45+
opts, cl, stop := testdriver.Run(t, testdriver.Options{
46+
Store: store,
47+
Clock: clock,
48+
GeneratePrivateKey: func(meta metadata.Metadata) (crypto.PrivateKey, error) {
49+
return nil, nil
50+
},
51+
GenerateRequest: func(meta metadata.Metadata) (*manager.CertificateRequestBundle, error) {
52+
return &manager.CertificateRequestBundle{
53+
Namespace: ns,
54+
}, nil
55+
},
56+
SignRequest: func(meta metadata.Metadata, key crypto.PrivateKey, request *x509.CertificateRequest) (csr []byte, err error) {
57+
return []byte{}, nil
58+
},
59+
WriteKeypair: func(meta metadata.Metadata, key crypto.PrivateKey, chain []byte, ca []byte) error {
60+
store.WriteFiles(meta, map[string][]byte{
61+
"ca": ca,
62+
"cert": chain,
63+
})
64+
nextIssuanceTime := clock.Now().Add(time.Hour)
65+
meta.NextIssuanceTime = &nextIssuanceTime
66+
return store.WriteMetadata(meta.VolumeID, meta)
67+
},
68+
})
69+
defer stop()
70+
71+
tmpDir, err := os.MkdirTemp("", "*")
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
defer os.RemoveAll(tmpDir)
76+
77+
// create a root, non-expiring context
78+
ctx := context.Background()
79+
80+
// We are going to submit this request multiple times, so lets just write it out once
81+
nodePublishVolumeRequest := &csi.NodePublishVolumeRequest{
82+
VolumeId: "test-vol",
83+
VolumeContext: map[string]string{
84+
"csi.storage.k8s.io/ephemeral": "true",
85+
"csi.storage.k8s.io/pod.name": "the-pod-name",
86+
"csi.storage.k8s.io/pod.namespace": ns,
87+
},
88+
TargetPath: tmpDir,
89+
Readonly: true,
90+
}
91+
92+
// create a context that expires in 2s (enough time for at least a single call of `issue`)
93+
twoSecondCtx, cancel := context.WithTimeout(ctx, time.Second*2)
94+
defer cancel()
95+
_, err = cl.NodePublishVolume(twoSecondCtx, nodePublishVolumeRequest)
96+
// assert that an error has been returned - we don't mind what kind of error, as due to the async nature of
97+
// de-registering metadata from the metadata store upon failures, there is a slim chance that a metadata read error
98+
// can be returned instead of a deadline exceeded error.
99+
if err == nil {
100+
t.Errorf("expected error but got nil")
101+
}
102+
103+
// ensure a single CertificateRequest exists, and fetch its UID so we can compare it later
104+
existingRequestUID := ensureOneRequestExists(ctx, t, opts.Client, ns, "")
105+
106+
// run NodePublishVolume once again, with a short timeout.
107+
// here we want to ensure that no second request is completed, and the timeout is reached again.
108+
// we still won't actually complete issuance here.
109+
twoSecondCtx, cancel = context.WithTimeout(ctx, time.Second*2)
110+
defer cancel()
111+
_, err = cl.NodePublishVolume(twoSecondCtx, nodePublishVolumeRequest)
112+
// assert that an error has been returned - we don't mind what kind of error, as due to the async nature of
113+
// de-registering metadata from the metadata store upon failures, there is a slim chance that a metadata read error
114+
// can be returned instead of a deadline exceeded error.
115+
if err == nil {
116+
t.Errorf("expected error but got nil")
117+
}
118+
// ensure the same certificaterequest object still exists
119+
ensureOneRequestExists(ctx, t, opts.Client, ns, existingRequestUID)
120+
121+
stopCh := make(chan struct{})
122+
defer close(stopCh)
123+
if issueBeforeCall {
124+
// we don't run this in a goroutine so we can be sure the certificaterequest is completed BEFORE the issue loop is entered
125+
testutil.IssueOneRequest(t, opts.Client, "certificaterequest-namespace", stopCh, selfSignedExampleCertificate, []byte("ca bytes"))
126+
} else {
127+
go func() {
128+
// allow 500ms before actually issuing the request so we can be sure we're within the issue() function call
129+
// when the certificaterequest is finally completed
130+
time.Sleep(time.Millisecond * 500)
131+
testutil.IssueOneRequest(t, opts.Client, "certificaterequest-namespace", stopCh, selfSignedExampleCertificate, []byte("ca bytes"))
132+
}()
133+
}
134+
135+
// call NodePublishVolume again. this time, we expect NodePublishVolume to return without an error and actually issue
136+
// the certificate using the existing request data.
137+
// We don't use an explicit timeout here to avoid any weird race conditions caused by shorter test timeouts.
138+
_, err = cl.NodePublishVolume(ctx, nodePublishVolumeRequest)
139+
if err != nil {
140+
t.Errorf("expected no error but got: %v", err)
141+
}
142+
// ensure the same certificaterequest object still exists
143+
ensureOneRequestExists(ctx, t, opts.Client, ns, existingRequestUID)
144+
145+
files, err := store.ReadFiles("test-vol")
146+
if err != nil {
147+
t.Fatal(err)
148+
}
149+
if !reflect.DeepEqual(files["ca"], []byte("ca bytes")) {
150+
t.Errorf("unexpected CA data: %v", files["ca"])
151+
}
152+
if !reflect.DeepEqual(files["cert"], selfSignedExampleCertificate) {
153+
t.Errorf("unexpected certificate data: %v", files["cert"])
154+
}
155+
}
156+
157+
func TestResumesExistingRequest_IssuedBetweenPublishCalls(t *testing.T) {
158+
testResumesExistingRequest(t, true)
159+
}
160+
161+
func TestResumesExistingRequest_IssuedDuringPublishCall(t *testing.T) {
162+
testResumesExistingRequest(t, false)
163+
}
164+
165+
// ensureOneRequestExists will fail the test if more than a single CertificateRequest exists.
166+
// If permittedUID is non-empty and a request DOES exist, it will also ensure that the existing request has
167+
// the given UID.
168+
// It will return the UID of the existing request.
169+
func ensureOneRequestExists(ctx context.Context, t *testing.T, client cmclient.Interface, namespace string, permittedUID types.UID) types.UID {
170+
// assert a single CertificateRequest object exists
171+
reqs, err := client.CertmanagerV1().CertificateRequests(namespace).List(ctx, metav1.ListOptions{})
172+
if err != nil {
173+
t.Fatal(err)
174+
}
175+
if len(reqs.Items) != 1 {
176+
t.Fatalf("expected to find one existing CertificateRequest but got %d", len(reqs.Items))
177+
}
178+
req := reqs.Items[0]
179+
if string(permittedUID) != "" && req.UID != permittedUID {
180+
t.Fatalf("existing request does not have expected UID of %q - this means the request has probably been deleted and re-created", permittedUID)
181+
}
182+
return req.UID
183+
}

0 commit comments

Comments
 (0)