Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion internal/cluster/validation_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)))
Expand Down
1 change: 1 addition & 0 deletions internal/featuresupport/feature_support_level.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
35 changes: 35 additions & 0 deletions internal/featuresupport/features_olm_operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
4 changes: 3 additions & 1 deletion internal/host/validation_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)))
Expand Down
2 changes: 2 additions & 0 deletions internal/operators/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
config.Sampling = 50
}

return config, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package networkobservability

import (
. "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 := nsData["metadata"].(map[string]interface{})
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 := subData["metadata"].(map[string]interface{})
Expect(metadata["name"]).To(Equal(SubscriptionName))
Expect(metadata["namespace"]).To(Equal(Namespace))

spec := subData["spec"].(map[string]interface{})
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 := ogData["metadata"].(map[string]interface{})
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 custom manifest (it's a multi-document YAML)
var flowCollectorData map[string]interface{}
err = yaml.Unmarshal(customManifest, &flowCollectorData)
Expect(err).ToNot(HaveOccurred())

Expect(flowCollectorData["kind"]).To(Equal("FlowCollector"))
metadata := flowCollectorData["metadata"].(map[string]interface{})
Expect(metadata["name"]).To(Equal("cluster"))
Expect(metadata["namespace"]).To(Equal("netobserv"))

spec := flowCollectorData["spec"].(map[string]interface{})
agent := spec["agent"].(map[string]interface{})
ebpf := agent["ebpf"].(map[string]interface{})
Expect(ebpf["sampling"]).To(Equal(100))

loki := spec["loki"].(map[string]interface{})
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())

var flowCollectorData map[string]interface{}
err = yaml.Unmarshal(customManifest, &flowCollectorData)
Expect(err).ToNot(HaveOccurred())

spec := flowCollectorData["spec"].(map[string]interface{})
agent := spec["agent"].(map[string]interface{})
ebpf := agent["ebpf"].(map[string]interface{})
Expect(ebpf["sampling"]).To(Equal(50)) // Default value

loki := spec["loki"].(map[string]interface{})
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*$`)))
})
})
})
Loading