Skip to content

Commit

Permalink
Begin usage of operator-framework APIs
Browse files Browse the repository at this point in the history
The operator-framework API[1] has lots of bundle manipulation tools
that we can leverage to make our own code simpler. This begins the
journey of leveraging those APIs. This removes a good bit of code
that we get from the library.

[1] https://github.com/operator-framework/api

Signed-off-by: Brad P. Crochet <brad@redhat.com>
  • Loading branch information
bcrochet committed Aug 26, 2022
1 parent 5f4cc98 commit 64f7128
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 278 deletions.
107 changes: 18 additions & 89 deletions certification/internal/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package bundle

import (
"context"
"errors"
"fmt"
"io"
"os"
Expand All @@ -12,15 +11,12 @@ import (
rbacv1 "k8s.io/api/rbac/v1"

"github.com/blang/semver"
operatorv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
"github.com/operator-framework/api/pkg/manifests"
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/internal/operatorsdk"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/yaml"
)

// versionsKey is the OpenShift versions in annotations.yaml that lists the versions allowed for an operator
const versionsKey = "com.redhat.openshift.versions"

// This table signifies what the NEXT release of OpenShift will
// deprecate, not what it matches up to.
var ocpToKubeVersion = map[string]string{
Expand Down Expand Up @@ -54,19 +50,19 @@ func Validate(ctx context.Context, operatorSdk operatorSdk, imagePath string) (*
if err != nil {
return nil, fmt.Errorf("could not open annotations.yaml: %v", err)
}
annotations, err := GetAnnotations(ctx, annotationsFile)
annotations, err := LoadAnnotations(ctx, annotationsFile)
if err != nil {
return nil, fmt.Errorf("unable to get annotations.yaml from the bundle: %v", err)
}

if versions, ok := annotations[versionsKey]; ok {
if annotations.OpenshiftVersions != "" {
// Check that the label range contains >= 4.9
targetVersion, err := targetVersion(versions)
targetVersion, err := targetVersion(annotations.OpenshiftVersions)
if err != nil {
// Could not parse the version, which probably means the annotation is invalid
return nil, fmt.Errorf("%v", err)
}
if k8sVer, ok := ocpToKubeVersion[targetVersion]; ok {
if k8sVer, found := ocpToKubeVersion[targetVersion]; found {
log.Debugf("OpenShift %s detected in annotations. Running with additional checks enabled.", targetVersion)
opts.OptionalValues = make(map[string]string)
opts.OptionalValues["k8s-version"] = k8sVer
Expand Down Expand Up @@ -138,100 +134,33 @@ func cleanStringToGetTheVersionToParse(value string) string {
return value
}

// GetAnnotations accepts a context, and an io.Reader that is expected to provide
// the annotations.yaml, and parses the annotations from there
func GetAnnotations(ctx context.Context, r io.Reader) (map[string]string, error) {
fileContents, err := io.ReadAll(r)
// LoadAnnotations reads an operator bundle's annotations.yaml from r.
func LoadAnnotations(ctx context.Context, r io.Reader) (*Annotations, error) {
annFile, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("fail to read metadata/annotation.yaml file in bundle: %v", err)
}

annotations, err := ExtractAnnotationsBytes(ctx, fileContents)
if err != nil {
return nil, fmt.Errorf("metadata/annotations.yaml found but is malformed: %v", err)
}

return annotations, nil
}

// extractAnnotationsBytes reads the annotation data read from a file and returns the expected format for that yaml
// represented as a map[string]string.
func ExtractAnnotationsBytes(ctx context.Context, annotationBytes []byte) (map[string]string, error) {
type metadata struct {
Annotations map[string]string
}

if len(annotationBytes) == 0 {
return nil, errors.New("the annotations file was empty")
}

var bundleMeta metadata
if err := yaml.Unmarshal(annotationBytes, &bundleMeta); err != nil {
return nil, fmt.Errorf("metadata/annotations.yaml found but is malformed: %v", err)
}

return bundleMeta.Annotations, nil
}

func GetCsvFilePathFromBundle(imageDir string) (string, error) {
log.Trace("reading clusterserviceversion file from the bundle")
log.Debug("image directory is ", imageDir)
matches, err := filepath.Glob(filepath.Join(imageDir, "manifests", "*.clusterserviceversion.yaml"))
if err != nil {
return "", fmt.Errorf("glob pattern is malformed: %v", err)
}
if len(matches) == 0 {
return "", fmt.Errorf("unable to find clusterserviceversion file in the bundle image: %v", os.ErrNotExist)
}
if len(matches) > 1 {
return "", fmt.Errorf("more than one CSV file detected in bundle")
}
log.Debugf("The path to csv file is %s", matches[0])
return matches[0], nil
}

func csvFromReader(ctx context.Context, csvReader io.Reader) (*operatorv1alpha1.ClusterServiceVersion, error) {
var csv operatorv1alpha1.ClusterServiceVersion
bts, err := io.ReadAll(csvReader)
if err != nil {
return nil, fmt.Errorf("could not get CSV from reader: %v", err)
}
err = yaml.Unmarshal(bts, &csv)
if err != nil {
return nil, fmt.Errorf("malformed CSV detected: %v", err)
if len(annFile) == 0 {
return nil, fmt.Errorf("annotations file was empty")
}

return &csv, nil
}

func GetSupportedInstallModes(ctx context.Context, csvReader io.Reader) (map[string]bool, error) {
csv, err := csvFromReader(ctx, csvReader)
if err != nil {
return nil, err
var annotationsFile AnnotationsFile
if err := yaml.Unmarshal(annFile, &annotationsFile); err != nil {
return nil, fmt.Errorf("unable to load the annotations file: %v", err)
}

installedModes := make(map[string]bool, len(csv.Spec.InstallModes))
for _, v := range csv.Spec.InstallModes {
if v.Supported {
installedModes[string(v.Type)] = true
}
}
return installedModes, nil
return &annotationsFile.Annotations, nil
}

// GetSecurityContextConstraints returns an string array of SCC resource names requested by the operator as specified
// in the csv
func GetSecurityContextConstraints(ctx context.Context, csvReader io.Reader) ([]string, error) {
var csv operatorv1alpha1.ClusterServiceVersion
bts, err := io.ReadAll(csvReader)
if err != nil {
return nil, fmt.Errorf("could not get CSV from reader: %v", err)
}
err = yaml.Unmarshal(bts, &csv)
func GetSecurityContextConstraints(ctx context.Context, bundlePath string) ([]string, error) {
bundle, err := manifests.GetBundleFromDir(bundlePath)
if err != nil {
return nil, fmt.Errorf("malformed CSV detected: %v", err)
return nil, fmt.Errorf("could not get bundle from dir: %s: %v", bundlePath, err)
}
for _, cp := range csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions {
for _, cp := range bundle.CSV.Spec.InstallStrategy.StrategySpec.ClusterPermissions {
for _, rule := range cp.Rules {
if hasSCCApiGroup(rule) && hasSCCResource(rule) {
return rule.ResourceNames, nil
Expand Down
130 changes: 6 additions & 124 deletions certification/internal/bundle/bundle_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package bundle

import (
"bytes"
"context"
"os"
"path/filepath"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -111,142 +111,24 @@ var _ = Describe("BundleValidateCheck", func() {
Expect(report).ToNot(BeNil())
})
})

Context("the annotations file has a bad OpenShift version", func() {
JustBeforeEach(func() {
err := os.WriteFile(filepath.Join(imageRef.ImageFSPath, metadataDir, annotationFilename), []byte(`annotations:
com.redhat.openshift.versions: "vfoo"`), 0o644)
Expect(err).ToNot(HaveOccurred())
})
It("should fail", func() {
report, err := Validate(context.Background(), fakeEngine, imageRef.ImageFSPath)
Expect(err).To(HaveOccurred())
Expect(report).To(BeNil())
})
})

Context("getting the CSV file from the bundle", func() {
var manifestsPath string

BeforeEach(func() {
manifestsPath = filepath.Join(imageRef.ImageFSPath, manifestsDir)
err := os.WriteFile(filepath.Join(manifestsPath, clusterServiceVersionFilename), []byte(""), 0o644)
Expect(err).ToNot(HaveOccurred())
})
Context("the CSV is malformed", func() {
It("should error", func() {
r := strings.NewReader("badcsv::bad")
images, err := GetSupportedInstallModes(context.TODO(), r)
Expect(err).To(HaveOccurred())
Expect(images).To(BeNil())
})
})
Context("the CSV could not be read", func() {
It("should error", func() {
images, err := GetSupportedInstallModes(context.TODO(), errReader(0))
Expect(err).To(HaveOccurred())
Expect(images).To(BeNil())
})
})
Context("the CSV exists by itself", func() {
It("should return the filename", func() {
filename, err := GetCsvFilePathFromBundle(imageRef.ImageFSPath)
Expect(err).ToNot(HaveOccurred())
Expect(filename).To(Equal(filepath.Join(manifestsPath, clusterServiceVersionFilename)))
})
})
Context("the CSV doesn't exist", func() {
JustBeforeEach(func() {
err := os.Remove(filepath.Join(manifestsPath, clusterServiceVersionFilename))
Expect(err).ToNot(HaveOccurred())
})
It("should return an error", func() {
filename, err := GetCsvFilePathFromBundle(imageRef.ImageFSPath)
Expect(err).To(HaveOccurred())
Expect(filename).To(Equal(""))
})
})
Context("there is more than one CSV", func() {
JustBeforeEach(func() {
err := os.WriteFile(filepath.Join(manifestsPath, "otheroperator.clusterserviceversion.yaml"), []byte(""), 0o664)
Expect(err).ToNot(HaveOccurred())
})
It("should return an error", func() {
filename, err := GetCsvFilePathFromBundle(imageRef.ImageFSPath)
Expect(err).To(HaveOccurred())
Expect(filename).To(Equal(""))
})
})
Context("there is a bad mount dir", func() {
It("should return an error", func() {
filename, err := GetCsvFilePathFromBundle("[]")
Expect(err).To(HaveOccurred())
Expect(filename).To(Equal(""))
})
})
})
})

Describe("Supported Install Modes", func() {
var csv string = `spec:
installModes:
- supported: true
type: OwnNamespace
- supported: true
type: SingleNamespace
- supported: false
type: MultiNamespace
- supported: true
type: AllNamespaces`

Context("CSV is valid", func() {
It("should return a map of 3", func() {
installModes, err := GetSupportedInstallModes(context.Background(), strings.NewReader(csv))
Expect(err).ToNot(HaveOccurred())
Expect(installModes).ToNot(BeNil())
Expect(len(installModes)).To(Equal(3))
Expect("MultiNamespace").ToNot(BeElementOf(installModes))
})
})

Context("reader is not valid", func() {
It("should error", func() {
installModes, err := GetSupportedInstallModes(context.Background(), errReader(0))
Expect(err).To(HaveOccurred())
Expect(installModes).To(BeNil())
})
})

Context("CSV is invalid", func() {
JustBeforeEach(func() {
csv = `invalid`
})
It("should error", func() {
installModes, err := GetSupportedInstallModes(context.Background(), strings.NewReader(csv))
Expect(err).To(HaveOccurred())
Expect(installModes).To(BeNil())
})
})
})

Describe("While ensuring that container util is working", func() {
// tests: extractAnnotationsBytes
Context("with an annotations yaml data read from disk", func() {
Context("with the correct format", func() {
data := []byte("annotations:\n foo: bar")

It("should properly marshal to a map[string]string", func() {
annotations, err := ExtractAnnotationsBytes(context.TODO(), data)
annotations, err := LoadAnnotations(context.TODO(), bytes.NewReader([]byte(annotations)))
Expect(err).ToNot(HaveOccurred())
Expect(annotations["foo"]).To(Equal("bar"))
Expect(annotations.DefaultChannelName).To(Equal("testChannel"))
})
})

Context("containing no data read in from the yaml file", func() {
data := []byte{}

It("should return an error", func() {
_, err := ExtractAnnotationsBytes(context.TODO(), data)
_, err := LoadAnnotations(context.TODO(), bytes.NewReader(data))
Expect(err).To(HaveOccurred())
})
})
Expand All @@ -255,14 +137,14 @@ var _ = Describe("BundleValidateCheck", func() {
data := []byte(`malformed`)

It("should return an error", func() {
_, err := ExtractAnnotationsBytes(context.TODO(), data)
_, err := LoadAnnotations(context.TODO(), bytes.NewReader(data))
Expect(err).To(HaveOccurred())
})
})

Context("a bad reader is sent to GetAnnotations", func() {
It("should return an error", func() {
annotations, err := GetAnnotations(context.Background(), errReader(0))
annotations, err := LoadAnnotations(context.TODO(), errReader(0))
Expect(err).To(HaveOccurred())
Expect(annotations).To(BeNil())
})
Expand Down
13 changes: 13 additions & 0 deletions certification/internal/bundle/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package bundle

import "github.com/operator-framework/api/pkg/manifests"

type AnnotationsFile struct {
Annotations Annotations `json:"annotations" yaml:"annotations"`
}

type Annotations struct {
manifests.Annotations

OpenshiftVersions string `json:"com.redhat.openshift.versions,omitempty" yaml:"com.redhat.openshift.versions,omitempty"`
}
10 changes: 5 additions & 5 deletions certification/internal/policy/operator/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ var (
"registry.access.redhat.com": {},
}

prioritizedInstallModes = []string{
string(operatorv1alpha1.InstallModeTypeOwnNamespace),
string(operatorv1alpha1.InstallModeTypeSingleNamespace),
string(operatorv1alpha1.InstallModeTypeMultiNamespace),
string(operatorv1alpha1.InstallModeTypeAllNamespaces),
prioritizedInstallModes = []operatorv1alpha1.InstallModeType{
operatorv1alpha1.InstallModeTypeOwnNamespace,
operatorv1alpha1.InstallModeTypeSingleNamespace,
operatorv1alpha1.InstallModeTypeMultiNamespace,
operatorv1alpha1.InstallModeTypeAllNamespaces,
}
)
Loading

0 comments on commit 64f7128

Please sign in to comment.