Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 fix channel naming validation #231

Merged
merged 4 commits into from
Mar 30, 2022
Merged
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
44 changes: 41 additions & 3 deletions pkg/manifests/bundleloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import (

// bundleLoader loads a bundle directory from disk
type bundleLoader struct {
dir string
bundle *Bundle
foundCSV bool
dir string
bundle *Bundle
foundCSV bool
annotationsFile AnnotationsFile
}

func NewBundleLoader(dir string) bundleLoader {
Expand All @@ -38,6 +39,15 @@ func (b *bundleLoader) LoadBundle() error {

errs = append(errs, b.calculateCompressedBundleSize())

// Set values from the annotations when the values are not loaded
awgreene marked this conversation as resolved.
Show resolved Hide resolved
channels := strings.Split(b.annotationsFile.Annotations.Channels, ",")
if len(channels) > 0 && len(b.bundle.Channels) == 0 {
b.bundle.Channels = channels
}
if len(b.annotationsFile.Annotations.DefaultChannelName) > 0 && len(b.bundle.DefaultChannel) == 0 {
b.bundle.DefaultChannel = b.annotationsFile.Annotations.DefaultChannelName
}

if !b.foundCSV {
errs = append(errs, fmt.Errorf("unable to find a csv in bundle directory %s", b.dir))
} else if b.bundle == nil {
Expand Down Expand Up @@ -108,6 +118,19 @@ func (b *bundleLoader) LoadBundleWalkFunc(path string, f os.FileInfo, err error)
return nil
}

annotationsFile := AnnotationsFile{}
if strings.HasPrefix(f.Name(), "annotations") {
annFile, err := readFile(path)
camilamacedo86 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
if err := yaml.Unmarshal(annFile, &annotationsFile); err == nil {
camilamacedo86 marked this conversation as resolved.
Show resolved Hide resolved
b.annotationsFile = annotationsFile
} else {
return fmt.Errorf("unable to load the annotations file %s: %s", path, err)
}
}

fileReader, err := os.Open(path)
if err != nil {
return fmt.Errorf("unable to load file %s: %s", path, err)
Expand Down Expand Up @@ -143,6 +166,21 @@ func (b *bundleLoader) LoadBundleWalkFunc(path string, f os.FileInfo, err error)
return utilerrors.NewAggregate(errs)
}

func readFile(file string) ([]byte, error) {
fileOpen, err := os.Open(file)
if err != nil {
return []byte{}, err
}
defer fileOpen.Close()

var byteValue []byte
byteValue, err = ioutil.ReadAll(fileOpen)
if err != nil {
return []byte{}, err
}
return byteValue, err
}

// loadBundle takes the directory that a CSV is in and assumes the rest of the objects in that directory
// are part of the bundle.
func loadBundle(csvName string, dir string) (*Bundle, error) {
Expand Down
56 changes: 56 additions & 0 deletions pkg/validation/internal/good_practices.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package internal
import (
goerrors "errors"
"fmt"
"strings"

"github.com/operator-framework/api/pkg/manifests"
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
Expand All @@ -17,6 +18,9 @@ import (
// This validator will raise an WARNING when:
//
// - The resources request for CPU and/or Memory are not defined for any of the containers found in the CSV
//
// - The channel names seems are not following the convention https://olm.operatorframework.io/docs/best-practices/channel-naming/
//
var GoodPracticesValidator interfaces.Validator = interfaces.ValidatorFunc(goodPracticesValidator)

func goodPracticesValidator(objs ...interface{}) (results []errors.ManifestResult) {
Expand Down Expand Up @@ -51,6 +55,10 @@ func validateGoodPracticesFrom(bundle *manifests.Bundle) errors.ManifestResult {
result.Add(errors.WarnFailedValidation(warn.Error(), bundle.CSV.GetName()))
}

channels := append(bundle.Channels, bundle.DefaultChannel)
if warn := validateHubChannels(channels); warn != nil {
result.Add(errors.WarnFailedValidation(warn.Error(), bundle.CSV.GetName()))
}
return result
}

Expand All @@ -75,3 +83,51 @@ func validateResourceRequests(csv *operatorsv1alpha1.ClusterServiceVersion) (err
}
return errs, warns
}

// validateHubChannels will check the channels. The motivation for the following check is to ensure that operators
// authors knows if their operator bundles are or not respecting the Naming Convention Rules.
// However, the operator authors still able to choose the names as please them.
func validateHubChannels(channels []string) error {
const candidate = "candidate"
const stable = "stable"
const fast = "fast"

channels = getUniqueValues(channels)
var channelsNotFollowingConventional []string
for _, channel := range channels {
if !strings.HasPrefix(channel, candidate) &&
!strings.HasPrefix(channel, stable) &&
!strings.HasPrefix(channel, fast) &&
channel != "" {
channelsNotFollowingConventional = append(channelsNotFollowingConventional, channel)
}

}

if len(channelsNotFollowingConventional) > 0 {
return fmt.Errorf("channel(s) %+q are not following the recommended naming convention: "+
awgreene marked this conversation as resolved.
Show resolved Hide resolved
"https://olm.operatorframework.io/docs/best-practices/channel-naming",
channelsNotFollowingConventional)
}

return nil
}

// getUniqueValues return the values without duplicates
func getUniqueValues(array []string) []string {
var result []string
for _, n := range array {
found := false
for _, v := range result {
if strings.TrimSpace(n) == strings.TrimSpace(v) {
found = true
break
}
}
if !found {
result = append(result, n)
camilamacedo86 marked this conversation as resolved.
Show resolved Hide resolved
}

}
return result
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this list is potentially used in a user-facing error message, can you call a sort function to guarantee a deterministic order?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is a nit that shows outside of the fixed scope.
Can we agree on doing that in a follow-up?

}
55 changes: 55 additions & 0 deletions pkg/validation/internal/good_practices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ func Test_ValidateGoodPractices(t *testing.T) {
},
errStrings: []string{"Error: Value etcdoperator.v0.9.4: unable to find a deployment to install in the CSV"},
},
{
name: "should raise an warn when the channel does not follows the convention",
wantWarning: true,
args: args{
bundleDir: "./testdata/bundle_with_metadata",
},
warnStrings: []string{"Warning: Value memcached-operator.v0.0.1: channel(s) [\"alpha\"] are not following the recommended naming convention: https://olm.operatorframework.io/docs/best-practices/channel-naming"},
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -92,3 +100,50 @@ func Test_ValidateGoodPractices(t *testing.T) {
})
}
}

func TestValidateHubChannels(t *testing.T) {
type args struct {
channels []string
}
tests := []struct {
name string
args args
wantWarn bool
warnStrings []string
}{
{
name: "should not return warning when the channel names following the convention",
args: args{
channels: []string{"fast", "candidate"},
},
wantWarn: false,
},
{
name: "should return warning when the channel names are NOT following the convention",
camilamacedo86 marked this conversation as resolved.
Show resolved Hide resolved
args: args{
channels: []string{"mychannel-4.5"},
},
wantWarn: true,
warnStrings: []string{"channel(s) [\"mychannel-4.5\"] are not following the recommended naming convention: https://olm.operatorframework.io/docs/best-practices/channel-naming"},
},
{
name: "should return warning when has 1 channel NOT following the convention along the others which follows up",
args: args{
channels: []string{"alpha", "fast-v2.1", "candidate-v2.2"},
},
wantWarn: true,
warnStrings: []string{"channel(s) [\"alpha\"] are not following the recommended naming convention: https://olm.operatorframework.io/docs/best-practices/channel-naming"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateHubChannels(tt.args.channels)
if (err != nil) != tt.wantWarn {
t.Errorf("validateHubChannels() error = %v, wantWarn %v", err, tt.wantWarn)
}
if len(tt.warnStrings) > 0 {
require.Contains(t, tt.warnStrings, err.Error())
}
})
}
}
31 changes: 0 additions & 31 deletions pkg/validation/internal/operatorhub.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,40 +215,9 @@ func validateBundleOperatorHub(bundle *manifests.Bundle, k8sVersion string) erro
result.Add(errors.WarnFailedValidation(warn.Error(), bundle.CSV.GetName()))
}

if warn := validateHubChannels(bundle.Channels); warn != nil {
result.Add(errors.WarnFailedValidation(warn.Error(), bundle.CSV.GetName()))
}

return result
}

// validateHubChannels will check the channels. The motivation for the following check is to ensure that operators
// authors knows if their operator bundles are or not respecting the Naming Convention Rules.
// However, the operator authors still able to choose the names as please them.
func validateHubChannels(channels []string) error {
awgreene marked this conversation as resolved.
Show resolved Hide resolved
const candidate = "candidate"
const stable = "stable"
const fast = "fast"

var channelsNotFollowingConventional []string
for _, channel := range channels {
if !strings.HasPrefix(channel, candidate) &&
!strings.HasPrefix(channel, stable) &&
!strings.HasPrefix(channel, fast) {
channelsNotFollowingConventional = append(channelsNotFollowingConventional, channel)
}

}

if len(channelsNotFollowingConventional) > 0 {
return fmt.Errorf("channel(s) %+q are not following the recommended naming convention: "+
"https://olm.operatorframework.io/docs/best-practices/channel-naming",
channelsNotFollowingConventional)
}

return nil
}

// validateHubCSVSpec will check the CSV against the criteria to publish an
// operator bundle in the OperatorHub.io
func validateHubCSVSpec(csv v1alpha1.ClusterServiceVersion) CSVChecks {
Expand Down
33 changes: 0 additions & 33 deletions pkg/validation/internal/operatorhub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,36 +342,3 @@ func TestCheckSpecMinKubeVersion(t *testing.T) {
})
}
}

func TestValidateHubChannels(t *testing.T) {
type args struct {
channels []string
}
tests := []struct {
name string
args args
wantWarn bool
}{
{
name: "should not return warning when the channel names following the convention",
args: args{
channels: []string{"fast", "candidate"},
},
wantWarn: false,
},
{
name: "should return warning when the channel names are NOT following the convention",
args: args{
channels: []string{"mychannel-4.5"},
},
wantWarn: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateHubChannels(tt.args.channels); (err != nil) != tt.wantWarn {
t.Errorf("validateHubChannels() error = %v, wantWarn %v", err, tt.wantWarn)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
apiVersion: apiextensions.k8s.io/v1
awgreene marked this conversation as resolved.
Show resolved Hide resolved
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.8.0
creationTimestamp: null
name: memcacheds.cache.example.com
spec:
group: cache.example.com
names:
kind: Memcached
listKind: MemcachedList
plural: memcacheds
singular: bundle_with_metadata
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Memcached is the Schema for the memcacheds API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: MemcachedSpec defines the desired state of Memcached
properties:
foo:
description: Foo is an example field of Memcached. Edit memcached_types.go
to remove/update
type: string
size:
description: Size defines the number of Memcached instances
format: int32
type: integer
type: object
status:
description: MemcachedStatus defines the observed state of Memcached
properties:
nodes:
description: Nodes store the name of the pods which are running Memcached
instances
items:
type: string
type: array
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
control-plane: controller-manager
name: memcached-operator-controller-manager-metrics-monitor
spec:
endpoints:
- bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
path: /metrics
port: https
scheme: https
tlsConfig:
insecureSkipVerify: true
selector:
matchLabels:
control-plane: controller-manager
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
control-plane: controller-manager
name: memcached-operator-controller-manager-metrics-service
spec:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: https
selector:
control-plane: controller-manager
status:
loadBalancer: {}
Loading