Skip to content

Commit ddc4b0f

Browse files
committed
feat: implement standardized PTP secret mounting with webhook validation
- Add PTP_SEC_FOLDER constant for mount path /etc/ptp-secret-mount/ - Add helper functions to extract secret name and key from sa_file path - Add webhook validation for sa_file path format and secret key existence - Implement deduplication in controller for unique secret mounts - Refactor validateSppInSecret to use minimal arguments - Mount entire secret as volume (all keys available as files)
1 parent 6349d8b commit ddc4b0f

File tree

12 files changed

+121
-340
lines changed

12 files changed

+121
-340
lines changed

api/v1/ptpconfig_types.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,18 @@ type PtpConfigList struct {
6262
}
6363

6464
type PtpProfile struct {
65-
Name *string `json:"name"`
66-
Interface *string `json:"interface,omitempty"`
67-
Ptp4lOpts *string `json:"ptp4lOpts,omitempty"`
68-
Phc2sysOpts *string `json:"phc2sysOpts,omitempty"`
69-
Ts2PhcOpts *string `json:"ts2phcOpts,omitempty"`
70-
Synce4lOpts *string `json:"synce4lOpts,omitempty"`
71-
ChronydOpts *string `json:"chronydOpts,omitempty"`
72-
Ptp4lConf *string `json:"ptp4lConf,omitempty"`
73-
Phc2sysConf *string `json:"phc2sysConf,omitempty"`
74-
Ts2PhcConf *string `json:"ts2phcConf,omitempty"`
75-
Synce4lConf *string `json:"synce4lConf,omitempty"`
76-
ChronydConf *string `json:"chronydConf,omitempty"`
77-
PtpSecretName *string `json:"ptpSecretName,omitempty"`
65+
Name *string `json:"name"`
66+
Interface *string `json:"interface,omitempty"`
67+
Ptp4lOpts *string `json:"ptp4lOpts,omitempty"`
68+
Phc2sysOpts *string `json:"phc2sysOpts,omitempty"`
69+
Ts2PhcOpts *string `json:"ts2phcOpts,omitempty"`
70+
Synce4lOpts *string `json:"synce4lOpts,omitempty"`
71+
ChronydOpts *string `json:"chronydOpts,omitempty"`
72+
Ptp4lConf *string `json:"ptp4lConf,omitempty"`
73+
Phc2sysConf *string `json:"phc2sysConf,omitempty"`
74+
Ts2PhcConf *string `json:"ts2phcConf,omitempty"`
75+
Synce4lConf *string `json:"synce4lConf,omitempty"`
76+
ChronydConf *string `json:"chronydConf,omitempty"`
7877
// +kubebuilder:validation:Enum=SCHED_OTHER;SCHED_FIFO;
7978
PtpSchedulingPolicy *string `json:"ptpSchedulingPolicy,omitempty"`
8079
// +kubebuilder:validation:Minimum=1

api/v1/ptpconfig_webhook.go

Lines changed: 77 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
Master PtpRole = 1
4444
Slave PtpRole = 0
4545
)
46+
const PTP_SEC_FOLDER = "/etc/ptp-secret-mount/"
4647

4748
// log is for logging in this package.
4849
var ptpconfiglog = logf.Log.WithName("ptpconfig-resource")
@@ -70,11 +71,6 @@ type Ptp4lConf struct {
7071
sections map[string]Ptp4lConfSection
7172
}
7273

73-
// PopulatePtp4lConf parses the ptp4l configuration
74-
func (p *Ptp4lConf) PopulatePtp4lConf(config *string, ptp4lopts *string) error {
75-
return p.populatePtp4lConf(config, ptp4lopts)
76-
}
77-
7874
// GetOption retrieves an option value from a specific section
7975
func (p *Ptp4lConf) GetOption(section, key string) string {
8076
if sec, ok := p.sections[section]; ok {
@@ -85,7 +81,7 @@ func (p *Ptp4lConf) GetOption(section, key string) string {
8581
return ""
8682
}
8783

88-
func (output *Ptp4lConf) populatePtp4lConf(config *string, ptp4lopts *string) error {
84+
func (output *Ptp4lConf) PopulatePtp4lConf(config *string, ptp4lopts *string) error {
8985
var string_config string
9086
if config != nil {
9187
string_config = *config
@@ -130,7 +126,7 @@ func (r *PtpConfig) validate() error {
130126

131127
for _, profile := range profiles {
132128
conf := &Ptp4lConf{}
133-
conf.populatePtp4lConf(profile.Ptp4lConf, profile.Ptp4lOpts)
129+
conf.PopulatePtp4lConf(profile.Ptp4lConf, profile.Ptp4lOpts)
134130

135131
// Validate that interface field only set in ordinary clock
136132
if profile.Interface != nil && *profile.Interface != "" {
@@ -216,100 +212,64 @@ func (r *PtpConfig) validate() error {
216212
}
217213
}
218214

219-
// Validate secret-related settings for this profile
220-
if profile.PtpSecretName != nil && *profile.PtpSecretName != "" {
221-
sppValue, err := getSppFromPtp4lConf(conf)
222-
if err != nil {
223-
return fmt.Errorf("failed to get spp value from ptp4lConf: %w", err)
224-
}
225-
if err := validateSecretExists(sppValue, *profile.PtpSecretName); err != nil {
226-
return fmt.Errorf("failed to validate secret from profile: %w", err)
227-
}
228-
saFilePath, err := getSaFileFromPtp4lConf(conf)
229-
if err != nil {
230-
return fmt.Errorf("failed to get sa file path from ptp4lConf: %w", err)
231-
}
232-
if err := validateSaFileSecretConflicts(saFilePath, *profile.PtpSecretName, r.Name, r.Namespace); err != nil {
233-
return fmt.Errorf("failed to validate secret conflicts from profile: %w", err)
234-
}
215+
// validate secret-related settings for this profile
216+
saFilePath, err := getSaFileFromPtp4lConf(conf)
217+
if err != nil {
218+
return fmt.Errorf("failed to get sa file path from ptp4lConf: %w", err)
235219
}
236-
}
237-
return nil
238-
}
239-
240-
// validateSecretConflicts checks if a single profile's sa_file + secret combination
241-
// conflicts with any existing PtpConfigs in the openshift-ptp namespace
242-
func validateSaFileSecretConflicts(saFilePath string, secretName string, name string, namespace string) error {
243-
if webhookClient == nil {
244-
ptpconfiglog.Info("webhook client not initialized, skipping cross-PtpConfig validation")
245-
return nil
246-
}
247-
248-
// List all existing PtpConfigs in openshift-ptp namespace
249-
ptpConfigList := &PtpConfigList{}
250-
if err := webhookClient.List(context.Background(), ptpConfigList, &client.ListOptions{
251-
Namespace: "openshift-ptp",
252-
}); err != nil {
253-
ptpconfiglog.Error(err, "failed to list PtpConfigs for validation")
254-
// Don't block creation if we can't list - fail open
255-
return nil
256-
}
257-
258-
// Check each existing PtpConfig for conflicts
259-
for _, existingConfig := range ptpConfigList.Items {
260-
// Skip checking against ourselves (for updates)
261-
if existingConfig.Name == name && existingConfig.Namespace == namespace {
262-
continue
220+
if err := validateSaFile(saFilePath); err != nil {
221+
return fmt.Errorf("failed to validate sa file: %w", err)
263222
}
264-
265-
// Check each profile in the existing config
266-
for _, existingProfile := range existingConfig.Spec.Profile {
267-
if existingProfile.PtpSecretName == nil || *existingProfile.PtpSecretName == "" {
268-
continue
269-
}
270-
if existingProfile.Ptp4lConf == nil {
271-
continue
272-
}
273-
274-
existingConf := &Ptp4lConf{}
275-
if err := existingConf.populatePtp4lConf(existingProfile.Ptp4lConf, existingProfile.Ptp4lOpts); err != nil {
276-
continue
277-
}
278-
globalSection, exists := existingConf.sections["[global]"]
279-
if !exists {
280-
continue
281-
}
282-
existingSaFile, exists := globalSection.options["sa_file"]
283-
if !exists {
284-
continue
285-
}
286-
// Check if same sa_file as our profile and different secret
287-
if existingSaFile == saFilePath && *existingProfile.PtpSecretName != secretName {
288-
return fmt.Errorf("sa_file '%s' conflict: PtpConfig '%s' already uses secret '%s' with this sa_file path, but this profile tries to use secret '%s'. All PtpConfigs using the same sa_file must reference the same secret",
289-
saFilePath, existingConfig.Name, *existingProfile.PtpSecretName, secretName)
290-
}
223+
secretName := GetSecretNameFromSaFilePath(saFilePath)
224+
sppValue, err := getSppFromPtp4lConf(conf)
225+
if err != nil {
226+
return fmt.Errorf("failed to get spp value from ptp4lConf: %w", err)
227+
}
228+
if err := validateSecretForSPP(sppValue, secretName); err != nil {
229+
return fmt.Errorf("failed to validate secret from profile: %w", err)
291230
}
292-
}
293231

232+
}
294233
return nil
295234
}
296235

297-
// validateSecretExists checks that a single profile's ptpSecretName references an existing secret
298-
func validateSecretExists(sppValue string, secretName string) error {
236+
// checking if the secret exists in the openshift-ptp namespace
237+
func isValidSecretName(secretName string) *corev1.Secret {
299238
if webhookClient == nil {
300239
ptpconfiglog.Info("webhook client not initialized, skipping secret existence validation")
301240
return nil
302241
}
303-
304-
// Try to get the secret from openshift-ptp namespace
305242
secret := &corev1.Secret{}
306243
err := webhookClient.Get(context.Background(), types.NamespacedName{
307244
Namespace: "openshift-ptp",
308245
Name: secretName,
309246
}, secret)
310-
311247
if err != nil {
312-
return fmt.Errorf("failed to get secret %q: %w", secretName, err)
248+
return nil
249+
}
250+
return secret
251+
}
252+
253+
// GetSecretNameFromSaFilePath extracts the secret name from the sa_file path
254+
func GetSecretNameFromSaFilePath(sa_file string) string {
255+
path := strings.TrimPrefix(sa_file, PTP_SEC_FOLDER)
256+
index := strings.Index(path, "/")
257+
return path[:index]
258+
}
259+
260+
// GetSecretKeyFromSaFilePath extracts the secret key from the sa_file path
261+
func GetSecretKeyFromSaFilePath(sa_file string) string {
262+
path := strings.TrimPrefix(sa_file, PTP_SEC_FOLDER)
263+
index := strings.Index(path, "/")
264+
return path[index+1:]
265+
}
266+
267+
// validateSecretExists checks that a single profile's ptpSecretName references an existing secret
268+
func validateSecretForSPP(sppValue string, secretName string) error {
269+
270+
secret := isValidSecretName(secretName)
271+
if secret == nil {
272+
return fmt.Errorf("secret %s does not exist", secretName)
313273
}
314274

315275
// Validate SPP (Security Parameter Profile) if specified
@@ -349,6 +309,38 @@ func getSaFileFromPtp4lConf(conf *Ptp4lConf) (string, error) {
349309
return saFileValue, nil
350310
}
351311

312+
// validateSaFile checks that the sa_file path is valid with prefix PTP_SEC_FOLDER
313+
// next directory must be a valid secret name, e.g. PTP_SEC_FOLDER/secret_name/secret_key
314+
// check that secret_key exists in the secret_name secret
315+
func validateSaFile(saFilePath string) error {
316+
if !strings.HasPrefix(saFilePath, PTP_SEC_FOLDER) {
317+
return fmt.Errorf("sa_file path '%s' is invalid; must start with '%s'", saFilePath, PTP_SEC_FOLDER)
318+
}
319+
path := strings.TrimPrefix(saFilePath, PTP_SEC_FOLDER)
320+
index := strings.Index(path, "/")
321+
if index == -1 || index == len(path)-1 {
322+
return fmt.Errorf("sa_file path '%s' is incomplete; must contain a secret key", saFilePath)
323+
}
324+
secretName := path[:index]
325+
if secretName == "" {
326+
return fmt.Errorf("sa_file path '%s' is invalid; must contain a secret name", saFilePath)
327+
}
328+
secret := isValidSecretName(secretName)
329+
if secret == nil {
330+
return fmt.Errorf("sa_file path '%s' has invalid secret name '%s'", saFilePath, secretName)
331+
}
332+
keyCandidate := path[index+1:]
333+
return validateKeyInSecret(secret, keyCandidate)
334+
}
335+
336+
// this function will recieve a secret and a key candidate and check if the key is part of the secret
337+
func validateKeyInSecret(secret *corev1.Secret, keyCandidate string) error {
338+
if _, exists := secret.Data[keyCandidate]; !exists {
339+
return fmt.Errorf("key '%s' is not part of the secret", keyCandidate)
340+
}
341+
return nil
342+
}
343+
352344
// validateSppInSecret checks that the spp value in ptp4lConf exists in the referenced secret
353345
func validateSppInSecret(sppValue string, secret *corev1.Secret) error {
354346
for key, value := range secret.Data {
@@ -435,7 +427,7 @@ func GetInterfaces(config PtpConfig, mode PtpRole) (interfaces []string) {
435427
}
436428
conf := &Ptp4lConf{}
437429
var dummy *string
438-
err := conf.populatePtp4lConf(config.Spec.Profile[0].Ptp4lConf, dummy)
430+
err := conf.PopulatePtp4lConf(config.Spec.Profile[0].Ptp4lConf, dummy)
439431
if err != nil {
440432
logrus.Warnf("ptp4l conf parsing failed, err=%s", err)
441433
}

api/v1/zz_generated.deepcopy.go

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bundle/manifests/ptp-operator.clusterserviceversion.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ metadata:
6161
categories: Networking
6262
certified: "false"
6363
containerImage: quay.io/openshift/origin-ptp-operator:4.21
64-
createdAt: "2025-11-25T10:35:27Z"
64+
createdAt: "2025-11-27T17:10:01Z"
6565
description: This software enables configuration of Precision Time Protocol(PTP)
6666
on Kubernetes. It detects hardware capable PTP devices on each node, and configures
6767
linuxptp processes such as ptp4l, phc2sys and timemaster.

bundle/manifests/ptp.openshift.io_ptpconfigs.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ spec:
9090
maximum: 65
9191
minimum: 1
9292
type: integer
93-
ptpSecretName:
94-
type: string
9593
ptpSettings:
9694
additionalProperties:
9795
type: string

config/crd/bases/ptp.openshift.io_ptpconfigs.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ spec:
9090
maximum: 65
9191
minimum: 1
9292
type: integer
93-
ptpSecretName:
94-
type: string
9593
ptpSettings:
9694
additionalProperties:
9795
type: string

0 commit comments

Comments
 (0)