Skip to content

Commit 9b67afd

Browse files
committed
Certificate support for image registry
Updates the ClusterExtension API to support a certificate to retreive a bundle from an image registry. Signed-off-by: Todd Short <[email protected]>
1 parent b4c928c commit 9b67afd

File tree

10 files changed

+162
-22
lines changed

10 files changed

+162
-22
lines changed

api/v1alpha1/clusterextension_types.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ const (
4343
UpgradeConstraintPolicyIgnore UpgradeConstraintPolicy = "Ignore"
4444
)
4545

46+
// Similar to NamespacedName, but with json
47+
type ClusterExtensionSecretRef struct {
48+
// Name of the secret
49+
Name string `json:"name"`
50+
// Namespace of the secret
51+
Namespace string `json:"namespace,omitempty"`
52+
}
53+
54+
type ClusterExtensionTLS struct {
55+
//+optional
56+
// InsecureSkipTLSVerify allows the HTTPS client to ignore the server certificate
57+
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
58+
// +optional
59+
// CertificateSecretRef references a Secret that contains the tls.crt certificate that
60+
// can verify the server certificate.
61+
// This fits the definition of NamespacedName (but that doesn't have json tags)
62+
CertificateSecretRef *ClusterExtensionSecretRef `json:"certificateSecretRef,omitempty"`
63+
}
64+
4665
// ClusterExtensionSpec defines the desired state of ClusterExtension
4766
type ClusterExtensionSpec struct {
4867
//+kubebuilder:validation:MaxLength:=48
@@ -78,6 +97,10 @@ type ClusterExtensionSpec struct {
7897
// the bundle may contain resources that are cluster-scoped or that are
7998
// installed in a different namespace. This namespace is expected to exist.
8099
InstallNamespace string `json:"installNamespace"`
100+
101+
//+optional
102+
// RegistryTLS defines the connection parameters to retrieve an image from a registry
103+
RegistryTLS *ClusterExtensionTLS `json:"registryTLS,omitempty"`
81104
}
82105

83106
const (

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 41 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/manager/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ func main() {
150150
setupLog.Error(err, "unable to start manager")
151151
os.Exit(1)
152152
}
153-
154153
httpClient, err := httputil.BuildHTTPClient(caCert)
155154
if err != nil {
156155
setupLog.Error(err, "unable to create catalogd http client")
@@ -210,6 +209,7 @@ func main() {
210209

211210
if err = (&controllers.ClusterExtensionReconciler{
212211
Client: cl,
212+
Reader: mgr.GetAPIReader(),
213213
BundleProvider: catalogClient,
214214
ActionClientGetter: acg,
215215
Unpacker: unpacker,

config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,30 @@ spec:
5656
maxLength: 48
5757
pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
5858
type: string
59+
registryTLS:
60+
description: RegistryTLS defines the connection parameters to retrieve
61+
an image from a registry
62+
properties:
63+
certificateSecretRef:
64+
description: |-
65+
CertificateSecretRef references a Secret that contains the tls.crt certificate that
66+
can verify the server certificate.
67+
This fits the definition of NamespacedName (but that doesn't have json tags)
68+
properties:
69+
name:
70+
description: Name of the secret
71+
type: string
72+
namespace:
73+
description: Namespace of the secret
74+
type: string
75+
required:
76+
- name
77+
type: object
78+
insecureSkipTLSVerify:
79+
description: InsecureSkipTLSVerify allows the HTTPS client to
80+
ignore the server certificate
81+
type: boolean
82+
type: object
5983
upgradeConstraintPolicy:
6084
default: Enforce
6185
description: Defines the policy for how to handle upgrade constraints

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ require (
1313
github.com/operator-framework/catalogd v0.14.0
1414
github.com/operator-framework/helm-operator-plugins v0.2.2-0.20240520180534-f463c36fedf9
1515
github.com/operator-framework/operator-registry v1.43.1
16-
github.com/operator-framework/rukpak v0.23.1
16+
github.com/operator-framework/rukpak v0.23.2-0.20240618133950-e1d8b0e32344
1717
github.com/spf13/pflag v1.0.5
1818
github.com/stretchr/testify v1.9.0
1919
go.uber.org/zap v1.27.0
@@ -123,7 +123,7 @@ require (
123123
github.com/google/btree v1.1.2 // indirect
124124
github.com/google/cel-go v0.17.8 // indirect
125125
github.com/google/gnostic-models v0.6.8 // indirect
126-
github.com/google/go-containerregistry v0.19.1 // indirect
126+
github.com/google/go-containerregistry v0.19.2 // indirect
127127
github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20240505154900-ff385a972813 // indirect
128128
github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20240505154900-ff385a972813 // indirect
129129
github.com/google/gofuzz v1.2.0 // indirect
@@ -195,7 +195,7 @@ require (
195195
github.com/sirupsen/logrus v1.9.3 // indirect
196196
github.com/skeema/knownhosts v1.2.2 // indirect
197197
github.com/spf13/cast v1.5.0 // indirect
198-
github.com/spf13/cobra v1.8.0 // indirect
198+
github.com/spf13/cobra v1.8.1 // indirect
199199
github.com/stoewer/go-strcase v1.3.0 // indirect
200200
github.com/stretchr/objx v0.5.2 // indirect
201201
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
152152
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
153153
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
154154
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
155+
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
155156
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
156157
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
157158
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
@@ -307,6 +308,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
307308
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
308309
github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY=
309310
github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
311+
github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w=
312+
github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
310313
github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20240505154900-ff385a972813 h1:PNR/Dkh697bQVCKLUekdd6LSNU9XkmXjrUbvH+wpfTg=
311314
github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20240505154900-ff385a972813/go.mod h1:5UXYZJNyCPf2YD+6J76geTiLAXA8fJbDy7mGQa5m5Vc=
312315
github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20240505154900-ff385a972813 h1:irEChX0pAmED+6auieJELA0JKeCakr6iDCTLjJUiT8k=
@@ -485,6 +488,8 @@ github.com/operator-framework/operator-registry v1.43.1 h1:ACahVHGIL/hINBXd3RKWq
485488
github.com/operator-framework/operator-registry v1.43.1/go.mod h1:qhssAIYWXDIW+nTg0C5i4iD9zpMtiXtfXqGUuUmGz5c=
486489
github.com/operator-framework/rukpak v0.23.1 h1:lam6+wysaVjZVpMdtl7DUbc+8ibCdlCfp+nB62K0aSU=
487490
github.com/operator-framework/rukpak v0.23.1/go.mod h1:DrQRNduAm0DWRSXpFhz8FA5g2GrJJ88sWpG5GiWmvPU=
491+
github.com/operator-framework/rukpak v0.23.2-0.20240618133950-e1d8b0e32344 h1:L2jGtjx9g3EgXUTDiVNgoqyBrP4m4RCyQspDjoDIFjg=
492+
github.com/operator-framework/rukpak v0.23.2-0.20240618133950-e1d8b0e32344/go.mod h1:peTAGzf4gmU+in2RJen84ZQ8lkdB3m6qy+nfNiVv0RY=
488493
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
489494
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
490495
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
@@ -554,6 +559,8 @@ github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
554559
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
555560
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
556561
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
562+
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
563+
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
557564
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
558565
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
559566
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=

internal/controllers/clusterextension_controller.go

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"helm.sh/helm/v3/pkg/postrender"
3737
"helm.sh/helm/v3/pkg/release"
3838
"helm.sh/helm/v3/pkg/storage/driver"
39+
corev1 "k8s.io/api/core/v1"
3940
"k8s.io/apimachinery/pkg/api/equality"
4041
apimeta "k8s.io/apimachinery/pkg/api/meta"
4142
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -79,6 +80,7 @@ import (
7980
// ClusterExtensionReconciler reconciles a ClusterExtension object
8081
type ClusterExtensionReconciler struct {
8182
client.Client
83+
client.Reader
8284
BundleProvider BundleProvider
8385
Unpacker rukpaksource.Unpacker
8486
ActionClientGetter helmclient.ActionClientGetter
@@ -96,10 +98,6 @@ type InstalledBundleGetter interface {
9698
GetInstalledBundle(ctx context.Context, ext *ocv1alpha1.ClusterExtension) (*ocv1alpha1.BundleMetadata, error)
9799
}
98100

99-
const (
100-
bundleConnectionAnnotation string = "bundle.connection.config/insecureSkipTLSVerify"
101-
)
102-
103101
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions,verbs=get;list;watch
104102
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/status,verbs=update;patch
105103
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/finalizers,verbs=update
@@ -249,7 +247,7 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp
249247
// Generate a BundleDeployment from the ClusterExtension to Unpack.
250248
// Note: The BundleDeployment here is not a k8s API, its a simple Go struct which
251249
// necessary embedded values.
252-
bd := r.generateBundleDeploymentForUnpack(bundle.Image, ext)
250+
bd := r.generateBundleDeploymentForUnpack(ctx, bundle.Image, ext)
253251
unpackResult, err := r.Unpacker.Unpack(ctx, bd)
254252
if err != nil {
255253
setStatusUnpackFailed(ext, err.Error())
@@ -533,7 +531,11 @@ func SetDeprecationStatus(ext *ocv1alpha1.ClusterExtension, bundle *catalogmetad
533531
}
534532
}
535533

536-
func (r *ClusterExtensionReconciler) generateBundleDeploymentForUnpack(bundlePath string, ce *ocv1alpha1.ClusterExtension) *rukpakv1alpha2.BundleDeployment {
534+
func (r *ClusterExtensionReconciler) generateBundleDeploymentForUnpack(ctx context.Context, bundlePath string, ce *ocv1alpha1.ClusterExtension) *rukpakv1alpha2.BundleDeployment {
535+
certData, err := r.getCertificateData(ctx, ce)
536+
if err != nil {
537+
log.FromContext(ctx).WithName("operator-controller").WithValues("cluster-extension", ce.GetName()).Error(err, "unable to get TLS certificate")
538+
}
537539
return &rukpakv1alpha2.BundleDeployment{
538540
TypeMeta: metav1.TypeMeta{
539541
Kind: ce.Kind,
@@ -550,21 +552,54 @@ func (r *ClusterExtensionReconciler) generateBundleDeploymentForUnpack(bundlePat
550552
Image: &rukpakv1alpha2.ImageSource{
551553
Ref: bundlePath,
552554
InsecureSkipTLSVerify: isInsecureSkipTLSVerifySet(ce),
555+
CertificateData: certData,
553556
},
554557
},
555558
},
556559
}
557560
}
558561

559562
func isInsecureSkipTLSVerifySet(ce *ocv1alpha1.ClusterExtension) bool {
560-
if ce == nil {
563+
if ce == nil || ce.Spec.RegistryTLS == nil {
561564
return false
562565
}
563-
value, ok := ce.Annotations[bundleConnectionAnnotation]
564-
if !ok {
565-
return false
566+
return ce.Spec.RegistryTLS.InsecureSkipTLSVerify
567+
}
568+
569+
func (r *ClusterExtensionReconciler) getCertificateData(ctx context.Context, ce *ocv1alpha1.ClusterExtension) (string, error) {
570+
if ce == nil || ce.Spec.RegistryTLS == nil || ce.Spec.RegistryTLS.CertificateSecretRef == nil {
571+
return "", nil
572+
}
573+
secretName := getNamespacedName(*ce.Spec.RegistryTLS.CertificateSecretRef)
574+
var secret = &corev1.Secret{}
575+
if err := r.Reader.Get(ctx, secretName, secret); err != nil {
576+
return "", fmt.Errorf("unable to get secret %v: %w", secretName, err)
577+
}
578+
if secret.Type != corev1.SecretTypeTLS {
579+
return "", fmt.Errorf("invalid type in secret %v: %v", secretName, secret.Type)
580+
}
581+
var certs []string
582+
// Get any 'ca.crt'
583+
data, ok := secret.Data[corev1.ServiceAccountRootCAKey]
584+
if ok && len(data) > 0 {
585+
certs = append(certs, string(data[:]))
586+
}
587+
// Get any 'tls.crt'
588+
data, ok = secret.Data[corev1.TLSCertKey]
589+
if ok && len(data) > 0 {
590+
certs = append(certs, string(data[:]))
591+
}
592+
if len(certs) == 0 {
593+
return "", fmt.Errorf("no data found in secret: %v", secretName)
594+
}
595+
return strings.Join(certs, "\n"), nil
596+
}
597+
598+
func getNamespacedName(name ocv1alpha1.ClusterExtensionSecretRef) types.NamespacedName {
599+
if name.Namespace == "" {
600+
return types.NamespacedName{Namespace: "default", Name: name.Name}
566601
}
567-
return value == "true"
602+
return types.NamespacedName{Namespace: name.Namespace, Name: name.Name}
568603
}
569604

570605
// SetupWithManager sets up the controller with the Manager.

test/e2e/cluster_extension_install_test.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,18 @@ func testInit(t *testing.T) (*ocv1alpha1.ClusterExtension, *catalogd.ClusterCata
4545
clusterExtension := &ocv1alpha1.ClusterExtension{
4646
ObjectMeta: metav1.ObjectMeta{
4747
Name: clusterExtensionName,
48-
Annotations: map[string]string{
49-
"bundle.connection.config/insecureSkipTLSVerify": "true",
50-
},
5148
},
5249
}
5350
return clusterExtension, extensionCatalog
5451
}
52+
func addCertToSpec(ce *ocv1alpha1.ClusterExtension) {
53+
ce.Spec.RegistryTLS = &ocv1alpha1.ClusterExtensionTLS{
54+
CertificateSecretRef: &ocv1alpha1.ClusterExtensionSecretRef{
55+
Name: "operator-controller-e2e-registry",
56+
Namespace: "operator-controller-e2e",
57+
},
58+
}
59+
}
5560

5661
func testCleanup(t *testing.T, cat *catalogd.ClusterCatalog, clusterExtension *ocv1alpha1.ClusterExtension) {
5762
require.NoError(t, c.Delete(context.Background(), cat))
@@ -78,6 +83,7 @@ func TestClusterExtensionInstallRegistry(t *testing.T) {
7883
PackageName: "prometheus",
7984
InstallNamespace: "default",
8085
}
86+
addCertToSpec(clusterExtension)
8187
t.Log("It resolves the specified package with correct bundle path")
8288
t.Log("By creating the ClusterExtension resource")
8389
require.NoError(t, c.Create(context.Background(), clusterExtension))
@@ -135,6 +141,7 @@ func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) {
135141
PackageName: pkgName,
136142
InstallNamespace: "default",
137143
}
144+
addCertToSpec(clusterExtension)
138145

139146
t.Log("By deleting the catalog first")
140147
require.NoError(t, c.Delete(context.Background(), extensionCatalog))
@@ -202,6 +209,7 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) {
202209
Version: "1.0.0",
203210
InstallNamespace: "default",
204211
}
212+
addCertToSpec(clusterExtension)
205213
require.NoError(t, c.Create(context.Background(), clusterExtension))
206214
t.Log("By eventually reporting a successful installation")
207215
require.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -248,6 +256,7 @@ func TestClusterExtensionForceInstallNonSuccessorVersion(t *testing.T) {
248256
Version: "1.0.0",
249257
InstallNamespace: "default",
250258
}
259+
addCertToSpec(clusterExtension)
251260
require.NoError(t, c.Create(context.Background(), clusterExtension))
252261
t.Log("By eventually reporting a successful resolution")
253262
require.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -293,6 +302,7 @@ func TestClusterExtensionInstallSuccessorVersion(t *testing.T) {
293302
Version: "1.0.0",
294303
InstallNamespace: "default",
295304
}
305+
addCertToSpec(clusterExtension)
296306
require.NoError(t, c.Create(context.Background(), clusterExtension))
297307
t.Log("By eventually reporting a successful resolution")
298308
require.EventuallyWithT(t, func(ct *assert.CollectT) {

test/e2e/cluster_extension_registryV1_limitations_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func TestClusterExtensionPackagesWithWebhooksAreNotAllowed(t *testing.T) {
2424
Version: "1.0.0",
2525
InstallNamespace: "default",
2626
}
27+
addCertToSpec(clusterExtension)
2728
require.NoError(t, c.Create(ctx, clusterExtension))
2829
require.EventuallyWithT(t, func(ct *assert.CollectT) {
2930
assert.NoError(ct, c.Get(ctx, types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))

0 commit comments

Comments
 (0)