Skip to content

Commit 42482f4

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 42482f4

File tree

12 files changed

+147
-366
lines changed

12 files changed

+147
-366
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: 99 additions & 110 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,65 @@ 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-
}
257220

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 {
221+
// Skip security validation if sa_file is not configured (auth disabled)
222+
if saFilePath == "" {
262223
continue
263224
}
264225

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-
}
226+
if err := validateSaFile(saFilePath); err != nil {
227+
return fmt.Errorf("failed to validate sa file: %w", err)
228+
}
229+
secretName := GetSecretNameFromSaFilePath(saFilePath)
230+
sppValue, err := getSppFromPtp4lConf(conf)
231+
if err != nil {
232+
return fmt.Errorf("failed to get spp value from ptp4lConf: %w", err)
233+
}
234+
secretKey := GetSecretKeyFromSaFilePath(saFilePath)
235+
if err := validateSppInSecretKey(sppValue, secretName, secretKey); err != nil {
236+
return fmt.Errorf("failed to validate spp in secret key: %w", err)
291237
}
292-
}
293238

239+
}
294240
return nil
295241
}
296242

297-
// validateSecretExists checks that a single profile's ptpSecretName references an existing secret
298-
func validateSecretExists(sppValue string, secretName string) error {
243+
// checking if the secret exists in the openshift-ptp namespace
244+
func getSecret(secretName string) *corev1.Secret {
299245
if webhookClient == nil {
300246
ptpconfiglog.Info("webhook client not initialized, skipping secret existence validation")
301247
return nil
302248
}
303-
304-
// Try to get the secret from openshift-ptp namespace
305249
secret := &corev1.Secret{}
306250
err := webhookClient.Get(context.Background(), types.NamespacedName{
307251
Namespace: "openshift-ptp",
308252
Name: secretName,
309253
}, secret)
310-
311254
if err != nil {
312-
return fmt.Errorf("failed to get secret %q: %w", secretName, err)
255+
return nil
313256
}
257+
return secret
258+
}
314259

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-
}
260+
// GetSecretNameFromSaFilePath extracts the secret name from the sa_file path
261+
func GetSecretNameFromSaFilePath(sa_file string) string {
262+
path := strings.TrimPrefix(sa_file, PTP_SEC_FOLDER)
263+
index := strings.Index(path, "/")
264+
return path[:index]
265+
}
319266

320-
return nil
267+
// GetSecretKeyFromSaFilePath extracts the secret key from the sa_file path
268+
func GetSecretKeyFromSaFilePath(sa_file string) string {
269+
path := strings.TrimPrefix(sa_file, PTP_SEC_FOLDER)
270+
index := strings.Index(path, "/")
271+
return path[index+1:]
321272
}
273+
322274
func getSppFromPtp4lConf(conf *Ptp4lConf) (string, error) {
323275
globalSection, exists := conf.sections["[global]"]
324276
if !exists {
@@ -334,6 +286,7 @@ func getSppFromPtp4lConf(conf *Ptp4lConf) (string, error) {
334286
}
335287
return sppValue, nil
336288
}
289+
337290
func getSaFileFromPtp4lConf(conf *Ptp4lConf) (string, error) {
338291
globalSection, exists := conf.sections["[global]"]
339292
if !exists {
@@ -349,38 +302,74 @@ func getSaFileFromPtp4lConf(conf *Ptp4lConf) (string, error) {
349302
return saFileValue, nil
350303
}
351304

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")
305+
// validateSaFile checks that the sa_file path is valid with prefix PTP_SEC_FOLDER
306+
// next directory must be a valid secret name, e.g. PTP_SEC_FOLDER/secret_name/secret_key
307+
// check that secret_key exists in the secret_name secret
308+
func validateSaFile(saFilePath string) error {
309+
if !strings.HasPrefix(saFilePath, PTP_SEC_FOLDER) {
310+
return fmt.Errorf("sa_file path '%s' is invalid; must start with '%s'", saFilePath, PTP_SEC_FOLDER)
311+
}
312+
path := strings.TrimPrefix(saFilePath, PTP_SEC_FOLDER)
313+
index := strings.Index(path, "/")
314+
if index == -1 || index == len(path)-1 {
315+
return fmt.Errorf("sa_file path '%s' is incomplete; must contain a secret key", saFilePath)
316+
}
317+
secretName := path[:index]
318+
if secretName == "" {
319+
return fmt.Errorf("sa_file path '%s' is invalid; must contain a secret name", saFilePath)
320+
}
321+
secret := getSecret(secretName)
322+
if secret == nil {
323+
return fmt.Errorf("sa_file path '%s' has invalid secret name '%s'", saFilePath, secretName)
324+
}
325+
keyCandidate := path[index+1:]
326+
return validateKeyInSecret(secret, keyCandidate)
327+
}
357328

358-
// Look for lines starting with "spp <number>"
359-
for _, line := range lines {
360-
line = strings.TrimSpace(line)
329+
// this function will recieve a secret and a key candidate and check if the key is part of the secret
330+
func validateKeyInSecret(secret *corev1.Secret, keyCandidate string) error {
331+
if _, exists := secret.Data[keyCandidate]; !exists {
332+
return fmt.Errorf("key '%s' is not part of the secret", keyCandidate)
333+
}
334+
return nil
335+
}
361336

362-
// Skip empty lines and comments
363-
if line == "" || strings.HasPrefix(line, "#") {
364-
continue
365-
}
337+
// validateSppInSecret checks that the spp value exists in a specific key of the secret
338+
func validateSppInSecretKey(sppValue string, secretName string, secretKey string) error {
339+
secret := getSecret(secretName)
340+
value, exists := secret.Data[secretKey]
341+
if !exists {
342+
return fmt.Errorf("key '%s' not found in secret '%s'", secretKey, secret.Name)
343+
}
366344

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]
345+
content := string(value)
346+
lines := strings.Split(content, "\n")
372347

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-
}
348+
// Look for lines starting with "spp <number>"
349+
for _, line := range lines {
350+
line = strings.TrimSpace(line)
351+
352+
// Skip empty lines and comments
353+
if line == "" || strings.HasPrefix(line, "#") {
354+
continue
355+
}
356+
357+
// Check if line starts with "spp "
358+
if strings.HasPrefix(strings.ToLower(line), "spp ") {
359+
parts := strings.Fields(line)
360+
if len(parts) >= 2 {
361+
secretSppValue := parts[1]
362+
363+
// Check if this matches the PtpConfig's spp
364+
if secretSppValue == sppValue {
365+
ptpconfiglog.Info("validated spp match", "spp", sppValue, "secret", secret.Name, "key", secretKey)
366+
return nil // Validation passed ✅
378367
}
379368
}
380369
}
381370
}
382371

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

386375
var _ webhook.Validator = &PtpConfig{}
@@ -435,7 +424,7 @@ func GetInterfaces(config PtpConfig, mode PtpRole) (interfaces []string) {
435424
}
436425
conf := &Ptp4lConf{}
437426
var dummy *string
438-
err := conf.populatePtp4lConf(config.Spec.Profile[0].Ptp4lConf, dummy)
427+
err := conf.PopulatePtp4lConf(config.Spec.Profile[0].Ptp4lConf, dummy)
439428
if err != nil {
440429
logrus.Warnf("ptp4l conf parsing failed, err=%s", err)
441430
}

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)