Skip to content

Commit 9dc7c36

Browse files
authored
fix: Correctly configure non-mirror registry certificates (#1039)
This PR updates the runtime extensions to include two mutation handlers for Nutanix clusters: - `nutanixclusterconfigpatch-gp.cluster-api-runtime-extensions-nutanix` - original name and original (buggy functionality). This will still be referenced from existing CCs and so must retain the same name. - `nutanixclusterv2configpatch-gp.cluster-api-runtime-extensions-nutanix` - new name with fixed functionality. This is now referenced from the example CC in this project and can be used by existing clusters by rebasing on a CC that references this handler name. The new handler fixes are: - CA certificates are now written to `/etc/containerd/certs.d/<registryHost>/ca.crt` as per https://github.com/containerd/containerd/blob/main/docs/hosts.md#support-for-dockers-certificate-file-pattern. This does not require a `hosts.toml` file for configuration, although that explicit configuration may be preferred (can do in future if so). - Remove non-mirror registry config from `/etc/containerd/certs.d/_default/hosts.toml` which was causing all registries to be configured as mirror registry.
1 parent b32e253 commit 9dc7c36

File tree

18 files changed

+1424
-45
lines changed

18 files changed

+1424
-45
lines changed

charts/cluster-api-runtime-extensions-nutanix/defaultclusterclasses/nutanix-cluster-class.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ spec:
6767
patches:
6868
- external:
6969
discoverVariablesExtension: nutanixclusterconfigvars-dv.cluster-api-runtime-extensions-nutanix
70-
generateExtension: nutanixclusterconfigpatch-gp.cluster-api-runtime-extensions-nutanix
70+
generateExtension: nutanixclusterv2configpatch-gp.cluster-api-runtime-extensions-nutanix
7171
name: cluster-config
7272
- external:
7373
discoverVariablesExtension: nutanixworkerconfigvars-dv.cluster-api-runtime-extensions-nutanix

hack/examples/overlays/clusterclasses/nutanix/kustomization.yaml.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ patches:
1919
value:
2020
- name: "cluster-config"
2121
external:
22-
generateExtension: "nutanixclusterconfigpatch-gp.cluster-api-runtime-extensions-nutanix"
22+
generateExtension: "nutanixclusterv2configpatch-gp.cluster-api-runtime-extensions-nutanix"
2323
discoverVariablesExtension: "nutanixclusterconfigvars-dv.cluster-api-runtime-extensions-nutanix"
2424
- name: "worker-config"
2525
external:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2023 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package mutation
5+
6+
import (
7+
"sigs.k8s.io/controller-runtime/pkg/manager"
8+
9+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation"
10+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/aws/mutation/cni/calico"
11+
deleteinv0280mirrors "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/deleteinv0280/generic/mutation/mirrors"
12+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/auditpolicy"
13+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/autorenewcerts"
14+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/containerdapplypatchesandrestart"
15+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/containerdmetrics"
16+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/containerdunprivilegedports"
17+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/coredns"
18+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/encryptionatrest"
19+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/etcd"
20+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/extraapiservercertsans"
21+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/httpproxy"
22+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/imageregistries/credentials"
23+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/kubernetesimagerepository"
24+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/users"
25+
)
26+
27+
// MetaMutators returns all generic patch handlers.
28+
func MetaMutators(mgr manager.Manager) []mutation.MetaMutator {
29+
return []mutation.MetaMutator{
30+
auditpolicy.NewPatch(),
31+
etcd.NewPatch(),
32+
coredns.NewPatch(),
33+
extraapiservercertsans.NewPatch(),
34+
httpproxy.NewPatch(mgr.GetClient()),
35+
kubernetesimagerepository.NewPatch(),
36+
credentials.NewPatch(mgr.GetClient()),
37+
deleteinv0280mirrors.NewPatch(mgr.GetClient()),
38+
calico.NewPatch(),
39+
users.NewPatch(),
40+
containerdmetrics.NewPatch(),
41+
containerdunprivilegedports.NewPatch(),
42+
encryptionatrest.NewPatch(mgr.GetClient(), encryptionatrest.RandomTokenGenerator),
43+
autorenewcerts.NewPatch(),
44+
45+
// Some patches may have changed containerd configuration.
46+
// We write the configuration changes to disk, and must run a command
47+
// to apply the changes to the actual containerd configuration.
48+
// We then must restart containerd for the configuration to take effect.
49+
// Therefore, we must apply this patch last.
50+
//
51+
// Containerd restart and readiness altogether could take ~5s.
52+
// We want to keep patch independent of each other and not share any state.
53+
// Therefore, We must always apply this patch regardless any other patch modified containerd configuration.
54+
containerdapplypatchesandrestart.NewPatch(),
55+
}
56+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright 2023 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package mirrors
5+
6+
import (
7+
"bytes"
8+
_ "embed"
9+
"fmt"
10+
"net/url"
11+
"path"
12+
"strings"
13+
"text/template"
14+
15+
cabpkv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
16+
17+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/common"
18+
)
19+
20+
const (
21+
containerdHostsConfigurationOnRemote = "/etc/containerd/certs.d/_default/hosts.toml"
22+
secretKeyForCACert = "ca.crt"
23+
)
24+
25+
var (
26+
//go:embed templates/hosts.toml.gotmpl
27+
containerdHostsConfiguration []byte
28+
29+
containerdHostsConfigurationTemplate = template.Must(
30+
template.New("").Parse(string(containerdHostsConfiguration)),
31+
)
32+
33+
//go:embed templates/containerd-registry-config-drop-in.toml
34+
containerdRegistryConfigDropIn []byte
35+
containerdRegistryConfigDropInFileOnRemote = common.ContainerdPatchPathOnRemote(
36+
"registry-config.toml",
37+
)
38+
39+
mirrorCACertPathOnRemoteFmt = "/etc/certs/%s.pem"
40+
)
41+
42+
type containerdConfig struct {
43+
URL string
44+
CASecretName string
45+
CACert string
46+
Mirror bool
47+
}
48+
49+
// fileNameFromURL returns a file name for a registry URL.
50+
// Follows a convention of replacing all non-alphanumeric characters with "-".
51+
func (c containerdConfig) filePathFromURL() (string, error) {
52+
registryURL, err := url.ParseRequestURI(c.URL)
53+
if err != nil {
54+
return "", fmt.Errorf("failed parsing registry URL: %w", err)
55+
}
56+
57+
registryHostWithPath := registryURL.Host
58+
if registryURL.Path != "" {
59+
registryHostWithPath = path.Join(registryURL.Host, registryURL.Path)
60+
}
61+
62+
replaced := strings.ReplaceAll(registryHostWithPath, "/", "-")
63+
64+
return fmt.Sprintf(mirrorCACertPathOnRemoteFmt, replaced), nil
65+
}
66+
67+
// Return true if configuration is a mirror or has a CA certificate.
68+
func (c containerdConfig) needContainerdConfiguration() bool {
69+
return c.CACert != "" || c.Mirror
70+
}
71+
72+
// Containerd registry configuration created at /etc/containerd/certs.d/_default/hosts.toml for:
73+
//
74+
// 1. Set the default mirror for all registries.
75+
// The upstream registry will be automatically used after all defined mirrors have been tried.
76+
// https://github.com/containerd/containerd/blob/main/docs/hosts.md#setup-default-mirror-for-all-registries
77+
//
78+
// 2. Setting CA certificate for global image registry mirror and image registries.
79+
func generateContainerdHostsFile(
80+
configs []containerdConfig,
81+
) (*cabpkv1.File, error) {
82+
if len(configs) == 0 {
83+
return nil, nil
84+
}
85+
86+
type templateInput struct {
87+
URL string
88+
CACertPath string
89+
Mirror bool
90+
}
91+
92+
inputs := make([]templateInput, 0, len(configs))
93+
94+
for _, config := range configs {
95+
if !config.needContainerdConfiguration() {
96+
continue
97+
}
98+
99+
formattedURL, err := formatURLForContainerd(config.URL)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed formatting image registry URL for Containerd: %w", err)
102+
}
103+
104+
input := templateInput{
105+
URL: formattedURL,
106+
Mirror: config.Mirror,
107+
}
108+
// CA cert is optional for mirror registry.
109+
// i.e. registry is using signed certificates. Insecure registry will not be allowed.
110+
if config.CACert != "" {
111+
var registryCACertPathOnRemote string
112+
registryCACertPathOnRemote, err = config.filePathFromURL()
113+
if err != nil {
114+
return nil, fmt.Errorf(
115+
"failed generating CA certificate file path from URL: %w",
116+
err,
117+
)
118+
}
119+
input.CACertPath = registryCACertPathOnRemote
120+
}
121+
122+
inputs = append(inputs, input)
123+
}
124+
125+
var b bytes.Buffer
126+
err := containerdHostsConfigurationTemplate.Execute(&b, inputs)
127+
if err != nil {
128+
return nil, fmt.Errorf("failed executing template for Containerd hosts.toml file: %w", err)
129+
}
130+
return &cabpkv1.File{
131+
Path: containerdHostsConfigurationOnRemote,
132+
// Trimming the leading and trailing whitespaces in the template did not work as expected with multiple configs.
133+
Content: fmt.Sprintf("%s\n", strings.TrimSpace(b.String())),
134+
Permissions: "0600",
135+
}, nil
136+
}
137+
138+
func generateRegistryCACertFiles(
139+
configs []containerdConfig,
140+
) ([]cabpkv1.File, error) {
141+
if len(configs) == 0 {
142+
return nil, nil
143+
}
144+
145+
var files []cabpkv1.File //nolint:prealloc // We don't know the size of the slice yet.
146+
147+
for _, config := range configs {
148+
if config.CASecretName == "" {
149+
continue
150+
}
151+
152+
registryCACertPathOnRemote, err := config.filePathFromURL()
153+
if err != nil {
154+
return nil, fmt.Errorf("failed generating CA certificate file path from URL: %w", err)
155+
}
156+
files = append(files, cabpkv1.File{
157+
Path: registryCACertPathOnRemote,
158+
Permissions: "0600",
159+
ContentFrom: &cabpkv1.FileSource{
160+
Secret: cabpkv1.SecretFileSource{
161+
Name: config.CASecretName,
162+
Key: secretKeyForCACert,
163+
},
164+
},
165+
})
166+
}
167+
168+
return files, nil
169+
}
170+
171+
func generateContainerdRegistryConfigDropInFile() []cabpkv1.File {
172+
return []cabpkv1.File{
173+
{
174+
Path: containerdRegistryConfigDropInFileOnRemote,
175+
Content: string(containerdRegistryConfigDropIn),
176+
Permissions: "0600",
177+
},
178+
}
179+
}
180+
181+
func formatURLForContainerd(uri string) (string, error) {
182+
mirrorURL, err := url.ParseRequestURI(uri)
183+
if err != nil {
184+
return "", fmt.Errorf("failed parsing mirror: %w", err)
185+
}
186+
187+
mirror := fmt.Sprintf("%s://%s", mirrorURL.Scheme, mirrorURL.Host)
188+
// assume Containerd expects the following pattern:
189+
// scheme://host/v2/path
190+
mirrorPath := "v2"
191+
if mirrorURL.Path != "" {
192+
mirrorPath = path.Join(mirrorPath, mirrorURL.Path)
193+
}
194+
// using path.Join on all elements incorrectly drops a "/" from "https://"
195+
return fmt.Sprintf("%s/%s", mirror, mirrorPath), nil
196+
}

0 commit comments

Comments
 (0)