Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 194 additions & 14 deletions api/v1/ptpconfig_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1

import (
"context"
"errors"
"fmt"
"regexp"
Expand All @@ -26,8 +27,11 @@ import (
"time"

"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
Expand All @@ -39,48 +43,64 @@ const (
Master PtpRole = 1
Slave PtpRole = 0
)
const PTP_SEC_FOLDER = "/etc/ptp-secret-mount/"

// log is for logging in this package.
var ptpconfiglog = logf.Log.WithName("ptpconfig-resource")
var profileRegEx = regexp.MustCompile(`^([\w\-_]+)(,\s*([\w\-_]+))*$`)
var clockTypes = []string{"T-GM", "T-BC"}

// webhookClient is used by the webhook to query existing PtpConfigs
var webhookClient client.Client

func (r *PtpConfig) SetupWebhookWithManager(mgr ctrl.Manager) error {
// Store the client for use in validation
webhookClient = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

//+kubebuilder:webhook:path=/validate-ptp-openshift-io-v1-ptpconfig,mutating=false,failurePolicy=fail,sideEffects=None,groups=ptp.openshift.io,resources=ptpconfigs,verbs=create;update,versions=v1,name=vptpconfig.kb.io,admissionReviewVersions=v1

type ptp4lConfSection struct {
type Ptp4lConfSection struct {
options map[string]string
}

type ptp4lConf struct {
sections map[string]ptp4lConfSection
type Ptp4lConf struct {
sections map[string]Ptp4lConfSection
}

// GetOption retrieves an option value from a specific section
func (p *Ptp4lConf) GetOption(section, key string) string {
if sec, ok := p.sections[section]; ok {
if val, ok := sec.options[key]; ok {
return val
}
}
return ""
}

func (output *ptp4lConf) populatePtp4lConf(config *string, ptp4lopts *string) error {
func (output *Ptp4lConf) PopulatePtp4lConf(config *string, ptp4lopts *string) error {
var string_config string
if config != nil {
string_config = *config
}
lines := strings.Split(string_config, "\n")
var currentSection string
output.sections = make(map[string]ptp4lConfSection)
output.sections = make(map[string]Ptp4lConfSection)

for _, line := range lines {
if strings.HasPrefix(line, "[") {
currentSection = line
currentLine := strings.Split(line, "]")

if len(currentLine) < 2 {
return errors.New("Section missing closing ']'")
return errors.New("section missing closing ']'")
}

currentSection = fmt.Sprintf("%s]", currentLine[0])
section := ptp4lConfSection{options: map[string]string{}}
section := Ptp4lConfSection{options: map[string]string{}}
output.sections[currentSection] = section
} else if currentSection != "" {
split := strings.IndexByte(line, ' ')
Expand All @@ -90,22 +110,23 @@ func (output *ptp4lConf) populatePtp4lConf(config *string, ptp4lopts *string) er
output.sections[currentSection] = section
}
} else {
return errors.New("Config option not in section")
return errors.New("config option not in section")
}
}
_, exist := output.sections["[global]"]
if !exist {
output.sections["[global]"] = ptp4lConfSection{options: map[string]string{}}
output.sections["[global]"] = Ptp4lConfSection{options: map[string]string{}}
}

return nil
}

func (r *PtpConfig) validate() error {
profiles := r.Spec.Profile

for _, profile := range profiles {
conf := &ptp4lConf{}
conf.populatePtp4lConf(profile.Ptp4lConf, profile.Ptp4lOpts)
conf := &Ptp4lConf{}
conf.PopulatePtp4lConf(profile.Ptp4lConf, profile.Ptp4lOpts)

// Validate that interface field only set in ordinary clock
if profile.Interface != nil && *profile.Interface != "" {
Expand Down Expand Up @@ -190,15 +211,173 @@ func (r *PtpConfig) validate() error {
}
}
}

// validate secret-related settings for this profile
saFilePath, err := getSaFileFromPtp4lConf(conf)
if err != nil {
return fmt.Errorf("failed to get sa file path from ptp4lConf: %w", err)
}

// Skip security validation if sa_file is not configured (auth disabled)
if saFilePath == "" {
continue
}

if err := validateSaFile(saFilePath); err != nil {
return fmt.Errorf("failed to validate sa file: %w", err)
}
secretName := GetSecretNameFromSaFilePath(saFilePath)
sppValue, err := getSppFromPtp4lConf(conf)
if err != nil {
return fmt.Errorf("failed to get spp value from ptp4lConf: %w", err)
}
secretKey := GetSecretKeyFromSaFilePath(saFilePath)
if err := validateSppInSecretKey(sppValue, secretName, secretKey); err != nil {
return fmt.Errorf("failed to validate spp in secret key: %w", err)
}

}
return nil
}

// checking if the secret exists in the openshift-ptp namespace
func getSecret(secretName string) *corev1.Secret {
if webhookClient == nil {
ptpconfiglog.Info("webhook client not initialized, skipping secret existence validation")
return nil
}
secret := &corev1.Secret{}
err := webhookClient.Get(context.Background(), types.NamespacedName{
Namespace: "openshift-ptp",
Name: secretName,
}, secret)
if err != nil {
return nil
}
return secret
}

// GetSecretNameFromSaFilePath extracts the secret name from the sa_file path
func GetSecretNameFromSaFilePath(sa_file string) string {
path := strings.TrimPrefix(sa_file, PTP_SEC_FOLDER)
index := strings.Index(path, "/")
return path[:index]
}

// GetSecretKeyFromSaFilePath extracts the secret key from the sa_file path
func GetSecretKeyFromSaFilePath(sa_file string) string {
path := strings.TrimPrefix(sa_file, PTP_SEC_FOLDER)
index := strings.Index(path, "/")
return path[index+1:]
}

func getSppFromPtp4lConf(conf *Ptp4lConf) (string, error) {
globalSection, exists := conf.sections["[global]"]
if !exists {
return "", nil
}
sppValue, exists := globalSection.options["spp"]

if !exists || sppValue == "" {
return "", nil
}
if sppValue == "" {
return "", errors.New("spp value is not set in the ptp4lConf")
}
return sppValue, nil
}

func getSaFileFromPtp4lConf(conf *Ptp4lConf) (string, error) {
globalSection, exists := conf.sections["[global]"]
if !exists {
return "", nil
}
saFileValue, exists := globalSection.options["sa_file"]
if !exists {
return "", nil
}
if saFileValue == "" {
return "", errors.New("sa_file value is not set in the ptp4lConf")
}
return saFileValue, nil
}

// validateSaFile checks that the sa_file path is valid with prefix PTP_SEC_FOLDER
// next directory must be a valid secret name, e.g. PTP_SEC_FOLDER/secret_name/secret_key
// check that secret_key exists in the secret_name secret
func validateSaFile(saFilePath string) error {
if !strings.HasPrefix(saFilePath, PTP_SEC_FOLDER) {
return fmt.Errorf("sa_file path '%s' is invalid; must start with '%s'", saFilePath, PTP_SEC_FOLDER)
}
path := strings.TrimPrefix(saFilePath, PTP_SEC_FOLDER)
index := strings.Index(path, "/")
if index == -1 || index == len(path)-1 {
return fmt.Errorf("sa_file path '%s' is incomplete; must contain a secret key", saFilePath)
}
secretName := path[:index]
if secretName == "" {
return fmt.Errorf("sa_file path '%s' is invalid; must contain a secret name", saFilePath)
}
secret := getSecret(secretName)
if secret == nil {
return fmt.Errorf("sa_file path '%s' has invalid secret name '%s'", saFilePath, secretName)
}
keyCandidate := path[index+1:]
return validateKeyInSecret(secret, keyCandidate)
}

// this function will recieve a secret and a key candidate and check if the key is part of the secret
func validateKeyInSecret(secret *corev1.Secret, keyCandidate string) error {
if _, exists := secret.Data[keyCandidate]; !exists {
return fmt.Errorf("key '%s' is not part of the secret", keyCandidate)
}
return nil
}

// validateSppInSecret checks that the spp value exists in a specific key of the secret
func validateSppInSecretKey(sppValue string, secretName string, secretKey string) error {
secret := getSecret(secretName)
value, exists := secret.Data[secretKey]
if !exists {
return fmt.Errorf("key '%s' not found in secret '%s'", secretKey, secret.Name)
}

content := string(value)
lines := strings.Split(content, "\n")

// Look for lines starting with "spp <number>"
for _, line := range lines {
line = strings.TrimSpace(line)

// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}

// Check if line starts with "spp "
if strings.HasPrefix(strings.ToLower(line), "spp ") {
parts := strings.Fields(line)
if len(parts) >= 2 {
secretSppValue := parts[1]

// Check if this matches the PtpConfig's spp
if secretSppValue == sppValue {
ptpconfiglog.Info("validated spp match", "spp", sppValue, "secret", secret.Name, "key", secretKey)
return nil // Validation passed ✅
}
}
}
}

return fmt.Errorf("spp '%s' not found in key '%s' of secret '%s'", sppValue, secretKey, secret.Name)
}

var _ webhook.Validator = &PtpConfig{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *PtpConfig) ValidateCreate() (admission.Warnings, error) {
ptpconfiglog.Info("validate create", "name", r.Name)
// validate() now includes secret validation for each profile
if err := r.validate(); err != nil {
return admission.Warnings{}, err
}
Expand All @@ -209,6 +388,7 @@ func (r *PtpConfig) ValidateCreate() (admission.Warnings, error) {
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *PtpConfig) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
ptpconfiglog.Info("validate update", "name", r.Name)
// validate() now includes secret validation for each profile
if err := r.validate(); err != nil {
return admission.Warnings{}, err
}
Expand All @@ -222,7 +402,7 @@ func (r *PtpConfig) ValidateDelete() (admission.Warnings, error) {
return admission.Warnings{}, nil
}

func getInterfaces(input *ptp4lConf, mode PtpRole) (interfaces []string) {
func getInterfaces(input *Ptp4lConf, mode PtpRole) (interfaces []string) {

for index, section := range input.sections {
sectionName := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(index, "[", ""), "]", ""))
Expand All @@ -242,9 +422,9 @@ func GetInterfaces(config PtpConfig, mode PtpRole) (interfaces []string) {
logrus.Warnf("No profile detected for ptpconfig %s", config.ObjectMeta.Name)
return interfaces
}
conf := &ptp4lConf{}
conf := &Ptp4lConf{}
var dummy *string
err := conf.populatePtp4lConf(config.Spec.Profile[0].Ptp4lConf, dummy)
err := conf.PopulatePtp4lConf(config.Spec.Profile[0].Ptp4lConf, dummy)
if err != nil {
logrus.Warnf("ptp4l conf parsing failed, err=%s", err)
}
Expand Down
44 changes: 44 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading