Skip to content

Commit 615ffd7

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 9283fbe commit 615ffd7

File tree

12 files changed

+144
-369
lines changed

12 files changed

+144
-369
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: 96 additions & 113 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,109 +212,59 @@ 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+
secretKey := GetSecretKeyFromSaFilePath(saFilePath)
229+
if err := validateSppInSecretKey(sppValue, secretName, secretKey); err != nil {
230+
return fmt.Errorf("failed to validate spp in secret key: %w", err)
291231
}
292-
}
293232

233+
}
294234
return nil
295235
}
296236

297-
// validateSecretExists checks that a single profile's ptpSecretName references an existing secret
298-
func validateSecretExists(sppValue string, secretName string) error {
237+
// checking if the secret exists in the openshift-ptp namespace
238+
func getSecret(secretName string) *corev1.Secret {
299239
if webhookClient == nil {
300240
ptpconfiglog.Info("webhook client not initialized, skipping secret existence validation")
301241
return nil
302242
}
303-
304-
// Try to get the secret from openshift-ptp namespace
305243
secret := &corev1.Secret{}
306244
err := webhookClient.Get(context.Background(), types.NamespacedName{
307245
Namespace: "openshift-ptp",
308246
Name: secretName,
309247
}, secret)
310-
311248
if err != nil {
312-
return fmt.Errorf("failed to get secret %q: %w", secretName, err)
249+
return nil
313250
}
251+
return secret
252+
}
314253

315-
// Validate SPP (Security Parameter Profile) if specified
316-
if err := validateSppInSecret(sppValue, secret); err != nil {
317-
return fmt.Errorf("failed to validate spp in secret %q: %w", secretName, err)
318-
}
254+
// GetSecretNameFromSaFilePath extracts the secret name from the sa_file path
255+
func GetSecretNameFromSaFilePath(sa_file string) string {
256+
path := strings.TrimPrefix(sa_file, PTP_SEC_FOLDER)
257+
index := strings.Index(path, "/")
258+
return path[:index]
259+
}
319260

320-
return nil
261+
// GetSecretKeyFromSaFilePath extracts the secret key from the sa_file path
262+
func GetSecretKeyFromSaFilePath(sa_file string) string {
263+
path := strings.TrimPrefix(sa_file, PTP_SEC_FOLDER)
264+
index := strings.Index(path, "/")
265+
return path[index+1:]
321266
}
267+
322268
func getSppFromPtp4lConf(conf *Ptp4lConf) (string, error) {
323269
globalSection, exists := conf.sections["[global]"]
324270
if !exists {
@@ -334,6 +280,7 @@ func getSppFromPtp4lConf(conf *Ptp4lConf) (string, error) {
334280
}
335281
return sppValue, nil
336282
}
283+
337284
func getSaFileFromPtp4lConf(conf *Ptp4lConf) (string, error) {
338285
globalSection, exists := conf.sections["[global]"]
339286
if !exists {
@@ -349,38 +296,74 @@ func getSaFileFromPtp4lConf(conf *Ptp4lConf) (string, error) {
349296
return saFileValue, nil
350297
}
351298

352-
// validateSppInSecret checks that the spp value in ptp4lConf exists in the referenced secret
353-
func validateSppInSecret(sppValue string, secret *corev1.Secret) error {
354-
for key, value := range secret.Data {
355-
content := string(value)
356-
lines := strings.Split(content, "\n")
299+
// validateSaFile checks that the sa_file path is valid with prefix PTP_SEC_FOLDER
300+
// next directory must be a valid secret name, e.g. PTP_SEC_FOLDER/secret_name/secret_key
301+
// check that secret_key exists in the secret_name secret
302+
func validateSaFile(saFilePath string) error {
303+
if !strings.HasPrefix(saFilePath, PTP_SEC_FOLDER) {
304+
return fmt.Errorf("sa_file path '%s' is invalid; must start with '%s'", saFilePath, PTP_SEC_FOLDER)
305+
}
306+
path := strings.TrimPrefix(saFilePath, PTP_SEC_FOLDER)
307+
index := strings.Index(path, "/")
308+
if index == -1 || index == len(path)-1 {
309+
return fmt.Errorf("sa_file path '%s' is incomplete; must contain a secret key", saFilePath)
310+
}
311+
secretName := path[:index]
312+
if secretName == "" {
313+
return fmt.Errorf("sa_file path '%s' is invalid; must contain a secret name", saFilePath)
314+
}
315+
secret := getSecret(secretName)
316+
if secret == nil {
317+
return fmt.Errorf("sa_file path '%s' has invalid secret name '%s'", saFilePath, secretName)
318+
}
319+
keyCandidate := path[index+1:]
320+
return validateKeyInSecret(secret, keyCandidate)
321+
}
322+
323+
// this function will recieve a secret and a key candidate and check if the key is part of the secret
324+
func validateKeyInSecret(secret *corev1.Secret, keyCandidate string) error {
325+
if _, exists := secret.Data[keyCandidate]; !exists {
326+
return fmt.Errorf("key '%s' is not part of the secret", keyCandidate)
327+
}
328+
return nil
329+
}
330+
331+
// validateSppInSecret checks that the spp value exists in a specific key of the secret
332+
func validateSppInSecretKey(sppValue string, secretName string, secretKey string) error {
333+
secret := getSecret(secretName)
334+
value, exists := secret.Data[secretKey]
335+
if !exists {
336+
return fmt.Errorf("key '%s' not found in secret '%s'", secretKey, secret.Name)
337+
}
338+
339+
content := string(value)
340+
lines := strings.Split(content, "\n")
357341

358-
// Look for lines starting with "spp <number>"
359-
for _, line := range lines {
360-
line = strings.TrimSpace(line)
342+
// Look for lines starting with "spp <number>"
343+
for _, line := range lines {
344+
line = strings.TrimSpace(line)
361345

362-
// Skip empty lines and comments
363-
if line == "" || strings.HasPrefix(line, "#") {
364-
continue
365-
}
346+
// Skip empty lines and comments
347+
if line == "" || strings.HasPrefix(line, "#") {
348+
continue
349+
}
366350

367-
// Check if line starts with "spp "
368-
if strings.HasPrefix(strings.ToLower(line), "spp ") {
369-
parts := strings.Fields(line)
370-
if len(parts) >= 2 {
371-
secretSppValue := parts[1]
351+
// Check if line starts with "spp "
352+
if strings.HasPrefix(strings.ToLower(line), "spp ") {
353+
parts := strings.Fields(line)
354+
if len(parts) >= 2 {
355+
secretSppValue := parts[1]
372356

373-
// Check if this matches the PtpConfig's spp
374-
if secretSppValue == sppValue {
375-
ptpconfiglog.Info("validated spp match", "spp", sppValue, "secret", secret.Name, "key", key)
376-
return nil // Early return - validation passed ✅
377-
}
357+
// Check if this matches the PtpConfig's spp
358+
if secretSppValue == sppValue {
359+
ptpconfiglog.Info("validated spp match", "spp", sppValue, "secret", secret.Name, "key", secretKey)
360+
return nil // Validation passed ✅
378361
}
379362
}
380363
}
381364
}
382365

383-
return fmt.Errorf("spp '%s' is not found in secret '%s' ", sppValue, secret.Name)
366+
return fmt.Errorf("spp '%s' not found in key '%s' of secret '%s'", sppValue, secretKey, secret.Name)
384367
}
385368

386369
var _ webhook.Validator = &PtpConfig{}
@@ -435,7 +418,7 @@ func GetInterfaces(config PtpConfig, mode PtpRole) (interfaces []string) {
435418
}
436419
conf := &Ptp4lConf{}
437420
var dummy *string
438-
err := conf.populatePtp4lConf(config.Spec.Profile[0].Ptp4lConf, dummy)
421+
err := conf.PopulatePtp4lConf(config.Spec.Profile[0].Ptp4lConf, dummy)
439422
if err != nil {
440423
logrus.Warnf("ptp4l conf parsing failed, err=%s", err)
441424
}

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)