diff --git a/internal/cluster/validation_id.go b/internal/cluster/validation_id.go index 16cc74b6902e..f1c759981444 100644 --- a/internal/cluster/validation_id.go +++ b/internal/cluster/validation_id.go @@ -58,6 +58,7 @@ const ( AreMetallbRequirementsSatisfied = ValidationID(models.ClusterValidationIDMetallbRequirementsSatisfied) IsLokiRequirementsSatisfied = ValidationID(models.ClusterValidationIDLokiRequirementsSatisfied) IsOpenShiftLoggingRequirementsSatisfied = ValidationID(models.ClusterValidationIDOpenshiftLoggingRequirementsSatisfied) + AreNetworkObservabilityRequirementsSatisfied = ValidationID(models.ClusterValidationIDNetworkObservabilityRequirementsSatisfied) ) func (v ValidationID) Category() (string, error) { @@ -98,7 +99,8 @@ func (v ValidationID) Category() (string, error) { AreOADPRequirementsSatisfied, AreMetallbRequirementsSatisfied, IsLokiRequirementsSatisfied, - IsOpenShiftLoggingRequirementsSatisfied: + IsOpenShiftLoggingRequirementsSatisfied, + AreNetworkObservabilityRequirementsSatisfied: return "operators", nil } return "", common.NewApiError(http.StatusInternalServerError, errors.Errorf("Unexpected cluster validation id %s", string(v))) diff --git a/internal/featuresupport/feature_support_level.go b/internal/featuresupport/feature_support_level.go index ef7a14fe6f3b..dd79af9f236f 100644 --- a/internal/featuresupport/feature_support_level.go +++ b/internal/featuresupport/feature_support_level.go @@ -61,6 +61,7 @@ var featuresList = map[models.FeatureSupportLevelID]SupportLevelFeature{ models.FeatureSupportLevelIDMETALLB: (&MetalLBFeature{}).New(), models.FeatureSupportLevelIDLOKI: (&LokiFeature{}).New(), models.FeatureSupportLevelIDOPENSHIFTLOGGING: (&OpenShiftLoggingFeature{}).New(), + models.FeatureSupportLevelIDNETWORKOBSERVABILITY: (&NetworkObservabilityFeature{}).New(), // Platform features models.FeatureSupportLevelIDNUTANIXINTEGRATION: (&NutanixIntegrationFeature{}).New(), diff --git a/internal/featuresupport/features_olm_operators.go b/internal/featuresupport/features_olm_operators.go index 87fe0276ceec..377c64252dfe 100644 --- a/internal/featuresupport/features_olm_operators.go +++ b/internal/featuresupport/features_olm_operators.go @@ -9,6 +9,7 @@ import ( "github.com/openshift/assisted-service/internal/operators/fenceagentsremediation" "github.com/openshift/assisted-service/internal/operators/kubedescheduler" "github.com/openshift/assisted-service/internal/operators/loki" + "github.com/openshift/assisted-service/internal/operators/networkobservability" "github.com/openshift/assisted-service/internal/operators/nodehealthcheck" "github.com/openshift/assisted-service/internal/operators/nodemaintenance" "github.com/openshift/assisted-service/internal/operators/numaresources" @@ -1239,3 +1240,37 @@ func (f *MetalLBFeature) getFeatureActiveLevel(cluster *common.Cluster, _ *model } return activeLevelNotActive } + +// NetworkObservabilityFeature describes the support for the Network Observability Operator. +type NetworkObservabilityFeature struct{} + +func (f *NetworkObservabilityFeature) New() SupportLevelFeature { + return &NetworkObservabilityFeature{} +} + +func (f *NetworkObservabilityFeature) getId() models.FeatureSupportLevelID { + return models.FeatureSupportLevelIDNETWORKOBSERVABILITY +} + +func (f *NetworkObservabilityFeature) GetName() string { + return networkobservability.FullName +} + +func (f *NetworkObservabilityFeature) getSupportLevel(filters SupportLevelFilters) (models.SupportLevel, models.IncompatibilityReason) { + return models.SupportLevelSupported, "" +} + +func (f *NetworkObservabilityFeature) getIncompatibleArchitectures(_ *string) []models.ArchitectureSupportLevelID { + return []models.ArchitectureSupportLevelID{} +} + +func (f *NetworkObservabilityFeature) getIncompatibleFeatures(string) []models.FeatureSupportLevelID { + return []models.FeatureSupportLevelID{} +} + +func (f *NetworkObservabilityFeature) getFeatureActiveLevel(cluster *common.Cluster, _ *models.InfraEnv, clusterUpdateParams *models.V2ClusterUpdateParams, _ *models.InfraEnvUpdateParams) featureActiveLevel { + if isOperatorActivated(networkobservability.Name, cluster, clusterUpdateParams) { + return activeLevelActive + } + return activeLevelNotActive +} diff --git a/internal/host/validation_id.go b/internal/host/validation_id.go index fef401b49f97..c91cf08f81ca 100644 --- a/internal/host/validation_id.go +++ b/internal/host/validation_id.go @@ -76,6 +76,7 @@ const ( AreMetalLBRequirementsSatisfied = validationID(models.HostValidationIDMetallbRequirementsSatisfied) AreLokiRequirementsSatisfied = validationID(models.HostValidationIDLokiRequirementsSatisfied) AreOpenShiftLoggingRequirementsSatisfied = validationID(models.HostValidationIDOpenshiftLoggingRequirementsSatisfied) + AreNetworkObservabilityRequirementsSatisfied = validationID(models.HostValidationIDNetworkObservabilityRequirementsSatisfied) ) func (v validationID) category() (string, error) { @@ -146,7 +147,8 @@ func (v validationID) category() (string, error) { AreOADPRequirementsSatisfied, AreMetalLBRequirementsSatisfied, AreLokiRequirementsSatisfied, - AreOpenShiftLoggingRequirementsSatisfied: + AreOpenShiftLoggingRequirementsSatisfied, + AreNetworkObservabilityRequirementsSatisfied: return "operators", nil } return "", common.NewApiError(http.StatusInternalServerError, errors.Errorf("Unexpected validation id %s", string(v))) diff --git a/internal/operators/builder.go b/internal/operators/builder.go index 1b5bd98fe4b3..970a67c69303 100644 --- a/internal/operators/builder.go +++ b/internal/operators/builder.go @@ -16,6 +16,7 @@ import ( "github.com/openshift/assisted-service/internal/operators/mce" "github.com/openshift/assisted-service/internal/operators/metallb" "github.com/openshift/assisted-service/internal/operators/mtv" + "github.com/openshift/assisted-service/internal/operators/networkobservability" "github.com/openshift/assisted-service/internal/operators/nmstate" "github.com/openshift/assisted-service/internal/operators/nodefeaturediscovery" "github.com/openshift/assisted-service/internal/operators/nodehealthcheck" @@ -89,6 +90,7 @@ func NewManager(log logrus.FieldLogger, manifestAPI manifestsapi.ManifestsAPI, o numaresources.NewNumaResourcesOperator(log), oadp.NewOadpOperator(log), metallb.NewMetalLBOperator(log), + networkobservability.NewNetworkObservabilityOperator(log), ) } diff --git a/internal/operators/networkobservability/networkobservability_config.go b/internal/operators/networkobservability/networkobservability_config.go new file mode 100644 index 000000000000..6ffa66cf1374 --- /dev/null +++ b/internal/operators/networkobservability/networkobservability_config.go @@ -0,0 +1,47 @@ +package networkobservability + +import ( + "encoding/json" + "fmt" +) + +const ( + Name = "network-observability" + FullName = "Network Observability Operator" + Namespace = "openshift-netobserv-operator" + SubscriptionName = "network-observability-operator" + Source = "redhat-operators" + SourceName = "netobserv-operator" + GroupName = "netobserv-operatorgroup" +) + +// Config holds the configuration for Network Observability Operator +type Config struct { + // CreateFlowCollector indicates whether to create a FlowCollector resource + CreateFlowCollector bool `json:"createFlowCollector,omitempty"` + // Sampling rate for eBPF agent (default: 50) + Sampling int `json:"sampling,omitempty"` +} + +// ParseProperties parses the properties JSON string into a Config struct +func ParseProperties(properties string) (*Config, error) { + config := &Config{ + CreateFlowCollector: false, // Default: don't create FlowCollector + Sampling: 50, // Default sampling rate + } + + if properties == "" { + return config, nil + } + + if err := json.Unmarshal([]byte(properties), config); err != nil { + return nil, fmt.Errorf("failed to parse network-observability properties: %w", err) + } + + // Validate sampling rate + if config.Sampling <= 0 { + return nil, fmt.Errorf("sampling rate must be positive, got %d", config.Sampling) + } + + return config, nil +} diff --git a/internal/operators/networkobservability/networkobservability_manifests_test.go b/internal/operators/networkobservability/networkobservability_manifests_test.go new file mode 100644 index 000000000000..cbe653a140a6 --- /dev/null +++ b/internal/operators/networkobservability/networkobservability_manifests_test.go @@ -0,0 +1,267 @@ +package networkobservability + +import ( + "bytes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/openshift/assisted-service/internal/common" + "github.com/openshift/assisted-service/models" + "gopkg.in/yaml.v3" +) + +var _ = Describe("Network Observability manifest generation", func() { + var ( + cluster *common.Cluster + operator *operator + ) + + BeforeEach(func() { + cluster = &common.Cluster{ + Cluster: models.Cluster{ + OpenshiftVersion: "4.12.0", + }, + } + operator = NewNetworkObservabilityOperator(common.GetTestLog()) + }) + + It("Generates the required manifests", func() { + manifests, customManifest, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(manifests).To(HaveLen(3)) + Expect(manifests).To(HaveKey("50_openshift-network-observability_ns.yaml")) + Expect(manifests).To(HaveKey("50_openshift-network-observability_subscription.yaml")) + Expect(manifests).To(HaveKey("50_openshift-network-observability_operator_group.yaml")) + // When FlowCollector is not created, customManifest may contain only YAML separators + Expect(string(customManifest)).To(Or(BeEmpty(), MatchRegexp(`^---\s*$`))) + }) + + It("Generates valid YAML for OpenShift manifests", func() { + openShiftManifests, _, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + for _, openShiftManifest := range openShiftManifests { + var object interface{} + err = yaml.Unmarshal(openShiftManifest, &object) + Expect(err).ToNot(HaveOccurred()) + } + }) + + It("Namespace manifest has correct content", func() { + manifests, _, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + + nsManifest := manifests["50_openshift-network-observability_ns.yaml"] + var nsData map[string]interface{} + err = yaml.Unmarshal(nsManifest, &nsData) + Expect(err).ToNot(HaveOccurred()) + + metadata, ok := nsData["metadata"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "Namespace manifest missing or invalid metadata key") + Expect(metadata["name"]).To(Equal(Namespace)) + }) + + It("Subscription manifest has correct content", func() { + manifests, _, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + + subManifest := manifests["50_openshift-network-observability_subscription.yaml"] + var subData map[string]interface{} + err = yaml.Unmarshal(subManifest, &subData) + Expect(err).ToNot(HaveOccurred()) + + metadata, ok := subData["metadata"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "Subscription manifest missing or invalid metadata key") + Expect(metadata["name"]).To(Equal(SubscriptionName)) + Expect(metadata["namespace"]).To(Equal(Namespace)) + + spec, ok := subData["spec"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "Subscription manifest missing or invalid spec key") + Expect(spec["channel"]).To(Equal("stable")) + Expect(spec["name"]).To(Equal(SourceName)) + Expect(spec["source"]).To(Equal(Source)) + Expect(spec["sourceNamespace"]).To(Equal("openshift-marketplace")) + }) + + It("OperatorGroup manifest has correct content", func() { + manifests, _, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + + ogManifest := manifests["50_openshift-network-observability_operator_group.yaml"] + var ogData map[string]interface{} + err = yaml.Unmarshal(ogManifest, &ogData) + Expect(err).ToNot(HaveOccurred()) + + metadata, ok := ogData["metadata"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "OperatorGroup manifest missing or invalid metadata key") + Expect(metadata["name"]).To(Equal(GroupName)) + Expect(metadata["namespace"]).To(Equal(Namespace)) + }) + + Context("FlowCollector generation", func() { + It("Does not generate FlowCollector when createFlowCollector is false", func() { + cluster.MonitoredOperators = []*models.MonitoredOperator{ + { + Name: Name, + Properties: `{"createFlowCollector": false}`, + }, + } + + manifests, customManifest, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(manifests).To(HaveLen(3)) + // When FlowCollector is not created, customManifest may contain only YAML separators + Expect(string(customManifest)).To(Or(BeEmpty(), MatchRegexp(`^---\s*$`))) + }) + + It("Generates FlowCollector when createFlowCollector is true", func() { + cluster.MonitoredOperators = []*models.MonitoredOperator{ + { + Name: Name, + Properties: `{"createFlowCollector": true, "sampling": 100}`, + }, + } + + manifests, customManifest, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(manifests).To(HaveLen(3)) + Expect(customManifest).ToNot(BeEmpty()) + + // Parse the first document from the custom manifest (supports multi-document YAML) + decoder := yaml.NewDecoder(bytes.NewReader(customManifest)) + var flowCollectorData map[string]interface{} + err = decoder.Decode(&flowCollectorData) + Expect(err).ToNot(HaveOccurred()) + Expect(flowCollectorData).ToNot(BeNil(), "FlowCollector manifest should not be nil") + + Expect(flowCollectorData["kind"]).To(Equal("FlowCollector")) + Expect(flowCollectorData).To(HaveKey("metadata"), "FlowCollector manifest missing metadata key") + metadata, ok := flowCollectorData["metadata"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector manifest metadata is not a map") + Expect(metadata["name"]).To(Equal("cluster")) + Expect(metadata["namespace"]).To(Equal("netobserv")) + + Expect(flowCollectorData).To(HaveKey("spec"), "FlowCollector manifest missing spec key") + spec, ok := flowCollectorData["spec"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector manifest spec is not a map") + Expect(spec).To(HaveKey("agent"), "FlowCollector spec missing agent key") + agent, ok := spec["agent"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector spec agent is not a map") + Expect(agent).To(HaveKey("ebpf"), "FlowCollector agent missing ebpf key") + ebpf, ok := agent["ebpf"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector agent ebpf is not a map") + Expect(ebpf).To(HaveKey("sampling"), "FlowCollector ebpf missing sampling key") + // Handle YAML numeric types (can be int, int64, or float64) + samplingValue := ebpf["sampling"] + switch v := samplingValue.(type) { + case int, int64: + Expect(v).To(BeNumerically("==", 100)) + case float64: + Expect(v).To(BeNumerically("==", 100)) + default: + Fail("sampling value is not a numeric type") + } + + Expect(spec).To(HaveKey("loki"), "FlowCollector spec missing loki key") + loki, ok := spec["loki"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector spec loki is not a map") + Expect(loki).To(HaveKey("enabled"), "FlowCollector loki missing enabled key") + Expect(loki["enabled"]).To(Equal(false)) + }) + + It("Uses default values when properties are not provided", func() { + cluster.MonitoredOperators = []*models.MonitoredOperator{ + { + Name: Name, + Properties: `{"createFlowCollector": true}`, + }, + } + + _, customManifest, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(customManifest).ToNot(BeEmpty()) + + // Parse the first document from the custom manifest (supports multi-document YAML) + decoder := yaml.NewDecoder(bytes.NewReader(customManifest)) + var flowCollectorData map[string]interface{} + err = decoder.Decode(&flowCollectorData) + Expect(err).ToNot(HaveOccurred()) + Expect(flowCollectorData).ToNot(BeNil(), "FlowCollector manifest should not be nil") + + Expect(flowCollectorData).To(HaveKey("spec"), "FlowCollector manifest missing spec key") + spec, ok := flowCollectorData["spec"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector manifest spec is not a map") + Expect(spec).To(HaveKey("agent"), "FlowCollector spec missing agent key") + agent, ok := spec["agent"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector spec agent is not a map") + Expect(agent).To(HaveKey("ebpf"), "FlowCollector agent missing ebpf key") + ebpf, ok := agent["ebpf"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector agent ebpf is not a map") + Expect(ebpf).To(HaveKey("sampling"), "FlowCollector ebpf missing sampling key") + // Handle YAML numeric types (can be int, int64, or float64) + samplingValue := ebpf["sampling"] + switch v := samplingValue.(type) { + case int, int64: + Expect(v).To(BeNumerically("==", 50)) // Default value + case float64: + Expect(v).To(BeNumerically("==", 50)) // Default value + default: + Fail("sampling value is not a numeric type") + } + + Expect(spec).To(HaveKey("loki"), "FlowCollector spec missing loki key") + loki, ok := spec["loki"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "FlowCollector spec loki is not a map") + Expect(loki).To(HaveKey("enabled"), "FlowCollector loki missing enabled key") + Expect(loki["enabled"]).To(Equal(false)) // Always false + }) + }) + + Context("Config parsing", func() { + It("Handles invalid JSON properties gracefully", func() { + cluster.MonitoredOperators = []*models.MonitoredOperator{ + { + Name: Name, + Properties: `{"invalid": json}`, + }, + } + + manifests, customManifest, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(manifests).To(HaveLen(3)) + // When FlowCollector is not created, customManifest may contain only YAML separators + Expect(string(customManifest)).To(Or(BeEmpty(), MatchRegexp(`^---\s*$`))) + }) + + It("Handles empty properties", func() { + cluster.MonitoredOperators = []*models.MonitoredOperator{ + { + Name: Name, + Properties: "", + }, + } + + manifests, customManifest, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(manifests).To(HaveLen(3)) + // When FlowCollector is not created, customManifest may contain only YAML separators + Expect(string(customManifest)).To(Or(BeEmpty(), MatchRegexp(`^---\s*$`))) + }) + + It("Invalid sampling values trigger error handling; ParseProperties rejects sampling <= 0, GenerateManifests logs a warning and uses safe defaults", func() { + cluster.MonitoredOperators = []*models.MonitoredOperator{ + { + Name: Name, + Properties: `{"createFlowCollector": true, "sampling": 0}`, + }, + } + + manifests, customManifest, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(manifests).To(HaveLen(3)) + // Invalid sampling values trigger error handling: ParseProperties rejects sampling <= 0, + // GenerateManifests logs a warning and then uses safe defaults (createFlowCollector: false). + // This is recovery behavior, not a feature. So customManifest should be empty or only contain YAML separators. + Expect(string(customManifest)).To(Or(BeEmpty(), MatchRegexp(`^---\s*$`))) + }) + }) +}) diff --git a/internal/operators/networkobservability/networkobservability_operator.go b/internal/operators/networkobservability/networkobservability_operator.go new file mode 100644 index 000000000000..29b26799c5c8 --- /dev/null +++ b/internal/operators/networkobservability/networkobservability_operator.go @@ -0,0 +1,184 @@ +package networkobservability + +import ( + "context" + "text/template" + + "github.com/lib/pq" + "github.com/openshift/assisted-service/internal/common" + "github.com/openshift/assisted-service/internal/operators/api" + operatorscommon "github.com/openshift/assisted-service/internal/operators/common" + "github.com/openshift/assisted-service/internal/templating" + "github.com/openshift/assisted-service/models" + logutil "github.com/openshift/assisted-service/pkg/log" + "github.com/sirupsen/logrus" +) + +const ( + clusterValidationID = "network-observability-requirements-satisfied" + hostValidationID = "network-observability-requirements-satisfied" +) + +type operator struct { + log logrus.FieldLogger + templates *template.Template +} + +var Operator = models.MonitoredOperator{ + Name: Name, + Namespace: Namespace, + OperatorType: models.OperatorTypeOlm, + SubscriptionName: SubscriptionName, + TimeoutSeconds: 60 * 60, + Bundles: pq.StringArray{}, +} + +// NewNetworkObservabilityOperator creates new instance of a Network Observability Operator installation plugin +func NewNetworkObservabilityOperator(log logrus.FieldLogger) *operator { + templates, err := templating.LoadTemplates(templatesRoot) + if err != nil { + log.Fatal(err.Error()) + } + return &operator{ + log: log.WithField("operator", Name), + templates: templates, + } +} + +// GetName reports the name of an operator this Operator manages +func (o *operator) GetName() string { + return Operator.Name +} + +func (o *operator) GetFullName() string { + return FullName +} + +// GetDependencies provides a list of dependencies of the Operator +func (o *operator) GetDependencies(cluster *common.Cluster) ([]string, error) { + return make([]string, 0), nil +} + +func (o *operator) GetDependenciesFeatureSupportID() []models.FeatureSupportLevelID { + return nil +} + +// GetClusterValidationIDs returns cluster validation IDs for the Operator +func (o *operator) GetClusterValidationIDs() []string { + return []string{clusterValidationID} +} + +// GetHostValidationID returns host validation ID for the Operator +func (o *operator) GetHostValidationID() string { + return hostValidationID +} + +// ValidateCluster verifies whether this operator is valid for given cluster +func (o *operator) ValidateCluster(_ context.Context, cluster *common.Cluster) ([]api.ValidationResult, error) { + result := []api.ValidationResult{{ + Status: api.Success, + ValidationId: clusterValidationID, + }} + + return result, nil +} + +// ValidateHost returns validationResult based on node type requirements such as memory and cpu +func (o *operator) ValidateHost(ctx context.Context, cluster *common.Cluster, host *models.Host, _ *models.ClusterHostRequirementsDetails) (api.ValidationResult, error) { + return api.ValidationResult{Status: api.Success, ValidationId: o.GetHostValidationID()}, nil +} + +// GenerateManifests generates manifests for the operator +func (o *operator) GenerateManifests(c *common.Cluster) (map[string][]byte, []byte, error) { + // Find the operator in cluster's MonitoredOperators to get properties + var config *Config + var err error + + for _, clusterOp := range c.MonitoredOperators { + if clusterOp.Name == o.GetName() { + config, err = ParseProperties(clusterOp.Properties) + if err != nil { + o.log.WithError(err).Warnf("Failed to parse properties for %s, using defaults", o.GetName()) + config, _ = ParseProperties("") + } + break + } + } + + // If operator not found in cluster, use default config + if config == nil { + config, _ = ParseProperties("") + } + + // Generate manifests using common.GenerateManifests + // The FlowCollector template uses conditionals to only render when CreateFlowCollector is true + return operatorscommon.GenerateManifests( + templatesRoot, o.templates, config, &Operator, + ) +} + +// GetProperties provides description of operator properties +func (o *operator) GetProperties() models.OperatorProperties { + return models.OperatorProperties{ + { + Name: "createFlowCollector", + DataType: models.OperatorPropertyDataTypeBoolean, + Description: "Whether to create a FlowCollector resource automatically. If false, only the operator will be installed.", + Mandatory: false, + DefaultValue: "false", + }, + { + Name: "sampling", + DataType: models.OperatorPropertyDataTypeInteger, + Description: "Sampling rate for eBPF agent. A value of 50 means one packet every 50 is sampled. Lower values increase resource utilization.", + Mandatory: false, + DefaultValue: "50", + }, + } +} + +// GetMonitoredOperator returns MonitoredOperator corresponding to the Network Observability Operator +func (o *operator) GetMonitoredOperator() *models.MonitoredOperator { + return &Operator +} + +// GetHostRequirements provides operator's requirements towards the host +func (o *operator) GetHostRequirements(ctx context.Context, cluster *common.Cluster, host *models.Host) (*models.ClusterHostRequirementsDetails, error) { + log := logutil.FromContext(ctx, o.log) + preflightRequirements, err := o.GetPreflightRequirements(ctx, cluster) + if err != nil { + log.WithError(err).Errorf("Cannot retrieve preflight requirements for host %s", host.ID) + return nil, err + } + return preflightRequirements.Requirements.Worker.Quantitative, nil +} + +// GetPreflightRequirements returns operator hardware requirements that can be determined with cluster data only +func (o *operator) GetPreflightRequirements(context context.Context, cluster *common.Cluster) (*models.OperatorHardwareRequirements, error) { + dependencies, err := o.GetDependencies(cluster) + if err != nil { + return &models.OperatorHardwareRequirements{}, err + } + + return &models.OperatorHardwareRequirements{ + OperatorName: o.GetName(), + Dependencies: dependencies, + Requirements: &models.HostTypeHardwareRequirementsWrapper{ + Master: &models.HostTypeHardwareRequirements{ + Quantitative: &models.ClusterHostRequirementsDetails{}, + }, + Worker: &models.HostTypeHardwareRequirements{ + Quantitative: &models.ClusterHostRequirementsDetails{}, + }, + }, + }, nil +} + +func (o *operator) GetFeatureSupportID() models.FeatureSupportLevelID { + return models.FeatureSupportLevelIDNETWORKOBSERVABILITY +} + +// GetBundleLabels returns the bundle labels for the operator +func (o *operator) GetBundleLabels(featureIDs []models.FeatureSupportLevelID) []string { + return []string(Operator.Bundles) +} diff --git a/internal/operators/networkobservability/networkobservability_operator_test.go b/internal/operators/networkobservability/networkobservability_operator_test.go new file mode 100644 index 000000000000..7ca4ca489546 --- /dev/null +++ b/internal/operators/networkobservability/networkobservability_operator_test.go @@ -0,0 +1,163 @@ +package networkobservability + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/openshift/assisted-service/internal/common" + "github.com/openshift/assisted-service/internal/operators/api" + "github.com/openshift/assisted-service/models" + "github.com/sirupsen/logrus" +) + +var _ = Describe("Network Observability Operator", func() { + var ( + log = logrus.New() + operator *operator + cluster *common.Cluster + ctx = context.TODO() + ) + + BeforeEach(func() { + operator = NewNetworkObservabilityOperator(log) + cluster = &common.Cluster{Cluster: models.Cluster{ + OpenshiftVersion: "4.12.0", + }} + }) + + Context("GetName", func() { + It("should return correct name", func() { + Expect(operator.GetName()).To(Equal(Name)) + }) + }) + + Context("GetFullName", func() { + It("should return correct full name", func() { + Expect(operator.GetFullName()).To(Equal(FullName)) + }) + }) + + Context("GetDependencies", func() { + It("should return no dependencies", func() { + deps, err := operator.GetDependencies(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(deps).To(BeEmpty()) + }) + }) + + Context("GetDependenciesFeatureSupportID", func() { + It("should return nil for no dependencies", func() { + deps := operator.GetDependenciesFeatureSupportID() + Expect(deps).To(BeNil()) + }) + }) + + Context("ValidateCluster", func() { + It("should always succeed", func() { + results, err := operator.ValidateCluster(ctx, cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(results).To(HaveLen(1)) + Expect(results[0].Status).To(Equal(api.Success)) + Expect(results[0].ValidationId).To(Equal("network-observability-requirements-satisfied")) + }) + }) + + Context("GetPreflightRequirements", func() { + It("should return zero requirements", func() { + reqs, err := operator.GetPreflightRequirements(ctx, cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(reqs.OperatorName).To(Equal(Name)) + Expect(reqs.Requirements.Master.Quantitative.CPUCores).To(Equal(int64(0))) + Expect(reqs.Requirements.Master.Quantitative.RAMMib).To(Equal(int64(0))) + Expect(reqs.Requirements.Worker.Quantitative.CPUCores).To(Equal(int64(0))) + Expect(reqs.Requirements.Worker.Quantitative.RAMMib).To(Equal(int64(0))) + }) + }) + + Context("GenerateManifests", func() { + It("should generate manifests successfully", func() { + manifests, customManifest, err := operator.GenerateManifests(cluster) + Expect(err).ToNot(HaveOccurred()) + Expect(manifests).To(HaveKey("50_openshift-network-observability_ns.yaml")) + Expect(manifests).To(HaveKey("50_openshift-network-observability_operator_group.yaml")) + Expect(manifests).To(HaveKey("50_openshift-network-observability_subscription.yaml")) + // When FlowCollector is not created, customManifest may contain only YAML separators + Expect(string(customManifest)).To(Or(BeEmpty(), MatchRegexp(`^---\s*$`))) + }) + }) + + Context("ValidateHost", func() { + It("should always succeed", func() { + host := &models.Host{ + Role: models.HostRoleWorker, + } + result, err := operator.ValidateHost(ctx, cluster, host, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(result.Status).To(Equal(api.Success)) + Expect(result.ValidationId).To(Equal("network-observability-requirements-satisfied")) + }) + }) + + Context("GetProperties", func() { + It("should return correct properties", func() { + props := operator.GetProperties() + Expect(props).To(HaveLen(2)) + Expect(props[0].Name).To(Equal("createFlowCollector")) + Expect(props[0].DataType).To(Equal(models.OperatorPropertyDataTypeBoolean)) + Expect(props[1].Name).To(Equal("sampling")) + Expect(props[1].DataType).To(Equal(models.OperatorPropertyDataTypeInteger)) + }) + }) + + Context("GetMonitoredOperator", func() { + It("should return monitored operator with correct values", func() { + monOp := operator.GetMonitoredOperator() + Expect(monOp).ToNot(BeNil()) + Expect(monOp.Name).To(Equal(Name)) + Expect(monOp.Namespace).To(Equal(Namespace)) + Expect(monOp.SubscriptionName).To(Equal(SubscriptionName)) + Expect(monOp.OperatorType).To(Equal(models.OperatorTypeOlm)) + }) + }) + + Context("GetHostRequirements", func() { + It("should return zero requirements for worker", func() { + host := &models.Host{Role: models.HostRoleWorker} + reqs, err := operator.GetHostRequirements(ctx, cluster, host) + Expect(err).ToNot(HaveOccurred()) + Expect(reqs).ToNot(BeNil()) + Expect(reqs.CPUCores).To(Equal(int64(0))) + Expect(reqs.RAMMib).To(Equal(int64(0))) + }) + }) + + Context("GetFeatureSupportID", func() { + It("should return NETWORKOBSERVABILITY feature ID", func() { + featureID := operator.GetFeatureSupportID() + Expect(featureID).To(Equal(models.FeatureSupportLevelIDNETWORKOBSERVABILITY)) + }) + }) + + Context("GetBundleLabels", func() { + It("should return empty bundles", func() { + bundles := operator.GetBundleLabels(nil) + Expect(bundles).To(BeEmpty()) + }) + }) + + Context("GetClusterValidationIDs", func() { + It("should return correct validation ID", func() { + ids := operator.GetClusterValidationIDs() + Expect(ids).To(HaveLen(1)) + Expect(ids[0]).To(Equal("network-observability-requirements-satisfied")) + }) + }) + + Context("GetHostValidationID", func() { + It("should return correct validation ID", func() { + id := operator.GetHostValidationID() + Expect(id).To(Equal("network-observability-requirements-satisfied")) + }) + }) +}) diff --git a/internal/operators/networkobservability/networkobservability_suite_test.go b/internal/operators/networkobservability/networkobservability_suite_test.go new file mode 100644 index 000000000000..5406e95768cd --- /dev/null +++ b/internal/operators/networkobservability/networkobservability_suite_test.go @@ -0,0 +1,13 @@ +package networkobservability + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestNetworkObservabilityOperator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Network Observability Operator Suite") +} diff --git a/internal/operators/networkobservability/networkobservability_templates.go b/internal/operators/networkobservability/networkobservability_templates.go new file mode 100644 index 000000000000..88f02ae2831e --- /dev/null +++ b/internal/operators/networkobservability/networkobservability_templates.go @@ -0,0 +1,19 @@ +package networkobservability + +import ( + "embed" + "io/fs" +) + +//go:embed templates +var templatesFS embed.FS + +var templatesRoot fs.FS + +func init() { + var err error + templatesRoot, err = fs.Sub(templatesFS, "templates") + if err != nil { + panic("failed to initialize templates filesystem: " + err.Error()) + } +} diff --git a/internal/operators/networkobservability/templates/custom/flow_collector.yaml b/internal/operators/networkobservability/templates/custom/flow_collector.yaml new file mode 100644 index 000000000000..18c152dcb754 --- /dev/null +++ b/internal/operators/networkobservability/templates/custom/flow_collector.yaml @@ -0,0 +1,16 @@ +{{ if and .Config .Config.CreateFlowCollector }} +apiVersion: flows.netobserv.io/v1beta2 +kind: FlowCollector +metadata: + name: cluster + namespace: netobserv +spec: + agent: + type: eBPF + ebpf: + sampling: {{ .Config.Sampling }} + loki: + enabled: false + exporters: [] +{{ end }} + diff --git a/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_ns.yaml b/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_ns.yaml new file mode 100644 index 000000000000..de2107c86c7b --- /dev/null +++ b/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_ns.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Operator.Namespace }} + diff --git a/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_operator_group.yaml b/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_operator_group.yaml new file mode 100644 index 000000000000..15929553e3ba --- /dev/null +++ b/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_operator_group.yaml @@ -0,0 +1,7 @@ +apiVersion: operators.coreos.com/v1 +kind: OperatorGroup +metadata: + name: netobserv-operatorgroup + namespace: {{ .Operator.Namespace }} +spec: {} + diff --git a/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_subscription.yaml b/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_subscription.yaml new file mode 100644 index 000000000000..5a6350cb8a3c --- /dev/null +++ b/internal/operators/networkobservability/templates/openshift/50_openshift-network-observability_subscription.yaml @@ -0,0 +1,12 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + name: {{ .Operator.SubscriptionName }} + namespace: {{ .Operator.Namespace }} +spec: + channel: stable + source: redhat-operators + sourceNamespace: openshift-marketplace + name: netobserv-operator + installPlanApproval: Automatic + diff --git a/models/cluster_validation_id.go b/models/cluster_validation_id.go index 8d830e8a0e92..edae3ca7a52c 100644 --- a/models/cluster_validation_id.go +++ b/models/cluster_validation_id.go @@ -173,6 +173,9 @@ const ( // ClusterValidationIDOpenshiftLoggingRequirementsSatisfied captures enum value "openshift-logging-requirements-satisfied" ClusterValidationIDOpenshiftLoggingRequirementsSatisfied ClusterValidationID = "openshift-logging-requirements-satisfied" + + // ClusterValidationIDNetworkObservabilityRequirementsSatisfied captures enum value "network-observability-requirements-satisfied" + ClusterValidationIDNetworkObservabilityRequirementsSatisfied ClusterValidationID = "network-observability-requirements-satisfied" ) // for schema @@ -180,7 +183,7 @@ var clusterValidationIdEnum []interface{} func init() { var res []ClusterValidationID - if err := json.Unmarshal([]byte(`["machine-cidr-defined","cluster-cidr-defined","service-cidr-defined","no-cidrs-overlapping","networks-same-address-families","network-prefix-valid","machine-cidr-equals-to-calculated-cidr","api-vips-defined","api-vips-valid","ingress-vips-defined","ingress-vips-valid","all-hosts-are-ready-to-install","sufficient-masters-count","dns-domain-defined","pull-secret-set","ntp-server-configured","lso-requirements-satisfied","ocs-requirements-satisfied","odf-requirements-satisfied","cnv-requirements-satisfied","lvm-requirements-satisfied","mce-requirements-satisfied","mtv-requirements-satisfied","osc-requirements-satisfied","network-type-valid","platform-requirements-satisfied","node-feature-discovery-requirements-satisfied","nvidia-gpu-requirements-satisfied","pipelines-requirements-satisfied","servicemesh-requirements-satisfied","serverless-requirements-satisfied","openshift-ai-requirements-satisfied","openshift-ai-gpu-requirements-satisfied","authorino-requirements-satisfied","nmstate-requirements-satisfied","amd-gpu-requirements-satisfied","kmm-requirements-satisfied","node-healthcheck-requirements-satisfied","self-node-remediation-requirements-satisfied","fence-agents-remediation-requirements-satisfied","node-maintenance-requirements-satisfied","kube-descheduler-requirements-satisfied","cluster-observability-requirements-satisfied","numa-resources-requirements-satisfied","oadp-requirements-satisfied","metallb-requirements-satisfied","loki-requirements-satisfied","openshift-logging-requirements-satisfied"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["machine-cidr-defined","cluster-cidr-defined","service-cidr-defined","no-cidrs-overlapping","networks-same-address-families","network-prefix-valid","machine-cidr-equals-to-calculated-cidr","api-vips-defined","api-vips-valid","ingress-vips-defined","ingress-vips-valid","all-hosts-are-ready-to-install","sufficient-masters-count","dns-domain-defined","pull-secret-set","ntp-server-configured","lso-requirements-satisfied","ocs-requirements-satisfied","odf-requirements-satisfied","cnv-requirements-satisfied","lvm-requirements-satisfied","mce-requirements-satisfied","mtv-requirements-satisfied","osc-requirements-satisfied","network-type-valid","platform-requirements-satisfied","node-feature-discovery-requirements-satisfied","nvidia-gpu-requirements-satisfied","pipelines-requirements-satisfied","servicemesh-requirements-satisfied","serverless-requirements-satisfied","openshift-ai-requirements-satisfied","openshift-ai-gpu-requirements-satisfied","authorino-requirements-satisfied","nmstate-requirements-satisfied","amd-gpu-requirements-satisfied","kmm-requirements-satisfied","node-healthcheck-requirements-satisfied","self-node-remediation-requirements-satisfied","fence-agents-remediation-requirements-satisfied","node-maintenance-requirements-satisfied","kube-descheduler-requirements-satisfied","cluster-observability-requirements-satisfied","numa-resources-requirements-satisfied","oadp-requirements-satisfied","metallb-requirements-satisfied","loki-requirements-satisfied","openshift-logging-requirements-satisfied","network-observability-requirements-satisfied"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/models/feature_support_level_id.go b/models/feature_support_level_id.go index e93d850c43c3..81114a647831 100644 --- a/models/feature_support_level_id.go +++ b/models/feature_support_level_id.go @@ -177,6 +177,9 @@ const ( // FeatureSupportLevelIDMETALLB captures enum value "METALLB" FeatureSupportLevelIDMETALLB FeatureSupportLevelID = "METALLB" + // FeatureSupportLevelIDNETWORKOBSERVABILITY captures enum value "NETWORK_OBSERVABILITY" + FeatureSupportLevelIDNETWORKOBSERVABILITY FeatureSupportLevelID = "NETWORK_OBSERVABILITY" + // FeatureSupportLevelIDDUALSTACKPRIMARYIPV6 captures enum value "DUAL_STACK_PRIMARY_IPV6" FeatureSupportLevelIDDUALSTACKPRIMARYIPV6 FeatureSupportLevelID = "DUAL_STACK_PRIMARY_IPV6" @@ -192,7 +195,7 @@ var featureSupportLevelIdEnum []interface{} func init() { var res []FeatureSupportLevelID - if err := json.Unmarshal([]byte(`["SNO","TNA","TNF","VIP_AUTO_ALLOC","CUSTOM_MANIFEST","SINGLE_NODE_EXPANSION","LVM","ODF","LSO","CNV","MCE","MTV","OSC","NUTANIX_INTEGRATION","BAREMETAL_PLATFORM","NONE_PLATFORM","VSPHERE_INTEGRATION","DUAL_STACK_VIPS","CLUSTER_MANAGED_NETWORKING","USER_MANAGED_NETWORKING","MINIMAL_ISO","FULL_ISO","EXTERNAL_PLATFORM_OCI","DUAL_STACK","PLATFORM_MANAGED_NETWORKING","EXTERNAL_PLATFORM","OVN_NETWORK_TYPE","SDN_NETWORK_TYPE","NODE_FEATURE_DISCOVERY","NVIDIA_GPU","PIPELINES","SERVICEMESH","SERVERLESS","OPENSHIFT_AI","NON_STANDARD_HA_CONTROL_PLANE","AUTHORINO","USER_MANAGED_LOAD_BALANCER","NMSTATE","AMD_GPU","KMM","NODE_HEALTHCHECK","SELF_NODE_REMEDIATION","FENCE_AGENTS_REMEDIATION","NODE_MAINTENANCE","KUBE_DESCHEDULER","CLUSTER_OBSERVABILITY","NUMA_RESOURCES","OADP","METALLB","DUAL_STACK_PRIMARY_IPV6","LOKI","OPENSHIFT_LOGGING"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["SNO","TNA","TNF","VIP_AUTO_ALLOC","CUSTOM_MANIFEST","SINGLE_NODE_EXPANSION","LVM","ODF","LSO","CNV","MCE","MTV","OSC","NUTANIX_INTEGRATION","BAREMETAL_PLATFORM","NONE_PLATFORM","VSPHERE_INTEGRATION","DUAL_STACK_VIPS","CLUSTER_MANAGED_NETWORKING","USER_MANAGED_NETWORKING","MINIMAL_ISO","FULL_ISO","EXTERNAL_PLATFORM_OCI","DUAL_STACK","PLATFORM_MANAGED_NETWORKING","EXTERNAL_PLATFORM","OVN_NETWORK_TYPE","SDN_NETWORK_TYPE","NODE_FEATURE_DISCOVERY","NVIDIA_GPU","PIPELINES","SERVICEMESH","SERVERLESS","OPENSHIFT_AI","NON_STANDARD_HA_CONTROL_PLANE","AUTHORINO","USER_MANAGED_LOAD_BALANCER","NMSTATE","AMD_GPU","KMM","NODE_HEALTHCHECK","SELF_NODE_REMEDIATION","FENCE_AGENTS_REMEDIATION","NODE_MAINTENANCE","KUBE_DESCHEDULER","CLUSTER_OBSERVABILITY","NUMA_RESOURCES","OADP","METALLB","DUAL_STACK_PRIMARY_IPV6","LOKI","OPENSHIFT_LOGGING","NETWORK_OBSERVABILITY"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/models/host_validation_id.go b/models/host_validation_id.go index f5fa6b1c704b..47e6c71bc28f 100644 --- a/models/host_validation_id.go +++ b/models/host_validation_id.go @@ -227,6 +227,9 @@ const ( // HostValidationIDOpenshiftLoggingRequirementsSatisfied captures enum value "openshift-logging-requirements-satisfied" HostValidationIDOpenshiftLoggingRequirementsSatisfied HostValidationID = "openshift-logging-requirements-satisfied" + + // HostValidationIDNetworkObservabilityRequirementsSatisfied captures enum value "network-observability-requirements-satisfied" + HostValidationIDNetworkObservabilityRequirementsSatisfied HostValidationID = "network-observability-requirements-satisfied" ) // for schema @@ -234,7 +237,7 @@ var hostValidationIdEnum []interface{} func init() { var res []HostValidationID - if err := json.Unmarshal([]byte(`["connected","media-connected","has-inventory","has-min-cpu-cores","has-min-valid-disks","has-min-memory","machine-cidr-defined","has-cpu-cores-for-role","has-memory-for-role","hostname-unique","hostname-valid","belongs-to-machine-cidr","ignition-downloadable","belongs-to-majority-group","valid-platform-network-settings","ntp-synced","time-synced-between-host-and-service","container-images-available","lso-requirements-satisfied","ocs-requirements-satisfied","odf-requirements-satisfied","lvm-requirements-satisfied","mce-requirements-satisfied","mtv-requirements-satisfied","osc-requirements-satisfied","sufficient-installation-disk-speed","cnv-requirements-satisfied","sufficient-network-latency-requirement-for-role","sufficient-packet-loss-requirement-for-role","has-default-route","api-domain-name-resolved-correctly","api-int-domain-name-resolved-correctly","apps-domain-name-resolved-correctly","release-domain-name-resolved-correctly","compatible-with-cluster-platform","dns-wildcard-not-configured","disk-encryption-requirements-satisfied","non-overlapping-subnets","vsphere-disk-uuid-enabled","compatible-agent","no-skip-installation-disk","no-skip-missing-disk","no-ip-collisions-in-network","no-iscsi-nic-belongs-to-machine-cidr","node-feature-discovery-requirements-satisfied","nvidia-gpu-requirements-satisfied","pipelines-requirements-satisfied","servicemesh-requirements-satisfied","serverless-requirements-satisfied","openshift-ai-requirements-satisfied","authorino-requirements-satisfied","mtu-valid","nmstate-requirements-satisfied","amd-gpu-requirements-satisfied","kmm-requirements-satisfied","node-healthcheck-requirements-satisfied","self-node-remediation-requirements-satisfied","fence-agents-remediation-requirements-satisfied","node-maintenance-requirements-satisfied","kube-descheduler-requirements-satisfied","cluster-observability-requirements-satisfied","numa-resources-requirements-satisfied","oadp-requirements-satisfied","metallb-requirements-satisfied","loki-requirements-satisfied","openshift-logging-requirements-satisfied"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["connected","media-connected","has-inventory","has-min-cpu-cores","has-min-valid-disks","has-min-memory","machine-cidr-defined","has-cpu-cores-for-role","has-memory-for-role","hostname-unique","hostname-valid","belongs-to-machine-cidr","ignition-downloadable","belongs-to-majority-group","valid-platform-network-settings","ntp-synced","time-synced-between-host-and-service","container-images-available","lso-requirements-satisfied","ocs-requirements-satisfied","odf-requirements-satisfied","lvm-requirements-satisfied","mce-requirements-satisfied","mtv-requirements-satisfied","osc-requirements-satisfied","sufficient-installation-disk-speed","cnv-requirements-satisfied","sufficient-network-latency-requirement-for-role","sufficient-packet-loss-requirement-for-role","has-default-route","api-domain-name-resolved-correctly","api-int-domain-name-resolved-correctly","apps-domain-name-resolved-correctly","release-domain-name-resolved-correctly","compatible-with-cluster-platform","dns-wildcard-not-configured","disk-encryption-requirements-satisfied","non-overlapping-subnets","vsphere-disk-uuid-enabled","compatible-agent","no-skip-installation-disk","no-skip-missing-disk","no-ip-collisions-in-network","no-iscsi-nic-belongs-to-machine-cidr","node-feature-discovery-requirements-satisfied","nvidia-gpu-requirements-satisfied","pipelines-requirements-satisfied","servicemesh-requirements-satisfied","serverless-requirements-satisfied","openshift-ai-requirements-satisfied","authorino-requirements-satisfied","mtu-valid","nmstate-requirements-satisfied","amd-gpu-requirements-satisfied","kmm-requirements-satisfied","node-healthcheck-requirements-satisfied","self-node-remediation-requirements-satisfied","fence-agents-remediation-requirements-satisfied","node-maintenance-requirements-satisfied","kube-descheduler-requirements-satisfied","cluster-observability-requirements-satisfied","numa-resources-requirements-satisfied","oadp-requirements-satisfied","metallb-requirements-satisfied","loki-requirements-satisfied","openshift-logging-requirements-satisfied","network-observability-requirements-satisfied"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 1e30acb14f70..ae3059028838 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -6274,7 +6274,8 @@ func init() { "oadp", "metallb", "loki", - "openshift-logging" + "openshift-logging", + "network-observability" ] } } @@ -7340,7 +7341,8 @@ func init() { "oadp-requirements-satisfied", "metallb-requirements-satisfied", "loki-requirements-satisfied", - "openshift-logging-requirements-satisfied" + "openshift-logging-requirements-satisfied", + "network-observability-requirements-satisfied" ] }, "cluster_default_config": { @@ -8258,7 +8260,8 @@ func init() { "METALLB", "DUAL_STACK_PRIMARY_IPV6", "LOKI", - "OPENSHIFT_LOGGING" + "OPENSHIFT_LOGGING", + "NETWORK_OBSERVABILITY" ], "x-nullable": false }, @@ -8944,7 +8947,8 @@ func init() { "oadp-requirements-satisfied", "metallb-requirements-satisfied", "loki-requirements-satisfied", - "openshift-logging-requirements-satisfied" + "openshift-logging-requirements-satisfied", + "network-observability-requirements-satisfied" ] }, "host_network": { @@ -17721,7 +17725,8 @@ func init() { "oadp", "metallb", "loki", - "openshift-logging" + "openshift-logging", + "network-observability" ] } } @@ -18905,7 +18910,8 @@ func init() { "oadp-requirements-satisfied", "metallb-requirements-satisfied", "loki-requirements-satisfied", - "openshift-logging-requirements-satisfied" + "openshift-logging-requirements-satisfied", + "network-observability-requirements-satisfied" ] }, "cluster_default_config": { @@ -19790,7 +19796,8 @@ func init() { "METALLB", "DUAL_STACK_PRIMARY_IPV6", "LOKI", - "OPENSHIFT_LOGGING" + "OPENSHIFT_LOGGING", + "NETWORK_OBSERVABILITY" ], "x-nullable": false }, @@ -20476,7 +20483,8 @@ func init() { "oadp-requirements-satisfied", "metallb-requirements-satisfied", "loki-requirements-satisfied", - "openshift-logging-requirements-satisfied" + "openshift-logging-requirements-satisfied", + "network-observability-requirements-satisfied" ] }, "host_network": { diff --git a/subsystem/operators_test.go b/subsystem/operators_test.go index 2991b1ddefef..fcb214f44363 100644 --- a/subsystem/operators_test.go +++ b/subsystem/operators_test.go @@ -30,6 +30,7 @@ import ( "github.com/openshift/assisted-service/internal/operators/mce" "github.com/openshift/assisted-service/internal/operators/metallb" "github.com/openshift/assisted-service/internal/operators/mtv" + "github.com/openshift/assisted-service/internal/operators/networkobservability" "github.com/openshift/assisted-service/internal/operators/nmstate" "github.com/openshift/assisted-service/internal/operators/nodefeaturediscovery" "github.com/openshift/assisted-service/internal/operators/nodehealthcheck" @@ -89,6 +90,7 @@ var _ = Describe("Operators endpoint tests", func() { metallb.Operator.Name, loki.Operator.Name, openshiftlogging.Operator.Name, + networkobservability.Operator.Name, )) }) diff --git a/swagger.yaml b/swagger.yaml index 72e7dbc213cb..14d36b2e6dfb 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -2732,6 +2732,7 @@ paths: - 'metallb' - 'loki' - 'openshift-logging' + - 'network-observability' "401": description: Unauthorized. schema: @@ -4435,6 +4436,7 @@ definitions: - 'NUMA_RESOURCES' - 'OADP' - 'METALLB' + - 'NETWORK_OBSERVABILITY' - 'DUAL_STACK_PRIMARY_IPV6' - 'LOKI' - 'OPENSHIFT_LOGGING' @@ -6968,6 +6970,7 @@ definitions: - 'metallb-requirements-satisfied' - 'loki-requirements-satisfied' - 'openshift-logging-requirements-satisfied' + - 'network-observability-requirements-satisfied' dhcp_allocation_request: type: object @@ -7356,6 +7359,7 @@ definitions: - 'metallb-requirements-satisfied' - 'loki-requirements-satisfied' - 'openshift-logging-requirements-satisfied' + - 'network-observability-requirements-satisfied' logs_type: type: string diff --git a/vendor/github.com/openshift/assisted-service/models/cluster_validation_id.go b/vendor/github.com/openshift/assisted-service/models/cluster_validation_id.go index 8d830e8a0e92..edae3ca7a52c 100644 --- a/vendor/github.com/openshift/assisted-service/models/cluster_validation_id.go +++ b/vendor/github.com/openshift/assisted-service/models/cluster_validation_id.go @@ -173,6 +173,9 @@ const ( // ClusterValidationIDOpenshiftLoggingRequirementsSatisfied captures enum value "openshift-logging-requirements-satisfied" ClusterValidationIDOpenshiftLoggingRequirementsSatisfied ClusterValidationID = "openshift-logging-requirements-satisfied" + + // ClusterValidationIDNetworkObservabilityRequirementsSatisfied captures enum value "network-observability-requirements-satisfied" + ClusterValidationIDNetworkObservabilityRequirementsSatisfied ClusterValidationID = "network-observability-requirements-satisfied" ) // for schema @@ -180,7 +183,7 @@ var clusterValidationIdEnum []interface{} func init() { var res []ClusterValidationID - if err := json.Unmarshal([]byte(`["machine-cidr-defined","cluster-cidr-defined","service-cidr-defined","no-cidrs-overlapping","networks-same-address-families","network-prefix-valid","machine-cidr-equals-to-calculated-cidr","api-vips-defined","api-vips-valid","ingress-vips-defined","ingress-vips-valid","all-hosts-are-ready-to-install","sufficient-masters-count","dns-domain-defined","pull-secret-set","ntp-server-configured","lso-requirements-satisfied","ocs-requirements-satisfied","odf-requirements-satisfied","cnv-requirements-satisfied","lvm-requirements-satisfied","mce-requirements-satisfied","mtv-requirements-satisfied","osc-requirements-satisfied","network-type-valid","platform-requirements-satisfied","node-feature-discovery-requirements-satisfied","nvidia-gpu-requirements-satisfied","pipelines-requirements-satisfied","servicemesh-requirements-satisfied","serverless-requirements-satisfied","openshift-ai-requirements-satisfied","openshift-ai-gpu-requirements-satisfied","authorino-requirements-satisfied","nmstate-requirements-satisfied","amd-gpu-requirements-satisfied","kmm-requirements-satisfied","node-healthcheck-requirements-satisfied","self-node-remediation-requirements-satisfied","fence-agents-remediation-requirements-satisfied","node-maintenance-requirements-satisfied","kube-descheduler-requirements-satisfied","cluster-observability-requirements-satisfied","numa-resources-requirements-satisfied","oadp-requirements-satisfied","metallb-requirements-satisfied","loki-requirements-satisfied","openshift-logging-requirements-satisfied"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["machine-cidr-defined","cluster-cidr-defined","service-cidr-defined","no-cidrs-overlapping","networks-same-address-families","network-prefix-valid","machine-cidr-equals-to-calculated-cidr","api-vips-defined","api-vips-valid","ingress-vips-defined","ingress-vips-valid","all-hosts-are-ready-to-install","sufficient-masters-count","dns-domain-defined","pull-secret-set","ntp-server-configured","lso-requirements-satisfied","ocs-requirements-satisfied","odf-requirements-satisfied","cnv-requirements-satisfied","lvm-requirements-satisfied","mce-requirements-satisfied","mtv-requirements-satisfied","osc-requirements-satisfied","network-type-valid","platform-requirements-satisfied","node-feature-discovery-requirements-satisfied","nvidia-gpu-requirements-satisfied","pipelines-requirements-satisfied","servicemesh-requirements-satisfied","serverless-requirements-satisfied","openshift-ai-requirements-satisfied","openshift-ai-gpu-requirements-satisfied","authorino-requirements-satisfied","nmstate-requirements-satisfied","amd-gpu-requirements-satisfied","kmm-requirements-satisfied","node-healthcheck-requirements-satisfied","self-node-remediation-requirements-satisfied","fence-agents-remediation-requirements-satisfied","node-maintenance-requirements-satisfied","kube-descheduler-requirements-satisfied","cluster-observability-requirements-satisfied","numa-resources-requirements-satisfied","oadp-requirements-satisfied","metallb-requirements-satisfied","loki-requirements-satisfied","openshift-logging-requirements-satisfied","network-observability-requirements-satisfied"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/vendor/github.com/openshift/assisted-service/models/feature_support_level_id.go b/vendor/github.com/openshift/assisted-service/models/feature_support_level_id.go index e93d850c43c3..81114a647831 100644 --- a/vendor/github.com/openshift/assisted-service/models/feature_support_level_id.go +++ b/vendor/github.com/openshift/assisted-service/models/feature_support_level_id.go @@ -177,6 +177,9 @@ const ( // FeatureSupportLevelIDMETALLB captures enum value "METALLB" FeatureSupportLevelIDMETALLB FeatureSupportLevelID = "METALLB" + // FeatureSupportLevelIDNETWORKOBSERVABILITY captures enum value "NETWORK_OBSERVABILITY" + FeatureSupportLevelIDNETWORKOBSERVABILITY FeatureSupportLevelID = "NETWORK_OBSERVABILITY" + // FeatureSupportLevelIDDUALSTACKPRIMARYIPV6 captures enum value "DUAL_STACK_PRIMARY_IPV6" FeatureSupportLevelIDDUALSTACKPRIMARYIPV6 FeatureSupportLevelID = "DUAL_STACK_PRIMARY_IPV6" @@ -192,7 +195,7 @@ var featureSupportLevelIdEnum []interface{} func init() { var res []FeatureSupportLevelID - if err := json.Unmarshal([]byte(`["SNO","TNA","TNF","VIP_AUTO_ALLOC","CUSTOM_MANIFEST","SINGLE_NODE_EXPANSION","LVM","ODF","LSO","CNV","MCE","MTV","OSC","NUTANIX_INTEGRATION","BAREMETAL_PLATFORM","NONE_PLATFORM","VSPHERE_INTEGRATION","DUAL_STACK_VIPS","CLUSTER_MANAGED_NETWORKING","USER_MANAGED_NETWORKING","MINIMAL_ISO","FULL_ISO","EXTERNAL_PLATFORM_OCI","DUAL_STACK","PLATFORM_MANAGED_NETWORKING","EXTERNAL_PLATFORM","OVN_NETWORK_TYPE","SDN_NETWORK_TYPE","NODE_FEATURE_DISCOVERY","NVIDIA_GPU","PIPELINES","SERVICEMESH","SERVERLESS","OPENSHIFT_AI","NON_STANDARD_HA_CONTROL_PLANE","AUTHORINO","USER_MANAGED_LOAD_BALANCER","NMSTATE","AMD_GPU","KMM","NODE_HEALTHCHECK","SELF_NODE_REMEDIATION","FENCE_AGENTS_REMEDIATION","NODE_MAINTENANCE","KUBE_DESCHEDULER","CLUSTER_OBSERVABILITY","NUMA_RESOURCES","OADP","METALLB","DUAL_STACK_PRIMARY_IPV6","LOKI","OPENSHIFT_LOGGING"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["SNO","TNA","TNF","VIP_AUTO_ALLOC","CUSTOM_MANIFEST","SINGLE_NODE_EXPANSION","LVM","ODF","LSO","CNV","MCE","MTV","OSC","NUTANIX_INTEGRATION","BAREMETAL_PLATFORM","NONE_PLATFORM","VSPHERE_INTEGRATION","DUAL_STACK_VIPS","CLUSTER_MANAGED_NETWORKING","USER_MANAGED_NETWORKING","MINIMAL_ISO","FULL_ISO","EXTERNAL_PLATFORM_OCI","DUAL_STACK","PLATFORM_MANAGED_NETWORKING","EXTERNAL_PLATFORM","OVN_NETWORK_TYPE","SDN_NETWORK_TYPE","NODE_FEATURE_DISCOVERY","NVIDIA_GPU","PIPELINES","SERVICEMESH","SERVERLESS","OPENSHIFT_AI","NON_STANDARD_HA_CONTROL_PLANE","AUTHORINO","USER_MANAGED_LOAD_BALANCER","NMSTATE","AMD_GPU","KMM","NODE_HEALTHCHECK","SELF_NODE_REMEDIATION","FENCE_AGENTS_REMEDIATION","NODE_MAINTENANCE","KUBE_DESCHEDULER","CLUSTER_OBSERVABILITY","NUMA_RESOURCES","OADP","METALLB","DUAL_STACK_PRIMARY_IPV6","LOKI","OPENSHIFT_LOGGING","NETWORK_OBSERVABILITY"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/vendor/github.com/openshift/assisted-service/models/host_validation_id.go b/vendor/github.com/openshift/assisted-service/models/host_validation_id.go index f5fa6b1c704b..47e6c71bc28f 100644 --- a/vendor/github.com/openshift/assisted-service/models/host_validation_id.go +++ b/vendor/github.com/openshift/assisted-service/models/host_validation_id.go @@ -227,6 +227,9 @@ const ( // HostValidationIDOpenshiftLoggingRequirementsSatisfied captures enum value "openshift-logging-requirements-satisfied" HostValidationIDOpenshiftLoggingRequirementsSatisfied HostValidationID = "openshift-logging-requirements-satisfied" + + // HostValidationIDNetworkObservabilityRequirementsSatisfied captures enum value "network-observability-requirements-satisfied" + HostValidationIDNetworkObservabilityRequirementsSatisfied HostValidationID = "network-observability-requirements-satisfied" ) // for schema @@ -234,7 +237,7 @@ var hostValidationIdEnum []interface{} func init() { var res []HostValidationID - if err := json.Unmarshal([]byte(`["connected","media-connected","has-inventory","has-min-cpu-cores","has-min-valid-disks","has-min-memory","machine-cidr-defined","has-cpu-cores-for-role","has-memory-for-role","hostname-unique","hostname-valid","belongs-to-machine-cidr","ignition-downloadable","belongs-to-majority-group","valid-platform-network-settings","ntp-synced","time-synced-between-host-and-service","container-images-available","lso-requirements-satisfied","ocs-requirements-satisfied","odf-requirements-satisfied","lvm-requirements-satisfied","mce-requirements-satisfied","mtv-requirements-satisfied","osc-requirements-satisfied","sufficient-installation-disk-speed","cnv-requirements-satisfied","sufficient-network-latency-requirement-for-role","sufficient-packet-loss-requirement-for-role","has-default-route","api-domain-name-resolved-correctly","api-int-domain-name-resolved-correctly","apps-domain-name-resolved-correctly","release-domain-name-resolved-correctly","compatible-with-cluster-platform","dns-wildcard-not-configured","disk-encryption-requirements-satisfied","non-overlapping-subnets","vsphere-disk-uuid-enabled","compatible-agent","no-skip-installation-disk","no-skip-missing-disk","no-ip-collisions-in-network","no-iscsi-nic-belongs-to-machine-cidr","node-feature-discovery-requirements-satisfied","nvidia-gpu-requirements-satisfied","pipelines-requirements-satisfied","servicemesh-requirements-satisfied","serverless-requirements-satisfied","openshift-ai-requirements-satisfied","authorino-requirements-satisfied","mtu-valid","nmstate-requirements-satisfied","amd-gpu-requirements-satisfied","kmm-requirements-satisfied","node-healthcheck-requirements-satisfied","self-node-remediation-requirements-satisfied","fence-agents-remediation-requirements-satisfied","node-maintenance-requirements-satisfied","kube-descheduler-requirements-satisfied","cluster-observability-requirements-satisfied","numa-resources-requirements-satisfied","oadp-requirements-satisfied","metallb-requirements-satisfied","loki-requirements-satisfied","openshift-logging-requirements-satisfied"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["connected","media-connected","has-inventory","has-min-cpu-cores","has-min-valid-disks","has-min-memory","machine-cidr-defined","has-cpu-cores-for-role","has-memory-for-role","hostname-unique","hostname-valid","belongs-to-machine-cidr","ignition-downloadable","belongs-to-majority-group","valid-platform-network-settings","ntp-synced","time-synced-between-host-and-service","container-images-available","lso-requirements-satisfied","ocs-requirements-satisfied","odf-requirements-satisfied","lvm-requirements-satisfied","mce-requirements-satisfied","mtv-requirements-satisfied","osc-requirements-satisfied","sufficient-installation-disk-speed","cnv-requirements-satisfied","sufficient-network-latency-requirement-for-role","sufficient-packet-loss-requirement-for-role","has-default-route","api-domain-name-resolved-correctly","api-int-domain-name-resolved-correctly","apps-domain-name-resolved-correctly","release-domain-name-resolved-correctly","compatible-with-cluster-platform","dns-wildcard-not-configured","disk-encryption-requirements-satisfied","non-overlapping-subnets","vsphere-disk-uuid-enabled","compatible-agent","no-skip-installation-disk","no-skip-missing-disk","no-ip-collisions-in-network","no-iscsi-nic-belongs-to-machine-cidr","node-feature-discovery-requirements-satisfied","nvidia-gpu-requirements-satisfied","pipelines-requirements-satisfied","servicemesh-requirements-satisfied","serverless-requirements-satisfied","openshift-ai-requirements-satisfied","authorino-requirements-satisfied","mtu-valid","nmstate-requirements-satisfied","amd-gpu-requirements-satisfied","kmm-requirements-satisfied","node-healthcheck-requirements-satisfied","self-node-remediation-requirements-satisfied","fence-agents-remediation-requirements-satisfied","node-maintenance-requirements-satisfied","kube-descheduler-requirements-satisfied","cluster-observability-requirements-satisfied","numa-resources-requirements-satisfied","oadp-requirements-satisfied","metallb-requirements-satisfied","loki-requirements-satisfied","openshift-logging-requirements-satisfied","network-observability-requirements-satisfied"]`), &res); err != nil { panic(err) } for _, v := range res {