Skip to content
Merged
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
98 changes: 98 additions & 0 deletions pkg/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"io"
"os"
"path/filepath"
"strings"

configv1 "github.com/openshift/api/config/v1"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand All @@ -15,6 +17,11 @@ import (
"k8s.io/client-go/kubernetes/scheme"
)

const (
CapabilityAnnotation = "capability.openshift.io/name"
DefaultClusterProfile = "self-managed-high-availability"
)

// resourceId uniquely identifies a Kubernetes resource.
// It is used to identify any duplicate resources within
// a given set of manifests.
Expand Down Expand Up @@ -98,6 +105,97 @@ func (m *Manifest) UnmarshalJSON(in []byte) error {
return validateResourceId(m.id)
}

// Include returns an error if the manifest fails an inclusion filter and should not be excluded from futher
// 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, includeTechPreview bool, profile *string, capabilities *configv1.ClusterVersionCapabilitiesStatus) error {

annotations := m.Obj.GetAnnotations()
if annotations == nil {
return fmt.Errorf("no annotations")
}

if excludeIdentifier != nil {
excludeAnnotation := fmt.Sprintf("exclude.release.openshift.io/%s", *excludeIdentifier)
if v := annotations[excludeAnnotation]; v == "true" {
return fmt.Errorf("%s=%s", excludeAnnotation, v)
}
}

featureGateAnnotation := "release.openshift.io/feature-gate"
featureGateAnnotationValue, featureGateAnnotationExists := annotations[featureGateAnnotation]
if featureGateAnnotationValue == string(configv1.TechPreviewNoUpgrade) && !includeTechPreview {
return fmt.Errorf("tech-preview excluded, and %s=%s", featureGateAnnotation, featureGateAnnotationValue)
}
// never include the manifest if the feature-gate annotation is outside of allowed values (only TechPreviewNoUpgrade is currently allowed)
if featureGateAnnotationExists && featureGateAnnotationValue != string(configv1.TechPreviewNoUpgrade) {
return fmt.Errorf("unrecognized value %s=%s", featureGateAnnotation, featureGateAnnotationValue)
}

if profile != nil {
profileAnnotation := fmt.Sprintf("include.release.openshift.io/%s", *profile)
if val, ok := annotations[profileAnnotation]; ok && val != "true" {
return fmt.Errorf("unrecognized value %s=%s", profileAnnotation, val)
} else if !ok {
return fmt.Errorf("%s unset", profileAnnotation)
}
}

// If there is no capabilities defined in a release then we do not need to check presence of capabilities in the manifest
if capabilities != nil {
return checkResourceEnablement(annotations, capabilities)
}
return nil
}

// checkResourceEnablement, given resource annotations and defined cluster capabilities, checks if the capability
// annotation exists. If so, each capability name is validated against the known set of capabilities. Each valid
// capability is then checked if it is disabled. If any invalid capabilities are found an error is returned listing
// all invalid capabilities. Otherwise, if any disabled capabilities are found an error is returned listing all
// disabled capabilities.
func checkResourceEnablement(annotations map[string]string, capabilities *configv1.ClusterVersionCapabilitiesStatus) error {
val, ok := annotations[CapabilityAnnotation]
if !ok {
return nil
}
caps := strings.Split(val, "+")
numCaps := len(caps)
unknownCaps := make([]string, 0, numCaps)
disabledCaps := make([]string, 0, numCaps)

var isKnownCap bool = false
var isEnabledCap bool = false

for _, c := range caps {
for _, knownCapability := range capabilities.KnownCapabilities {
if c == string(knownCapability) {
isKnownCap = true
}
}
if !isKnownCap {
unknownCaps = append(unknownCaps, c)
continue
}
for _, enabledCapability := range capabilities.EnabledCapabilities {
if c == string(enabledCapability) {
isEnabledCap = true
}

}
if !isEnabledCap {
disabledCaps = append(disabledCaps, c)
}
}
if len(unknownCaps) > 0 {
return fmt.Errorf("unrecognized capability names: %s", strings.Join(unknownCaps, ", "))
}
if len(disabledCaps) > 0 {
return fmt.Errorf("disabled capabilities: %s", strings.Join(disabledCaps, ", "))
}
return nil
}

// ManifestsFromFiles reads files and returns Manifests in the same order.
// 'files' should be list of absolute paths for the manifests on disk. An
// error is returned for each manifest that defines a duplicate resource
Expand Down
135 changes: 135 additions & 0 deletions pkg/manifest/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package manifest

import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -10,6 +11,9 @@ import (
"testing"

"github.com/davecgh/go-spew/spew"
configv1 "github.com/openshift/api/config/v1"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
)
Expand Down Expand Up @@ -581,3 +585,134 @@ func setupTestFS(t *testing.T, d dir) (string, func() error) {
}
return root, cleanup
}

func Test_include(t *testing.T) {
tests := []struct {
name string
exclude string
includeTechPreview bool
profile string
annotations map[string]interface{}
caps configv1.ClusterVersionCapabilitiesStatus

expected error
}{
{
name: "exclusion identifier set",
exclude: "identifier",
profile: DefaultClusterProfile,
annotations: map[string]interface{}{
"exclude.release.openshift.io/identifier": "true",
"include.release.openshift.io/self-managed-high-availability": "true"},
expected: fmt.Errorf("exclude.release.openshift.io/identifier=true"),
},
{
name: "profile selection works",
profile: "single-node",
annotations: map[string]interface{}{"include.release.openshift.io/self-managed-high-availability": "true"},
expected: fmt.Errorf("include.release.openshift.io/single-node unset"),
},
{
name: "profile selection works included",
profile: DefaultClusterProfile,
annotations: map[string]interface{}{"include.release.openshift.io/self-managed-high-availability": "true"},
},
{
name: "correct techpreview value is excluded if techpreview off",
profile: DefaultClusterProfile,
annotations: map[string]interface{}{
"include.release.openshift.io/self-managed-high-availability": "true",
"release.openshift.io/feature-gate": "TechPreviewNoUpgrade",
},
expected: fmt.Errorf("tech-preview excluded, and release.openshift.io/feature-gate=TechPreviewNoUpgrade"),
},
{
name: "correct techpreview value is included if techpreview on",
includeTechPreview: true,
profile: DefaultClusterProfile,
annotations: map[string]interface{}{
"include.release.openshift.io/self-managed-high-availability": "true",
"release.openshift.io/feature-gate": "TechPreviewNoUpgrade",
},
},
{
name: "incorrect techpreview value is not excluded if techpreview off",
profile: DefaultClusterProfile,
annotations: map[string]interface{}{
"include.release.openshift.io/self-managed-high-availability": "true",
"release.openshift.io/feature-gate": "Other",
},
expected: fmt.Errorf("unrecognized value release.openshift.io/feature-gate=Other"),
},
{
name: "incorrect techpreview value is not excluded if techpreview on",
includeTechPreview: true,
profile: DefaultClusterProfile,
annotations: map[string]interface{}{
"include.release.openshift.io/self-managed-high-availability": "true",
"release.openshift.io/feature-gate": "Other",
},
expected: fmt.Errorf("unrecognized value release.openshift.io/feature-gate=Other"),
},
{
name: "default profile selection excludes without annotation",
profile: DefaultClusterProfile,
annotations: map[string]interface{}{},
expected: fmt.Errorf("include.release.openshift.io/self-managed-high-availability unset"),
},
{
name: "default profile selection excludes with no annotation",
profile: DefaultClusterProfile,
annotations: nil,
expected: fmt.Errorf("no annotations"),
},
{
name: "unrecognized capability works",
profile: DefaultClusterProfile,
annotations: map[string]interface{}{
"include.release.openshift.io/self-managed-high-availability": "true",
CapabilityAnnotation: "cap1"},
expected: fmt.Errorf("unrecognized capability names: cap1"),
},
{
name: "disabled capability works",
profile: DefaultClusterProfile,
annotations: map[string]interface{}{
"include.release.openshift.io/self-managed-high-availability": "true",
CapabilityAnnotation: "cap1"},
caps: configv1.ClusterVersionCapabilitiesStatus{
KnownCapabilities: []configv1.ClusterVersionCapability{"cap1"},
},
expected: fmt.Errorf("disabled capabilities: cap1"),
},
{
name: "enabled capability works",
profile: DefaultClusterProfile,
annotations: map[string]interface{}{
"include.release.openshift.io/self-managed-high-availability": "true",
CapabilityAnnotation: "cap1"},
caps: configv1.ClusterVersionCapabilitiesStatus{
KnownCapabilities: []configv1.ClusterVersionCapability{"cap1"},
EnabledCapabilities: []configv1.ClusterVersionCapability{"cap1"},
},
},
}

for _, tt := range tests {
metadata := map[string]interface{}{}
t.Run(tt.name, func(t *testing.T) {
if tt.annotations != nil {
metadata["annotations"] = tt.annotations
}
m := Manifest{
Obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": metadata,
},
},
}
err := m.Include(&tt.exclude, tt.includeTechPreview, &tt.profile, &tt.caps)
assert.Equal(t, err, tt.expected)
})
}
}