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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 194 additions & 7 deletions pkg/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"os"
"path/filepath"
"reflect"
"strconv"
"strings"

"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog/v2"

configv1 "github.com/openshift/api/config/v1"
features "github.com/openshift/api/features"
Expand All @@ -23,10 +25,11 @@ import (
)

const (
CapabilityAnnotation = "capability.openshift.io/name"
DefaultClusterProfile = "self-managed-high-availability"
featureSetAnnotation = "release.openshift.io/feature-set"
featureGateAnnotation = "release.openshift.io/feature-gate"
CapabilityAnnotation = "capability.openshift.io/name"
DefaultClusterProfile = "self-managed-high-availability"
featureSetAnnotation = "release.openshift.io/feature-set"
featureGateAnnotation = "release.openshift.io/feature-gate"
majorVersionAnnotation = "release.openshift.io/major-version"
)

var knownFeatureSets = sets.Set[string]{}
Expand Down Expand Up @@ -232,12 +235,176 @@ func checkFeatureGates(enabledGates sets.Set[string], annotations map[string]str
return nil
}

type versionOperator string

const (
versionOperatorEquals versionOperator = "=="
versionOperatorGreaterThanEqual versionOperator = ">="
versionOperatorLessThanEqual versionOperator = "<="
versionOperatorGreaterThan versionOperator = ">"
versionOperatorLessThan versionOperator = "<"
versionOperatorNotEqual versionOperator = "!="
)

type requirement interface {
evaluate(clusterVersion uint64) bool
String() string
}

// versionRequirement represents a parsed version requirement with operator and target version
type versionRequirement struct {
operator versionOperator
version uint64
}

// evaluate checks if the cluster version satisfies the requirement
func (v versionRequirement) evaluate(clusterVersion uint64) bool {
switch v.operator {
// No operator prefix will be treated as equals.
case versionOperatorEquals, "":
return clusterVersion == v.version
case versionOperatorGreaterThanEqual:
return clusterVersion >= v.version
case versionOperatorLessThanEqual:
return clusterVersion <= v.version
case versionOperatorGreaterThan:
return clusterVersion > v.version
case versionOperatorLessThan:
return clusterVersion < v.version
case versionOperatorNotEqual:
return clusterVersion != v.version
default:
return false
}
}

func (v versionRequirement) String() string {
return fmt.Sprintf("%s%d", v.operator, v.version)
}

type multipleRequirement struct {
requirements []requirement
}

func (m multipleRequirement) evaluate(clusterVersion uint64) bool {
for _, req := range m.requirements {
if !req.evaluate(clusterVersion) {
return false
}
}
return true
}

func (m multipleRequirement) String() string {
requirementsStrings := make([]string, len(m.requirements))
for i, req := range m.requirements {
requirementsStrings[i] = req.String()
}
return strings.Join(requirementsStrings, "&")
}

// parseVersionRequirement parses a version requirement string and returns the operator and version
func parseVersionRequirement(req string) (requirement, error) {
multipleRequirements := strings.Split(req, "&")
if len(multipleRequirements) > 1 {
requirements := make([]requirement, len(multipleRequirements))
for i, req := range multipleRequirements {
var err error
requirements[i], err = parseVersionRequirement(req)
if err != nil {
return versionRequirement{}, err
}
}
return multipleRequirement{requirements: requirements}, nil
}

req = strings.TrimSpace(req)
if req == "" {
return versionRequirement{}, fmt.Errorf("empty version requirement")
}

// Handle operators (>=, <=, >, <, ==. !=)
var operator versionOperator
var versionStr string

switch {
case strings.HasPrefix(req, string(versionOperatorGreaterThanEqual)):
operator = versionOperatorGreaterThanEqual
versionStr = strings.TrimPrefix(req, string(versionOperatorGreaterThanEqual))
case strings.HasPrefix(req, string(versionOperatorLessThanEqual)):
operator = versionOperatorLessThanEqual
versionStr = strings.TrimPrefix(req, string(versionOperatorLessThanEqual))
case strings.HasPrefix(req, string(versionOperatorGreaterThan)):
operator = versionOperatorGreaterThan
versionStr = strings.TrimPrefix(req, string(versionOperatorGreaterThan))
case strings.HasPrefix(req, string(versionOperatorLessThan)):
operator = versionOperatorLessThan
versionStr = strings.TrimPrefix(req, string(versionOperatorLessThan))
case strings.HasPrefix(req, string(versionOperatorEquals)):
operator = versionOperatorEquals
versionStr = strings.TrimPrefix(req, string(versionOperatorEquals))
case strings.HasPrefix(req, string(versionOperatorNotEqual)):
operator = versionOperatorNotEqual
versionStr = strings.TrimPrefix(req, string(versionOperatorNotEqual))
default:
versionStr = req
}

// At this point, we either have an integer value or an unknown operator.
// With an unknown operator, we will return an error.
version, err := strconv.ParseUint(strings.TrimSpace(versionStr), 10, 64)
if err != nil {
return versionRequirement{}, fmt.Errorf("invalid major version %q in requirement %q: %v", versionStr, req, err)
}

return versionRequirement{operator: operator, version: version}, nil
}

// checkMajorVersion validates if manifest should be included based on major version requirements
func checkMajorVersion(clusterMajorVersion uint64, annotations map[string]string) error {
if annotations == nil {
return nil // No annotations, include by default
}
majorVersionRequirements, ok := annotations[majorVersionAnnotation]
if !ok {
return nil // No requirements, include by default
}

requirements := strings.Split(majorVersionRequirements, ",")
var inclusionReqs []requirement

// Parse all requirements
for _, req := range requirements {
req = strings.TrimSpace(req)
if req == "" {
continue
}

parsedReq, err := parseVersionRequirement(req)
if err != nil {
return fmt.Errorf("invalid version requirement %q in annotation: %v", req, err)
}

inclusionReqs = append(inclusionReqs, parsedReq)
}

for _, inclReq := range inclusionReqs {
if inclReq.evaluate(clusterMajorVersion) {
klog.V(4).Infof("major version %d satisfies inclusion requirement %s", clusterMajorVersion, inclReq.String())
return nil // Found a matching inclusion requirement
}
}

// No inclusion requirements matched
return fmt.Errorf("major version %d does not satisfy any inclusion requirements", clusterMajorVersion)
}

// Include returns an error if the manifest fails an inclusion filter and should be excluded from further
// processing by cluster version operator. Pointer arguments can be set nil to avoid excluding based on that
// filter. For example, setting profile non-nil and capabilities nil will return an error if the manifest's
// profile does not match, but will never return an error about capability issues.
func (m *Manifest) Include(excludeIdentifier *string, requiredFeatureSet *string, profile *string, capabilities *configv1.ClusterVersionCapabilitiesStatus, overrides []configv1.ComponentOverride, enabledFeatureGates sets.Set[string]) error {
return m.IncludeAllowUnknownCapabilities(excludeIdentifier, requiredFeatureSet, profile, capabilities, overrides, enabledFeatureGates, false)
func (m *Manifest) Include(excludeIdentifier *string, requiredFeatureSet *string, profile *string, capabilities *configv1.ClusterVersionCapabilitiesStatus, overrides []configv1.ComponentOverride, enabledFeatureGates sets.Set[string], majorVersion *uint64) error {
return m.IncludeAllowUnknownCapabilities(excludeIdentifier, requiredFeatureSet, profile, capabilities, overrides, enabledFeatureGates, majorVersion, false)
}

// IncludeAllowUnknownCapabilities returns an error if the manifest fails an inclusion filter and should be excluded from
Expand All @@ -247,7 +414,7 @@ func (m *Manifest) Include(excludeIdentifier *string, requiredFeatureSet *string
// to capabilities filtering. When set to true a manifest will not be excluded simply because it contains an unknown
// capability. This is necessary to allow updates to an OCP version containing newly defined capabilities.
func (m *Manifest) IncludeAllowUnknownCapabilities(excludeIdentifier *string, requiredFeatureSet *string, profile *string,
capabilities *configv1.ClusterVersionCapabilitiesStatus, overrides []configv1.ComponentOverride, enabledFeatureGates sets.Set[string], allowUnknownCapabilities bool) error {
capabilities *configv1.ClusterVersionCapabilitiesStatus, overrides []configv1.ComponentOverride, enabledFeatureGates sets.Set[string], majorVersion *uint64, allowUnknownCapabilities bool) error {

annotations := m.Obj.GetAnnotations()
if annotations == nil {
Expand Down Expand Up @@ -282,6 +449,18 @@ func (m *Manifest) IncludeAllowUnknownCapabilities(excludeIdentifier *string, re
}
}

// Major version filtering
if majorVersion != nil {
if !isFeatureGate(m.GVK) && !isCustomResource(m.GVK) {
return fmt.Errorf("major version filtering is only supported for feature gates and custom resources")
}

err := checkMajorVersion(*majorVersion, annotations)
if err != nil {
return err
}
}

if profile != nil {
profileAnnotation := fmt.Sprintf("include.release.openshift.io/%s", *profile)
if val, ok := annotations[profileAnnotation]; ok && val != "true" {
Expand Down Expand Up @@ -481,3 +660,11 @@ func addIfNotDuplicateResource(manifest Manifest, resourceIds map[resourceId]boo
}
return fmt.Errorf("duplicate resource: (%s)", manifest.id)
}

func isFeatureGate(gvk schema.GroupVersionKind) bool {
return gvk.Group == "config.openshift.io" && gvk.Kind == "FeatureGate"
}

func isCustomResource(gvk schema.GroupVersionKind) bool {
return gvk.Group == "apiextensions.k8s.io/" && gvk.Kind == "CustomResourceDefinition"
}
Loading