├── pkg ├── manifests │ ├── testdata │ │ ├── invalid_bundle_with_hidden │ │ │ ├── .hidden │ │ │ ├── test.example.com_tests_crd.yaml │ │ │ └── test-operator.clusterserviceversion.yaml │ │ ├── invalid_bundle_with_subdir │ │ │ ├── foo │ │ │ │ └── .keep │ │ │ ├── test.example.com_tests_crd.yaml │ │ │ └── test-operator.clusterserviceversion.yaml │ │ ├── valid_package │ │ │ ├── package.yaml │ │ │ ├── 0.9.4 │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ └── 0.9.2 │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ └── valid_bundle │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ ├── directory.go │ ├── bundle.go │ ├── packagemanifest.go │ ├── directory_test.go │ ├── bundlemeta.go │ └── packagemanifestloader.go ├── operators │ ├── doc.go │ ├── v1 │ │ ├── doc.go │ │ ├── groupversion_info.go │ │ ├── operatorgroup_test.go │ │ ├── operatorcondition_types.go │ │ ├── operator_types.go │ │ ├── olmconfig_types.go │ │ └── olmconfig_test.go │ ├── v2 │ │ ├── doc.go │ │ ├── groupversion_info.go │ │ └── operatorcondition_types.go │ ├── v1alpha1 │ │ ├── doc.go │ │ ├── installplan_test.go │ │ └── register.go │ ├── v1alpha2 │ │ ├── doc.go │ │ ├── groupversion_info.go │ │ └── operatorgroup_types.go │ ├── reference │ │ └── reference.go │ ├── install │ │ └── install.go │ └── register.go ├── validation │ ├── internal │ │ ├── testdata │ │ │ ├── invalid_bundle_sa │ │ │ │ ├── sa.yaml │ │ │ │ ├── sa2.yaml │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ ├── categories.json │ │ │ ├── objects │ │ │ │ ├── invalid_pdb_maxUnavailable.yaml │ │ │ │ ├── invalid_pdb_minAvailable.yaml │ │ │ │ ├── valid_priorityclass.yaml │ │ │ │ ├── invalid_priorityclass.yaml │ │ │ │ ├── valid_pdb.yaml │ │ │ │ ├── valid_role_get_pdb.yaml │ │ │ │ ├── invalid_role_create_pdb.yaml │ │ │ │ ├── valid_role_get_scc.yaml │ │ │ │ └── invalid_role_modify_scc.yaml │ │ │ ├── bundle_with_deprecated_resources │ │ │ │ ├── policy.yaml │ │ │ │ ├── priorityclass.yaml │ │ │ │ ├── memcached-operator-controller-manager_v1_serviceaccount.yaml │ │ │ │ ├── role.yaml │ │ │ │ ├── memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml │ │ │ │ ├── memcached-operator-webhook-service_v1_service.yaml │ │ │ │ ├── memcached-operator-controller-manager-metrics-service_v1_service.yaml │ │ │ │ ├── memcached-operator-manager-config_v1_configmap.yaml │ │ │ │ ├── memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml │ │ │ │ ├── webhook.yaml │ │ │ │ └── cache.example.com_memcacheds.yaml │ │ │ ├── valid_bundle_v1 │ │ │ │ ├── memcached-operator-controller-manager_v1_serviceaccount.yaml │ │ │ │ ├── memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml │ │ │ │ ├── memcached-operator-webhook-service_v1_service.yaml │ │ │ │ ├── memcached-operator-controller-manager-metrics-service_v1_service.yaml │ │ │ │ ├── memcached-operator-manager-config_v1_configmap.yaml │ │ │ │ ├── memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml │ │ │ │ └── cache.example.com_memcacheds.yaml │ │ │ ├── deprecated_api_1_25 │ │ │ │ ├── policy.yaml │ │ │ │ ├── horizontal-pod.yaml │ │ │ │ └── cache.example.com_memcacheds.yaml │ │ │ ├── removed_api_1_25 │ │ │ │ ├── policy.yaml │ │ │ │ ├── horizontal-pod.yaml │ │ │ │ └── cache.example.com_memcacheds.yaml │ │ │ ├── invalid_bundle_3 │ │ │ │ ├── test.example.com_tests_v1beta1_crd.yaml │ │ │ │ ├── test.example.com_tests_v1_crd.yaml │ │ │ │ └── test-operator.clusterserviceversion.yaml │ │ │ ├── invalid_bundle │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ ├── valid_bundle │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ ├── invalid_bundle_2 │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ ├── valid_bundle_v1beta1 │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ ├── invalid_bundle_operatorhub │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ ├── valid_bundle_custom_categories │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ ├── deprecatedVersion.crd.yaml │ │ │ ├── valid_bundle_2 │ │ │ │ ├── test.example.com_tests_v1_crd.yaml │ │ │ │ ├── test.example.com_testtwos_v1beta1_crd.yaml │ │ │ │ └── test-operator.clusterserviceversion.yaml │ │ │ ├── removed_api_1_26 │ │ │ │ ├── horizontal-pod.yaml │ │ │ │ └── cache.example.com_memcacheds.yaml │ │ │ ├── correct.og.yaml │ │ │ ├── badAnnotationNames.og.yaml │ │ │ ├── duplicateVersions.crd.yaml │ │ │ ├── v1beta1.crd.yaml │ │ │ ├── bundle_with_metadata │ │ │ │ ├── metadata │ │ │ │ │ └── annotations.yaml │ │ │ │ └── manifests │ │ │ │ │ └── cache.example.com_memcacheds.yaml │ │ │ ├── noConversionReviewVersions.crd.yaml │ │ │ ├── v1.crd.yaml │ │ │ ├── dockerfile │ │ │ │ ├── bundle_without_label.Dockerfile │ │ │ │ ├── valid_bundle.Dockerfile │ │ │ │ ├── valid_bundle_4_8.Dockerfile │ │ │ │ ├── invalid_bundle_equals_upper.Dockerfile │ │ │ │ ├── invalid_bundle_range_upper.Dockerfile │ │ │ │ └── invalid_bundle_range_upper_coma.Dockerfile │ │ │ ├── invalid_min_kube_version.csv.yaml │ │ │ ├── correct.csv.empty.example.yaml │ │ │ ├── invalid.alm-examples.csv.yaml │ │ │ ├── correct.csv.olm.properties.annotation.yaml │ │ │ └── badName.csv.yaml │ │ ├── operatorgroup_test.go │ │ ├── standardcategories_test.go │ │ ├── operatorgroup.go │ │ ├── standardcapabilities_test.go │ │ ├── test_suite.go │ │ ├── typecheck.go │ │ ├── standardcapabilities.go │ │ ├── operatorhubv2_test.go │ │ ├── package_manifest.go │ │ ├── crd.go │ │ ├── operatorhubv2.go │ │ ├── annotations.go │ │ ├── crd_test.go │ │ ├── standardcategories.go │ │ ├── package_manifest_test.go │ │ └── object_test.go │ ├── testdata │ │ ├── invalid_package │ │ │ ├── package.yaml │ │ │ ├── 0.9.2 │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ └── 0.9.4 │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ ├── valid_package │ │ │ ├── package.yaml │ │ │ ├── 0.9.2 │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ │ └── 0.9.4 │ │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ ├── valid_bundle │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ ├── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ │ ├── invalid_bundle │ │ │ ├── etcdbackups.etcd.database.coreos.com.crd.yaml │ │ │ └── etcdrestores.etcd.database.coreos.com.crd.yaml │ │ └── dockerfile │ │ │ └── valid_bundle.Dockerfile │ ├── doc.go │ ├── interfaces │ │ └── validator.go │ └── validation_test.go ├── apis │ └── scorecard │ │ └── v1alpha3 │ │ ├── doc.go │ │ ├── register.go │ │ ├── formatter.go │ │ ├── test_types.go │ │ └── configuration_types.go ├── encoding │ ├── testdata │ │ └── etcdclusters.etcd.database.coreos.com.crd.yaml │ ├── encoding_test.go │ └── encoding.go ├── constraints │ ├── cel_test.go │ └── constraint.go └── lib │ ├── version │ ├── version_test.go │ └── version.go │ └── release │ └── release.go ├── crds ├── doc.go └── defs_test.go ├── tools.go ├── .gitignore ├── OWNERS ├── .github ├── dependabot.yml └── workflows │ ├── go-verdiff.yaml │ ├── verify.yml │ ├── go.yaml │ └── stale.yaml ├── cmd └── operator-verify │ ├── main.go │ └── manifests │ └── cmd.go ├── hack └── boilerplate.go.txt ├── DCO └── RELEASE.md /pkg/manifests/testdata/invalid_bundle_with_hidden/.hidden: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/invalid_bundle_with_subdir/foo/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crds/doc.go: -------------------------------------------------------------------------------- 1 | // Package crds contains CustomResourceDefinition manifests for operator-framework APIs. 2 | package crds 3 | -------------------------------------------------------------------------------- /pkg/operators/doc.go: -------------------------------------------------------------------------------- 1 | // +kubebuilder:skip 2 | 3 | // Package operators contains all resource types of the operators.coreos.com API group. 4 | package operators 5 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_sa/sa.yaml: -------------------------------------------------------------------------------- 1 | kind: ServiceAccount 2 | apiVersion: v1 3 | metadata: 4 | name: etcd-operator 5 | namespace: etcd 6 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_sa/sa2.yaml: -------------------------------------------------------------------------------- 1 | kind: ServiceAccount 2 | apiVersion: v1 3 | metadata: 4 | name: etcd-operator2 5 | namespace: etcd 6 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | // Generate embedded files. 7 | _ "github.com/go-bindata/go-bindata/v3/go-bindata" 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/operators/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +groupName=operators.coreos.com 2 | 3 | // Package v1 contains resources types for version v1 of the operators.coreos.com API group. 4 | package v1 5 | -------------------------------------------------------------------------------- /pkg/operators/v2/doc.go: -------------------------------------------------------------------------------- 1 | // +groupName=operators.coreos.com 2 | 3 | // Package v2 contains resources types for version v2 of the operators.coreos.com API group. 4 | package v2 5 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_package/package.yaml: -------------------------------------------------------------------------------- 1 | channels: 2 | - currentCSV: etcdoperator.v0.9.4 3 | name: singlenamespace-alpha 4 | defaultChannel: singlenamespace-alpha 5 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories":[ 3 | "Cloud Pak", 4 | "Registry", 5 | "MyCoolThing", 6 | "This/Or & That" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_package/package.yaml: -------------------------------------------------------------------------------- 1 | channels: 2 | - currentCSV: etcdoperator.v0.9.4 3 | name: singlenamespace-alpha 4 | defaultChannel: singlenamespace-alpha 5 | packageName: etcd 6 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_package/package.yaml: -------------------------------------------------------------------------------- 1 | channels: 2 | - currentCSV: etcdoperator.v0.9.4 3 | name: singlenamespace-alpha 4 | defaultChannel: singlenamespace-alpha 5 | packageName: etcd 6 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/invalid_pdb_maxUnavailable.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: busybox-pdb 5 | spec: 6 | maxUnavailable: 0 7 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/invalid_pdb_minAvailable.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: busybox-pdb 5 | spec: 6 | minAvailable: 100% 7 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/valid_priorityclass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scheduling.k8s.io/v1 2 | kind: PriorityClass 3 | metadata: 4 | name: super-priority 5 | value: 1000 6 | globalDefault: false 7 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: busybox-pdb 5 | spec: 6 | maxUnavailable: 0 7 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/invalid_priorityclass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scheduling.k8s.io/v1 2 | kind: PriorityClass 3 | metadata: 4 | name: super-priority 5 | value: 1000 6 | globalDefault: true 7 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/priorityclass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scheduling.k8s.io/v1beta1 2 | kind: PriorityClass 3 | metadata: 4 | name: super-priority 5 | value: 1000 6 | globalDefault: true 7 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager_v1_serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | creationTimestamp: null 5 | name: memcached-operator-controller-manager 6 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/valid_pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: busybox-pdb 5 | spec: 6 | minAvailable: 2 7 | selector: 8 | matchLabels: 9 | app: busybox 10 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager_v1_serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | creationTimestamp: null 5 | name: memcached-operator-controller-manager 6 | -------------------------------------------------------------------------------- /pkg/apis/scorecard/v1alpha3/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package,register 2 | // +groupName=scorecard.operatorframework.io 3 | 4 | // Package v1alpha3 contains resources types for version v1alpha3 of the scorecard.operatorframework.com API group. 5 | package v1alpha3 6 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/deprecated_api_1_25/policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: memcached-operator-policy-manager 5 | spec: 6 | minAvailable: 2 7 | selector: 8 | matchLabels: 9 | app: memcached-operator -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/removed_api_1_25/policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: memcached-operator-policy-manager 5 | spec: 6 | minAvailable: 2 7 | selector: 8 | matchLabels: 9 | app: memcached-operator -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: Role 3 | metadata: 4 | creationTimestamp: null 5 | name: memcached-role 6 | rules: 7 | - nonResourceURLs: 8 | - /metrics 9 | verbs: 10 | - get 11 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/valid_role_get_pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | namespace: default 5 | name: pdb-reader 6 | rules: 7 | - apiGroups: ["policy"] 8 | resources: ["poddisruptionbudgets"] 9 | verbs: ["get", "list"] 10 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/invalid_role_create_pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | namespace: default 5 | name: pdb-modifier 6 | rules: 7 | - apiGroups: ["policy"] 8 | resources: ["poddisruptionbudgets"] 9 | verbs: ["create", "list"] 10 | -------------------------------------------------------------------------------- /pkg/operators/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // +groupName=operators.coreos.com 2 | // +k8s:deepcopy-gen=package 3 | // +k8s:conversion-gen=github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators 4 | 5 | // Package v1alpha1 contains resources types for version v1alpha1 of the operators.coreos.com API group. 6 | package v1alpha1 7 | -------------------------------------------------------------------------------- /pkg/operators/v1alpha2/doc.go: -------------------------------------------------------------------------------- 1 | // +groupName=operators.coreos.com 2 | // +k8s:deepcopy-gen=package 3 | // +k8s:conversion-gen=github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators 4 | 5 | // Package v1alpha2 contains resources types for version v1alpha2 of the operators.coreos.com API group. 6 | package v1alpha2 7 | -------------------------------------------------------------------------------- /pkg/apis/scorecard/v1alpha3/register.go: -------------------------------------------------------------------------------- 1 | package v1alpha3 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime/schema" 5 | ) 6 | 7 | var ( 8 | // GroupVersion is the group and version of this package. Used for parsing purposes only. 9 | GroupVersion = schema.GroupVersion{Group: "scorecard.operatorframework.io", Version: "v1alpha3"} 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | name: memcached-operator-metrics-reader 6 | rules: 7 | - nonResourceURLs: 8 | - /metrics 9 | verbs: 10 | - get 11 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | name: memcached-operator-metrics-reader 6 | rules: 7 | - nonResourceURLs: 8 | - /metrics 9 | verbs: 10 | - get 11 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/valid_role_get_scc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: role-name 5 | namespace: namespace 6 | rules: 7 | - apiGroups: 8 | - security.openshift.io 9 | resourceNames: 10 | - scc-name 11 | resources: 12 | - securitycontextconstraints 13 | verbs: 14 | - use 15 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-webhook-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | name: memcached-operator-webhook-service 6 | spec: 7 | ports: 8 | - port: 443 9 | targetPort: 9443 10 | selector: 11 | control-plane: controller-manager 12 | status: 13 | loadBalancer: {} 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-webhook-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | name: memcached-operator-webhook-service 6 | spec: 7 | ports: 8 | - port: 443 9 | targetPort: 9443 10 | selector: 11 | control-plane: controller-manager 12 | status: 13 | loadBalancer: {} 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/objects/invalid_role_modify_scc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: invalid-scc 5 | namespace: namespace 6 | rules: 7 | - apiGroups: 8 | - security.openshift.io 9 | resourceNames: 10 | - privileged 11 | resources: 12 | - securitycontextconstraints 13 | verbs: 14 | - update 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | ### macOS template 15 | # General 16 | *.DS_Store 17 | .AppleDouble 18 | .LSOverride 19 | 20 | .idea/* 21 | vendor/ 22 | bin/ 23 | 24 | .vscode 25 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_3/test.example.com_tests_v1beta1_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: tests.test.example.com 5 | spec: 6 | group: test.example.com 7 | names: 8 | kind: Test 9 | listKind: TestList 10 | plural: tests 11 | singular: test 12 | scope: Namespaced 13 | version: v1alpha1 14 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # approval == this is a good idea /approve 2 | approvers: 3 | - camilamacedo86 4 | - kevinrizza 5 | - joelanford 6 | - perdasilva 7 | - grokspawn 8 | - tmshort 9 | 10 | # review == this code is good /lgtm 11 | reviewers: 12 | - camilamacedo86 13 | - kevinrizza 14 | - anik120 15 | - exdx 16 | - joelanford 17 | - ankitathomas 18 | - perdasilva 19 | - grokspawn 20 | - oceanc80 21 | - tmshort 22 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_bundle/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_package/0.9.4/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_bundle/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_bundle/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_package/0.9.2/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_bundle/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_bundle/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/invalid_bundle_with_hidden/test.example.com_tests_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: tests.test.example.com 5 | spec: 6 | group: test.example.com 7 | names: 8 | kind: Test 9 | listKind: TestList 10 | plural: tests 11 | singular: test 12 | scope: Namespaced 13 | versions: 14 | - name: v1alpha1 15 | served: true 16 | storage: true 17 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/invalid_bundle_with_subdir/test.example.com_tests_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: tests.test.example.com 5 | spec: 6 | group: test.example.com 7 | names: 8 | kind: Test 9 | listKind: TestList 10 | plural: tests 11 | singular: test 12 | scope: Namespaced 13 | versions: 14 | - name: v1alpha1 15 | served: true 16 | storage: true 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_bundle/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_package/0.9.2/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_package/0.9.4/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_package/0.9.2/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_package/0.9.4/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_package/0.9.2/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_package/0.9.4/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_2/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_3/test.example.com_tests_v1_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: tests.test.example.com 5 | spec: 6 | group: test.example.com 7 | names: 8 | kind: Test 9 | listKind: TestList 10 | plural: tests 11 | singular: test 12 | scope: Namespaced 13 | versions: 14 | - name: v1alpha1 15 | served: true 16 | storage: true 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_sa/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_package/0.9.2/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_package/0.9.4/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_package/0.9.2/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_package/0.9.4/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_2/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_sa/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1beta1/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | groups: 12 | k8s-dependencies: 13 | patterns: 14 | - "k8s.io/*" 15 | - "sigs.k8s.io/*" 16 | golang-x-deps: 17 | patterns: 18 | - "golang.org/x/*" -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_operatorhub/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1beta1/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_operatorhub/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_custom_categories/etcdbackups.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdbackups.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdBackup 9 | listKind: EtcdBackupList 10 | plural: etcdbackups 11 | singular: etcdbackup 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/deprecatedVersion.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_custom_categories/etcdrestores.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdrestores.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdRestore 9 | listKind: EtcdRestoreList 10 | plural: etcdrestores 11 | singular: etcdrestore 12 | scope: Namespaced 13 | version: v1beta2 14 | -------------------------------------------------------------------------------- /pkg/encoding/testdata/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager-metrics-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | control-plane: controller-manager 7 | name: memcached-operator-controller-manager-metrics-service 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | status: 16 | loadBalancer: {} 17 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_bundle/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_bundle/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_package/0.9.2/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/valid_package/0.9.4/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_package/0.9.2/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/testdata/valid_package/0.9.4/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager-metrics-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | control-plane: controller-manager 7 | name: memcached-operator-controller-manager-metrics-service 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | status: 16 | loadBalancer: {} 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_2/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_package/0.9.2/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/testdata/invalid_package/0.9.4/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_sa/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_2/test.example.com_tests_v1_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: tests.test.example.com 5 | spec: 6 | group: test.example.com 7 | names: 8 | kind: Test 9 | listKind: TestList 10 | plural: tests 11 | singular: test 12 | scope: Namespaced 13 | versions: 14 | - name: v1alpha1 15 | served: true 16 | storage: true 17 | - name: v1beta1 18 | served: true 19 | storage: false 20 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1beta1/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_operatorhub/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/removed_api_1_25/horizontal-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2beta1 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: memcached-operator-hpa 5 | spec: 6 | scaleTargetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: memcached-operator-controller-manager 10 | minReplicas: 1 11 | maxReplicas: 10 12 | metrics: 13 | - type: Resource 14 | resource: 15 | name: cpu 16 | target: 17 | type: Utilization 18 | averageUtilization: 50 -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/removed_api_1_26/horizontal-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2beta2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: memcached-operator-hpa 5 | spec: 6 | scaleTargetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: memcached-operator-controller-manager 10 | minReplicas: 1 11 | maxReplicas: 10 12 | metrics: 13 | - type: Resource 14 | resource: 15 | name: cpu 16 | target: 17 | type: Utilization 18 | averageUtilization: 50 -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_custom_categories/etcdclusters.etcd.database.coreos.com.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | version: v1beta2 17 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/deprecated_api_1_25/horizontal-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2beta1 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: memcached-operator-hpa 5 | spec: 6 | scaleTargetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: memcached-operator-controller-manager 10 | minReplicas: 1 11 | maxReplicas: 10 12 | metrics: 13 | - type: Resource 14 | resource: 15 | name: cpu 16 | target: 17 | type: Utilization 18 | averageUtilization: 50 -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/correct.og.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1 2 | kind: OperatorGroup 3 | metadata: 4 | annotations: 5 | olm.providedAPIs: NginxIngressController.v1alpha1.k8s.nginx.org 6 | creationTimestamp: "2021-02-25T23:47:13Z" 7 | generateName: nginx- 8 | generation: 1 9 | name: nginx-hbvsw 10 | namespace: nginx 11 | resourceVersion: "58637752" 12 | selfLink: /apis/operators.coreos.com/v1/namespaces/nginx/operatorgroups/nginx-hbvsw 13 | uid: 81a05c50-aea3-4959-9e86-b1ad8b74e899 14 | spec: 15 | targetNamespaces: 16 | - nginx -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_2/test.example.com_testtwos_v1beta1_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: testtwos.test.example.com 5 | spec: 6 | group: test.example.com 7 | names: 8 | kind: TestTwo 9 | listKind: TestTwoList 10 | plural: testtwos 11 | singular: testtwo 12 | scope: Namespaced 13 | version: v1beta1 14 | versions: 15 | - name: v1beta1 16 | served: true 17 | storage: true 18 | - name: v1alpha1 19 | served: true 20 | storage: false 21 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/badAnnotationNames.og.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1 2 | kind: OperatorGroup 3 | metadata: 4 | annotations: 5 | olm.providedapis: NginxIngressController.v1alpha1.k8s.nginx.org 6 | creationTimestamp: "2021-02-25T23:47:13Z" 7 | generateName: nginx- 8 | generation: 1 9 | name: nginx-hbvsw 10 | namespace: nginx 11 | resourceVersion: "58637752" 12 | selfLink: /apis/operators.coreos.com/v1/namespaces/nginx/operatorgroups/nginx-hbvsw 13 | uid: 81a05c50-aea3-4959-9e86-b1ad8b74e899 14 | spec: 15 | targetNamespaces: 16 | - nginx -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-manager-config_v1_configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | controller_manager_config.yaml: | 4 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 5 | kind: ControllerManagerConfig 6 | health: 7 | healthProbeBindAddress: :8081 8 | metrics: 9 | bindAddress: 127.0.0.1:8080 10 | webhook: 11 | port: 9443 12 | leaderElection: 13 | leaderElect: true 14 | resourceName: 86f835c3.example.com 15 | kind: ConfigMap 16 | metadata: 17 | name: memcached-operator-manager-config 18 | -------------------------------------------------------------------------------- /cmd/operator-verify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | manifests "github.com/operator-framework/api/cmd/operator-verify/manifests" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func main() { 13 | rootCmd := &cobra.Command{ 14 | Use: "operator-verify", 15 | Short: "Operator manifest validation tool", 16 | Long: `operator-verify is a CLI tool that calls functions in pkg/validation.`, 17 | } 18 | 19 | rootCmd.AddCommand(manifests.NewCmd()) 20 | if err := rootCmd.Execute(); err != nil { 21 | fmt.Println(err) 22 | os.Exit(1) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-manager-config_v1_configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | controller_manager_config.yaml: | 4 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 5 | kind: ControllerManagerConfig 6 | health: 7 | healthProbeBindAddress: :8081 8 | metrics: 9 | bindAddress: 127.0.0.1:8080 10 | webhook: 11 | port: 9443 12 | leaderElection: 13 | leaderElect: true 14 | resourceName: 86f835c3.example.com 15 | kind: ConfigMap 16 | metadata: 17 | name: memcached-operator-manager-config 18 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/duplicateVersions.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | group: etcd.database.coreos.com 7 | names: 8 | kind: EtcdCluster 9 | listKind: EtcdClusterList 10 | plural: etcdclusters 11 | shortNames: 12 | - etcdclus 13 | - etcd 14 | singular: etcdcluster 15 | scope: Namespaced 16 | versions: 17 | - name: v1beta2 18 | served: true 19 | storage: true 20 | - name: v1beta2 21 | served: true 22 | storage: false 23 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1/memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: memcached-operator-controller-manager-metrics-monitor 7 | spec: 8 | endpoints: 9 | - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 10 | path: /metrics 11 | port: https 12 | scheme: https 13 | tlsConfig: 14 | insecureSkipVerify: true 15 | selector: 16 | matchLabels: 17 | control-plane: controller-manager 18 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/memcached-operator-controller-manager-metrics-monitor_monitoring.coreos.com_v1_servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: memcached-operator-controller-manager-metrics-monitor 7 | spec: 8 | endpoints: 9 | - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 10 | path: /metrics 11 | port: https 12 | scheme: https 13 | tlsConfig: 14 | insecureSkipVerify: true 15 | selector: 16 | matchLabels: 17 | control-plane: controller-manager 18 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/v1beta1.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | conversion: 7 | strategy: Webhook 8 | webhookClientConfig: 9 | service: 10 | namespace: system 11 | name: webhook-service 12 | path: /convert 13 | group: etcd.database.coreos.com 14 | names: 15 | kind: EtcdCluster 16 | listKind: EtcdClusterList 17 | plural: etcdclusters 18 | shortNames: 19 | - etcdclus 20 | - etcd 21 | singular: etcdcluster 22 | preserveUnknownFields: false 23 | scope: Namespaced 24 | version: v1beta2 25 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_metadata/metadata/annotations.yaml: -------------------------------------------------------------------------------- 1 | annotations: 2 | # Core bundle_with_metadata annotations. 3 | operators.operatorframework.io.bundle.mediatype.v1: registry+v1 4 | operators.operatorframework.io.bundle.manifests.v1: manifests/ 5 | operators.operatorframework.io.bundle.metadata.v1: metadata/ 6 | operators.operatorframework.io.bundle.package.v1: memcache-operator 7 | operators.operatorframework.io.bundle.channels.v1: alpha 8 | operators.operatorframework.io.bundle.channel.default.v1: alpha 9 | 10 | # Annotations for testing. 11 | operators.operatorframework.io.test.mediatype.v1: scorecard+v1 12 | operators.operatorframework.io.test.config.v1: tests/scorecard/ 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/go-verdiff.yaml: -------------------------------------------------------------------------------- 1 | name: go-verdiff 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | go-verdiff: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v6 11 | with: 12 | fetch-depth: 0 13 | - name: Check golang version 14 | run: | 15 | export LABELS="$(gh api repos/$OWNER/$REPO/pulls/$PR --jq '.labels.[].name')" 16 | hack/tools/check-go-version.sh -b "${{ github.event.pull_request.base.sha }}" 17 | shell: bash 18 | env: 19 | GH_TOKEN: ${{ github.token }} 20 | OWNER: ${{ github.repository_owner }} 21 | REPO: ${{ github.event.repository.name }} 22 | PR: ${{ github.event.pull_request.number }} 23 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/noConversionReviewVersions.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | conversion: 7 | strategy: Webhook 8 | webhook: 9 | clientConfig: 10 | service: 11 | namespace: system 12 | name: webhook-service 13 | path: /convert 14 | group: etcd.database.coreos.com 15 | names: 16 | kind: EtcdCluster 17 | listKind: EtcdClusterList 18 | plural: etcdclusters 19 | shortNames: 20 | - etcdclus 21 | - etcd 22 | singular: etcdcluster 23 | scope: Namespaced 24 | versions: 25 | - name: v1beta2 26 | served: true 27 | storage: true 28 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/v1.crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: etcdclusters.etcd.database.coreos.com 5 | spec: 6 | conversion: 7 | strategy: Webhook 8 | webhook: 9 | clientConfig: 10 | service: 11 | namespace: system 12 | name: webhook-service 13 | path: /convert 14 | conversionReviewVersions: 15 | - v1 16 | group: etcd.database.coreos.com 17 | names: 18 | kind: EtcdCluster 19 | listKind: EtcdClusterList 20 | plural: etcdclusters 21 | shortNames: 22 | - etcdclus 23 | - etcd 24 | singular: etcdcluster 25 | scope: Namespaced 26 | versions: 27 | - name: v1beta2 28 | served: true 29 | storage: true 30 | -------------------------------------------------------------------------------- /pkg/manifests/directory.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | // GetManifestsDir parses all bundles and a package manifest from a directory 4 | func GetManifestsDir(dir string) (*PackageManifest, []*Bundle, error) { 5 | loader := NewPackageManifestLoader(dir) 6 | 7 | err := loader.LoadPackage() 8 | if err != nil { 9 | return nil, nil, err 10 | } 11 | 12 | return loader.pkg, loader.bundles, nil 13 | } 14 | 15 | // GetBundleFromDir takes a raw directory containg an Operator Bundle and 16 | // serializes its component files (CSVs, CRDs, other native kube manifests) 17 | // and returns it as a Bundle 18 | func GetBundleFromDir(dir string) (*Bundle, error) { 19 | loader := NewBundleLoader(dir) 20 | 21 | err := loader.LoadBundle() 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return loader.bundle, nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/operators/reference/reference.go: -------------------------------------------------------------------------------- 1 | package reference 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 7 | k8sscheme "k8s.io/client-go/kubernetes/scheme" 8 | ref "k8s.io/client-go/tools/reference" 9 | 10 | "github.com/operator-framework/api/pkg/operators/install" 11 | ) 12 | 13 | var scheme = runtime.NewScheme() 14 | 15 | func init() { 16 | // Register all OLM types with the scheme 17 | install.Install(scheme) 18 | utilruntime.Must(k8sscheme.AddToScheme(scheme)) 19 | } 20 | 21 | // GetReference returns an ObjectReference for the given object. 22 | // The objects dynamic type must be an OLM type. 23 | func GetReference(obj runtime.Object) (*corev1.ObjectReference, error) { 24 | return ref.GetReference(scheme, obj) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/dockerfile/bundle_without_label.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | 10 | # Labels for testing. 11 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 12 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 13 | 14 | # Copy files to locations specified by labels. 15 | COPY bundle/manifests /manifests/ 16 | COPY bundle/metadata /metadata/ 17 | COPY bundle/tests/scorecard /tests/scorecard/ 18 | -------------------------------------------------------------------------------- /pkg/operators/install/install.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime" 5 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 6 | 7 | operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" 8 | operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" 9 | operatorsv1alpha2 "github.com/operator-framework/api/pkg/operators/v1alpha2" 10 | ) 11 | 12 | // Install registers the API group and adds all of its types to the given scheme. 13 | func Install(scheme *runtime.Scheme) { 14 | utilruntime.Must(operatorsv1alpha1.AddToScheme(scheme)) 15 | utilruntime.Must(operatorsv1alpha2.AddToScheme(scheme)) 16 | utilruntime.Must(operatorsv1.AddToScheme(scheme)) 17 | utilruntime.Must(scheme.SetVersionPriority(operatorsv1.GroupVersion, operatorsv1alpha2.GroupVersion, operatorsv1alpha1.SchemeGroupVersion)) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/validation/testdata/dockerfile/valid_bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL com.redhat.openshift.versions="v4.6-v4.8" 10 | 11 | # Labels for testing. 12 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 13 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 14 | 15 | # Copy files to locations specified by labels. 16 | COPY bundle/manifests /manifests/ 17 | COPY bundle/metadata /metadata/ 18 | COPY bundle/tests/scorecard /tests/scorecard/ 19 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/dockerfile/valid_bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL com.redhat.openshift.versions="v4.6-v4.8" 10 | 11 | # Labels for testing. 12 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 13 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 14 | 15 | # Copy files to locations specified by labels. 16 | COPY bundle/manifests /manifests/ 17 | COPY bundle/metadata /metadata/ 18 | COPY bundle/tests/scorecard /tests/scorecard/ 19 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/dockerfile/valid_bundle_4_8.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL com.redhat.openshift.versions="=v4.8" 10 | 11 | # Labels for testing. 12 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 13 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 14 | 15 | # Copy files to locations specified by labels. 16 | COPY bundle/manifests /manifests/ 17 | COPY bundle/metadata /metadata/ 18 | COPY bundle/tests/scorecard /tests/scorecard/ 19 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/dockerfile/invalid_bundle_equals_upper.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL com.redhat.openshift.versions="=v4.9" 10 | 11 | # Labels for testing. 12 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 13 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 14 | 15 | # Copy files to locations specified by labels. 16 | COPY bundle/manifests /manifests/ 17 | COPY bundle/metadata /metadata/ 18 | COPY bundle/tests/scorecard /tests/scorecard/ 19 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/dockerfile/invalid_bundle_range_upper.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL com.redhat.openshift.versions="v4.6-v4.9" 10 | 11 | # Labels for testing. 12 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 13 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 14 | 15 | # Copy files to locations specified by labels. 16 | COPY bundle/manifests /manifests/ 17 | COPY bundle/metadata /metadata/ 18 | COPY bundle/tests/scorecard /tests/scorecard/ 19 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/dockerfile/invalid_bundle_range_upper_coma.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=memcached-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL com.redhat.openshift.versions="v4.6,v4.7" 10 | 11 | # Labels for testing. 12 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 13 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 14 | 15 | # Copy files to locations specified by labels. 16 | COPY bundle/manifests /manifests/ 17 | COPY bundle/metadata /metadata/ 18 | COPY bundle/tests/scorecard /tests/scorecard/ 19 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_min_kube_version.csv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | name: test-operator.v0.0.1 5 | namespace: placeholder 6 | spec: 7 | minKubeVersion: 1.21 8 | displayName: test-operator 9 | install: 10 | strategy: deployment 11 | installModes: 12 | - supported: true 13 | type: OwnNamespace 14 | - supported: true 15 | type: SingleNamespace 16 | - supported: false 17 | type: MultiNamespace 18 | - supported: true 19 | type: AllNamespaces 20 | keywords: 21 | - test-operator 22 | links: 23 | - name: Test Operator 24 | url: https://test-operator.domain 25 | maintainers: 26 | - email: your@email.com 27 | name: Maintainer Name 28 | maturity: alpha 29 | provider: 30 | name: Provider Name 31 | url: https://your.domain 32 | version: 0.0.1 -------------------------------------------------------------------------------- /pkg/manifests/testdata/invalid_bundle_with_hidden/test-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | capabilities: Basic Install 6 | name: test-operator.v0.0.1 7 | namespace: placeholder 8 | spec: 9 | apiservicedefinitions: {} 10 | customresourcedefinitions: 11 | owned: 12 | - description: Test is the Schema for the tests API 13 | kind: Test 14 | name: tests.test.example.com 15 | version: v1alpha1 16 | installModes: 17 | - supported: true 18 | type: OwnNamespace 19 | - supported: true 20 | type: SingleNamespace 21 | - supported: false 22 | type: MultiNamespace 23 | - supported: true 24 | type: AllNamespaces 25 | keywords: 26 | - test-operator 27 | links: 28 | - name: Test Operator 29 | url: https://test-operator.domain 30 | maintainers: 31 | - email: your@email.com 32 | name: Maintainer Name 33 | maturity: alpha 34 | provider: 35 | name: Provider Name 36 | url: https://your.domain 37 | version: 0.0.1 38 | -------------------------------------------------------------------------------- /pkg/manifests/testdata/invalid_bundle_with_subdir/test-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | capabilities: Basic Install 6 | name: test-operator.v0.0.1 7 | namespace: placeholder 8 | spec: 9 | apiservicedefinitions: {} 10 | customresourcedefinitions: 11 | owned: 12 | - description: Test is the Schema for the tests API 13 | kind: Test 14 | name: tests.test.example.com 15 | version: v1alpha1 16 | installModes: 17 | - supported: true 18 | type: OwnNamespace 19 | - supported: true 20 | type: SingleNamespace 21 | - supported: false 22 | type: MultiNamespace 23 | - supported: true 24 | type: AllNamespaces 25 | keywords: 26 | - test-operator 27 | links: 28 | - name: Test Operator 29 | url: https://test-operator.domain 30 | maintainers: 31 | - email: your@email.com 32 | name: Maintainer Name 33 | maturity: alpha 34 | provider: 35 | name: Provider Name 36 | url: https://your.domain 37 | version: 0.0.1 38 | -------------------------------------------------------------------------------- /pkg/operators/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // +kubebuilder:object:generate=true 2 | 3 | // Package v1 contains API Schema definitions for the operator v1 API group. 4 | package v1 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/scheme" 9 | ) 10 | 11 | var ( 12 | // GroupVersion is group version used to register these objects. 13 | GroupVersion = schema.GroupVersion{Group: "operators.coreos.com", Version: "v1"} 14 | 15 | // SchemeGroupVersion is required for compatibility with client generation. 16 | SchemeGroupVersion = GroupVersion 17 | 18 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 19 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 20 | 21 | // AddToScheme adds the types in this group-version to the given scheme. 22 | AddToScheme = SchemeBuilder.AddToScheme 23 | ) 24 | 25 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 26 | func Resource(resource string) schema.GroupResource { 27 | return GroupVersion.WithResource(resource).GroupResource() 28 | } 29 | -------------------------------------------------------------------------------- /pkg/operators/v2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // +kubebuilder:object:generate=true 2 | 3 | // Package v2 contains API Schema definitions for the operator v2 API group. 4 | package v2 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/scheme" 9 | ) 10 | 11 | var ( 12 | // GroupVersion is group version used to register these objects. 13 | GroupVersion = schema.GroupVersion{Group: "operators.coreos.com", Version: "v2"} 14 | 15 | // SchemeGroupVersion is required for compatibility with client generation. 16 | SchemeGroupVersion = GroupVersion 17 | 18 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 19 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 20 | 21 | // AddToScheme adds the types in this group-version to the given scheme. 22 | AddToScheme = SchemeBuilder.AddToScheme 23 | ) 24 | 25 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 26 | func Resource(resource string) schema.GroupResource { 27 | return GroupVersion.WithResource(resource).GroupResource() 28 | } 29 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid_bundle_3/test-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | capabilities: Basic Install 6 | name: test-operator.v0.0.1 7 | namespace: placeholder 8 | spec: 9 | apiservicedefinitions: {} 10 | customresourcedefinitions: 11 | owned: 12 | - description: Test is the Schema for the tests API 13 | kind: Test 14 | name: tests.test.example.com 15 | version: v1alpha1 16 | installModes: 17 | - supported: true 18 | type: OwnNamespace 19 | - supported: true 20 | type: SingleNamespace 21 | - supported: false 22 | type: MultiNamespace 23 | - supported: true 24 | type: AllNamespaces 25 | keywords: 26 | - test-operator 27 | links: 28 | - name: Test Operator 29 | url: https://test-operator.domain 30 | maintainers: 31 | - email: your@email.com 32 | name: Maintainer Name 33 | maturity: alpha 34 | provider: 35 | name: Provider Name 36 | url: https://your.domain 37 | version: 0.0.1 38 | -------------------------------------------------------------------------------- /pkg/validation/doc.go: -------------------------------------------------------------------------------- 1 | // This package defines the valid Operator manifests directory format 2 | // by exposing a set of Validator's to verify a directory and 3 | // its constituent manifests. A manifests directory consists of a 4 | // package manifest and a set of versioned Bundles. Each Bundle contains a 5 | // ClusterServiceVersion and one or more CustomResourceDefinition's. 6 | // 7 | // Errors and warnings, both represented by the Error type, are returned 8 | // by exported functions for missing mandatory and optional fields, 9 | // respectively. Each Error implements the error interface. 10 | // 11 | // - Bundle format: https://github.com/operator-framework/operator-registry/#manifest-format 12 | // 13 | // - ClusterServiceVersion documentation: https://github.com/operator-framework/operator-lifecycle-manager/blob/master/Documentation/design/building-your-csv.md 14 | // 15 | // - Package manifest documentation: https://github.com/operator-framework/operator-lifecycle-manager#discovery-catalogs-and-automated-upgrades 16 | 17 | // - CustomResourceDefinition documentation: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ 18 | package validation 19 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_2/test-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | capabilities: Basic Install 6 | name: test-operator.v0.0.1 7 | namespace: placeholder 8 | spec: 9 | apiservicedefinitions: {} 10 | customresourcedefinitions: 11 | owned: 12 | - description: Test is the Schema for the tests API 13 | kind: Test 14 | name: tests.test.example.com 15 | version: v1alpha1 16 | - description: TestTwo is the Schema for the tests API 17 | kind: TestTwo 18 | name: TestTwos.test.example.com 19 | version: v1beta1 20 | installModes: 21 | - supported: true 22 | type: OwnNamespace 23 | - supported: true 24 | type: SingleNamespace 25 | - supported: false 26 | type: MultiNamespace 27 | - supported: true 28 | type: AllNamespaces 29 | keywords: 30 | - test-operator 31 | links: 32 | - name: Test Operator 33 | url: https://test-operator.domain 34 | maintainers: 35 | - email: your@email.com 36 | name: Maintainer Name 37 | maturity: alpha 38 | provider: 39 | name: Provider Name 40 | url: https://your.domain 41 | version: 0.0.1 42 | -------------------------------------------------------------------------------- /pkg/encoding/encoding_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGzipBase64EncodeDecode(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | source string 14 | }{ 15 | { 16 | name: "Encode-Decode-CSV", 17 | source: "testdata/etcdoperator.v0.9.4.clusterserviceversion.yaml", 18 | }, 19 | { 20 | name: "Encode-Decode-CRD", 21 | source: "testdata/etcdclusters.etcd.database.coreos.com.crd.yaml", 22 | }, 23 | } 24 | 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | data, err := os.ReadFile(tt.source) 28 | require.NoError(t, err, "unable to load from file %s", tt.source) 29 | 30 | encoded, err := GzipBase64Encode(data) 31 | require.NoError(t, err, "unexpected error while encoding data") 32 | 33 | require.Lessf(t, len(encoded), len(data), 34 | "encoded data (%d bytes) isn't lesser than original data (%d bytes)", 35 | len(encoded), len(data)) 36 | 37 | decoded, err := GzipBase64Decode(encoded) 38 | require.NoError(t, err, "unexpected error while decoding data") 39 | 40 | require.Equal(t, data, decoded, "decoded data doesn't match original data") 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/operators/register.go: -------------------------------------------------------------------------------- 1 | package operators 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | ) 7 | 8 | const ( 9 | // GroupName is the group name used in this package. 10 | GroupName = "operators.coreos.com" 11 | // GroupVersion is the group version used in this package. 12 | GroupVersion = runtime.APIVersionInternal 13 | 14 | // LEGACY: Exported kind names, remove after major version bump 15 | 16 | // ClusterServiceVersionKind is the kind name for ClusterServiceVersion resources. 17 | ClusterServiceVersionKind = "ClusterServiceVersion" 18 | // CatalogSourceKind is the kind name for CatalogSource resources. 19 | CatalogSourceKind = "CatalogSource" 20 | // InstallPlanKind is the kind name for InstallPlan resources. 21 | InstallPlanKind = "InstallPlan" 22 | // SubscriptionKind is the kind name for Subscription resources. 23 | SubscriptionKind = "Subscription" 24 | // OperatorKind is the kind name for Operator resources. 25 | OperatorKind = "Operator" 26 | // OperatorGroupKind is the kind name for OperatorGroup resources. 27 | OperatorGroupKind = "OperatorGroup" 28 | ) 29 | 30 | // SchemeGroupVersion is group version used to register these objects 31 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} 32 | -------------------------------------------------------------------------------- /crds/defs_test.go: -------------------------------------------------------------------------------- 1 | package crds 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | ) 9 | 10 | var emptyCRD = &apiextensionsv1.CustomResourceDefinition{} 11 | 12 | func TestGetters(t *testing.T) { 13 | tests := []struct { 14 | description string 15 | get func() *apiextensionsv1.CustomResourceDefinition 16 | }{ 17 | { 18 | description: "CatalogSource", 19 | get: CatalogSource, 20 | }, 21 | { 22 | description: "ClusterServiceVersion", 23 | get: ClusterServiceVersion, 24 | }, 25 | { 26 | description: "InstallPlan", 27 | get: InstallPlan, 28 | }, 29 | { 30 | description: "OperatorGroup", 31 | get: OperatorGroup, 32 | }, 33 | { 34 | description: "Operator", 35 | get: Operator, 36 | }, 37 | { 38 | description: "Subscription", 39 | get: Subscription, 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.description, func(t *testing.T) { 44 | defer func() { 45 | if x := recover(); x != nil { 46 | t.Errorf("panic loading crd: %v", x) 47 | } 48 | }() 49 | 50 | crd := tt.get() 51 | if crd == nil || reflect.DeepEqual(crd, emptyCRD) { 52 | t.Error("loaded CustomResourceDefinition is empty") 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/encoding/encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "io" 8 | ) 9 | 10 | // GzipBase64Encode applies gzip compression to the given bytes, followed by base64 encoding. 11 | func GzipBase64Encode(data []byte) ([]byte, error) { 12 | buf := &bytes.Buffer{} 13 | 14 | bWriter := base64.NewEncoder(base64.StdEncoding, buf) 15 | zWriter := gzip.NewWriter(bWriter) 16 | _, err := zWriter.Write(data) 17 | if err != nil { 18 | zWriter.Close() 19 | bWriter.Close() 20 | return nil, err 21 | } 22 | 23 | // Ensure all gzipped bytes are flushed to the underlying base64 encoder 24 | err = zWriter.Close() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | // Ensure all base64d bytes are flushed to the underlying buffer 30 | err = bWriter.Close() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return buf.Bytes(), nil 36 | } 37 | 38 | // GzipBase64Decode applies base64 decoding to the given bytes, followed by gzip decompression. 39 | func GzipBase64Decode(data []byte) ([]byte, error) { 40 | bBuffer := bytes.NewReader(data) 41 | 42 | bReader := base64.NewDecoder(base64.StdEncoding, bBuffer) 43 | zReader, err := gzip.NewReader(bReader) 44 | if err != nil { 45 | return nil, err 46 | } 47 | defer zReader.Close() 48 | 49 | return io.ReadAll(zReader) 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: verify 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | paths: 8 | - '**' 9 | workflow_dispatch: 10 | merge_group: 11 | jobs: 12 | verify: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: actions/setup-go@v6 17 | with: 18 | go-version-file: go.mod 19 | - name: Run the verify target 20 | run: | 21 | export GOPATH=$(go env GOPATH) 22 | make verify 23 | 24 | kind: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v6 28 | - uses: actions/setup-go@v6 29 | with: 30 | go-version-file: go.mod 31 | - name: Deploy Kind 32 | # Not guaranteed to have patch releases available and node image tags are full versions (i.e v1.28.0 - no v1.28, v1.29, etc.) 33 | # The KIND_NODE_VERSION is set by getting the version of the k8s.io/client-go dependency from the go.mod 34 | # and sets major version to "1" and the patch version to "0". For example, a client-go version of v0.28.5 35 | # will map to a KIND_NODE_VERSION of 1.28.0 36 | run: make kind-cluster 37 | - name: Apply CRDs 38 | run: | 39 | set -e 40 | for crd in $(ls crds/*.yaml); do 41 | kubectl create -f $crd 42 | done -------------------------------------------------------------------------------- /pkg/manifests/bundle.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 5 | apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | 8 | operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" 9 | ) 10 | 11 | type Bundle struct { 12 | Name string 13 | Objects []*unstructured.Unstructured 14 | Package string 15 | Channels []string 16 | DefaultChannel string 17 | BundleImage string 18 | CSV *operatorsv1alpha1.ClusterServiceVersion 19 | V1beta1CRDs []*apiextensionsv1beta1.CustomResourceDefinition 20 | V1CRDs []*apiextensionsv1.CustomResourceDefinition 21 | Dependencies []*Dependency 22 | // CompressedSize stores the gzip size of the bundle 23 | CompressedSize int64 24 | // Size stores the size of the bundle 25 | Size int64 26 | } 27 | 28 | func (b *Bundle) ObjectsToValidate() []interface{} { 29 | objs := []interface{}{} 30 | for _, crd := range b.V1CRDs { 31 | objs = append(objs, crd) 32 | } 33 | for _, crd := range b.V1beta1CRDs { 34 | objs = append(objs, crd) 35 | } 36 | objs = append(objs, b.CSV) 37 | 38 | for _, o := range b.Objects { 39 | objs = append(objs, o) 40 | } 41 | objs = append(objs, b) 42 | 43 | return objs 44 | } 45 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/webhook.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1beta1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | creationTimestamp: null 6 | name: mutating-webhook-configuration 7 | webhooks: 8 | - clientConfig: 9 | caBundle: Cg== 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /mutate-cache-example-com-v1alpha1-memcached 14 | failurePolicy: Fail 15 | name: mmemcached.kb.io 16 | rules: 17 | - apiGroups: 18 | - cache.example.com 19 | apiVersions: 20 | - v1alpha1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - memcacheds 26 | 27 | --- 28 | apiVersion: admissionregistration.k8s.io/v1beta1 29 | kind: ValidatingWebhookConfiguration 30 | metadata: 31 | creationTimestamp: null 32 | name: validating-webhook-configuration 33 | webhooks: 34 | - clientConfig: 35 | caBundle: Cg== 36 | service: 37 | name: webhook-service 38 | namespace: system 39 | path: /validate-cache-example-com-v1alpha1-memcached 40 | failurePolicy: Fail 41 | name: vmemcached.kb.io 42 | rules: 43 | - apiGroups: 44 | - cache.example.com 45 | apiVersions: 46 | - v1alpha1 47 | operations: 48 | - CREATE 49 | - UPDATE 50 | resources: 51 | - memcacheds -------------------------------------------------------------------------------- /pkg/validation/internal/operatorgroup_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | "testing" 7 | 8 | operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" 9 | "github.com/operator-framework/api/pkg/validation/errors" 10 | "sigs.k8s.io/yaml" 11 | ) 12 | 13 | func TestValidateOperatorGroup(t *testing.T) { 14 | cases := []struct { 15 | validatorFuncTest 16 | operatorGroupPath string 17 | }{ 18 | { 19 | validatorFuncTest{ 20 | description: "successfully validated", 21 | }, 22 | filepath.Join("testdata", "correct.og.yaml"), 23 | }, 24 | { 25 | validatorFuncTest{ 26 | description: "invalid annotation name for operator group", 27 | wantErr: true, 28 | errors: []errors.Error{ 29 | errors.ErrFailedValidation("provided annotation olm.providedapis uses wrong case and should be olm.providedAPIs instead", "nginx-hbvsw"), 30 | }, 31 | }, 32 | filepath.Join("testdata", "badAnnotationNames.og.yaml"), 33 | }, 34 | } 35 | for _, c := range cases { 36 | b, err := ioutil.ReadFile(c.operatorGroupPath) 37 | if err != nil { 38 | t.Fatalf("Error reading OperatorGroup path %s: %v", c.operatorGroupPath, err) 39 | } 40 | og := operatorsv1.OperatorGroup{} 41 | if err = yaml.Unmarshal(b, &og); err != nil { 42 | t.Fatalf("Error unmarshalling OperatorGroup at path %s: %v", c.operatorGroupPath, err) 43 | } 44 | result := validateOperatorGroupV1(&og) 45 | c.check(t, result) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/validation/internal/standardcategories_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/operator-framework/api/pkg/manifests" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestValidateCategories(t *testing.T) { 12 | var table = []struct { 13 | description string 14 | directory string 15 | hasError bool 16 | errStrings []string 17 | }{ 18 | { 19 | description: "registryv1 bundle/valid bundle", 20 | directory: "./testdata/valid_bundle", 21 | hasError: false, 22 | }, 23 | { 24 | description: "registryv1 bundle/invald bundle operatorhubio", 25 | directory: "./testdata/invalid_bundle_operatorhub", 26 | hasError: true, 27 | errStrings: []string{ 28 | `Error: Value : (etcdoperator.v0.9.4) csv.Metadata.Annotations["categories"] value "Magic" is not in the set of standard categories`, 29 | }, 30 | }, 31 | } 32 | 33 | for _, tt := range table { 34 | // Validate the bundle object 35 | bundle, err := manifests.GetBundleFromDir(tt.directory) 36 | require.NoError(t, err) 37 | 38 | results := StandardCategoriesValidator.Validate(bundle) 39 | 40 | if len(results) > 0 { 41 | require.Equal(t, results[0].HasError(), tt.hasError) 42 | if results[0].HasError() { 43 | require.Equal(t, len(tt.errStrings), len(results[0].Errors)) 44 | 45 | for _, err := range results[0].Errors { 46 | errString := err.Error() 47 | require.Contains(t, tt.errStrings, errString) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/validation/internal/operatorgroup.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" 5 | operatorsv1alpha2 "github.com/operator-framework/api/pkg/operators/v1alpha2" 6 | "github.com/operator-framework/api/pkg/validation/errors" 7 | interfaces "github.com/operator-framework/api/pkg/validation/interfaces" 8 | ) 9 | 10 | // OperatorGroupValidator is a validator for OperatorGroup 11 | var OperatorGroupValidator interfaces.Validator = interfaces.ValidatorFunc(validateOperatorGroups) 12 | 13 | func validateOperatorGroups(objs ...interface{}) (results []errors.ManifestResult) { 14 | for _, obj := range objs { 15 | switch v := obj.(type) { 16 | case *operatorsv1.OperatorGroup: 17 | results = append(results, validateOperatorGroupV1(v)) 18 | case *operatorsv1alpha2.OperatorGroup: 19 | results = append(results, validateOperatorGroupV1Alpha2(v)) 20 | } 21 | } 22 | return results 23 | } 24 | 25 | func validateOperatorGroupV1Alpha2(operatorGroup *operatorsv1alpha2.OperatorGroup) (result errors.ManifestResult) { 26 | // validate case sensitive annotation names 27 | result.Add(ValidateAnnotationNames(operatorGroup.GetAnnotations(), operatorGroup.GetName())...) 28 | return result 29 | } 30 | 31 | func validateOperatorGroupV1(operatorGroup *operatorsv1.OperatorGroup) (result errors.ManifestResult) { 32 | // validate case sensitive annotation names 33 | result.Add(ValidateAnnotationNames(operatorGroup.GetAnnotations(), operatorGroup.GetName())...) 34 | return result 35 | } 36 | -------------------------------------------------------------------------------- /pkg/validation/internal/standardcapabilities_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/operator-framework/api/pkg/manifests" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestValidateCapabilities(t *testing.T) { 12 | var table = []struct { 13 | description string 14 | directory string 15 | hasError bool 16 | errStrings []string 17 | }{ 18 | { 19 | description: "registryv1 bundle/valid bundle", 20 | directory: "./testdata/valid_bundle", 21 | hasError: false, 22 | }, 23 | { 24 | description: "registryv1 bundle/invald bundle operatorhubio", 25 | directory: "./testdata/invalid_bundle_operatorhub", 26 | hasError: true, 27 | errStrings: []string{ 28 | `Error: Value : (etcdoperator.v0.9.4) csv.Metadata.Annotations.Capabilities "Installs and stuff" is not a valid capabilities level`, 29 | }, 30 | }, 31 | } 32 | 33 | for _, tt := range table { 34 | // Validate the bundle object 35 | bundle, err := manifests.GetBundleFromDir(tt.directory) 36 | require.NoError(t, err) 37 | 38 | results := StandardCapabilitiesValidator.Validate(bundle) 39 | 40 | if len(results) > 0 { 41 | require.Equal(t, results[0].HasError(), tt.hasError) 42 | if results[0].HasError() { 43 | require.Equal(t, len(tt.errStrings), len(results[0].Errors)) 44 | 45 | for _, err := range results[0].Errors { 46 | errString := err.Error() 47 | require.Contains(t, tt.errStrings, errString) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/manifests/packagemanifest.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | // PackageManifest holds information about a package, which is a reference to one (or more) 4 | // channels under a single package. 5 | type PackageManifest struct { 6 | // PackageName is the name of the overall package, ala `etcd`. 7 | PackageName string `json:"packageName" yaml:"packageName"` 8 | 9 | // Channels are the declared channels for the package, ala `stable` or `alpha`. 10 | Channels []PackageChannel `json:"channels" yaml:"channels"` 11 | 12 | // DefaultChannelName is, if specified, the name of the default channel for the package. The 13 | // default channel will be installed if no other channel is explicitly given. If the package 14 | // has a single channel, then that channel is implicitly the default. 15 | DefaultChannelName string `json:"defaultChannel" yaml:"defaultChannel"` 16 | } 17 | 18 | // IsEmpty returns true if the PackageManifest instance is equal to the zero value 19 | func (p *PackageManifest) IsEmpty() bool { 20 | return p.PackageName == "" && len(p.Channels) == 0 && p.DefaultChannelName == "" 21 | } 22 | 23 | // PackageChannel defines a single channel under a package, pointing to a version of that 24 | // package. 25 | type PackageChannel struct { 26 | // Name is the name of the channel, e.g. `alpha` or `stable` 27 | Name string `json:"name" yaml:"name"` 28 | 29 | // CurrentCSVName defines a reference to the CSV holding the version of this package currently 30 | // for the channel. 31 | CurrentCSVName string `json:"currentCSV" yaml:"currentCSV"` 32 | } 33 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /pkg/manifests/directory_test.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestGetBundleDir(t *testing.T) { 10 | bundle, err := GetBundleFromDir("./testdata/valid_bundle") 11 | require.NoError(t, err) 12 | require.Equal(t, "etcdoperator.v0.9.4", bundle.Name) 13 | require.NotNil(t, bundle.CSV) 14 | require.Equal(t, 3, len(bundle.V1beta1CRDs)) 15 | require.Equal(t, 4, len(bundle.Objects)) 16 | } 17 | 18 | func TestGetPackage(t *testing.T) { 19 | pkg, bundles, err := GetManifestsDir("./testdata/valid_package") 20 | require.NoError(t, err) 21 | require.NotNil(t, pkg) 22 | require.Equal(t, "etcd", pkg.PackageName) 23 | require.Equal(t, 2, len(bundles)) 24 | require.Equal(t, "etcdoperator.v0.9.2", bundles[0].Name) 25 | require.NotNil(t, bundles[0].CSV) 26 | require.Equal(t, 3, len(bundles[0].V1beta1CRDs)) 27 | require.Equal(t, "etcdoperator.v0.9.4", bundles[1].Name) 28 | require.NotNil(t, bundles[1].CSV) 29 | require.Equal(t, 2, len(bundles[1].V1beta1CRDs)) 30 | require.Equal(t, 1, len(bundles[1].V1CRDs)) 31 | } 32 | 33 | func TestLoadBundle(t *testing.T) { 34 | var err error 35 | 36 | _, err = loadBundle("test-operator.v0.0.1", "./testdata/invalid_bundle_with_subdir") 37 | require.EqualError(t, err, "bundle manifests dir contains directory: testdata/invalid_bundle_with_subdir/foo") 38 | _, err = loadBundle("test-operator.v0.0.1", "./testdata/invalid_bundle_with_hidden") 39 | require.EqualError(t, err, "bundle manifests dir has hidden file: testdata/invalid_bundle_with_hidden/.hidden") 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - '**' 9 | workflow_dispatch: 10 | merge_group: 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v6 18 | - name: Set up Go 19 | uses: actions/setup-go@v6 20 | with: 21 | go-version-file: go.mod 22 | id: go 23 | - name: Cache dependencies 24 | uses: actions/cache@v4 25 | with: 26 | path: | 27 | ~/.cache/go-build 28 | ~/go/pkg/mod 29 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 30 | restore-keys: | 31 | ${{ runner.os }}-go- 32 | - name: unit-test 33 | run: go test -v ./... -coverprofile cover.out 34 | - uses: codecov/codecov-action@v5 35 | with: 36 | disable_search: true 37 | files: cover.out 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | 40 | go-apidiff: 41 | name: go-apidiff 42 | if: github.event_name == 'pull_request' 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Check out code into the Go module directory 46 | uses: actions/checkout@v6 47 | with: 48 | fetch-depth: 0 49 | - name: Set up Go 50 | uses: actions/setup-go@v6 51 | with: 52 | go-version-file: go.mod 53 | id: go 54 | - name: Print out Go env 55 | run: go env 56 | - name: Run go-apidiff 57 | uses: joelanford/go-apidiff@main 58 | -------------------------------------------------------------------------------- /pkg/operators/v1alpha1/installplan_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestOrderSteps(t *testing.T) { 10 | tests := []struct { 11 | description string 12 | in []*Step 13 | }{ 14 | { 15 | description: "EmptyList", 16 | in: []*Step{}, 17 | }, 18 | { 19 | description: "csvsRDS", 20 | in: []*Step{step(crdKind), step(ClusterServiceVersionKind), step(crdKind), step(crdKind)}, 21 | }, 22 | { 23 | description: "csvsCRDSAndRandomKinds", 24 | in: []*Step{step(crdKind), step(ClusterServiceVersionKind), step(crdKind), step(crdKind), step("These"), step("are"), step("random"), step("Kinds")}, 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | t.Run(tt.description, func(t *testing.T) { 30 | result := OrderSteps(tt.in) 31 | require.EqualValues(t, len(result), len(tt.in)) 32 | require.True(t, isOrdered(result)) 33 | }) 34 | } 35 | } 36 | 37 | func step(kind string) *Step { 38 | resource := StepResource{} 39 | resource.Kind = kind 40 | 41 | result := &Step{} 42 | result.Resource = resource 43 | return result 44 | } 45 | 46 | func isOrdered(steps []*Step) bool { 47 | var crdSeen, otherResourceSeen bool 48 | for _, step := range steps { 49 | switch step.Resource.Kind { 50 | case ClusterServiceVersionKind: 51 | if crdSeen || otherResourceSeen { 52 | return false 53 | } 54 | case crdKind: 55 | crdSeen = true 56 | if otherResourceSeen { 57 | return false 58 | } 59 | default: 60 | otherResourceSeen = true 61 | } 62 | } 63 | return true 64 | } 65 | -------------------------------------------------------------------------------- /pkg/operators/v1alpha2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | // +kubebuilder:object:generate=true 16 | 17 | // Package v1alpha2 contains API Schema definitions for the discovery v1alpha2 API group. 18 | package v1alpha2 19 | 20 | import ( 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "sigs.k8s.io/controller-runtime/pkg/scheme" 23 | ) 24 | 25 | var ( 26 | // GroupVersion is group version used to register these objects. 27 | GroupVersion = schema.GroupVersion{Group: "operators.coreos.com", Version: "v1alpha2"} 28 | 29 | // SchemeGroupVersion is required for compatibility with client generation. 30 | SchemeGroupVersion = GroupVersion 31 | 32 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 33 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 34 | 35 | // AddToScheme adds the types in this group-version to the given scheme. 36 | AddToScheme = SchemeBuilder.AddToScheme 37 | ) 38 | 39 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 40 | func Resource(resource string) schema.GroupResource { 41 | return GroupVersion.WithResource(resource).GroupResource() 42 | } 43 | -------------------------------------------------------------------------------- /pkg/operators/v1/operatorgroup_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestUpgradeStrategy(t *testing.T) { 10 | tests := []struct { 11 | description string 12 | og *OperatorGroup 13 | expected UpgradeStrategy 14 | }{ 15 | { 16 | description: "NoSpec", 17 | og: &OperatorGroup{}, 18 | expected: UpgradeStrategyDefault, 19 | }, 20 | { 21 | description: "NoUpgradeStrategy", 22 | og: &OperatorGroup{ 23 | Spec: OperatorGroupSpec{}, 24 | }, 25 | expected: UpgradeStrategyDefault, 26 | }, 27 | { 28 | description: "NoUpgradeStrategy", 29 | og: &OperatorGroup{ 30 | Spec: OperatorGroupSpec{ 31 | UpgradeStrategy: "", 32 | }, 33 | }, 34 | expected: UpgradeStrategyDefault, 35 | }, 36 | { 37 | description: "NonSupportedUpgradeStrategy", 38 | og: &OperatorGroup{ 39 | Spec: OperatorGroupSpec{ 40 | UpgradeStrategy: "foo", 41 | }, 42 | }, 43 | expected: UpgradeStrategyDefault, 44 | }, 45 | { 46 | description: "DefaultUpgradeStrategy", 47 | og: &OperatorGroup{ 48 | Spec: OperatorGroupSpec{ 49 | UpgradeStrategy: "Default", 50 | }, 51 | }, 52 | expected: UpgradeStrategyDefault, 53 | }, 54 | { 55 | description: "UnsafeFailForwardUpgradeStrategy", 56 | og: &OperatorGroup{ 57 | Spec: OperatorGroupSpec{ 58 | UpgradeStrategy: "TechPreviewUnsafeFailForward", 59 | }, 60 | }, 61 | expected: UpgradeStrategyUnsafeFailForward, 62 | }, 63 | } 64 | 65 | for _, tt := range tests { 66 | t.Run(tt.description, func(t *testing.T) { 67 | require.EqualValues(t, tt.expected, tt.og.UpgradeStrategy()) 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/validation/interfaces/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "github.com/operator-framework/api/pkg/validation/errors" 5 | ) 6 | 7 | // Validator is an interface for validating arbitrary objects. 8 | type Validator interface { 9 | // Validate takes a list of arbitrary objects and returns a slice of results, 10 | // one for each object validated. 11 | Validate(...interface{}) []errors.ManifestResult 12 | // WithValidators returns a Validator appended to a variable number of 13 | // Validator's. 14 | WithValidators(...Validator) Validators 15 | } 16 | 17 | // ValidatorFunc implements Validator. ValidatorFunc can be used as a wrapper 18 | // for functions that run object validators. 19 | type ValidatorFunc func(...interface{}) []errors.ManifestResult 20 | 21 | // Validate runs the ValidatorFunc on objs. 22 | func (f ValidatorFunc) Validate(objs ...interface{}) (results []errors.ManifestResult) { 23 | return f(objs...) 24 | } 25 | 26 | // WithValidators appends the ValidatorFunc to vals. 27 | func (f ValidatorFunc) WithValidators(vals ...Validator) Validators { 28 | return append(vals, f) 29 | } 30 | 31 | // Validators is a set of Validator's that implements Validate. 32 | type Validators []Validator 33 | 34 | // Validate invokes each Validator in Validators, collecting and returning 35 | // the results. 36 | func (validators Validators) Validate(objs ...interface{}) (results []errors.ManifestResult) { 37 | for _, validator := range validators { 38 | results = append(results, validator.Validate(objs...)...) 39 | } 40 | return results 41 | } 42 | 43 | // WithValidators appends vals to Validators. 44 | func (validators Validators) WithValidators(vals ...Validator) Validators { 45 | return append(vals, validators...) 46 | } 47 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Releases of this repo target [semver][semver] tags pushed by repo admins. 4 | To request a release, please ping an admin on [#olm-dev][slack-olm-dev] 5 | or [#operator-sdk-dev][slack-osdk-dev] Kubernetes Slack channels, or 6 | post to the [operator-framework group][of-ggroup]. 7 | 8 | ## Tags 9 | 10 | As per semver, all releases containing new features must map to a major or minor version increase. 11 | 12 | **Patch releases must only contain bug fixes. Releases containing features must be major or minor releases.** 13 | 14 | ## Process 15 | 16 | In your local shell (assuming you have repo admin privileges): 17 | 18 | ```sh 19 | export PREVIOUS_RELEASE_TAG=$(git describe --tags --abbrev=0) 20 | export RELEASE_TAG="vX.Y.Z" 21 | git checkout master 22 | git pull master 23 | git fetch --all 24 | git tag $RELEASE_TAG 25 | # Assuming the 'upstream' remote points to the operator-framework repo. 26 | git push upstream refs/tags/$RELEASE_TAG 27 | ``` 28 | 29 | Then create release notes while still on the `master` branch: 30 | 31 | ```sh 32 | while read -r line; do echo $line | awk '{f = $1; $1 = ""; print "-"$0; }'; done <<< $(git log $PREVIOUS_RELEASE_TAG..$RELEASE_TAG --format=oneline --no-merges) 33 | ``` 34 | 35 | **You cannot cut a patch release if any of these release notes start with `feat:` or `feature:`.** 36 | 37 | Copy them into the Github release [description form][release-desc-page], 38 | select `vX.Y.Z` in the `Tag version` form, and click `Publish release`. 39 | 40 | [semver]:https://semver.org/ 41 | [slack-olm-dev]:https://kubernetes.slack.com/messages/olm-dev 42 | [slack-osdk-dev]:https://kubernetes.slack.com/messages/operator-sdk-dev 43 | [of-ggroup]:https://groups.google.com/forum/#!forum/operator-framework 44 | [release-desc-page]:https://github.com/operator-framework/api/releases/new 45 | -------------------------------------------------------------------------------- /pkg/validation/internal/test_suite.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/operator-framework/api/pkg/validation/errors" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type validatorFuncTest struct { 12 | description string 13 | wantErr, wantWarn bool 14 | errors []errors.Error 15 | } 16 | 17 | func (c validatorFuncTest) check(t *testing.T, result errors.ManifestResult) { 18 | if c.wantErr { 19 | if !result.HasError() { 20 | t.Errorf("%s: expected errors %#v, got nil", c.description, c.errors) 21 | } else { 22 | errs, _ := splitErrorsWarnings(c.errors) 23 | checkErrorsMatch(t, errs, result.Errors) 24 | } 25 | } 26 | if c.wantWarn { 27 | if !result.HasWarn() { 28 | t.Errorf("%s: expected warnings %#v, got nil", c.description, c.errors) 29 | } else { 30 | _, warns := splitErrorsWarnings(c.errors) 31 | checkErrorsMatch(t, warns, result.Warnings) 32 | } 33 | } 34 | if !c.wantErr && !c.wantWarn && (result.HasError() || result.HasWarn()) { 35 | t.Errorf("%s: expected no errors or warnings, got:\n%v", c.description, result) 36 | } 37 | } 38 | 39 | func splitErrorsWarnings(all []errors.Error) (errs, warns []errors.Error) { 40 | for _, a := range all { 41 | if a.Level == errors.LevelError { 42 | errs = append(errs, a) 43 | } else { 44 | warns = append(warns, a) 45 | } 46 | } 47 | return 48 | } 49 | 50 | func checkErrorsMatch(t *testing.T, errs1, errs2 []errors.Error) { 51 | // Do string matching on error types for test purposes. 52 | for i, err := range errs1 { 53 | if badErr, ok := err.BadValue.(error); ok && badErr != nil { 54 | errs1[i].BadValue = badErr.Error() 55 | } 56 | } 57 | for i, err := range errs2 { 58 | if badErr, ok := err.BadValue.(error); ok && badErr != nil { 59 | errs2[i].BadValue = badErr.Error() 60 | } 61 | } 62 | require.ElementsMatch(t, errs1, errs2) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/apis/scorecard/v1alpha3/formatter.go: -------------------------------------------------------------------------------- 1 | package v1alpha3 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func (s Test) MarshalText() string { 10 | var sb strings.Builder 11 | 12 | sb.WriteString(fmt.Sprintf("%s\n", strings.Repeat("-", 80))) 13 | sb.WriteString(fmt.Sprintf("Image: %s\n", s.Spec.Image)) 14 | 15 | if len(s.Spec.Entrypoint) > 0 { 16 | sb.WriteString(fmt.Sprintf("Entrypoint: %s\n", s.Spec.Entrypoint)) 17 | } 18 | 19 | if len(s.Spec.Labels) > 0 { 20 | sb.WriteString("Labels:\n") 21 | for labelKey, labelValue := range s.Spec.Labels { 22 | sb.WriteString(fmt.Sprintf("\t%q:%q\n", labelKey, labelValue)) 23 | } 24 | } 25 | if len(s.Status.Results) > 0 { 26 | sb.WriteString("Results:\n") 27 | for _, result := range s.Status.Results { 28 | if len(result.Name) > 0 { 29 | sb.WriteString(fmt.Sprintf("\tName: %s\n", result.Name)) 30 | } 31 | sb.WriteString("\tState: ") 32 | switch result.State { 33 | case PassState, FailState, ErrorState: 34 | sb.WriteString(string(result.State)) 35 | sb.WriteString("\n") 36 | default: 37 | sb.WriteString("unknown") 38 | } 39 | sb.WriteString("\n") 40 | 41 | if len(result.Suggestions) > 0 { 42 | sb.WriteString("\tSuggestions:\n") 43 | for _, suggestion := range result.Suggestions { 44 | sb.WriteString(fmt.Sprintf("\t\t%s\n", suggestion)) 45 | } 46 | } 47 | 48 | if len(result.Errors) > 0 { 49 | sb.WriteString("\tErrors:\n") 50 | for _, err := range result.Errors { 51 | sb.WriteString(fmt.Sprintf("\t\t%s\n", err)) 52 | } 53 | } 54 | 55 | if result.Log != "" { 56 | sb.WriteString("\tLog:\n") 57 | scanner := bufio.NewScanner(strings.NewReader(result.Log)) 58 | for scanner.Scan() { 59 | sb.WriteString(fmt.Sprintf("\t\t%s\n", scanner.Text())) 60 | } 61 | } 62 | sb.WriteString("\n") 63 | } 64 | } 65 | return sb.String() 66 | } 67 | -------------------------------------------------------------------------------- /pkg/operators/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | 8 | "github.com/operator-framework/api/pkg/operators" 9 | ) 10 | 11 | const ( 12 | // GroupName is the group name used in this package. 13 | GroupName = operators.GroupName 14 | // GroupVersion is the group version used in this package. 15 | GroupVersion = "v1alpha1" 16 | ) 17 | 18 | // SchemeGroupVersion is group version used to register these objects 19 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: GroupVersion} 20 | 21 | // Kind takes an unqualified kind and returns back a Group qualified GroupKind 22 | func Kind(kind string) schema.GroupKind { 23 | return SchemeGroupVersion.WithKind(kind).GroupKind() 24 | } 25 | 26 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 27 | func Resource(resource string) schema.GroupResource { 28 | return SchemeGroupVersion.WithResource(resource).GroupResource() 29 | } 30 | 31 | var ( 32 | // SchemeBuilder initializes a scheme builder 33 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 34 | // AddToScheme is a global function that registers this API group & version to a scheme 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | 37 | // localSchemeBuilder is expected by generated conversion functions 38 | localSchemeBuilder = &SchemeBuilder 39 | ) 40 | 41 | // addKnownTypes adds the list of known types to Scheme 42 | func addKnownTypes(scheme *runtime.Scheme) error { 43 | scheme.AddKnownTypes(SchemeGroupVersion, 44 | &CatalogSource{}, 45 | &CatalogSourceList{}, 46 | &InstallPlan{}, 47 | &InstallPlanList{}, 48 | &Subscription{}, 49 | &SubscriptionList{}, 50 | &ClusterServiceVersion{}, 51 | &ClusterServiceVersionList{}, 52 | ) 53 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/manifests/bundlemeta.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | // AnnotationsFile holds annotation information about a bundle 4 | type AnnotationsFile struct { 5 | // annotations is a list of annotations for a given bundle 6 | Annotations Annotations `json:"annotations" yaml:"annotations"` 7 | } 8 | 9 | // Annotations is a list of annotations for a given bundle 10 | type Annotations struct { 11 | // PackageName is the name of the overall package, ala `etcd`. 12 | PackageName string `json:"operators.operatorframework.io.bundle.package.v1" yaml:"operators.operatorframework.io.bundle.package.v1"` 13 | 14 | // Channels are a comma separated list of the declared channels for the bundle, ala `stable` or `alpha`. 15 | Channels string `json:"operators.operatorframework.io.bundle.channels.v1" yaml:"operators.operatorframework.io.bundle.channels.v1"` 16 | 17 | // DefaultChannelName is, if specified, the name of the default channel for the package. The 18 | // default channel will be installed if no other channel is explicitly given. If the package 19 | // has a single channel, then that channel is implicitly the default. 20 | DefaultChannelName string `json:"operators.operatorframework.io.bundle.channel.default.v1" yaml:"operators.operatorframework.io.bundle.channel.default.v1"` 21 | } 22 | 23 | // DependenciesFile holds dependency information about a bundle 24 | type DependenciesFile struct { 25 | // Dependencies is a list of dependencies for a given bundle 26 | Dependencies []Dependency `json:"dependencies" yaml:"dependencies"` 27 | } 28 | 29 | // Dependencies is a list of dependencies for a given bundle 30 | type Dependency struct { 31 | // The type of dependency. It can be `olm.package` for operator-version based 32 | // dependency or `olm.gvk` for gvk based dependency. This field is required. 33 | Type string `json:"type" yaml:"type"` 34 | 35 | // The value of the dependency (either GVKDependency or PackageDependency) 36 | Value string `json:"value" yaml:"value"` 37 | } 38 | -------------------------------------------------------------------------------- /pkg/operators/v1/operatorcondition_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | const ( 8 | // Upgradeable indicates that the operator is upgradeable 9 | Upgradeable string = "Upgradeable" 10 | ) 11 | 12 | // OperatorConditionSpec allows a cluster admin to convey information about the state of an operator to OLM, potentially overriding state reported by the operator. 13 | type OperatorConditionSpec struct { 14 | ServiceAccounts []string `json:"serviceAccounts,omitempty"` 15 | Deployments []string `json:"deployments,omitempty"` 16 | Overrides []metav1.Condition `json:"overrides,omitempty"` 17 | } 18 | 19 | // OperatorConditionStatus allows an operator to convey information its state to OLM. The status may trail the actual 20 | // state of a system. 21 | type OperatorConditionStatus struct { 22 | Conditions []metav1.Condition `json:"conditions,omitempty"` 23 | } 24 | 25 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 26 | // +genclient 27 | // +kubebuilder:resource:shortName=condition,categories=olm 28 | // +kubebuilder:subresource:status 29 | // OperatorCondition is a Custom Resource of type `OperatorCondition` which is used to convey information to OLM about the state of an operator. 30 | type OperatorCondition struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata"` 33 | 34 | Spec OperatorConditionSpec `json:"spec,omitempty"` 35 | Status OperatorConditionStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 39 | // OperatorConditionList represents a list of Conditions. 40 | type OperatorConditionList struct { 41 | metav1.TypeMeta `json:",inline"` 42 | metav1.ListMeta `json:"metadata"` 43 | 44 | Items []OperatorCondition `json:"items"` 45 | } 46 | 47 | func init() { 48 | SchemeBuilder.Register(&OperatorCondition{}, &OperatorConditionList{}) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/constraints/cel_test.go: -------------------------------------------------------------------------------- 1 | package constraints 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCelLibrary(t *testing.T) { 10 | props := make([]map[string]interface{}, 1) 11 | props[0] = map[string]interface{}{ 12 | "type": "olm.test", 13 | "value": "1.0.0", 14 | } 15 | 16 | propertiesMap := map[string]interface{}{"properties": props} 17 | 18 | tests := []struct { 19 | name string 20 | rule string 21 | out bool 22 | isErr bool 23 | }{ 24 | { 25 | name: "ValidCelExpression/True", 26 | rule: "properties.exists(p, p.type == 'olm.test' && (semver_compare(p.value, '1.0.0') == 0))", 27 | out: true, 28 | isErr: false, 29 | }, 30 | { 31 | name: "ValidCelExpression/NotEqual/False", 32 | rule: "properties.exists(p, p.type == 'olm.test' && (semver_compare(p.value, '1.0.1') == 0))", 33 | out: false, 34 | isErr: false, 35 | }, 36 | { 37 | name: "ValidCelExpression/Less/False", 38 | rule: "properties.exists(p, p.type == 'olm.test' && (semver_compare(p.value, '1.0.0') < 0))", 39 | isErr: false, 40 | }, 41 | { 42 | name: "ValidCelExpression/Larger/False", 43 | rule: "properties.exists(p, p.type == 'olm.test' && (semver_compare(p.value, '1.0.0') > 0))", 44 | isErr: false, 45 | }, 46 | { 47 | name: "InvalidCelExpression/NotExistedFunc", 48 | rule: "properties.exists(p, p.type == 'olm.test' && (doesnt_exist(p.value, '1.0.0') == 0))", 49 | isErr: true, 50 | }, 51 | { 52 | name: "InvalidCelExpression/NonBoolReturn", 53 | rule: "1", 54 | isErr: true, 55 | }, 56 | } 57 | 58 | validator := NewCelEnvironment() 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | prog, err := validator.Validate(tt.rule) 62 | if tt.isErr { 63 | assert.Error(t, err) 64 | } else { 65 | result, err := prog.Evaluate(propertiesMap) 66 | assert.NoError(t, err) 67 | assert.Equal(t, result, tt.out) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/validation/validation_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/operator-framework/api/pkg/manifests" 7 | "github.com/operator-framework/api/pkg/validation/errors" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestValidateSuccess(t *testing.T) { 13 | bundle, err := manifests.GetBundleFromDir("./testdata/valid_bundle") 14 | require.NoError(t, err) 15 | 16 | results := AllValidators.Validate(bundle, map[string]string{ 17 | "index-path": "./testdata/dockerfile/valid_bundle.Dockerfile"}) 18 | 19 | for _, result := range results { 20 | require.Equal(t, false, result.HasError()) 21 | } 22 | } 23 | 24 | func TestValidate_WithErrors(t *testing.T) { 25 | bundle, err := manifests.GetBundleFromDir("./testdata/invalid_bundle") 26 | require.NoError(t, err) 27 | 28 | results := DefaultBundleValidators.Validate(bundle) 29 | for _, result := range results { 30 | require.Equal(t, true, result.HasError()) 31 | } 32 | require.Equal(t, 1, len(results)) 33 | } 34 | 35 | func TestValidatePackageSuccess(t *testing.T) { 36 | pkg, bundles, err := manifests.GetManifestsDir("./testdata/valid_package") 37 | require.NoError(t, err) 38 | 39 | objs := []interface{}{} 40 | for _, obj := range bundles { 41 | objs = append(objs, obj) 42 | } 43 | objs = append(objs, pkg) 44 | 45 | results := AllValidators.Validate(objs...) 46 | for _, result := range results { 47 | require.Equal(t, false, result.HasError()) 48 | } 49 | } 50 | 51 | func TestValidatePackageError(t *testing.T) { 52 | pkg, _, err := manifests.GetManifestsDir("./testdata/invalid_package") 53 | require.NoError(t, err) 54 | 55 | objs := []interface{}{pkg} 56 | 57 | results := AllValidators.Validate(objs...) 58 | require.Equal(t, 1, len(results)) 59 | require.True(t, results[0].HasError()) 60 | pkgErrs := results[0].Errors 61 | require.Equal(t, 1, len(pkgErrs)) 62 | require.Equal(t, errors.ErrInvalidPackageManifest("packageName empty", pkg.PackageName), pkgErrs[0]) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/lib/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | semver "github.com/blang/semver/v4" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestOperatorVersionMarshal(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | in OperatorVersion 15 | out []byte 16 | err error 17 | }{ 18 | { 19 | name: "MMP", 20 | in: OperatorVersion{semver.MustParse("1.2.3")}, 21 | out: []byte(`"1.2.3"`), 22 | }, 23 | { 24 | name: "empty", 25 | in: OperatorVersion{semver.Version{}}, 26 | out: []byte(`"0.0.0"`), 27 | }, 28 | { 29 | name: "with-timestamp", 30 | in: OperatorVersion{semver.MustParse("1.2.3-1556715351")}, 31 | out: []byte(`"1.2.3-1556715351"`), 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | m, err := tt.in.MarshalJSON() 37 | require.Equal(t, tt.out, m, string(m)) 38 | require.Equal(t, tt.err, err) 39 | }) 40 | } 41 | } 42 | 43 | func TestOperatorVersionUnmarshal(t *testing.T) { 44 | type TestStruct struct { 45 | Version OperatorVersion `json:"v"` 46 | } 47 | tests := []struct { 48 | name string 49 | in []byte 50 | out TestStruct 51 | err error 52 | }{ 53 | { 54 | name: "MMP", 55 | in: []byte(`{"v": "1.2.3"}`), 56 | out: TestStruct{Version: OperatorVersion{semver.MustParse("1.2.3")}}, 57 | }, 58 | { 59 | name: "empty", 60 | in: []byte(`{"v": "0.0.0"}`), 61 | out: TestStruct{Version: OperatorVersion{semver.Version{Major: 0, Minor: 0, Patch: 0}}}, 62 | }, 63 | { 64 | name: "with-timestamp", 65 | in: []byte(`{"v": "1.2.3-1556715351"}`), 66 | out: TestStruct{OperatorVersion{semver.MustParse("1.2.3-1556715351")}}, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | s := TestStruct{} 72 | err := json.Unmarshal(tt.in, &s) 73 | require.Equal(t, tt.out, s) 74 | require.Equal(t, tt.err, err) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/correct.csv.empty.example.yaml: -------------------------------------------------------------------------------- 1 | #! validate-crd: deploy/chart/templates/0000_30_02-clusterserviceversion.crd.yaml 2 | #! parse-kind: ClusterServiceVersion 3 | apiVersion: operators.coreos.com/v1alpha1 4 | kind: ClusterServiceVersion 5 | metadata: 6 | name: etcdoperator.v0.9.0 7 | namespace: placeholder 8 | annotations: 9 | "alm-examples": "" 10 | spec: 11 | minKubeVersion: 1.21.0 12 | version: 0.9.0 13 | installModes: 14 | - type: AllNamespaces 15 | supported: true 16 | install: 17 | strategy: deployment 18 | spec: 19 | permissions: 20 | - serviceAccountName: etcd-operator 21 | rules: 22 | - apiGroups: 23 | - etcd.database.coreos.com 24 | resources: 25 | - etcdclusters 26 | - etcdbackups 27 | - etcdrestores 28 | verbs: 29 | - "*" 30 | deployments: 31 | - name: etcd-operator 32 | spec: 33 | replicas: 1 34 | template: 35 | metadata: 36 | name: etcd-operator-alm-owned 37 | labels: 38 | name: etcd-operator-alm-owned 39 | spec: 40 | serviceAccountName: etcd-operator 41 | containers: 42 | - name: etcd-operator 43 | command: 44 | - etcd-operator 45 | - --create-crd=false 46 | image: quay.io/coreos/etcd-operator@sha256:db563baa8194fcfe39d1df744ed70024b0f1f9e9b55b5923c2f3a413c44dc6b8 47 | env: 48 | - name: MY_POD_NAMESPACE 49 | valueFrom: 50 | fieldRef: 51 | fieldPath: metadata.namespace 52 | - name: MY_POD_NAME 53 | valueFrom: 54 | fieldRef: 55 | fieldPath: metadata.name 56 | customresourcedefinitions: 57 | owned: 58 | - name: etcdclusters.etcd.database.coreos.com 59 | version: v1beta2 60 | kind: EtcdCluster 61 | 62 | 63 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/invalid.alm-examples.csv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | alm-examples: '[{"apiVersion":"local.storage.openshift.io/v1","kind":"LocalVolume","metadata":{"name":"example"},"spec":{"storageClassDevices":[{"devicePaths":["/dev/disk/by-id/ata-crucial",],"fsType": "ext4", "storageClassName": "foobar", "volumeMode": "Filesystem" } ] } }, { "apiVersion": "local.storage.openshift.io/v1alpha1", "kind": "LocalVolumeSet", "metadata": { "name": "example-localvolumeset" }, "spec": { "deviceInclusionSpec": { "deviceMechanicalProperties": [ "Rotational", "NonRotational" ], "deviceTypes": [ "RawDisk" ], "maxSize": "100G", "minSize": "10G" }, "maxDeviceCount": 10, "nodeSelector": { "nodeSelectorTerms": [ { "matchExpressions": [ { "key": "kubernetes.io/hostname", "operator": "In", "values": [ "worker-0", "worker-1" ] } ] } ] }, "storageClassName": "example-storageclass", "volumeMode": "Block" } }, { "apiVersion": "local.storage.openshift.io/v1alpha1", "kind": "LocalVolumeDiscovery", "metadata": { "name": "auto-discover-devices" }, "spec": { "nodeSelector": { "nodeSelectorTerms": [ { "matchExpressions": [ { "key":"kubernetes.io/hostname","operator":"In","values":["worker-0","worker-1"]}]}]}}}]' 6 | capabilities: Basic Install 7 | name: test-operator.v0.0.1 8 | namespace: placeholder 9 | spec: 10 | minKubeVersion: 1.21.0 11 | displayName: test-operator 12 | install: 13 | strategy: deployment 14 | installModes: 15 | - supported: true 16 | type: OwnNamespace 17 | - supported: true 18 | type: SingleNamespace 19 | - supported: false 20 | type: MultiNamespace 21 | - supported: true 22 | type: AllNamespaces 23 | keywords: 24 | - test-operator 25 | links: 26 | - name: Test Operator 27 | url: https://test-operator.domain 28 | maintainers: 29 | - email: your@email.com 30 | name: Maintainer Name 31 | maturity: alpha 32 | provider: 33 | name: Provider Name 34 | url: https://your.domain 35 | version: 0.0.1 36 | -------------------------------------------------------------------------------- /pkg/validation/internal/typecheck.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/operator-framework/api/pkg/validation/errors" 8 | ) 9 | 10 | // Recursive function that traverses a nested struct passed in as reflect 11 | // value, and reports for errors/warnings in case of nil struct field values. 12 | // TODO: make iterative 13 | func checkEmptyFields(result *errors.ManifestResult, v reflect.Value, parentStructName string) { 14 | if v.Kind() != reflect.Struct { 15 | return 16 | } 17 | typ := v.Type() 18 | 19 | for i := 0; i < v.NumField(); i++ { 20 | fieldValue := v.Field(i) 21 | fieldType := typ.Field(i) 22 | 23 | tag := fieldType.Tag.Get("json") 24 | // Ignore fields that are subsets of a primitive field. 25 | if tag == "" { 26 | continue 27 | } 28 | 29 | // Omitted field tags will contain ",omitempty", and ignored tags will 30 | // match "-" exactly, respectively. 31 | isOptionalField := strings.Contains(tag, ",omitempty") || strings.Contains(tag, ",omitzero") || tag == "-" 32 | emptyVal := fieldValue.IsZero() 33 | 34 | newParentStructName := fieldType.Name 35 | if parentStructName != "" { 36 | newParentStructName = parentStructName + "." + newParentStructName 37 | } 38 | 39 | switch fieldValue.Kind() { 40 | case reflect.Struct: 41 | updateResult(result, "struct", newParentStructName, emptyVal, isOptionalField) 42 | if !emptyVal { 43 | checkEmptyFields(result, fieldValue, newParentStructName) 44 | } 45 | default: 46 | updateResult(result, "field", newParentStructName, emptyVal, isOptionalField) 47 | } 48 | } 49 | } 50 | 51 | // Returns updated ManifestResult with missing optional/mandatory field/struct objects. 52 | func updateResult(result *errors.ManifestResult, typeName string, newParentStructName string, emptyVal bool, isOptionalField bool) { 53 | if !emptyVal { 54 | return 55 | } 56 | if !isOptionalField && newParentStructName != "Status" { 57 | result.Add(errors.ErrFieldMissing("required field missing", newParentStructName, typeName)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/lib/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | semver "github.com/blang/semver/v4" 7 | ) 8 | 9 | // +k8s:openapi-gen=true 10 | // OperatorVersion is a wrapper around semver.Version which supports correct 11 | // marshaling to YAML and JSON. 12 | // +kubebuilder:validation:Type=string 13 | type OperatorVersion struct { 14 | semver.Version `json:"-"` 15 | } 16 | 17 | // DeepCopyInto creates a deep-copy of the Version value. 18 | func (v *OperatorVersion) DeepCopyInto(out *OperatorVersion) { 19 | out.Major = v.Major 20 | out.Minor = v.Minor 21 | out.Patch = v.Patch 22 | 23 | if v.Pre != nil { 24 | pre := make([]semver.PRVersion, len(v.Pre)) 25 | copy(pre, v.Pre) 26 | out.Pre = pre 27 | } 28 | 29 | if v.Build != nil { 30 | build := make([]string, len(v.Build)) 31 | copy(build, v.Build) 32 | out.Build = build 33 | } 34 | } 35 | 36 | // MarshalJSON implements the encoding/json.Marshaler interface. 37 | func (v OperatorVersion) MarshalJSON() ([]byte, error) { 38 | return json.Marshal(v.String()) 39 | } 40 | 41 | // UnmarshalJSON implements the encoding/json.Unmarshaler interface. 42 | func (v *OperatorVersion) UnmarshalJSON(data []byte) (err error) { 43 | var versionString string 44 | 45 | if err = json.Unmarshal(data, &versionString); err != nil { 46 | return 47 | } 48 | 49 | version := semver.Version{} 50 | version, err = semver.ParseTolerant(versionString) 51 | if err != nil { 52 | return err 53 | } 54 | v.Version = version 55 | return 56 | } 57 | 58 | // OpenAPISchemaType is used by the kube-openapi generator when constructing 59 | // the OpenAPI spec of this type. 60 | // 61 | // See: https://github.com/kubernetes/kube-openapi/tree/master/pkg/generators 62 | func (_ OperatorVersion) OpenAPISchemaType() []string { return []string{"string"} } 63 | 64 | // OpenAPISchemaFormat is used by the kube-openapi generator when constructing 65 | // the OpenAPI spec of this type. 66 | // "semver" is not a standard openapi format but tooling may use the value regardless 67 | func (_ OperatorVersion) OpenAPISchemaFormat() string { return "semver" } 68 | -------------------------------------------------------------------------------- /pkg/operators/v2/operatorcondition_types.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | const ( 8 | // Upgradeable indicates that the operator is upgradeable 9 | Upgradeable string = "Upgradeable" 10 | ) 11 | 12 | // ConditionType codifies a condition's type. 13 | type ConditionType string 14 | 15 | // OperatorConditionSpec allows an operator to report state to OLM and provides 16 | // cluster admin with the ability to manually override state reported by the operator. 17 | type OperatorConditionSpec struct { 18 | ServiceAccounts []string `json:"serviceAccounts,omitempty"` 19 | Deployments []string `json:"deployments,omitempty"` 20 | Overrides []metav1.Condition `json:"overrides,omitempty"` 21 | Conditions []metav1.Condition `json:"conditions,omitempty"` 22 | } 23 | 24 | // OperatorConditionStatus allows OLM to convey which conditions have been observed. 25 | type OperatorConditionStatus struct { 26 | Conditions []metav1.Condition `json:"conditions,omitempty"` 27 | } 28 | 29 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 30 | // +genclient 31 | // +kubebuilder:storageversion 32 | // +kubebuilder:resource:shortName=condition,categories=olm 33 | // +kubebuilder:subresource:status 34 | // OperatorCondition is a Custom Resource of type `OperatorCondition` which is used to convey information to OLM about the state of an operator. 35 | type OperatorCondition struct { 36 | metav1.TypeMeta `json:",inline"` 37 | metav1.ObjectMeta `json:"metadata"` 38 | 39 | Spec OperatorConditionSpec `json:"spec,omitempty"` 40 | Status OperatorConditionStatus `json:"status,omitempty"` 41 | } 42 | 43 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 44 | // OperatorConditionList represents a list of Conditions. 45 | type OperatorConditionList struct { 46 | metav1.TypeMeta `json:",inline"` 47 | metav1.ListMeta `json:"metadata"` 48 | 49 | Items []OperatorCondition `json:"items"` 50 | } 51 | 52 | func init() { 53 | SchemeBuilder.Register(&OperatorCondition{}, &OperatorConditionList{}) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/validation/internal/standardcapabilities.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/operator-framework/api/pkg/manifests" 7 | "github.com/operator-framework/api/pkg/validation/errors" 8 | interfaces "github.com/operator-framework/api/pkg/validation/interfaces" 9 | ) 10 | 11 | var StandardCapabilitiesValidator interfaces.Validator = interfaces.ValidatorFunc(validateCapabilities) 12 | 13 | var validCapabilities = map[string]struct{}{ 14 | "Basic Install": {}, 15 | "Seamless Upgrades": {}, 16 | "Full Lifecycle": {}, 17 | "Deep Insights": {}, 18 | "Auto Pilot": {}, 19 | } 20 | 21 | func validateCapabilities(objs ...interface{}) (results []errors.ManifestResult) { 22 | for _, obj := range objs { 23 | switch v := obj.(type) { 24 | case *manifests.Bundle: 25 | results = append(results, validateCapabilitiesBundle(v)) 26 | } 27 | } 28 | 29 | return results 30 | } 31 | 32 | func validateCapabilitiesBundle(bundle *manifests.Bundle) errors.ManifestResult { 33 | result := errors.ManifestResult{Name: bundle.Name} 34 | csvCategoryCheck := CSVChecks{csv: *bundle.CSV, errs: []error{}, warns: []error{}} 35 | 36 | csvChecksResult := checkCapabilities(csvCategoryCheck) 37 | for _, err := range csvChecksResult.errs { 38 | result.Add(errors.ErrInvalidCSV(err.Error(), bundle.CSV.GetName())) 39 | } 40 | for _, warn := range csvChecksResult.warns { 41 | result.Add(errors.WarnInvalidCSV(warn.Error(), bundle.CSV.GetName())) 42 | } 43 | 44 | return result 45 | } 46 | 47 | // checkAnnotations will validate the values informed via annotations such as; capabilities and categories 48 | func checkCapabilities(checks CSVChecks) CSVChecks { 49 | if checks.csv.GetAnnotations() == nil { 50 | checks.csv.SetAnnotations(make(map[string]string)) 51 | } 52 | 53 | if capability, ok := checks.csv.ObjectMeta.Annotations["capabilities"]; ok { 54 | if _, ok := validCapabilities[capability]; !ok { 55 | checks.errs = append(checks.errs, fmt.Errorf("csv.Metadata.Annotations.Capabilities %q is not a valid capabilities level", capability)) 56 | } 57 | } 58 | return checks 59 | } 60 | -------------------------------------------------------------------------------- /pkg/validation/internal/operatorhubv2_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/operator-framework/api/pkg/manifests" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestValidateBundleOperatorHubV2(t *testing.T) { 12 | var table = []struct { 13 | description string 14 | directory string 15 | hasError bool 16 | errStrings []string 17 | }{ 18 | { 19 | description: "registryv1 bundle/valid bundle", 20 | directory: "./testdata/valid_bundle", 21 | hasError: false, 22 | }, 23 | { 24 | description: "registryv1 bundle/invald bundle operatorhubio", 25 | directory: "./testdata/invalid_bundle_operatorhub", 26 | hasError: true, 27 | errStrings: []string{ 28 | `Error: Value : (etcdoperator.v0.9.4) csv.Spec.Provider.Name not specified`, 29 | `Error: Value : (etcdoperator.v0.9.4) csv.Spec.Maintainers elements should contain both name and email`, 30 | `Error: Value : (etcdoperator.v0.9.4) csv.Spec.Maintainers email invalidemail is invalid: mail: missing '@' or angle-addr`, 31 | `Error: Value : (etcdoperator.v0.9.4) csv.Spec.Links elements should contain both name and url`, 32 | `Error: Value : (etcdoperator.v0.9.4) csv.Spec.Links url https//coreos.com/operators/etcd/docs/latest/ is invalid: parse "https//coreos.com/operators/etcd/docs/latest/": invalid URI for request`, 33 | `Error: Value : (etcdoperator.v0.9.4) csv.Spec.Icon should only have one element`, 34 | `Error: Value : (etcdoperator.v0.9.4) csv.Spec.Version must be set`, 35 | }, 36 | }, 37 | } 38 | 39 | for _, tt := range table { 40 | // Validate the bundle object 41 | bundle, err := manifests.GetBundleFromDir(tt.directory) 42 | require.NoError(t, err) 43 | 44 | results := OperatorHubV2Validator.Validate(bundle) 45 | 46 | if len(results) > 0 { 47 | require.Equal(t, results[0].HasError(), tt.hasError) 48 | if results[0].HasError() { 49 | require.Equal(t, len(tt.errStrings), len(results[0].Errors)) 50 | 51 | for _, err := range results[0].Errors { 52 | errString := err.Error() 53 | require.Contains(t, tt.errStrings, errString) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/apis/scorecard/v1alpha3/test_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha3 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // State is a type used to indicate the result state of a Test. 8 | type State string 9 | 10 | const ( 11 | // PassState occurs when a Test's ExpectedPoints == MaximumPoints. 12 | PassState State = "pass" 13 | // FailState occurs when a Test's ExpectedPoints == 0. 14 | FailState State = "fail" 15 | // ErrorState occurs when a Test encounters a fatal error and the reported points should not be considered. 16 | ErrorState State = "error" 17 | ) 18 | 19 | // TestResult contains the results of an individual scorecard test 20 | type TestResult struct { 21 | // Name is the name of the test 22 | Name string `json:"name,omitempty"` 23 | // Log holds a log produced from the test (if applicable) 24 | Log string `json:"log,omitempty"` 25 | // State is the final state of the test 26 | State State `json:"state"` 27 | // Errors is a list of the errors that occurred during the test (this can include both fatal and non-fatal errors) 28 | Errors []string `json:"errors,omitempty"` 29 | // Suggestions is a list of suggestions for the user to improve their score (if applicable) 30 | Suggestions []string `json:"suggestions,omitempty"` 31 | // CreationTimestamp of the result from an individual test 32 | CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty"` 33 | } 34 | 35 | // TestStatus contains collection of testResults. 36 | type TestStatus struct { 37 | Results []TestResult `json:"results,omitempty"` 38 | } 39 | 40 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 41 | 42 | // Test specifies a single test run. 43 | type Test struct { 44 | metav1.TypeMeta `json:",inline"` 45 | Spec TestConfiguration `json:"spec,omitempty"` 46 | Status TestStatus `json:"status,omitempty"` 47 | } 48 | 49 | // TestList is a list of tests. 50 | type TestList struct { 51 | metav1.TypeMeta `json:",inline"` 52 | Items []Test `json:"items"` 53 | } 54 | 55 | func NewTest() Test { 56 | return Test{ 57 | TypeMeta: metav1.TypeMeta{ 58 | APIVersion: GroupVersion.String(), 59 | Kind: "Test", 60 | }, 61 | } 62 | } 63 | 64 | func NewTestList() TestList { 65 | return TestList{ 66 | TypeMeta: metav1.TypeMeta{ 67 | APIVersion: GroupVersion.String(), 68 | Kind: "TestList", 69 | }, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/validation/internal/package_manifest.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/operator-framework/api/pkg/manifests" 7 | "github.com/operator-framework/api/pkg/validation/errors" 8 | interfaces "github.com/operator-framework/api/pkg/validation/interfaces" 9 | ) 10 | 11 | var PackageManifestValidator interfaces.Validator = interfaces.ValidatorFunc(validatePackageManifests) 12 | 13 | func validatePackageManifests(objs ...interface{}) (results []errors.ManifestResult) { 14 | for _, obj := range objs { 15 | switch v := obj.(type) { 16 | case *manifests.PackageManifest: 17 | results = append(results, validatePackageManifest(v)) 18 | } 19 | } 20 | return results 21 | } 22 | 23 | func validatePackageManifest(pkg *manifests.PackageManifest) errors.ManifestResult { 24 | result := errors.ManifestResult{Name: pkg.PackageName} 25 | result.Add(validateChannels(pkg)...) 26 | return result 27 | } 28 | 29 | func validateChannels(pkg *manifests.PackageManifest) (errs []errors.Error) { 30 | if pkg.PackageName == "" { 31 | errs = append(errs, errors.ErrInvalidPackageManifest("packageName empty", pkg.PackageName)) 32 | } 33 | numChannels := len(pkg.Channels) 34 | if numChannels == 0 { 35 | errs = append(errs, errors.ErrInvalidPackageManifest("channels empty", pkg.PackageName)) 36 | return errs 37 | } 38 | if pkg.DefaultChannelName == "" && numChannels > 1 { 39 | errs = append(errs, errors.ErrInvalidPackageManifest("default channel is empty but more than one channel exists", pkg.PackageName)) 40 | } 41 | 42 | seen := map[string]struct{}{} 43 | for i, c := range pkg.Channels { 44 | if c.Name == "" { 45 | errs = append(errs, errors.ErrInvalidPackageManifest(fmt.Sprintf("channel %d name is empty", i), pkg.PackageName)) 46 | } 47 | if c.CurrentCSVName == "" { 48 | errs = append(errs, errors.ErrInvalidPackageManifest(fmt.Sprintf("channel %q currentCSV is empty", c.Name), pkg.PackageName)) 49 | } 50 | if _, ok := seen[c.Name]; ok { 51 | errs = append(errs, errors.ErrInvalidPackageManifest(fmt.Sprintf("duplicate package manifest channel name %q", c.Name), pkg.PackageName)) 52 | } 53 | seen[c.Name] = struct{}{} 54 | } 55 | if _, found := seen[pkg.DefaultChannelName]; pkg.DefaultChannelName != "" && !found { 56 | errs = append(errs, errors.ErrInvalidPackageManifest(fmt.Sprintf("default channel %q not found in the list of declared channels", pkg.DefaultChannelName), pkg.PackageName)) 57 | } 58 | 59 | return errs 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | # This workflow automatically marks issues and pull requests as stale after 90 days of inactivity 2 | # and closes them after an additional 30 days if no further activity occurs. 3 | # 4 | # Key behavior: 5 | # - After 90 days of no activity: 6 | # - Open issues and pull requests are labeled with "lifecycle/stale" 7 | # - A comment is posted to notify contributors about the inactivity 8 | # 9 | # - After 30 additional days (i.e., 120 days total): 10 | # - If still inactive and still labeled "lifecycle/stale", the issue or PR is closed 11 | # - A closing comment is posted to explain why it was closed 12 | # 13 | # - Activity such as a comment, commit, or label removal during the stale period 14 | # will remove the "lifecycle/stale" label and reset the clock 15 | # 16 | # - Items with any of the following labels will never be marked stale or closed: 17 | # - security 18 | # - planned 19 | # - priority/critical 20 | # - lifecycle/frozen 21 | # - verified 22 | # 23 | # This workflow uses: https://github.com/actions/stale 24 | name: "Close stale issues and PRs" 25 | on: 26 | schedule: 27 | - cron: "0 1 * * *" # Runs daily at 01:00 UTC (adjust as needed) 28 | 29 | jobs: 30 | stale: 31 | runs-on: ubuntu-latest 32 | permissions: 33 | issues: write # allow labeling, commenting, closing issues 34 | pull-requests: write # allow labeling, commenting, closing PRs 35 | steps: 36 | - uses: actions/stale@v10 37 | with: 38 | repo-token: ${{ secrets.GITHUB_TOKEN }} 39 | days-before-stale: 90 40 | days-before-close: 30 41 | stale-issue-label: "lifecycle/stale" 42 | stale-pr-label: "lifecycle/stale" 43 | stale-issue-message: > 44 | Issues go stale after 90 days of inactivity. If there is no further 45 | activity, the issue will be closed in another 30 days. 46 | stale-pr-message: > 47 | PRs go stale after 90 days of inactivity. If there is no further 48 | activity, the PR will be closed in another 30 days. 49 | close-issue-message: "This issue has been closed due to inactivity." 50 | close-pr-message: "This pull request has been closed due to inactivity." 51 | exempt-issue-labels: "security,planned,priority/critical,lifecycle/frozen,verified" 52 | exempt-pr-labels: "security,planned,priority/critical,lifecycle/frozen,verified" 53 | operations-per-run: 30 54 | -------------------------------------------------------------------------------- /cmd/operator-verify/manifests/cmd.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "github.com/operator-framework/api/pkg/manifests" 5 | "github.com/operator-framework/api/pkg/validation" 6 | "github.com/operator-framework/api/pkg/validation/errors" 7 | 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewCmd() *cobra.Command { 13 | rootCmd := &cobra.Command{ 14 | Use: "manifests", 15 | Short: "Validates all manifests in a directory", 16 | Long: `'operator-verify manifests' validates a bundle in the supplied directory 17 | and prints errors and warnings corresponding to each manifest found to be 18 | invalid. Manifests are only validated if a validator for that manifest 19 | type/kind, ex. CustomResourceDefinition, is implemented in the Operator 20 | validation library.`, 21 | Run: manifestsFunc, 22 | } 23 | 24 | rootCmd.Flags().Bool("operatorhub_validate", false, "enable optional UI validation for operatorhub.io") 25 | rootCmd.Flags().Bool("object_validate", false, "enable optional bundle object validation") 26 | 27 | return rootCmd 28 | } 29 | 30 | func manifestsFunc(cmd *cobra.Command, args []string) { 31 | bundle, err := manifests.GetBundleFromDir(args[0]) 32 | if err != nil { 33 | log.Fatalf("Error generating bundle from directory: %s", err.Error()) 34 | } 35 | if bundle == nil { 36 | log.Fatalf("Error generating bundle from directory") 37 | } 38 | 39 | operatorHubValidate, err := cmd.Flags().GetBool("operatorhub_validate") 40 | if err != nil { 41 | log.Fatalf("Unable to parse operatorhub_validate parameter") 42 | } 43 | 44 | bundleObjectValidate, err := cmd.Flags().GetBool("object_validate") 45 | if err != nil { 46 | log.Fatalf("Unable to parse object_validate parameter: %v", err) 47 | } 48 | 49 | validators := validation.DefaultBundleValidators 50 | if operatorHubValidate { 51 | validators = validators.WithValidators(validation.OperatorHubValidator) 52 | } 53 | if bundleObjectValidate { 54 | validators = validators.WithValidators(validation.ObjectValidator) 55 | } 56 | 57 | results := validators.Validate(bundle.ObjectsToValidate()...) 58 | nonEmptyResults := []errors.ManifestResult{} 59 | for _, result := range results { 60 | if result.HasError() || result.HasWarn() { 61 | nonEmptyResults = append(nonEmptyResults, result) 62 | } 63 | } 64 | 65 | for _, result := range nonEmptyResults { 66 | for _, err := range result.Errors { 67 | log.Error(err.Error()) 68 | } 69 | for _, err := range result.Warnings { 70 | log.Warn(err.Error()) 71 | } 72 | } 73 | 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/removed_api_1_25/cache.example.com_memcacheds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.4.1 6 | creationTimestamp: null 7 | name: memcacheds.cache.example.com 8 | spec: 9 | group: cache.example.com 10 | names: 11 | kind: Memcached 12 | listKind: MemcachedList 13 | plural: memcacheds 14 | singular: memcached 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Memcached is the Schema for the memcacheds API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: MemcachedSpec defines the desired state of Memcached 36 | properties: 37 | foo: 38 | description: Foo is an example field of Memcached. Edit memcached_types.go 39 | to remove/update 40 | type: string 41 | size: 42 | description: Size defines the number of Memcached instances 43 | format: int32 44 | type: integer 45 | type: object 46 | status: 47 | description: MemcachedStatus defines the observed state of Memcached 48 | properties: 49 | nodes: 50 | description: Nodes store the name of the pods which are running Memcached 51 | instances 52 | items: 53 | type: string 54 | type: array 55 | type: object 56 | type: object 57 | served: true 58 | storage: true 59 | subresources: 60 | status: {} 61 | status: 62 | acceptedNames: 63 | kind: "" 64 | plural: "" 65 | conditions: [] 66 | storedVersions: [] 67 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/removed_api_1_26/cache.example.com_memcacheds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.4.1 6 | creationTimestamp: null 7 | name: memcacheds.cache.example.com 8 | spec: 9 | group: cache.example.com 10 | names: 11 | kind: Memcached 12 | listKind: MemcachedList 13 | plural: memcacheds 14 | singular: memcached 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Memcached is the Schema for the memcacheds API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: MemcachedSpec defines the desired state of Memcached 36 | properties: 37 | foo: 38 | description: Foo is an example field of Memcached. Edit memcached_types.go 39 | to remove/update 40 | type: string 41 | size: 42 | description: Size defines the number of Memcached instances 43 | format: int32 44 | type: integer 45 | type: object 46 | status: 47 | description: MemcachedStatus defines the observed state of Memcached 48 | properties: 49 | nodes: 50 | description: Nodes store the name of the pods which are running Memcached 51 | instances 52 | items: 53 | type: string 54 | type: array 55 | type: object 56 | type: object 57 | served: true 58 | storage: true 59 | subresources: 60 | status: {} 61 | status: 62 | acceptedNames: 63 | kind: "" 64 | plural: "" 65 | conditions: [] 66 | storedVersions: [] 67 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/valid_bundle_v1/cache.example.com_memcacheds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.4.1 6 | creationTimestamp: null 7 | name: memcacheds.cache.example.com 8 | spec: 9 | group: cache.example.com 10 | names: 11 | kind: Memcached 12 | listKind: MemcachedList 13 | plural: memcacheds 14 | singular: memcached 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Memcached is the Schema for the memcacheds API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: MemcachedSpec defines the desired state of Memcached 36 | properties: 37 | foo: 38 | description: Foo is an example field of Memcached. Edit memcached_types.go 39 | to remove/update 40 | type: string 41 | size: 42 | description: Size defines the number of Memcached instances 43 | format: int32 44 | type: integer 45 | type: object 46 | status: 47 | description: MemcachedStatus defines the observed state of Memcached 48 | properties: 49 | nodes: 50 | description: Nodes store the name of the pods which are running Memcached 51 | instances 52 | items: 53 | type: string 54 | type: array 55 | type: object 56 | type: object 57 | served: true 58 | storage: true 59 | subresources: 60 | status: {} 61 | status: 62 | acceptedNames: 63 | kind: "" 64 | plural: "" 65 | conditions: [] 66 | storedVersions: [] 67 | -------------------------------------------------------------------------------- /pkg/validation/internal/crd.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/operator-framework/api/pkg/validation/errors" 8 | interfaces "github.com/operator-framework/api/pkg/validation/interfaces" 9 | 10 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" 11 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" 12 | v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 13 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 14 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | ) 17 | 18 | var scheme = runtime.NewScheme() 19 | 20 | func init() { 21 | install.Install(scheme) 22 | } 23 | 24 | var CRDValidator interfaces.Validator = interfaces.ValidatorFunc(validateCRDs) 25 | 26 | func validateCRDs(objs ...interface{}) (results []errors.ManifestResult) { 27 | for _, obj := range objs { 28 | switch v := obj.(type) { 29 | case *v1beta1.CustomResourceDefinition: 30 | results = append(results, validateV1Beta1CRD(v)) 31 | case *v1.CustomResourceDefinition: 32 | results = append(results, validateV1CRD(v)) 33 | } 34 | } 35 | return results 36 | } 37 | 38 | func validateV1Beta1CRD(crd *v1beta1.CustomResourceDefinition) (result errors.ManifestResult) { 39 | internalCRD := &apiextensions.CustomResourceDefinition{} 40 | v1beta1.SetObjectDefaults_CustomResourceDefinition(crd) 41 | err := scheme.Converter().Convert(crd, internalCRD, nil) 42 | if err != nil { 43 | result.Add(errors.ErrInvalidParse("error converting crd", err)) 44 | return result 45 | } 46 | 47 | result = validateInternalCRD(internalCRD) 48 | return result 49 | } 50 | 51 | func validateV1CRD(crd *v1.CustomResourceDefinition) (result errors.ManifestResult) { 52 | internalCRD := &apiextensions.CustomResourceDefinition{} 53 | v1.SetObjectDefaults_CustomResourceDefinition(crd) 54 | err := scheme.Converter().Convert(crd, internalCRD, nil) 55 | if err != nil { 56 | result.Add(errors.ErrInvalidParse("error converting crd", err)) 57 | return result 58 | } 59 | 60 | result = validateInternalCRD(internalCRD) 61 | return result 62 | } 63 | 64 | func validateInternalCRD(crd *apiextensions.CustomResourceDefinition) (result errors.ManifestResult) { 65 | errList := validation.ValidateCustomResourceDefinition(context.TODO(), crd) 66 | for _, err := range errList { 67 | if !strings.Contains(err.Field, "openAPIV3Schema") && !strings.Contains(err.Field, "status") { 68 | result.Add(errors.NewError(errors.ErrorType(err.Type), err.Error(), err.Field, err.BadValue)) 69 | } 70 | } 71 | 72 | if result.HasError() { 73 | result.Name = crd.GetName() 74 | } 75 | return result 76 | } 77 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/deprecated_api_1_25/cache.example.com_memcacheds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.4.1 6 | creationTimestamp: null 7 | name: memcacheds.cache.example.com 8 | spec: 9 | group: cache.example.com 10 | names: 11 | kind: Memcached 12 | listKind: MemcachedList 13 | plural: memcacheds 14 | singular: memcached 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Memcached is the Schema for the memcacheds API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: MemcachedSpec defines the desired state of Memcached 36 | properties: 37 | foo: 38 | description: Foo is an example field of Memcached. Edit memcached_types.go 39 | to remove/update 40 | type: string 41 | size: 42 | description: Size defines the number of Memcached instances 43 | format: int32 44 | type: integer 45 | type: object 46 | status: 47 | description: MemcachedStatus defines the observed state of Memcached 48 | properties: 49 | nodes: 50 | description: Nodes store the name of the pods which are running Memcached 51 | instances 52 | items: 53 | type: string 54 | type: array 55 | type: object 56 | type: object 57 | served: true 58 | storage: true 59 | subresources: 60 | status: {} 61 | status: 62 | acceptedNames: 63 | kind: "" 64 | plural: "" 65 | conditions: [] 66 | storedVersions: [] 67 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_deprecated_resources/cache.example.com_memcacheds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.4.1 6 | creationTimestamp: null 7 | name: memcacheds.cache.example.com 8 | spec: 9 | group: cache.example.com 10 | names: 11 | kind: Memcached 12 | listKind: MemcachedList 13 | plural: memcacheds 14 | singular: memcached 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Memcached is the Schema for the memcacheds API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: MemcachedSpec defines the desired state of Memcached 36 | properties: 37 | foo: 38 | description: Foo is an example field of Memcached. Edit memcached_types.go 39 | to remove/update 40 | type: string 41 | size: 42 | description: Size defines the number of Memcached instances 43 | format: int32 44 | type: integer 45 | type: object 46 | status: 47 | description: MemcachedStatus defines the observed state of Memcached 48 | properties: 49 | nodes: 50 | description: Nodes store the name of the pods which are running Memcached 51 | instances 52 | items: 53 | type: string 54 | type: array 55 | type: object 56 | type: object 57 | served: true 58 | storage: true 59 | subresources: 60 | status: {} 61 | status: 62 | acceptedNames: 63 | kind: "" 64 | plural: "" 65 | conditions: [] 66 | storedVersions: [] 67 | -------------------------------------------------------------------------------- /pkg/validation/internal/operatorhubv2.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/operator-framework/api/pkg/manifests" 5 | "github.com/operator-framework/api/pkg/operators/v1alpha1" 6 | "github.com/operator-framework/api/pkg/validation/errors" 7 | interfaces "github.com/operator-framework/api/pkg/validation/interfaces" 8 | ) 9 | 10 | var OperatorHubV2Validator interfaces.Validator = interfaces.ValidatorFunc(validateOperatorHubV2) 11 | 12 | func validateOperatorHubV2(objs ...interface{}) (results []errors.ManifestResult) { 13 | // Obtain the k8s version if informed via the objects an optional 14 | k8sVersion := "" 15 | for _, obj := range objs { 16 | switch obj.(type) { 17 | case map[string]string: 18 | k8sVersion = obj.(map[string]string)[k8sVersionKey] 19 | if len(k8sVersion) > 0 { 20 | break 21 | } 22 | } 23 | } 24 | 25 | for _, obj := range objs { 26 | switch v := obj.(type) { 27 | case *manifests.Bundle: 28 | results = append(results, validateBundleOperatorHubV2(v, k8sVersion)) 29 | } 30 | } 31 | 32 | return results 33 | } 34 | 35 | func validateBundleOperatorHubV2(bundle *manifests.Bundle, k8sVersion string) errors.ManifestResult { 36 | result := errors.ManifestResult{Name: bundle.Name} 37 | 38 | if bundle == nil { 39 | result.Add(errors.ErrInvalidBundle("Bundle is nil", nil)) 40 | return result 41 | } 42 | 43 | if bundle.CSV == nil { 44 | result.Add(errors.ErrInvalidBundle("Bundle csv is nil", bundle.Name)) 45 | return result 46 | } 47 | 48 | csvChecksResult := validateHubCSVSpecV2(*bundle.CSV) 49 | for _, err := range csvChecksResult.errs { 50 | result.Add(errors.ErrInvalidCSV(err.Error(), bundle.CSV.GetName())) 51 | } 52 | for _, warn := range csvChecksResult.warns { 53 | result.Add(errors.WarnInvalidCSV(warn.Error(), bundle.CSV.GetName())) 54 | } 55 | 56 | errs, warns := validateDeprecatedAPIS(bundle, k8sVersion) 57 | for _, err := range errs { 58 | result.Add(errors.ErrFailedValidation(err.Error(), bundle.CSV.GetName())) 59 | } 60 | for _, warn := range warns { 61 | result.Add(errors.WarnFailedValidation(warn.Error(), bundle.CSV.GetName())) 62 | } 63 | 64 | return result 65 | } 66 | 67 | // validateHubCSVSpec will check the CSV against the criteria to publish an 68 | // operator bundle in the OperatorHub.io 69 | func validateHubCSVSpecV2(csv v1alpha1.ClusterServiceVersion) CSVChecks { 70 | checks := CSVChecks{csv: csv, errs: []error{}, warns: []error{}} 71 | 72 | checks = checkSpecProviderName(checks) 73 | checks = checkSpecMaintainers(checks) 74 | checks = checkSpecLinks(checks) 75 | checks = checkSpecVersion(checks) 76 | checks = checkSpecIcon(checks) 77 | checks = checkSpecMinKubeVersion(checks) 78 | 79 | return checks 80 | } 81 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/bundle_with_metadata/manifests/cache.example.com_memcacheds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.8.0 6 | creationTimestamp: null 7 | name: memcacheds.cache.example.com 8 | spec: 9 | group: cache.example.com 10 | names: 11 | kind: Memcached 12 | listKind: MemcachedList 13 | plural: memcacheds 14 | singular: bundle_with_metadata 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Memcached is the Schema for the memcacheds API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: MemcachedSpec defines the desired state of Memcached 36 | properties: 37 | foo: 38 | description: Foo is an example field of Memcached. Edit memcached_types.go 39 | to remove/update 40 | type: string 41 | size: 42 | description: Size defines the number of Memcached instances 43 | format: int32 44 | type: integer 45 | type: object 46 | status: 47 | description: MemcachedStatus defines the observed state of Memcached 48 | properties: 49 | nodes: 50 | description: Nodes store the name of the pods which are running Memcached 51 | instances 52 | items: 53 | type: string 54 | type: array 55 | type: object 56 | type: object 57 | served: true 58 | storage: true 59 | subresources: 60 | status: {} 61 | status: 62 | acceptedNames: 63 | kind: "" 64 | plural: "" 65 | conditions: [] 66 | storedVersions: [] 67 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/correct.csv.olm.properties.annotation.yaml: -------------------------------------------------------------------------------- 1 | #! validate-crd: deploy/chart/templates/0000_30_02-clusterserviceversion.crd.yaml 2 | #! parse-kind: ClusterServiceVersion 3 | apiVersion: operators.coreos.com/v1alpha1 4 | kind: ClusterServiceVersion 5 | metadata: 6 | name: etcdoperator.v0.9.0 7 | namespace: placeholder 8 | annotations: 9 | alm-examples: '[{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdCluster","metadata":{"name":"example","namespace":"default"},"spec":{"size":3,"version":"3.2.13"}},{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdRestore","metadata":{"name":"example-etcd-cluster"},"spec":{"etcdCluster":{"name":"example-etcd-cluster"},"backupStorageType":"S3","s3":{"path":"","awsSecret":""}}},{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdBackup","metadata":{"name":"example-etcd-cluster-backup"},"spec":{"etcdEndpoints":[""],"storageType":"S3","s3":{"path":"","awsSecret":""}}}]' 10 | olm.properties: '[{"type": "foo", "value": "bar"}]' 11 | spec: 12 | minKubeVersion: 1.21.0 13 | version: 0.9.0 14 | installModes: 15 | - type: AllNamespaces 16 | supported: true 17 | install: 18 | strategy: deployment 19 | spec: 20 | permissions: 21 | - serviceAccountName: etcd-operator 22 | rules: 23 | - apiGroups: 24 | - etcd.database.coreos.com 25 | resources: 26 | - etcdclusters 27 | - etcdbackups 28 | - etcdrestores 29 | verbs: 30 | - "*" 31 | deployments: 32 | - name: etcd-operator 33 | spec: 34 | replicas: 1 35 | template: 36 | metadata: 37 | name: etcd-operator-alm-owned 38 | labels: 39 | name: etcd-operator-alm-owned 40 | spec: 41 | serviceAccountName: etcd-operator 42 | containers: 43 | - name: etcd-operator 44 | command: 45 | - etcd-operator 46 | - --create-crd=false 47 | image: quay.io/coreos/etcd-operator@sha256:db563baa8194fcfe39d1df744ed70024b0f1f9e9b55b5923c2f3a413c44dc6b8 48 | env: 49 | - name: MY_POD_NAMESPACE 50 | valueFrom: 51 | fieldRef: 52 | fieldPath: metadata.namespace 53 | - name: MY_POD_NAME 54 | valueFrom: 55 | fieldRef: 56 | fieldPath: metadata.name 57 | customresourcedefinitions: 58 | owned: 59 | - name: etcdclusters.etcd.database.coreos.com 60 | version: v1beta2 61 | kind: EtcdCluster -------------------------------------------------------------------------------- /pkg/lib/release/release.go: -------------------------------------------------------------------------------- 1 | package release 2 | 3 | import ( 4 | "encoding/json" 5 | "slices" 6 | "strings" 7 | 8 | semver "github.com/blang/semver/v4" 9 | ) 10 | 11 | // +k8s:openapi-gen=true 12 | // OperatorRelease is a wrapper around a slice of semver.PRVersion which supports correct 13 | // marshaling to YAML and JSON. 14 | // +kubebuilder:validation:Type=string 15 | // +kubebuilder:validation:MaxLength=20 16 | // +kubebuilder:validation:XValidation:rule="self.matches('^[0-9A-Za-z-]+(\\\\.[0-9A-Za-z-]+)*$')",message="release version must be composed of dot-separated identifiers containing only alphanumerics and hyphens" 17 | // +kubebuilder:validation:XValidation:rule="!self.split('.').exists(x, x.matches('^0[0-9]+$'))",message="numeric identifiers in release version must not have leading zeros" 18 | type OperatorRelease struct { 19 | Release []semver.PRVersion `json:"-"` 20 | } 21 | 22 | // DeepCopyInto creates a deep-copy of the Version value. 23 | func (v *OperatorRelease) DeepCopyInto(out *OperatorRelease) { 24 | out.Release = slices.Clone(v.Release) 25 | } 26 | 27 | // MarshalJSON implements the encoding/json.Marshaler interface. 28 | func (v OperatorRelease) MarshalJSON() ([]byte, error) { 29 | segments := []string{} 30 | for _, segment := range v.Release { 31 | segments = append(segments, segment.String()) 32 | } 33 | return json.Marshal(strings.Join(segments, ".")) 34 | } 35 | 36 | // UnmarshalJSON implements the encoding/json.Unmarshaler interface. 37 | func (v *OperatorRelease) UnmarshalJSON(data []byte) (err error) { 38 | var versionString string 39 | 40 | if err = json.Unmarshal(data, &versionString); err != nil { 41 | return 42 | } 43 | 44 | segments := strings.Split(versionString, ".") 45 | for _, segment := range segments { 46 | release, err := semver.NewPRVersion(segment) 47 | if err != nil { 48 | return err 49 | } 50 | v.Release = append(v.Release, release) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // OpenAPISchemaType is used by the kube-openapi generator when constructing 57 | // the OpenAPI spec of this type. 58 | // 59 | // See: https://github.com/kubernetes/kube-openapi/tree/master/pkg/generators 60 | func (_ OperatorRelease) OpenAPISchemaType() []string { return []string{"string"} } 61 | 62 | // OpenAPISchemaFormat is used by the kube-openapi generator when constructing 63 | // the OpenAPI spec of this type. 64 | // "semver" is not a standard openapi format but tooling may use the value regardless 65 | func (_ OperatorRelease) OpenAPISchemaFormat() string { return "semver" } 66 | 67 | func (r OperatorRelease) String() string { 68 | segments := []string{} 69 | for _, segment := range r.Release { 70 | segments = append(segments, segment.String()) 71 | } 72 | return strings.Join(segments, ".") 73 | } 74 | -------------------------------------------------------------------------------- /pkg/validation/internal/annotations.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | v1 "github.com/operator-framework/api/pkg/operators/v1" 8 | "github.com/operator-framework/api/pkg/operators/v1alpha1" 9 | "github.com/operator-framework/api/pkg/validation/errors" 10 | ) 11 | 12 | const olmpropertiesAnnotation = "olm.properties" 13 | 14 | // CaseSensitiveAnnotationKeySet is a set of annotation keys that are case sensitive 15 | // and can be used for validation purposes. The key is always lowercase and the value 16 | // contains the expected case sensitive string. This may not be an exhaustive list. 17 | var CaseSensitiveAnnotationKeySet = map[string]string{ 18 | 19 | strings.ToLower(v1.OperatorGroupAnnotationKey): v1.OperatorGroupAnnotationKey, 20 | strings.ToLower(v1.OperatorGroupNamespaceAnnotationKey): v1.OperatorGroupNamespaceAnnotationKey, 21 | strings.ToLower(v1.OperatorGroupTargetsAnnotationKey): v1.OperatorGroupTargetsAnnotationKey, 22 | strings.ToLower(v1.OperatorGroupProvidedAPIsAnnotationKey): v1.OperatorGroupProvidedAPIsAnnotationKey, 23 | strings.ToLower(v1alpha1.SkipRangeAnnotationKey): v1alpha1.SkipRangeAnnotationKey, 24 | } 25 | 26 | /* 27 | ValidateAnnotationNames will check annotation keys to ensure they are using 28 | proper case. Uses CaseSensitiveAnnotationKeySet as a source for keys 29 | which are known to be case sensitive. It also checks to see if the olm.properties 30 | annotation is defined in order to add a warning if present. This function can be 31 | used anywhere annotations need to be checked for case sensitivity. 32 | 33 | # Arguments 34 | 35 | • annotations: annotations map usually obtained from ObjectMeta.GetAnnotations() 36 | 37 | • value: is the field or file that caused an error or warning 38 | 39 | # Returns 40 | 41 | • errs: Any errors that may have been detected with the annotation keys provided 42 | */ 43 | func ValidateAnnotationNames(annotations map[string]string, value interface{}) (errs []errors.Error) { 44 | // for every annotation provided 45 | for annotationKey := range annotations { 46 | // check the case sensitive key set for a matching lowercase annotation 47 | if knownCaseSensitiveKey, ok := CaseSensitiveAnnotationKeySet[strings.ToLower(annotationKey)]; ok { 48 | // we have a case-insensitive match... now check to see if the case is really correct 49 | if annotationKey != knownCaseSensitiveKey { 50 | // annotation key supplied is invalid due to bad case. 51 | errs = append(errs, errors.ErrFailedValidation(fmt.Sprintf("provided annotation %s uses wrong case and should be %s instead", annotationKey, knownCaseSensitiveKey), value)) 52 | } 53 | } 54 | 55 | if annotationKey == olmpropertiesAnnotation { 56 | errs = append( 57 | errs, 58 | errors.WarnPropertiesAnnotationUsed( 59 | fmt.Sprintf( 60 | "found %s annotation, please define these properties in metadata/properties.yaml instead", 61 | annotationKey, 62 | ))) 63 | } 64 | } 65 | return errs 66 | } 67 | -------------------------------------------------------------------------------- /pkg/validation/internal/crd_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/operator-framework/api/pkg/validation/errors" 8 | "github.com/stretchr/testify/require" 9 | v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 10 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 11 | 12 | "sigs.k8s.io/yaml" 13 | ) 14 | 15 | func TestValidateCRD(t *testing.T) { 16 | var table = []struct { 17 | description string 18 | filePath string 19 | version string 20 | hasError bool 21 | errString string 22 | }{ 23 | { 24 | description: "registryv1 bundle/valid v1beta1 CRD", 25 | filePath: "./testdata/v1beta1.crd.yaml", 26 | version: "v1beta1", 27 | hasError: false, 28 | errString: "", 29 | }, 30 | { 31 | description: "registryv1 bundle invalid v1beta1 CRD duplicate version", 32 | filePath: "./testdata/duplicateVersions.crd.yaml", 33 | version: "v1beta1", 34 | hasError: true, 35 | errString: "must contain unique version names", 36 | }, 37 | { 38 | description: "registryv1 bundle/valid v1 CRD", 39 | filePath: "./testdata/v1.crd.yaml", 40 | version: "v1", 41 | hasError: false, 42 | errString: "", 43 | }, 44 | { 45 | description: "registryv1 bundle invalid v1 CRD deprecated .spec.version field", 46 | filePath: "./testdata/deprecatedVersion.crd.yaml", 47 | version: "v1", 48 | hasError: true, 49 | errString: "must have exactly one version marked as storage version", 50 | }, 51 | { 52 | description: "registryv1 bundle invalid CRD no conversionReviewVersions", 53 | filePath: "./testdata/noConversionReviewVersions.crd.yaml", 54 | version: "v1", 55 | hasError: true, 56 | errString: "spec.conversion.conversionReviewVersions: Required value", 57 | }, 58 | } 59 | for _, tt := range table { 60 | b, err := ioutil.ReadFile(tt.filePath) 61 | if err != nil { 62 | t.Fatalf("Error reading CRD path %s: %v", tt.filePath, err) 63 | } 64 | 65 | results := []errors.ManifestResult{} 66 | switch tt.version { 67 | case "v1": 68 | crd := &v1.CustomResourceDefinition{} 69 | if err = yaml.Unmarshal(b, crd); err != nil { 70 | t.Fatalf("Error unmarshalling CRD at path %s: %v", tt.filePath, err) 71 | } 72 | results = CRDValidator.Validate(crd) 73 | default: 74 | crd := &v1beta1.CustomResourceDefinition{} 75 | if err = yaml.Unmarshal(b, crd); err != nil { 76 | t.Fatalf("Error unmarshalling CRD at path %s: %v", tt.filePath, err) 77 | } 78 | results = CRDValidator.Validate(crd) 79 | } 80 | 81 | if len(results) > 0 { 82 | if results[0].HasError() { 83 | if !tt.hasError { 84 | t.Errorf("%s: expected no errors, got: %+q", tt.description, results[0].Errors) 85 | } else { 86 | require.Contains(t, results[0].Errors[0].Error(), tt.errString, tt.description) 87 | } 88 | } else if tt.hasError { 89 | t.Errorf("%s: expected error %q, got none", tt.description, tt.errString) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/operators/v1/operator_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | // OperatorSpec defines the desired state of Operator 9 | type OperatorSpec struct{} 10 | 11 | // OperatorStatus defines the observed state of an Operator and its components 12 | type OperatorStatus struct { 13 | // Components describes resources that compose the operator. 14 | // +optional 15 | Components *Components `json:"components,omitempty"` 16 | } 17 | 18 | // ConditionType codifies a condition's type. 19 | type ConditionType string 20 | 21 | // Condition represent the latest available observations of an component's state. 22 | type Condition struct { 23 | // Type of condition. 24 | Type ConditionType `json:"type"` 25 | // Status of the condition, one of True, False, Unknown. 26 | Status corev1.ConditionStatus `json:"status"` 27 | // The reason for the condition's last transition. 28 | // +optional 29 | Reason string `json:"reason,omitempty"` 30 | // A human readable message indicating details about the transition. 31 | // +optional 32 | Message string `json:"message,omitempty"` 33 | // Last time the condition was probed 34 | // +optional 35 | LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` 36 | // Last time the condition transitioned from one status to another. 37 | // +optional 38 | LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"` 39 | } 40 | 41 | // Components tracks the resources that compose an operator. 42 | type Components struct { 43 | // LabelSelector is a label query over a set of resources used to select the operator's components 44 | LabelSelector *metav1.LabelSelector `json:"labelSelector"` 45 | // Refs are a set of references to the operator's component resources, selected with LabelSelector. 46 | // +optional 47 | Refs []RichReference `json:"refs,omitempty"` 48 | } 49 | 50 | // RichReference is a reference to a resource, enriched with its status conditions. 51 | type RichReference struct { 52 | *corev1.ObjectReference `json:",inline"` 53 | // Conditions represents the latest state of the component. 54 | // +optional 55 | // +patchMergeKey=type 56 | // +patchStrategy=merge 57 | Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` 58 | } 59 | 60 | // +genclient 61 | // +genclient:nonNamespaced 62 | // +kubebuilder:object:root=true 63 | // +kubebuilder:storageversion 64 | // +kubebuilder:resource:categories=olm,scope=Cluster 65 | // +kubebuilder:subresource:status 66 | 67 | // Operator represents a cluster operator. 68 | type Operator struct { 69 | metav1.TypeMeta `json:",inline"` 70 | metav1.ObjectMeta `json:"metadata,omitempty"` 71 | 72 | Spec OperatorSpec `json:"spec,omitempty"` 73 | Status OperatorStatus `json:"status,omitempty"` 74 | } 75 | 76 | // +genclient:nonNamespaced 77 | // +kubebuilder:object:root=true 78 | 79 | // OperatorList contains a list of Operators. 80 | type OperatorList struct { 81 | metav1.TypeMeta `json:",inline"` 82 | metav1.ListMeta `json:"metadata,omitempty"` 83 | Items []Operator `json:"items"` 84 | } 85 | 86 | func init() { 87 | SchemeBuilder.Register(&Operator{}, &OperatorList{}) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/apis/scorecard/v1alpha3/configuration_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha3 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // ConfigurationKind is the default scorecard componentconfig kind. 8 | const ConfigurationKind = "Configuration" 9 | 10 | // Configuration represents the set of test configurations which scorecard would run. 11 | type Configuration struct { 12 | metav1.TypeMeta `json:",inline" yaml:",inline"` 13 | 14 | // Do not use metav1.ObjectMeta because this "object" should not be treated as an actual object. 15 | Metadata struct { 16 | // Name is a required field for kustomize-able manifests, and is not used on-cluster (nor is the config itself). 17 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 18 | } `json:"metadata,omitempty" yaml:"metadata,omitempty"` 19 | 20 | // Stages is a set of test stages to run. Once a stage is finished, the next stage in the slice will be run. 21 | Stages []StageConfiguration `json:"stages" yaml:"stages"` 22 | 23 | // Storage is the optional storage configuration 24 | Storage Storage `json:"storage,omitempty" yaml:"storage,omitempty"` 25 | 26 | // ServiceAccount is the service account under which scorecard tests are run. This field is optional. If left unset, the `default` service account will be used. 27 | ServiceAccount string `json:"serviceaccount,omitempty" yaml:"serviceaccount,omitempty"` 28 | } 29 | 30 | // StageConfiguration configures a set of tests to be run. 31 | type StageConfiguration struct { 32 | // Parallel, if true, will run each test in tests in parallel. 33 | // The default is to wait until a test finishes to run the next. 34 | Parallel bool `json:"parallel,omitempty" yaml:"parallel,omitempty"` 35 | // Tests are a list of tests to run. 36 | Tests []TestConfiguration `json:"tests" yaml:"tests"` 37 | } 38 | 39 | // TestConfiguration configures a specific scorecard test, identified by entrypoint. 40 | type TestConfiguration struct { 41 | // Image is the name of the test image. 42 | Image string `json:"image" yaml:"image"` 43 | // UniqueID is is an optional unique test identifier of the test image. 44 | UniqueID string `json:"uniqueID,omitempty" yaml:"uniqueID,omitempty"` 45 | // Entrypoint is a list of commands and arguments passed to the test image. 46 | Entrypoint []string `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"` 47 | // Labels further describe the test and enable selection. 48 | Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` 49 | // Storage is the optional storage configuration for the test image. 50 | Storage Storage `json:"storage,omitempty" yaml:"storage,omitempty"` 51 | } 52 | 53 | // Storage configures custom storage options 54 | type Storage struct { 55 | // Spec contains the storage configuration options 56 | Spec StorageSpec `json:"spec" yaml:"spec"` 57 | } 58 | 59 | // StorageSpec contains storage configuration options 60 | type StorageSpec struct { 61 | // MountPath configures the path to mount directories in the test pod 62 | MountPath MountPath `json:"mountPath,omitempty" yaml:"mountPath,omitempty"` 63 | } 64 | 65 | // MountPath configures the path to mount directories in the test pod 66 | type MountPath struct { 67 | // Path is the fully qualified path that a directory should be mounted in the test pod 68 | Path string `json:"path,omitempty" yaml:"path,omitempty"` 69 | } 70 | -------------------------------------------------------------------------------- /pkg/operators/v1/olmconfig_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "time" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | const ( 10 | DisabledCopiedCSVsConditionType = "DisabledCopiedCSVs" 11 | ) 12 | 13 | // OLMConfigSpec is the spec for an OLMConfig resource. 14 | type OLMConfigSpec struct { 15 | Features *Features `json:"features,omitempty"` 16 | } 17 | 18 | // Features contains the list of configurable OLM features. 19 | type Features struct { 20 | 21 | // DisableCopiedCSVs is used to disable OLM's "Copied CSV" feature 22 | // for operators installed at the cluster scope, where a cluster 23 | // scoped operator is one that has been installed in an 24 | // OperatorGroup that targets all namespaces. 25 | // When reenabled, OLM will recreate the "Copied CSVs" for each 26 | // cluster scoped operator. 27 | DisableCopiedCSVs *bool `json:"disableCopiedCSVs,omitempty"` 28 | // PackageServerSyncInterval is used to define the sync interval for 29 | // packagerserver pods. Packageserver pods periodically check the 30 | // status of CatalogSources; this specifies the period using duration 31 | // format (e.g. "60m"). For this parameter, only hours ("h"), minutes 32 | // ("m"), and seconds ("s") may be specified. When not specified, the 33 | // period defaults to the value specified within the packageserver. 34 | // +optional 35 | // +kubebuilder:validation:Type=string 36 | // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(s|m|h))+$" 37 | PackageServerSyncInterval *metav1.Duration `json:"packageServerSyncInterval,omitempty"` 38 | } 39 | 40 | // OLMConfigStatus is the status for an OLMConfig resource. 41 | type OLMConfigStatus struct { 42 | Conditions []metav1.Condition `json:"conditions,omitempty"` 43 | } 44 | 45 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 46 | // +genclient 47 | // +genclient:nonNamespaced 48 | // +kubebuilder:storageversion 49 | // +kubebuilder:resource:categories=olm,scope=Cluster 50 | // +kubebuilder:subresource:status 51 | 52 | // OLMConfig is a resource responsible for configuring OLM. 53 | type OLMConfig struct { 54 | metav1.TypeMeta `json:",inline"` 55 | metav1.ObjectMeta `json:"metadata"` 56 | 57 | Spec OLMConfigSpec `json:"spec,omitempty"` 58 | Status OLMConfigStatus `json:"status,omitempty"` 59 | } 60 | 61 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 62 | 63 | // OLMConfigList is a list of OLMConfig resources. 64 | type OLMConfigList struct { 65 | metav1.TypeMeta `json:",inline"` 66 | metav1.ListMeta `json:"metadata"` 67 | // +listType=set 68 | Items []OLMConfig `json:"items"` 69 | } 70 | 71 | func init() { 72 | SchemeBuilder.Register(&OLMConfig{}, &OLMConfigList{}) 73 | } 74 | 75 | // CopiedCSVsAreEnabled returns true if and only if the olmConfigs DisableCopiedCSVs is set and true, 76 | // otherwise false is returned 77 | func (config *OLMConfig) CopiedCSVsAreEnabled() bool { 78 | if config == nil || config.Spec.Features == nil || config.Spec.Features.DisableCopiedCSVs == nil { 79 | return true 80 | } 81 | 82 | return !*config.Spec.Features.DisableCopiedCSVs 83 | } 84 | 85 | func (config *OLMConfig) PackageServerSyncInterval() *time.Duration { 86 | if config == nil || config.Spec.Features == nil || config.Spec.Features.PackageServerSyncInterval == nil { 87 | return nil 88 | } 89 | return &config.Spec.Features.PackageServerSyncInterval.Duration 90 | } 91 | -------------------------------------------------------------------------------- /pkg/validation/internal/standardcategories.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/operator-framework/api/pkg/manifests" 9 | "github.com/operator-framework/api/pkg/validation/errors" 10 | interfaces "github.com/operator-framework/api/pkg/validation/interfaces" 11 | ) 12 | 13 | var StandardCategoriesValidator interfaces.Validator = interfaces.ValidatorFunc(validateCategories) 14 | 15 | var validCategories = map[string]struct{}{ 16 | "AI/Machine Learning": {}, 17 | "Application Runtime": {}, 18 | "Big Data": {}, 19 | "Cloud Provider": {}, 20 | "Developer Tools": {}, 21 | "Database": {}, 22 | "Integration & Delivery": {}, 23 | "Logging & Tracing": {}, 24 | "Monitoring": {}, 25 | "Modernization & Migration": {}, 26 | "Networking": {}, 27 | "OpenShift Optional": {}, 28 | "Security": {}, 29 | "Storage": {}, 30 | "Streaming & Messaging": {}, 31 | "Observability": {}, 32 | } 33 | 34 | func validateCategories(objs ...interface{}) (results []errors.ManifestResult) { 35 | for _, obj := range objs { 36 | switch v := obj.(type) { 37 | case *manifests.Bundle: 38 | results = append(results, validateCategoriesBundle(v)) 39 | } 40 | } 41 | 42 | return results 43 | } 44 | 45 | func validateCategoriesBundle(bundle *manifests.Bundle) errors.ManifestResult { 46 | result := errors.ManifestResult{Name: bundle.Name} 47 | csvCategoryCheck := CSVChecks{csv: *bundle.CSV, errs: []error{}, warns: []error{}} 48 | 49 | csvChecksResult := checkCategories(csvCategoryCheck) 50 | for _, err := range csvChecksResult.errs { 51 | result.Add(errors.ErrInvalidCSV(err.Error(), bundle.CSV.GetName())) 52 | } 53 | for _, warn := range csvChecksResult.warns { 54 | result.Add(errors.WarnInvalidCSV(warn.Error(), bundle.CSV.GetName())) 55 | } 56 | 57 | return result 58 | } 59 | 60 | func checkCategories(checks CSVChecks) CSVChecks { 61 | if checks.csv.GetAnnotations() == nil { 62 | checks.csv.SetAnnotations(make(map[string]string)) 63 | } 64 | 65 | if categories, ok := checks.csv.ObjectMeta.Annotations["categories"]; ok { 66 | categorySlice := strings.Split(categories, ",") 67 | 68 | // use custom categories for validation if provided 69 | customCategoriesPath := os.Getenv("OPERATOR_BUNDLE_CATEGORIES") 70 | if customCategoriesPath != "" { 71 | customCategories, err := extractCategories(customCategoriesPath) 72 | if err != nil { 73 | checks.errs = append(checks.errs, fmt.Errorf("could not extract custom categories from categories %#v: %s", customCategories, err)) 74 | } else { 75 | for _, category := range categorySlice { 76 | if _, ok := customCategories[strings.TrimSpace(category)]; !ok { 77 | checks.errs = append(checks.errs, fmt.Errorf("csv.Metadata.Annotations[\"categories\"] value %q is not in the set of custom categories", category)) 78 | } 79 | } 80 | } 81 | } else { 82 | // use default categories 83 | for _, category := range categorySlice { 84 | if _, ok := validCategories[strings.TrimSpace(category)]; !ok { 85 | checks.errs = append(checks.errs, fmt.Errorf("csv.Metadata.Annotations[\"categories\"] value %q is not in the set of standard categories", category)) 86 | } 87 | } 88 | } 89 | } 90 | return checks 91 | } 92 | -------------------------------------------------------------------------------- /pkg/validation/internal/testdata/badName.csv.yaml: -------------------------------------------------------------------------------- 1 | #! validate-crd: deploy/chart/templates/0000_30_02-clusterserviceversion.crd.yaml 2 | #! parse-kind: ClusterServiceVersion 3 | apiVersion: operators.coreos.com/v1alpha1 4 | kind: ClusterServiceVersion 5 | metadata: 6 | name: someoperatorwithanextremelylongnamethatmakenosensewhatsoever.v999.999.999 7 | namespace: placeholder 8 | annotations: 9 | capabilities: Full Lifecycle 10 | tectonic-visibility: ocs 11 | alm-examples: '[{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdCluster","metadata":{"name":"example","namespace":"default"},"spec":{"size":3,"version":"3.2.13"}},{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdRestore","metadata":{"name":"example-etcd-cluster"},"spec":{"etcdCluster":{"name":"example-etcd-cluster"},"backupStorageType":"S3","s3":{"path":"","awsSecret":""}}},{"apiVersion":"etcd.database.coreos.com/v1beta2","kind":"EtcdBackup","metadata":{"name":"example-etcd-cluster-backup"},"spec":{"etcdEndpoints":[""],"storageType":"S3","s3":{"path":"","awsSecret":""}}}]' 12 | description: etcd is a distributed key value store providing a reliable way to store data across a cluster of machines. 13 | spec: 14 | minKubeVersion: 1.21.0 15 | displayName: etcd 16 | description: something 17 | keywords: ['etcd', 'key value', 'database', 'coreos', 'open source'] 18 | version: 0.9.0 19 | icon: 20 | - base64data: N8AmEL9X4ABACNSKMHAgb34AAAAAElFTkSuQmCC 21 | mediatype: image/png 22 | installModes: 23 | - type: OwnNamespace 24 | supported: true 25 | - type: SingleNamespace 26 | supported: true 27 | - type: MultiNamespace 28 | supported: false 29 | - type: AllNamespaces 30 | supported: true 31 | install: 32 | strategy: deployment 33 | spec: 34 | permissions: 35 | - serviceAccountName: etcd-operator 36 | rules: 37 | - apiGroups: 38 | - etcd.database.coreos.com 39 | resources: 40 | - etcdclusters 41 | - etcdbackups 42 | - etcdrestores 43 | verbs: 44 | - "*" 45 | - apiGroups: 46 | - "" 47 | resources: 48 | - pods 49 | - services 50 | - endpoints 51 | - persistentvolumeclaims 52 | - events 53 | verbs: 54 | - "*" 55 | - apiGroups: 56 | - apps 57 | resources: 58 | - deployments 59 | verbs: 60 | - "*" 61 | - apiGroups: 62 | - "" 63 | resources: 64 | - secrets 65 | verbs: 66 | - get 67 | deployments: 68 | - name: etcd-operator 69 | spec: 70 | replicas: 1 71 | selector: 72 | matchLabels: 73 | name: etcd-operator-alm-owned 74 | template: 75 | metadata: 76 | name: etcd-operator-alm-owned 77 | labels: 78 | name: etcd-operator-alm-owned 79 | spec: 80 | serviceAccountName: etcd-operator 81 | customresourcedefinitions: 82 | owned: 83 | - name: etcdclusters.etcd.database.coreos.com 84 | version: v1beta2 85 | kind: EtcdCluster 86 | - name: etcdbackups.etcd.database.coreos.com 87 | version: v1beta2 88 | kind: EtcdBackup 89 | - name: etcdrestores.etcd.database.coreos.com 90 | version: v1beta2 91 | kind: EtcdRestore 92 | -------------------------------------------------------------------------------- /pkg/operators/v1/olmconfig_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func boolPointer(in bool) *bool { 12 | return &in 13 | } 14 | func TestPackageServerSyncInterval(t *testing.T) { 15 | five := time.Minute * 5 16 | one := time.Second * 60 17 | 18 | fiveParsed, err := time.ParseDuration("5m") 19 | require.NoError(t, err) 20 | 21 | oneParsed, err := time.ParseDuration("60s") 22 | require.NoError(t, err) 23 | 24 | tests := []struct { 25 | description string 26 | olmConfig *OLMConfig 27 | expected *time.Duration 28 | }{ 29 | { 30 | description: "NilConfig", 31 | olmConfig: nil, 32 | expected: nil, 33 | }, 34 | { 35 | description: "MissingSpec", 36 | olmConfig: &OLMConfig{}, 37 | expected: nil, 38 | }, 39 | { 40 | description: "MissingFeatures", 41 | olmConfig: &OLMConfig{ 42 | Spec: OLMConfigSpec{}, 43 | }, 44 | expected: nil, 45 | }, 46 | { 47 | description: "MissingPackageServerInterval", 48 | olmConfig: &OLMConfig{ 49 | Spec: OLMConfigSpec{}, 50 | }, 51 | expected: nil, 52 | }, 53 | { 54 | description: "PackageServerInterval5m", 55 | olmConfig: &OLMConfig{ 56 | Spec: OLMConfigSpec{ 57 | Features: &Features{ 58 | PackageServerSyncInterval: &metav1.Duration{Duration: fiveParsed}, 59 | }, 60 | }, 61 | }, 62 | expected: &five, 63 | }, 64 | { 65 | description: "PackageServerInterval60s", 66 | olmConfig: &OLMConfig{ 67 | Spec: OLMConfigSpec{ 68 | Features: &Features{ 69 | PackageServerSyncInterval: &metav1.Duration{Duration: oneParsed}, 70 | }, 71 | }, 72 | }, 73 | expected: &one, 74 | }, 75 | } 76 | 77 | for _, tt := range tests { 78 | t.Run(tt.description, func(t *testing.T) { 79 | require.EqualValues(t, tt.expected, tt.olmConfig.PackageServerSyncInterval()) 80 | }) 81 | } 82 | } 83 | func TestCopiedCSVsAreEnabled(t *testing.T) { 84 | tests := []struct { 85 | description string 86 | olmConfig *OLMConfig 87 | expected bool 88 | }{ 89 | { 90 | description: "NilConfig", 91 | olmConfig: nil, 92 | expected: true, 93 | }, 94 | { 95 | description: "MissingSpec", 96 | olmConfig: &OLMConfig{}, 97 | expected: true, 98 | }, 99 | { 100 | description: "MissingFeatures", 101 | olmConfig: &OLMConfig{ 102 | Spec: OLMConfigSpec{}, 103 | }, 104 | expected: true, 105 | }, 106 | { 107 | description: "MissingDisableCopiedCSVs", 108 | olmConfig: &OLMConfig{ 109 | Spec: OLMConfigSpec{}, 110 | }, 111 | expected: true, 112 | }, 113 | { 114 | description: "CopiedCSVsDisabled", 115 | olmConfig: &OLMConfig{ 116 | Spec: OLMConfigSpec{ 117 | Features: &Features{ 118 | DisableCopiedCSVs: boolPointer(true), 119 | }, 120 | }, 121 | }, 122 | expected: false, 123 | }, 124 | { 125 | description: "CopiedCSVsEnabled", 126 | olmConfig: &OLMConfig{ 127 | Spec: OLMConfigSpec{ 128 | Features: &Features{ 129 | DisableCopiedCSVs: boolPointer(false), 130 | }, 131 | }, 132 | }, 133 | expected: true, 134 | }, 135 | } 136 | 137 | for _, tt := range tests { 138 | t.Run(tt.description, func(t *testing.T) { 139 | require.EqualValues(t, tt.expected, tt.olmConfig.CopiedCSVsAreEnabled()) 140 | }) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /pkg/operators/v1alpha2/operatorgroup_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha2 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | const ( 12 | OperatorGroupAnnotationKey = "olm.operatorGroup" 13 | OperatorGroupNamespaceAnnotationKey = "olm.operatorNamespace" 14 | OperatorGroupTargetsAnnotationKey = "olm.targetNamespaces" 15 | OperatorGroupProvidedAPIsAnnotationKey = "olm.providedAPIs" 16 | 17 | OperatorGroupKind = "OperatorGroup" 18 | ) 19 | 20 | // OperatorGroupSpec is the spec for an OperatorGroup resource. 21 | type OperatorGroupSpec struct { 22 | // Selector selects the OperatorGroup's target namespaces. 23 | // +optional 24 | Selector *metav1.LabelSelector `json:"selector,omitempty"` 25 | 26 | // TargetNamespaces is an explicit set of namespaces to target. 27 | // If it is set, Selector is ignored. 28 | // +optional 29 | TargetNamespaces []string `json:"targetNamespaces,omitempty"` 30 | 31 | // ServiceAccountName is the admin specified service account which will be 32 | // used to deploy operator(s) in this operator group. 33 | ServiceAccountName string `json:"serviceAccountName,omitempty"` 34 | 35 | // Static tells OLM not to update the OperatorGroup's providedAPIs annotation 36 | // +optional 37 | StaticProvidedAPIs bool `json:"staticProvidedAPIs,omitempty"` 38 | } 39 | 40 | // OperatorGroupStatus is the status for an OperatorGroupResource. 41 | type OperatorGroupStatus struct { 42 | // Namespaces is the set of target namespaces for the OperatorGroup. 43 | Namespaces []string `json:"namespaces,omitempty"` 44 | 45 | // ServiceAccountRef references the service account object specified. 46 | ServiceAccountRef *corev1.ObjectReference `json:"serviceAccountRef,omitempty"` 47 | 48 | // LastUpdated is a timestamp of the last time the OperatorGroup's status was Updated. 49 | LastUpdated *metav1.Time `json:"lastUpdated"` 50 | } 51 | 52 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 53 | // +genclient 54 | // +kubebuilder:resource:shortName=og,categories=olm 55 | // +kubebuilder:subresource:status 56 | 57 | // OperatorGroup is the unit of multitenancy for OLM managed operators. 58 | // It constrains the installation of operators in its namespace to a specified set of target namespaces. 59 | type OperatorGroup struct { 60 | metav1.TypeMeta `json:",inline"` 61 | metav1.ObjectMeta `json:"metadata"` 62 | 63 | // +optional 64 | Spec OperatorGroupSpec `json:"spec"` 65 | Status OperatorGroupStatus `json:"status,omitempty"` 66 | } 67 | 68 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 69 | 70 | // OperatorGroupList is a list of OperatorGroup resources. 71 | type OperatorGroupList struct { 72 | metav1.TypeMeta `json:",inline"` 73 | metav1.ListMeta `json:"metadata"` 74 | 75 | Items []OperatorGroup `json:"items"` 76 | } 77 | 78 | func (o *OperatorGroup) BuildTargetNamespaces() string { 79 | sort.Strings(o.Status.Namespaces) 80 | return strings.Join(o.Status.Namespaces, ",") 81 | } 82 | 83 | // IsServiceAccountSpecified returns true if the spec has a service account name specified. 84 | func (o *OperatorGroup) IsServiceAccountSpecified() bool { 85 | if o.Spec.ServiceAccountName == "" { 86 | return false 87 | } 88 | 89 | return true 90 | } 91 | 92 | // HasServiceAccountSynced returns true if the service account specified has been synced. 93 | func (o *OperatorGroup) HasServiceAccountSynced() bool { 94 | if o.IsServiceAccountSpecified() && o.Status.ServiceAccountRef != nil { 95 | return true 96 | } 97 | 98 | return false 99 | } 100 | -------------------------------------------------------------------------------- /pkg/validation/internal/package_manifest_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/operator-framework/api/pkg/manifests" 7 | "github.com/operator-framework/api/pkg/validation/errors" 8 | ) 9 | 10 | func TestValidatePackageManifest(t *testing.T) { 11 | pkgName := "test-package" 12 | 13 | cases := []struct { 14 | validatorFuncTest 15 | pkg *manifests.PackageManifest 16 | }{ 17 | { 18 | validatorFuncTest{ 19 | description: "successful validation", 20 | }, 21 | &manifests.PackageManifest{ 22 | Channels: []manifests.PackageChannel{ 23 | {Name: "foo", CurrentCSVName: "bar"}, 24 | }, 25 | DefaultChannelName: "foo", 26 | PackageName: "test-package", 27 | }, 28 | }, 29 | { 30 | validatorFuncTest{ 31 | description: "successful validation no default channel with only one channel", 32 | }, 33 | &manifests.PackageManifest{ 34 | Channels: []manifests.PackageChannel{ 35 | {Name: "foo", CurrentCSVName: "bar"}, 36 | }, 37 | PackageName: "test-package", 38 | }, 39 | }, 40 | { 41 | validatorFuncTest{ 42 | description: "no default channel and more than one channel", 43 | wantErr: true, 44 | errors: []errors.Error{ 45 | errors.ErrInvalidPackageManifest("default channel is empty but more than one channel exists", pkgName), 46 | }, 47 | }, 48 | &manifests.PackageManifest{ 49 | Channels: []manifests.PackageChannel{ 50 | {Name: "foo", CurrentCSVName: "bar"}, 51 | {Name: "foo2", CurrentCSVName: "baz"}, 52 | }, 53 | PackageName: "test-package", 54 | }, 55 | }, 56 | { 57 | validatorFuncTest{ 58 | description: "default channel does not exist in channels", 59 | wantErr: true, 60 | errors: []errors.Error{ 61 | errors.ErrInvalidPackageManifest(`default channel "baz" not found in the list of declared channels`, pkgName), 62 | }, 63 | }, 64 | &manifests.PackageManifest{ 65 | Channels: []manifests.PackageChannel{ 66 | {Name: "foo", CurrentCSVName: "bar"}, 67 | }, 68 | DefaultChannelName: "baz", 69 | PackageName: "test-package", 70 | }, 71 | }, 72 | { 73 | validatorFuncTest{ 74 | description: "channels are empty", 75 | wantErr: true, 76 | errors: []errors.Error{ 77 | errors.ErrInvalidPackageManifest("channels empty", pkgName), 78 | }, 79 | }, 80 | &manifests.PackageManifest{ 81 | Channels: nil, 82 | DefaultChannelName: "baz", 83 | PackageName: "test-package", 84 | }, 85 | }, 86 | { 87 | validatorFuncTest{ 88 | description: "one channel's CSVName is empty", 89 | wantErr: true, 90 | errors: []errors.Error{ 91 | errors.ErrInvalidPackageManifest(`channel "foo" currentCSV is empty`, pkgName), 92 | }, 93 | }, 94 | &manifests.PackageManifest{ 95 | Channels: []manifests.PackageChannel{{Name: "foo"}}, 96 | DefaultChannelName: "foo", 97 | PackageName: "test-package", 98 | }, 99 | }, 100 | { 101 | validatorFuncTest{ 102 | description: "duplicate channel name", 103 | wantErr: true, 104 | errors: []errors.Error{ 105 | errors.ErrInvalidPackageManifest(`duplicate package manifest channel name "foo"`, pkgName), 106 | }, 107 | }, 108 | &manifests.PackageManifest{ 109 | Channels: []manifests.PackageChannel{ 110 | {Name: "foo", CurrentCSVName: "bar"}, 111 | {Name: "foo", CurrentCSVName: "baz"}, 112 | }, 113 | DefaultChannelName: "foo", 114 | PackageName: "test-package", 115 | }, 116 | }, 117 | } 118 | 119 | for _, c := range cases { 120 | result := validatePackageManifest(c.pkg) 121 | c.check(t, result) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pkg/constraints/constraint.go: -------------------------------------------------------------------------------- 1 | package constraints 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | // OLMConstraintType is the schema "type" key for all constraints known to OLM 10 | // (except for legacy types). 11 | const OLMConstraintType = "olm.constraint" 12 | 13 | // Constraint holds parsed, potentially nested dependency constraints. 14 | type Constraint struct { 15 | // Constraint failure message that surfaces in resolution 16 | // This field is optional 17 | FailureMessage string `json:"failureMessage,omitempty" yaml:"failureMessage,omitempty"` 18 | 19 | // The cel struct that contraints CEL expression 20 | Cel *Cel `json:"cel,omitempty" yaml:"cel,omitempty"` 21 | 22 | // Package defines a constraint for a package within a version range. 23 | Package *PackageConstraint `json:"package,omitempty" yaml:"package,omitempty"` 24 | 25 | // GVK defines a constraint for a GVK. 26 | GVK *GVKConstraint `json:"gvk,omitempty" yaml:"gvk,omitempty"` 27 | 28 | // All, Any, and Not are compound constraints. See this enhancement for details: 29 | // https://github.com/operator-framework/enhancements/blob/master/enhancements/compound-bundle-constraints.md 30 | All *CompoundConstraint `json:"all,omitempty" yaml:"all,omitempty"` 31 | Any *CompoundConstraint `json:"any,omitempty" yaml:"any,omitempty"` 32 | // A note on Not: this constraint isn't particularly useful by itself. 33 | // It should be used within an All constraint alongside some other constraint type 34 | // since saying "do not use any of these GVKs/packages/etc." without an alternative 35 | // doesn't make sense. 36 | Not *CompoundConstraint `json:"not,omitempty" yaml:"not,omitempty"` 37 | } 38 | 39 | // CompoundConstraint holds a list of potentially nested constraints 40 | // over which a boolean operation is applied. 41 | type CompoundConstraint struct { 42 | Constraints []Constraint `json:"constraints" yaml:"constraints"` 43 | } 44 | 45 | // GVKConstraint defines a GVK constraint. 46 | type GVKConstraint struct { 47 | Group string `json:"group" yaml:"group"` 48 | Kind string `json:"kind" yaml:"kind"` 49 | Version string `json:"version" yaml:"version"` 50 | } 51 | 52 | // PackageConstraint defines a package constraint. 53 | type PackageConstraint struct { 54 | // PackageName is the name of the package. 55 | PackageName string `json:"packageName" yaml:"packageName"` 56 | // VersionRange required for the package. 57 | VersionRange string `json:"versionRange" yaml:"versionRange"` 58 | } 59 | 60 | // maxConstraintSize defines the maximum raw size in bytes of an olm.constraint. 61 | // 64Kb seems reasonable, since this number allows for long description strings 62 | // and either few deep nestings or shallow nestings and long constraints lists, 63 | // but not both. 64 | // QUESTION: make this configurable? 65 | const maxConstraintSize = 2 << 16 66 | 67 | // ErrMaxConstraintSizeExceeded is returned when a constraint's size > maxConstraintSize. 68 | var ErrMaxConstraintSizeExceeded = fmt.Errorf("olm.constraint value is greater than max constraint size %d bytes", maxConstraintSize) 69 | 70 | // Parse parses an olm.constraint property's value recursively into a Constraint. 71 | // Unknown value schemas result in an error. Constraints that exceed the number of bytes 72 | // defined by maxConstraintSize result results in an error. 73 | func Parse(v json.RawMessage) (c Constraint, err error) { 74 | // There is no way to explicitly limit nesting depth. 75 | // From https://github.com/golang/go/issues/31789#issuecomment-538134396, 76 | // the recommended approach is to error out if raw input size 77 | // is greater than some threshold. 78 | if len(v) > maxConstraintSize { 79 | return c, ErrMaxConstraintSizeExceeded 80 | } 81 | 82 | d := json.NewDecoder(bytes.NewBuffer(v)) 83 | d.DisallowUnknownFields() 84 | err = d.Decode(&c) 85 | 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /pkg/manifests/packagemanifestloader.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | utilerrors "k8s.io/apimachinery/pkg/util/errors" 11 | "k8s.io/apimachinery/pkg/util/yaml" 12 | 13 | operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" 14 | ) 15 | 16 | // bundleLoader loads a bundle directory from disk 17 | type packageManifestLoader struct { 18 | dir string 19 | bundles []*Bundle 20 | pkg *PackageManifest 21 | } 22 | 23 | func NewPackageManifestLoader(dir string) packageManifestLoader { 24 | return packageManifestLoader{ 25 | dir: dir, 26 | } 27 | } 28 | 29 | func (p *packageManifestLoader) LoadPackage() error { 30 | errs := make([]error, 0) 31 | if err := filepath.Walk(p.dir, collectWalkErrs(p.LoadPackagesWalkFunc, &errs)); err != nil { 32 | errs = append(errs, err) 33 | } 34 | 35 | if err := filepath.Walk(p.dir, collectWalkErrs(p.LoadBundleWalkFunc, &errs)); err != nil { 36 | errs = append(errs, err) 37 | } 38 | 39 | return utilerrors.NewAggregate(errs) 40 | } 41 | 42 | // LoadPackagesWalkFunc attempts to unmarshal the file at the given path into a PackageManifest resource. 43 | // If unmarshaling is successful, the PackageManifest is added to the loader's store. 44 | func (p *packageManifestLoader) LoadPackagesWalkFunc(path string, f os.FileInfo, err error) error { 45 | if f == nil { 46 | return fmt.Errorf("invalid file: %v", f) 47 | } 48 | 49 | if f.IsDir() { 50 | if strings.HasPrefix(f.Name(), ".") { 51 | return filepath.SkipDir 52 | } 53 | return nil 54 | } 55 | 56 | if strings.HasPrefix(f.Name(), ".") { 57 | return nil 58 | } 59 | 60 | fileReader, err := os.Open(path) 61 | if err != nil { 62 | return fmt.Errorf("unable to load package from file %s: %s", path, err) 63 | } 64 | defer fileReader.Close() 65 | 66 | decoder := yaml.NewYAMLOrJSONDecoder(fileReader, 30) 67 | manifest := PackageManifest{} 68 | if err = decoder.Decode(&manifest); err != nil { 69 | if err != nil { 70 | return fmt.Errorf("could not decode contents of file %s into package: %s", path, err) 71 | } 72 | } 73 | 74 | if manifest.IsEmpty() { 75 | return nil 76 | } 77 | 78 | if p.pkg != nil { 79 | return fmt.Errorf("multiple package manifest files found in directory") 80 | } 81 | 82 | p.pkg = &manifest 83 | 84 | return nil 85 | } 86 | 87 | func (p *packageManifestLoader) LoadBundleWalkFunc(path string, f os.FileInfo, err error) error { 88 | if f == nil { 89 | return fmt.Errorf("invalid file: %v", f) 90 | } 91 | 92 | if f.IsDir() { 93 | if strings.HasPrefix(f.Name(), ".") { 94 | return filepath.SkipDir 95 | } 96 | return nil 97 | } 98 | 99 | if strings.HasPrefix(f.Name(), ".") { 100 | return nil 101 | } 102 | 103 | fileReader, err := os.Open(path) 104 | if err != nil { 105 | return fmt.Errorf("unable to load file %s: %s", path, err) 106 | } 107 | defer fileReader.Close() 108 | 109 | decoder := yaml.NewYAMLOrJSONDecoder(fileReader, 30) 110 | csv := unstructured.Unstructured{} 111 | 112 | if err = decoder.Decode(&csv); err != nil { 113 | return nil 114 | } 115 | 116 | if csv.GetKind() != operatorsv1alpha1.ClusterServiceVersionKind { 117 | return nil 118 | } 119 | 120 | var errs []error 121 | bundle, err := loadBundle(csv.GetName(), filepath.Dir(path)) 122 | if err != nil { 123 | errs = append(errs, fmt.Errorf("error loading objs in directory: %s", err)) 124 | } 125 | 126 | if bundle == nil || bundle.CSV == nil { 127 | errs = append(errs, fmt.Errorf("no bundle csv found")) 128 | return utilerrors.NewAggregate(errs) 129 | } 130 | 131 | p.bundles = append(p.bundles, bundle) 132 | 133 | return utilerrors.NewAggregate(errs) 134 | } 135 | -------------------------------------------------------------------------------- /pkg/validation/internal/object_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | func TestValidateObject(t *testing.T) { 12 | var table = []struct { 13 | description string 14 | path string 15 | error bool 16 | warning bool 17 | detail string 18 | }{ 19 | { 20 | description: "valid PDB", 21 | path: "./testdata/objects/valid_pdb.yaml", 22 | }, 23 | { 24 | description: "invalid PDB - minAvailable set to 100%", 25 | path: "./testdata/objects/invalid_pdb_minAvailable.yaml", 26 | error: true, 27 | detail: "minAvailable field cannot be set to 100%", 28 | }, 29 | { 30 | description: "invalid PDB - maxUnavailable set to 0", 31 | path: "./testdata/objects/invalid_pdb_maxUnavailable.yaml", 32 | error: true, 33 | detail: "maxUnavailable field cannot be set to 0 or 0%", 34 | }, 35 | { 36 | description: "valid priorityclass", 37 | path: "./testdata/objects/valid_priorityclass.yaml", 38 | }, 39 | { 40 | description: "invalid priorityclass - global default set to true", 41 | path: "./testdata/objects/invalid_priorityclass.yaml", 42 | error: true, 43 | detail: "globalDefault field cannot be set to true", 44 | }, 45 | { 46 | description: "valid pdb role", 47 | path: "./testdata/objects/valid_role_get_pdb.yaml", 48 | }, 49 | { 50 | description: "invalid role - modify pdb", 51 | path: "./testdata/objects/invalid_role_create_pdb.yaml", 52 | warning: true, 53 | detail: "RBAC includes permission to create/update poddisruptionbudgets, which could impact cluster stability", 54 | }, 55 | { 56 | description: "valid scc role", 57 | path: "./testdata/objects/valid_role_get_scc.yaml", 58 | }, 59 | { 60 | description: "invalid scc role - modify default scc", 61 | path: "./testdata/objects/invalid_role_modify_scc.yaml", 62 | error: true, 63 | detail: "RBAC includes permission to modify default securitycontextconstraints, which could impact cluster stability", 64 | }, 65 | } 66 | 67 | for _, tt := range table { 68 | u := unstructured.Unstructured{} 69 | o, err := ioutil.ReadFile(tt.path) 70 | if err != nil { 71 | t.Fatalf("reading yaml object file: %s", err) 72 | } 73 | if err := yaml.Unmarshal(o, &u); err != nil { 74 | t.Fatalf("unmarshalling object at path %s: %v", tt.path, err) 75 | } 76 | 77 | results := ObjectValidator.Validate(&u) 78 | 79 | // check errors 80 | if len(results[0].Errors) > 0 && tt.error == false { 81 | t.Fatalf("received errors %#v when no validation error expected for %s", results, tt.path) 82 | } 83 | if len(results[0].Errors) == 0 && tt.error == true { 84 | t.Fatalf("received no errors when validation error expected for %s", tt.path) 85 | } 86 | if len(results[0].Errors) > 0 { 87 | if results[0].Errors[0].Detail != tt.detail { 88 | t.Fatalf("expected validation error detail %s, got %s", tt.detail, results[0].Errors[0].Detail) 89 | } 90 | } 91 | 92 | // check warnings 93 | if len(results[0].Warnings) > 0 && tt.warning == false { 94 | t.Fatalf("received errors %#v when no validation warning expected for %s", results, tt.path) 95 | } 96 | if len(results[0].Warnings) == 0 && tt.warning == true { 97 | t.Fatalf("received no errors when validation warning expected for %s", tt.path) 98 | } 99 | if len(results[0].Warnings) > 0 { 100 | if results[0].Warnings[0].Detail != tt.detail { 101 | t.Fatalf("expected validation warning detail %s, got %s", tt.detail, results[0].Warnings[0].Detail) 102 | } 103 | } 104 | } 105 | 106 | } 107 | --------------------------------------------------------------------------------