Skip to content

Commit a30588c

Browse files
committed
Invalidate registries cache upon configuration change to allow dynamic config updates without restart
Signed-off-by: dtfranz <[email protected]>
1 parent 5b5ccab commit a30588c

File tree

4 files changed

+160
-4
lines changed

4 files changed

+160
-4
lines changed

cmd/manager/main.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ func main() {
291291
}
292292

293293
unpacker := &source.ContainersImageRegistry{
294-
BaseCachePath: filepath.Join(cachePath, "unpack"),
294+
BaseCachePath: filepath.Join(cachePath, "unpack"),
295+
RegistriesConfTimestamp: time.Now(),
295296
SourceContextFunc: func(logger logr.Logger) (*types.SystemContext, error) {
296297
srcContext := &types.SystemContext{
297298
DockerCertPath: caCertDir,
@@ -306,7 +307,8 @@ func main() {
306307
return nil, fmt.Errorf("could not stat auth file, error: %w", err)
307308
}
308309
return srcContext, nil
309-
}}
310+
},
311+
}
310312

311313
clusterExtensionFinalizers := crfinalizer.NewFinalizers()
312314
if err := clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupUnpackCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {

internal/rukpak/source/containers_image.go

+60-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"io/fs"
910
"os"
1011
"path/filepath"
12+
"strings"
13+
"sync"
14+
"time"
1115

1216
"github.com/containerd/containerd/archive"
1317
"github.com/containers/image/v5/copy"
@@ -17,6 +21,7 @@ import (
1721
"github.com/containers/image/v5/oci/layout"
1822
"github.com/containers/image/v5/pkg/blobinfocache/none"
1923
"github.com/containers/image/v5/pkg/compression"
24+
"github.com/containers/image/v5/pkg/sysregistriesv2"
2025
"github.com/containers/image/v5/signature"
2126
"github.com/containers/image/v5/types"
2227
"github.com/go-logr/logr"
@@ -25,9 +30,17 @@ import (
2530
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2631
)
2732

33+
const (
34+
// Pattern match for symlinks mounted via configmap: YYYY_MM_DD_hh_mm_ss.s
35+
mountedSymlinkTimestampFormat = "2006_01_02_15_04_05.999999999"
36+
containersConfigFolder = "/etc/containers/"
37+
)
38+
2839
type ContainersImageRegistry struct {
29-
BaseCachePath string
30-
SourceContextFunc func(logger logr.Logger) (*types.SystemContext, error)
40+
BaseCachePath string
41+
SourceContextFunc func(logger logr.Logger) (*types.SystemContext, error)
42+
RegistriesConfTimestamp time.Time
43+
checkConfigLock sync.RWMutex
3144
}
3245

3346
func (i *ContainersImageRegistry) Unpack(ctx context.Context, bundle *BundleSource) (*Result, error) {
@@ -45,6 +58,17 @@ func (i *ContainersImageRegistry) Unpack(ctx context.Context, bundle *BundleSour
4558
if err != nil {
4659
return nil, err
4760
}
61+
62+
//////////////////////////////////////////////////////
63+
//
64+
// Verify that registries configuration is up-to-date
65+
//
66+
//////////////////////////////////////////////////////
67+
err = i.checkUpdatedConfiguration()
68+
if err != nil {
69+
return nil, fmt.Errorf("error checking registries configuration: %w", err)
70+
}
71+
4872
//////////////////////////////////////////////////////
4973
//
5074
// Resolve a canonical reference for the image.
@@ -171,6 +195,35 @@ func (i *ContainersImageRegistry) Cleanup(_ context.Context, bundle *BundleSourc
171195
return deleteRecursive(i.bundlePath(bundle.Name))
172196
}
173197

198+
func (i *ContainersImageRegistry) checkUpdatedConfiguration() error {
199+
i.checkConfigLock.Lock()
200+
defer i.checkConfigLock.Unlock()
201+
dirEntries, err := os.ReadDir(containersConfigFolder)
202+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
203+
// Ignore not found errors
204+
return fmt.Errorf("could not read registries configuration directory, error: %w", err)
205+
}
206+
for _, entry := range dirEntries {
207+
if entry.Type().IsRegular() || !strings.HasPrefix(entry.Name(), "..") {
208+
// Skip 'normal' entries
209+
continue
210+
}
211+
// Trim the leading '..' for symlinks
212+
trimmed := strings.TrimPrefix(entry.Name(), "..")
213+
t, err := time.Parse(mountedSymlinkTimestampFormat, trimmed)
214+
if err != nil {
215+
continue
216+
} else if !t.Equal(i.RegistriesConfTimestamp) {
217+
// We've found the timestamped symlink and it's been updated
218+
i.RegistriesConfTimestamp = t
219+
sysregistriesv2.InvalidateCache()
220+
return nil
221+
}
222+
}
223+
// No issue if the file was not found - likely we just don't have mounted registries configuration.
224+
return nil
225+
}
226+
174227
func (i *ContainersImageRegistry) bundlePath(bundleName string) string {
175228
return filepath.Join(i.BaseCachePath, bundleName)
176229
}
@@ -250,6 +303,11 @@ func (i *ContainersImageRegistry) unpackImage(ctx context.Context, unpackPath st
250303
if err != nil {
251304
return fmt.Errorf("error creating image source: %w", err)
252305
}
306+
defer func() {
307+
if err := layoutSrc.Close(); err != nil {
308+
panic(err)
309+
}
310+
}()
253311

254312
if err := os.MkdirAll(unpackPath, 0700); err != nil {
255313
return fmt.Errorf("error creating unpack directory: %w", err)

test/e2e/cluster_extension_install_test.go

+76
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,82 @@ func TestClusterExtensionInstallRegistry(t *testing.T) {
358358
}
359359
}
360360

361+
func TestClusterExtensionInstallRegistryDynamic(t *testing.T) {
362+
// NOTE: Like 'TestClusterExtensionInstallRegistry', this test also requires extra configuration in /etc/containers/registries.conf
363+
packageName := "dynamic"
364+
365+
t.Log("When a cluster extension is installed from a catalog")
366+
t.Log("When the extension bundle format is registry+v1")
367+
368+
clusterExtension, extensionCatalog, sa, ns := testInit(t)
369+
defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns)
370+
defer getArtifactsOutput(t)
371+
372+
clusterExtension.Spec = ocv1.ClusterExtensionSpec{
373+
Source: ocv1.SourceConfig{
374+
SourceType: "Catalog",
375+
Catalog: &ocv1.CatalogSource{
376+
PackageName: packageName,
377+
Selector: &metav1.LabelSelector{
378+
MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name},
379+
},
380+
},
381+
},
382+
Namespace: ns.Name,
383+
ServiceAccount: ocv1.ServiceAccountReference{
384+
Name: sa.Name,
385+
},
386+
}
387+
t.Log("It updates the registries.conf file contents")
388+
cm := corev1.ConfigMap{
389+
ObjectMeta: metav1.ObjectMeta{
390+
Name: "e2e-registries-conf",
391+
Namespace: "olmv1-system",
392+
},
393+
Data: map[string]string{
394+
"registries.conf": `[[registry]]
395+
prefix = "dynamic-registry.operator-controller-e2e.svc.cluster.local:5000"
396+
location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000"`,
397+
},
398+
}
399+
require.NoError(t, c.Update(context.Background(), &cm))
400+
401+
t.Log("It resolves the specified package with correct bundle path")
402+
t.Log("By creating the ClusterExtension resource")
403+
require.NoError(t, c.Create(context.Background(), clusterExtension))
404+
405+
t.Log("By eventually reporting a successful resolution and bundle path")
406+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
407+
assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
408+
}, 2*time.Minute, pollInterval)
409+
410+
// Give the check 2 minutes instead of the typical 1 for the pod's
411+
// files to update from the configmap change.
412+
// The theoretical max time is the kubelet sync period of 1 minute +
413+
// ConfigMap cache TTL of 1 minute = 2 minutes
414+
t.Log("By eventually reporting progressing as True")
415+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
416+
assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
417+
cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing)
418+
if assert.NotNil(ct, cond) {
419+
assert.Equal(ct, metav1.ConditionTrue, cond.Status)
420+
assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
421+
}
422+
}, 2*time.Minute, pollInterval)
423+
424+
t.Log("By eventually installing the package successfully")
425+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
426+
assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
427+
cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
428+
if assert.NotNil(ct, cond) {
429+
assert.Equal(ct, metav1.ConditionTrue, cond.Status)
430+
assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
431+
assert.Contains(ct, cond.Message, "Installed bundle")
432+
assert.NotEmpty(ct, clusterExtension.Status.Install.Bundle)
433+
}
434+
}, pollDuration, pollInterval)
435+
}
436+
361437
func TestClusterExtensionInstallRegistryMultipleBundles(t *testing.T) {
362438
t.Log("When a cluster extension is installed from a catalog")
363439

testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml

+20
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,23 @@ properties:
6969
value:
7070
packageName: test-mirrored
7171
version: 1.2.0
72+
---
73+
schema: olm.package
74+
name: dynamic
75+
defaultChannel: beta
76+
---
77+
schema: olm.channel
78+
name: beta
79+
package: dynamic
80+
entries:
81+
- name: dynamic-operator.1.2.0
82+
---
83+
schema: olm.bundle
84+
name: dynamic-operator.1.2.0
85+
package: dynamic
86+
image: dynamic-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.0.0
87+
properties:
88+
- type: olm.package
89+
value:
90+
packageName: dynamic
91+
version: 1.2.0

0 commit comments

Comments
 (0)