diff --git a/cmd/main.go b/cmd/main.go index 673c6c173..74ce9f0da 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -182,6 +182,7 @@ func main() { daemon.PtpNamespace, stdoutToSocket, kubeClient, + cfg, // REST config for dynamic client to update PtpConfig status ptpConfUpdate, stopCh, plugins, @@ -219,7 +220,7 @@ func main() { runHybridMode(cfg, cp, nodeName, ptpConfUpdate, daemonInstance, ptpClient, nodeName, &hwconfigs, &refreshNodePtpDevice, tickerPull, sigCh, closeProcessManager) } } else { - runLegacyMode(cp, nodeName, ptpConfUpdate, ptpClient, nodeName, &hwconfigs, &refreshNodePtpDevice, tickerPull, sigCh, closeProcessManager) + runLegacyMode(cp, nodeName, ptpConfUpdate, daemonInstance, ptpClient, nodeName, &hwconfigs, &refreshNodePtpDevice, tickerPull, sigCh, closeProcessManager) } } @@ -397,7 +398,7 @@ func runHybridMode(cfg *rest.Config, cp *cliParams, nodeName string, ptpConfUpda } // runLegacyMode runs in legacy file-based mode without any controllers -func runLegacyMode(cp *cliParams, _ string, ptpConfUpdate *daemon.LinuxPTPConfUpdate, ptpClient *ptpclient.Clientset, nodeNameForDevice string, hwconfigs *[]ptpv1.HwConfig, refreshNodePtpDevice *bool, tickerPull *time.Ticker, sigCh chan os.Signal, closeProcessManager chan bool) { +func runLegacyMode(cp *cliParams, _ string, ptpConfUpdate *daemon.LinuxPTPConfUpdate, _ *daemon.Daemon, ptpClient *ptpclient.Clientset, nodeNameForDevice string, hwconfigs *[]ptpv1.HwConfig, refreshNodePtpDevice *bool, tickerPull *time.Ticker, sigCh chan os.Signal, closeProcessManager chan bool) { glog.Info("Running in legacy file-based mode (no controllers)") for { diff --git a/go.mod b/go.mod index de7bfd0ce..40fe6bea8 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( golang.org/x/sync v0.18.0 gonum.org/v1/gonum v0.16.0 k8s.io/api v0.28.3 + k8s.io/apiextensions-apiserver v0.28.3 k8s.io/apimachinery v0.28.3 k8s.io/client-go v0.28.3 k8s.io/klog/v2 v2.130.1 @@ -86,7 +87,6 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect howett.net/plist v1.0.0 // indirect - k8s.io/apiextensions-apiserver v0.28.3 // indirect k8s.io/component-base v0.28.3 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index f59dc88ef..e0b63554a 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -37,6 +37,7 @@ import ( "github.com/golang/glog" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" ) @@ -307,6 +308,10 @@ type Daemon struct { // kubeClient allows interaction with Kubernetes, including the node we are running on. kubeClient *kubernetes.Clientset + // restConfig is the Kubernetes REST config used to create dynamic clients + // for updating PtpConfig status when hardware plugin validation errors are found + restConfig *rest.Config + ptpUpdate *LinuxPTPConfUpdate processManager *ProcessManager @@ -357,6 +362,7 @@ func New( namespace string, stdoutToSocket bool, kubeClient *kubernetes.Clientset, + restConfig *rest.Config, ptpUpdate *LinuxPTPConfUpdate, stopCh <-chan struct{}, plugins []string, @@ -371,6 +377,9 @@ func New( } InitializeOffsetMaps() pluginManager := registerPlugins(plugins) + // Set RestConfig and Namespace for plugin validation status updates + pluginManager.RestConfig = restConfig + pluginManager.Namespace = namespace eventChannel := make(chan event.EventChannel, 100) pm := &ProcessManager{ process: nil, @@ -403,6 +412,7 @@ func New( namespace: namespace, stdoutToSocket: stdoutToSocket, kubeClient: kubeClient, + restConfig: restConfig, ptpUpdate: ptpUpdate, pluginManager: pluginManager, hwconfigs: hwconfigs, @@ -654,6 +664,11 @@ func (dn *Daemon) applyNodePTPProfiles() error { dn.pluginManager.PopulateHwConfig(dn.hwconfigs) *dn.refreshNodePtpDevice = true dn.readyTracker.setConfig(true) + + // Validate plugin names and report errors to PtpConfig status + // This is called after profiles are applied to ensure proper status updates + dn.pluginManager.ValidateAndReportPluginErrors(dn.ptpUpdate.NodeProfiles) + return nil } diff --git a/pkg/daemon/daemon_internal_test.go b/pkg/daemon/daemon_internal_test.go index 1d7f5fe59..4c25dac1d 100644 --- a/pkg/daemon/daemon_internal_test.go +++ b/pkg/daemon/daemon_internal_test.go @@ -120,6 +120,7 @@ func applyTestProfile(t *testing.T, profile *ptpv1.PtpProfile) { "openshift-ptp", false, nil, + nil, // restConfig not needed for tests &LinuxPTPConfUpdate{ UpdateCh: make(chan bool), NodeProfiles: []ptpv1.PtpProfile{*profile}, @@ -206,6 +207,7 @@ func Test_applyProfile_TBC(t *testing.T) { "openshift-ptp", false, nil, + nil, // restConfig not needed for tests &LinuxPTPConfUpdate{ UpdateCh: make(chan bool), NodeProfiles: []ptpv1.PtpProfile{}, diff --git a/pkg/hardwareconfig/hardwareconfig_test.go b/pkg/hardwareconfig/hardwareconfig_test.go index 38fa4196d..e0d2a7e42 100644 --- a/pkg/hardwareconfig/hardwareconfig_test.go +++ b/pkg/hardwareconfig/hardwareconfig_test.go @@ -590,6 +590,7 @@ func TestApplyConditionDesiredStatesWithRealData(t *testing.T) { if targetCondition == nil { t.Fatalf("Condition '%s' not found in hardware config", tc.conditionName) + return // unreachable but satisfies staticcheck } // Validate the expected number of desired states diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 07b15040a..f7f692c9c 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -1,7 +1,17 @@ package plugin import ( + "context" + "fmt" + "time" + + "github.com/golang/glog" ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" ) // Plugin type @@ -19,6 +29,11 @@ type Plugin struct { type PluginManager struct { //nolint:revive Plugins map[string]*Plugin Data map[string]*interface{} + // RestConfig is used to create dynamic client for PtpConfig status updates + // when reporting hardware plugin configuration errors + RestConfig *rest.Config + // Namespace is the namespace where PtpConfig resources are located + Namespace string } // New type @@ -93,3 +108,281 @@ func (pm *PluginManager) ProcessLog(pname string, log string) string { } return ret } + +// ValidateAndReportPluginErrors validates hardware plugin names in profiles and reports +// errors to PtpConfig status. This detects typos in the hardware plugin configuration +// section (e.g., "e81" instead of "e810") and reports them via the PtpConfig status. +// When plugins are corrected, this also clears the warning condition. +// This method should be called after profiles are applied to ensure status is updated. +func (pm *PluginManager) ValidateAndReportPluginErrors(profiles []ptpv1.PtpProfile) { + if pm.RestConfig == nil { + glog.V(2).Infof("RestConfig not available in PluginManager, skipping hardware plugin validation") + return + } + + // Track profiles with errors vs valid profiles + profilesWithErrors := make(map[string]string) // profileName -> error message + profilesValid := make(map[string]bool) // profileName -> true (valid plugins) + + // Get valid plugin names from registered plugins + var validNames []string + for name := range pm.Plugins { + validNames = append(validNames, name) + } + + // Check each profile for unrecognized plugins + for _, profile := range profiles { + profileName := "" + if profile.Name != nil { + profileName = *profile.Name + } + + if len(profile.Plugins) == 0 { + // No plugins configured - mark as valid (no warning needed) + profilesValid[profileName] = true + continue + } + + // Validate plugin names against registered plugins + var unrecognized []string + for pluginName := range profile.Plugins { + if _, exists := pm.Plugins[pluginName]; !exists { + unrecognized = append(unrecognized, pluginName) + } + } + + if len(unrecognized) == 0 { + // All plugins are valid - mark for clearing any existing warning + profilesValid[profileName] = true + continue + } + + // Build error message + errMsg := fmt.Sprintf("Profile '%s' contains unrecognized hardware plugin(s): %v. Valid plugins are: %v", + profileName, unrecognized, validNames) + glog.Warningf("Plugin validation error: %s", errMsg) + profilesWithErrors[profileName] = errMsg + } + + // Update PtpConfig status for profiles with errors + for profileName, errMsg := range profilesWithErrors { + pm.updatePtpConfigStatusWithPluginError(profileName, errMsg) + } + + // Clear warning condition for profiles that are now valid + for profileName := range profilesValid { + pm.clearPtpConfigPluginWarning(profileName) + } +} + +// updatePtpConfigStatusWithPluginError finds the PtpConfig containing the specified profile +// and updates its status with a hardware plugin configuration warning. +func (pm *PluginManager) updatePtpConfigStatusWithPluginError(profileName, errMsg string) { + ctx := context.Background() + + dynClient, err := dynamic.NewForConfig(pm.RestConfig) + if err != nil { + glog.Errorf("Failed to create dynamic client for PtpConfig status update: %v", err) + return + } + + ptpConfigGVR := schema.GroupVersionResource{ + Group: "ptp.openshift.io", + Version: "v1", + Resource: "ptpconfigs", + } + + // List all PtpConfigs to find the one containing this profile + ptpConfigList, err := dynClient.Resource(ptpConfigGVR).Namespace(pm.Namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + glog.Errorf("Failed to list PtpConfigs: %v", err) + return + } + + for _, item := range ptpConfigList.Items { + // Check if this PtpConfig contains the profile + profiles, found, _ := unstructured.NestedSlice(item.Object, "spec", "profile") + if !found { + continue + } + + for _, p := range profiles { + profileMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + name, _, _ := unstructured.NestedString(profileMap, "name") + if name == profileName { + // Found the PtpConfig containing this profile, update its status + pm.setPtpConfigStatusCondition(ctx, dynClient, ptpConfigGVR, item.GetNamespace(), item.GetName(), errMsg) + return + } + } + } + + glog.Warningf("Could not find PtpConfig containing profile '%s' to report plugin error", profileName) +} + +// setPtpConfigStatusCondition updates the status.conditions of a PtpConfig with a warning +func (pm *PluginManager) setPtpConfigStatusCondition(ctx context.Context, dynClient dynamic.Interface, gvr schema.GroupVersionResource, namespace, name, errMsg string) { + // Get current resource + resource, err := dynClient.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + glog.Errorf("Failed to get PtpConfig %s/%s for status update: %v", namespace, name, err) + return + } + + // Build the condition + condition := map[string]interface{}{ + "type": "HardwarePluginConfigurationWarning", + "status": "True", + "reason": "InvalidPluginName", + "message": errMsg, + "lastTransitionTime": time.Now().UTC().Format("2006-01-02T15:04:05Z"), + } + + // Get existing conditions + conditions, _, _ := unstructured.NestedSlice(resource.Object, "status", "conditions") + + // Update existing condition or add new one + conditionFound := false + for i, c := range conditions { + if condMap, ok := c.(map[string]interface{}); ok { + if condMap["type"] == "HardwarePluginConfigurationWarning" { + conditions[i] = condition + conditionFound = true + break + } + } + } + if !conditionFound { + conditions = append(conditions, condition) + } + + // Set the conditions back + if setErr := unstructured.SetNestedSlice(resource.Object, conditions, "status", "conditions"); setErr != nil { + glog.Errorf("Failed to set conditions in PtpConfig %s/%s: %v", namespace, name, setErr) + return + } + + // Update the status subresource + _, err = dynClient.Resource(gvr).Namespace(namespace).UpdateStatus(ctx, resource, metav1.UpdateOptions{}) + if err != nil { + glog.Errorf("Failed to update PtpConfig %s/%s status with plugin validation error: %v", namespace, name, err) + } else { + glog.Infof("Updated PtpConfig '%s/%s' status with hardware plugin configuration warning", namespace, name) + } +} + +// clearPtpConfigPluginWarning removes the HardwarePluginConfigurationWarning condition +// from a PtpConfig when all plugins are now valid (i.e., user fixed the typo). +func (pm *PluginManager) clearPtpConfigPluginWarning(profileName string) { + ctx := context.Background() + + dynClient, err := dynamic.NewForConfig(pm.RestConfig) + if err != nil { + glog.V(2).Infof("Failed to create dynamic client for clearing plugin warning: %v", err) + return + } + + ptpConfigGVR := schema.GroupVersionResource{ + Group: "ptp.openshift.io", + Version: "v1", + Resource: "ptpconfigs", + } + + // List all PtpConfigs to find the one containing this profile + ptpConfigList, err := dynClient.Resource(ptpConfigGVR).Namespace(pm.Namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + glog.V(2).Infof("Failed to list PtpConfigs for clearing warning: %v", err) + return + } + + for _, item := range ptpConfigList.Items { + // Check if this PtpConfig contains the profile + profiles, found, _ := unstructured.NestedSlice(item.Object, "spec", "profile") + if !found { + continue + } + + for _, p := range profiles { + profileMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + name, _, _ := unstructured.NestedString(profileMap, "name") + if name == profileName { + // Found the PtpConfig - check if it has the warning condition and remove it + pm.removePluginWarningCondition(ctx, dynClient, ptpConfigGVR, item.GetNamespace(), item.GetName()) + return + } + } + } +} + +// removePluginWarningCondition removes the HardwarePluginConfigurationWarning condition from status +func (pm *PluginManager) removePluginWarningCondition(ctx context.Context, dynClient dynamic.Interface, gvr schema.GroupVersionResource, namespace, name string) { + // Get current resource + resource, err := dynClient.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + glog.V(2).Infof("Failed to get PtpConfig %s/%s for clearing warning: %v", namespace, name, err) + return + } + + // Get existing conditions + conditions, found, _ := unstructured.NestedSlice(resource.Object, "status", "conditions") + if !found || len(conditions) == 0 { + // No conditions to remove + return + } + + // Filter out the HardwarePluginConfigurationWarning condition + var newConditions []interface{} + warningFound := false + for _, c := range conditions { + if condMap, ok := c.(map[string]interface{}); ok { + if condMap["type"] == "HardwarePluginConfigurationWarning" { + warningFound = true + continue // Skip this condition (remove it) + } + } + newConditions = append(newConditions, c) + } + + if !warningFound { + // No warning condition to remove + return + } + + // Set the filtered conditions back + if setErr := unstructured.SetNestedSlice(resource.Object, newConditions, "status", "conditions"); setErr != nil { + glog.Errorf("Failed to remove warning condition from PtpConfig %s/%s: %v", namespace, name, setErr) + return + } + + // Update the status subresource + _, err = dynClient.Resource(gvr).Namespace(namespace).UpdateStatus(ctx, resource, metav1.UpdateOptions{}) + if err != nil { + glog.V(2).Infof("Failed to update PtpConfig %s/%s status when clearing warning: %v", namespace, name, err) + } else { + glog.Infof("Cleared hardware plugin configuration warning from PtpConfig '%s/%s'", namespace, name) + } +} + +// ValidateProfilePlugins checks if all plugin names in the profile are valid. +// Returns a slice of unrecognized plugin names (empty if all are valid). +// This is used to detect typos in the hardware plugin configuration section +// of PtpConfig and report them to the PtpConfig status. +// The validPlugins map should contain all valid plugin names as keys (e.g., from mapping.PluginMapping). +func ValidateProfilePlugins(profile *ptpv1.PtpProfile, validPlugins map[string]New) []string { + var unrecognized []string + if profile == nil || profile.Plugins == nil { + return unrecognized + } + for pluginName := range profile.Plugins { + if _, exists := validPlugins[pluginName]; !exists { + unrecognized = append(unrecognized, pluginName) + } + } + return unrecognized +} diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go new file mode 100644 index 000000000..a10a3efb4 --- /dev/null +++ b/pkg/plugin/plugin_test.go @@ -0,0 +1,166 @@ +package plugin + +import ( + "testing" + + ptpv1 "github.com/k8snetworkplumbingwg/ptp-operator/api/v1" + "github.com/stretchr/testify/assert" + apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +// mockValidPlugins simulates the real mapping.PluginMapping for testing +// without creating import cycles +var mockValidPlugins = map[string]New{ + "e810": nil, + "e825": nil, + "e830": nil, + "reference": nil, + "ntpfailover": nil, +} + +func TestValidateProfilePlugins(t *testing.T) { + tests := []struct { + name string + plugins map[string]*apiextensions.JSON + expectedErrors []string + }{ + { + name: "nil plugins - should pass", + plugins: nil, + expectedErrors: []string{}, + }, + { + name: "valid plugin e810", + plugins: map[string]*apiextensions.JSON{ + "e810": {}, + }, + expectedErrors: []string{}, + }, + { + name: "valid plugin e825", + plugins: map[string]*apiextensions.JSON{ + "e825": {}, + }, + expectedErrors: []string{}, + }, + { + name: "valid plugin e830", + plugins: map[string]*apiextensions.JSON{ + "e830": {}, + }, + expectedErrors: []string{}, + }, + { + name: "valid plugin reference", + plugins: map[string]*apiextensions.JSON{ + "reference": {}, + }, + expectedErrors: []string{}, + }, + { + name: "valid plugin ntpfailover", + plugins: map[string]*apiextensions.JSON{ + "ntpfailover": {}, + }, + expectedErrors: []string{}, + }, + { + name: "typo e81 instead of e810", + plugins: map[string]*apiextensions.JSON{ + "e81": {}, + }, + expectedErrors: []string{"e81"}, + }, + { + name: "typo e82 instead of e825", + plugins: map[string]*apiextensions.JSON{ + "e82": {}, + }, + expectedErrors: []string{"e82"}, + }, + { + name: "typo E810 (case sensitivity)", + plugins: map[string]*apiextensions.JSON{ + "E810": {}, + }, + expectedErrors: []string{"E810"}, + }, + { + name: "multiple valid plugins", + plugins: map[string]*apiextensions.JSON{ + "e810": {}, + "reference": {}, + }, + expectedErrors: []string{}, + }, + { + name: "mix of valid and invalid plugins", + plugins: map[string]*apiextensions.JSON{ + "e810": {}, + "e81": {}, + "invalid": {}, + }, + expectedErrors: []string{"e81", "invalid"}, + }, + { + name: "completely wrong plugin name", + plugins: map[string]*apiextensions.JSON{ + "nvidia_mellanox": {}, + }, + expectedErrors: []string{"nvidia_mellanox"}, + }, + { + name: "empty string plugin name", + plugins: map[string]*apiextensions.JSON{ + "": {}, + }, + expectedErrors: []string{""}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + profileName := "test-profile" + profile := &ptpv1.PtpProfile{ + Name: &profileName, + Plugins: tc.plugins, + } + + unrecognized := ValidateProfilePlugins(profile, mockValidPlugins) + + if len(tc.expectedErrors) == 0 { + assert.Empty(t, unrecognized, "Expected no unrecognized plugins") + } else { + assert.Equal(t, len(tc.expectedErrors), len(unrecognized), + "Expected %d unrecognized plugins, got %d: %v", + len(tc.expectedErrors), len(unrecognized), unrecognized) + + for _, expected := range tc.expectedErrors { + assert.Contains(t, unrecognized, expected, + "Expected unrecognized plugin '%s' not found in %v", expected, unrecognized) + } + } + }) + } +} + +func TestValidateProfilePlugins_NilProfile(t *testing.T) { + unrecognized := ValidateProfilePlugins(nil, mockValidPlugins) + assert.Empty(t, unrecognized, "Nil profile should return empty slice") +} + +func TestValidateProfilePlugins_EmptyValidPlugins(t *testing.T) { + profileName := "test-profile" + profile := &ptpv1.PtpProfile{ + Name: &profileName, + Plugins: map[string]*apiextensions.JSON{ + "e810": {}, + }, + } + + // With empty valid plugins map, all plugins should be unrecognized + emptyValid := map[string]New{} + unrecognized := ValidateProfilePlugins(profile, emptyValid) + assert.Equal(t, 1, len(unrecognized), "All plugins should be unrecognized with empty valid map") + assert.Contains(t, unrecognized, "e810") +} diff --git a/test/ptpconfig-invalid-plugin-test.yaml b/test/ptpconfig-invalid-plugin-test.yaml new file mode 100644 index 000000000..b0325a153 --- /dev/null +++ b/test/ptpconfig-invalid-plugin-test.yaml @@ -0,0 +1,70 @@ +# PtpConfig CR for testing hardware plugin validation +# This config contains an invalid plugin name "e81" (typo of "e810") +# When applied, the linuxptp-daemon should: +# 1. Log a warning about the unrecognized plugin +# 2. Update the PtpConfig status with a HardwarePluginConfigurationWarning condition +# +# Usage: +# kubectl apply -f ptpconfig-invalid-plugin-test.yaml +# +# Verify the warning: +# kubectl get ptpconfig test-invalid-plugin -n openshift-ptp -o jsonpath='{.status.conditions}' | jq +# +# Expected output: +# [{ +# "type": "HardwarePluginConfigurationWarning", +# "status": "True", +# "reason": "InvalidPluginName", +# "message": "Profile 'test-invalid-plugin' contains unrecognized hardware plugin(s): [e81]. Valid plugins are: [e810 e825 e830 reference ntpfailover]" +# }] +# +# Clean up: +# kubectl delete -f ptpconfig-invalid-plugin-test.yaml + +apiVersion: ptp.openshift.io/v1 +kind: PtpConfig +metadata: + name: test-invalid-plugin + namespace: openshift-ptp +spec: + profile: + - name: test-invalid-plugin + interface: ens1f0 + phc2sysOpts: -a -r -n 24 + ptp4lOpts: -2 --summary_interval -4 + ptp4lConf: | + [global] + twoStepFlag 1 + domainNumber 24 + clockAccuracy 0xFE + offsetScaledLogVariance 0xFFFF + dataset_comparison G.8275.x + G.8275.defaultDS.localPriority 128 + logAnnounceInterval -3 + logSyncInterval -4 + logMinDelayReqInterval -4 + announceReceiptTimeout 6 + network_transport L2 + delay_mechanism E2E + time_stamping hardware + tsproc_mode filter + delay_filter moving_median + delay_filter_length 10 + [ens1f0] + masterOnly 0 + plugins: + # Invalid plugin name - "e81" is a typo of "e810" + # This should trigger a HardwarePluginConfigurationWarning in the status + e81: + enableDefaultConfig: false + ptpClockThreshold: + holdOverTimeout: 5 + maxOffsetThreshold: 100 + minOffsetThreshold: -100 + recommend: + - match: + # Matches all nodes - adjust as needed for your test environment + - nodeLabel: node-role.kubernetes.io/worker + priority: 10 + profile: test-invalid-plugin + diff --git a/test/ptpconfig-valid-plugin-test.yaml b/test/ptpconfig-valid-plugin-test.yaml new file mode 100644 index 000000000..505798ed0 --- /dev/null +++ b/test/ptpconfig-valid-plugin-test.yaml @@ -0,0 +1,59 @@ +# PtpConfig CR for testing hardware plugin validation (valid case) +# This config contains a valid plugin name "e810" +# When applied, the linuxptp-daemon should NOT add any warning to the status +# +# Usage: +# kubectl apply -f ptpconfig-valid-plugin-test.yaml +# +# Verify no warning (status.conditions should be empty or not contain HardwarePluginConfigurationWarning): +# kubectl get ptpconfig test-valid-plugin -n openshift-ptp -o jsonpath='{.status.conditions}' | jq +# +# Clean up: +# kubectl delete -f ptpconfig-valid-plugin-test.yaml + +apiVersion: ptp.openshift.io/v1 +kind: PtpConfig +metadata: + name: test-valid-plugin + namespace: openshift-ptp +spec: + profile: + - name: test-valid-plugin + interface: ens1f0 + phc2sysOpts: -a -r -n 24 + ptp4lOpts: -2 --summary_interval -4 + ptp4lConf: | + [global] + twoStepFlag 1 + domainNumber 24 + clockAccuracy 0xFE + offsetScaledLogVariance 0xFFFF + dataset_comparison G.8275.x + G.8275.defaultDS.localPriority 128 + logAnnounceInterval -3 + logSyncInterval -4 + logMinDelayReqInterval -4 + announceReceiptTimeout 6 + network_transport L2 + delay_mechanism E2E + time_stamping hardware + tsproc_mode filter + delay_filter moving_median + delay_filter_length 10 + [ens1f0] + masterOnly 0 + plugins: + # Valid plugin name - e810 is a recognized plugin + e810: + enableDefaultConfig: false + ptpClockThreshold: + holdOverTimeout: 5 + maxOffsetThreshold: 100 + minOffsetThreshold: -100 + recommend: + - match: + # Matches all nodes - adjust as needed for your test environment + - nodeLabel: node-role.kubernetes.io/worker + priority: 10 + profile: test-valid-plugin +