Skip to content

Commit

Permalink
add library pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
exdx committed Apr 7, 2021
1 parent 0177fb8 commit d004c0a
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 125 deletions.
31 changes: 8 additions & 23 deletions internal/cmd/operator_list_custom_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
func newOperatorListCustomResourcesCmd(cfg *action.Configuration) *cobra.Command {
crLister := action.NewOperatorListCustomResources(cfg)
output := new(string)
allNamespaces := new(bool)

cmd := &cobra.Command{
Use: "list-custom-resources <operator>",
Expand All @@ -34,21 +33,8 @@ func newOperatorListCustomResourcesCmd(cfg *action.Configuration) *cobra.Command
}

crLister.PackageName = args[0]
crLister.AllNamespaces = *allNamespaces

op, err := crLister.FindOperator(cmd.Context())
if err != nil {
log.Fatal(err)
}
log.Printf("found operator %s", op.Name)

crds, err := crLister.Unzip(cmd.Context(), op)
if err != nil {
log.Fatal(err)
}
log.Printf("found owned crds #%v", crds)

list, err := crLister.List(cmd.Context(), crds)
list, err := crLister.Run(cmd.Context())
if err != nil {
log.Fatal(err)
}
Expand All @@ -58,33 +44,32 @@ func newOperatorListCustomResourcesCmd(cfg *action.Configuration) *cobra.Command
tw := tabwriter.NewWriter(os.Stdout, 3, 4, 2, ' ', 0)
_, _ = fmt.Fprintf(tw, "NAME\tNAMESPACE\tKIND\tAPIVERSION\tAGE\n")
for _, cr := range list.Items {
age := time.Since(cr.CreationTimestamp.Time)
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s", cr.Name, cr.Namespace, cr.Kind, cr.APIVersion, duration.HumanDuration(age))
age := time.Since(cr.GetCreationTimestamp().Time)
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s", cr.GetName(), cr.GetNamespace(), cr.GetKind(), cr.GetAPIVersion(), duration.HumanDuration(age))
}
_ = tw.Flush()
} else if *output == "json" {
for _, cr := range list.Items {
fmt.Printf("#%v", cr.String())
j, _ := cr.MarshalJSON()
fmt.Printf("#%v", string(j))
}
} else if *output == "yaml" {
//TODO
}
},
}


bindOperatorListCustomResourcesFlags(cmd.Flags(), allNamespaces, output)
bindOperatorListCustomResourcesFlags(cmd.Flags(), output)
return cmd
}

func bindOperatorListCustomResourcesFlags(fs *pflag.FlagSet, allNamespaces *bool, output *string) {
fs.BoolVarP(allNamespaces, "all-namespaces", "A", false, "list operators in all namespaces")
func bindOperatorListCustomResourcesFlags(fs *pflag.FlagSet, output *string) {
fs.StringVarP(output, "output", "o", "", "Determines format for list output. One of json or yaml.")
}

func notValid(output *string) bool {
a := *output
if a == "" || a == "json" || a == "yaml" {
if a == "" || a == "json" || a == "yaml" {
return true
}
return false
Expand Down
109 changes: 7 additions & 102 deletions internal/pkg/action/operator_list_custom_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,17 @@ package action

import (
"context"
"fmt"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"sigs.k8s.io/controller-runtime/pkg/client"
"strings"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"github.com/operator-framework/api/pkg/operators/v1alpha1"
"github.com/operator-framework/api/pkg/operators/v2alpha1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
lib "github.com/operator-framework/kubectl-operator/pkg/action"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
)

type CustomResourceLister interface {
List(ctx context.Context, ownedCRDS []v1alpha1.CRDDescription) (metav1.PartialObjectMetadataList, error)
}

type CustomResourceDefinitionFinder interface {
FindOperator(ctx context.Context, packageName, namespace string) (*v2alpha1.Operator, error)
Unzip(ctx context.Context, operator *v2alpha1.Operator) ([]v1alpha1.CRDDescription, error)
}

// OperatorListCustomResources knows how to find and list custom resources given a package name and namespace.
type OperatorListCustomResources struct {
config *Configuration
config *Configuration
PackageName string
AllNamespaces bool
}

func NewOperatorListCustomResources(cfg *Configuration) *OperatorListCustomResources {
Expand All @@ -38,93 +21,15 @@ func NewOperatorListCustomResources(cfg *Configuration) *OperatorListCustomResou
}
}

// FindOperator finds an operator object on-cluster provided a package and namespace. Returns a copy of the operator.
func (o *OperatorListCustomResources) FindOperator(ctx context.Context) (*v2alpha1.Operator, error) {
func (o *OperatorListCustomResources) Run(ctx context.Context) (*unstructured.UnstructuredList, error) {
opKey := types.NamespacedName{
Name: o.PackageName,
Namespace: o.config.Namespace,
}
operator := v2alpha1.Operator{}
err := o.config.Client.Get(ctx, opKey, &operator)
if err != nil {
if k8serrors.IsNotFound(err) {
return nil, ErrPackageNotFound{o.PackageName}
}
return nil, err
}
return &operator, nil
}

// Unzip finds the CSV referenced by the provided operator and then inspects the spec.customresourcedefinitions.owned
// section of the CSV to return a list of APIs that are owned by the CSV.
func (o *OperatorListCustomResources) Unzip(ctx context.Context, operator *v2alpha1.Operator) ([]v1alpha1.CRDDescription, error) {
csv := v1alpha1.ClusterServiceVersion{}
csvKey := types.NamespacedName{}

if operator.Status.Components == nil {
return nil, fmt.Errorf("could not find underlying components for operator %s", operator.Name)
}
for _, resource := range operator.Status.Components.Refs {
if resource.Kind == v1alpha1.ClusterServiceVersionKind {
csvKey.Name = resource.Name
csvKey.Namespace = resource.Namespace
break
}
continue
}

if csvKey.Name == "" && csvKey.Namespace == "" {
return nil, fmt.Errorf("could not find underlying CSV for operator %s", operator.Name)
}

err := o.config.Client.Get(ctx, csvKey, &csv)
result, err := lib.ListAll(ctx, o.config.Client, opKey)
if err != nil {
return nil, fmt.Errorf("could not get %s CSV on cluster: %s", csvKey.String(), err)
}

// check if owned CRDs are defined on the csv
if len(csv.Spec.CustomResourceDefinitions.Owned) == 0 {
return nil, fmt.Errorf("no owned CustomResourceDefinitions specified on CSV %s, no custom resources to display", csvKey.String())
}

return csv.Spec.CustomResourceDefinitions.Owned, nil
}

// List takes in a list of CRDs and finds the associated CRs metadata on-cluster using the client-go meta client.
// Note: List() can return a potentially unbounded list that callers may need to paginate.
func (o *OperatorListCustomResources) List(ctx context.Context, crds []v1alpha1.CRDDescription) (*metav1.PartialObjectMetadataList, error) {
result := &metav1.PartialObjectMetadataList{}

for _, c := range crds {
// find CRD to determine scope
// set up GVR for CRs based on CRD metadata
crd := apiextensionsv1.CustomResourceDefinition{}
crdKey := types.NamespacedName{
Name: c.Name,
}
err := o.config.Client.Get(ctx, crdKey, &crd)
if err != nil {
return nil, nil
}
}

for _, c := range crds {
// setup GVR
gvr := schema.GroupVersionResource{
Group: c.Name,
Version: c.Version,
Resource: strings.ToLower(c.Kind) + "s", // is this always a valid assumption?
}

list := metav1.PartialObjectMetadataList{}
err := o.config.Client.List(ctx, &list )
if err != nil && !k8serrors.IsNotFound(err) {
return nil, fmt.Errorf("finding crs for gvr %s: %s", gvr.String(), err)
}
for _, item := range r.Items {
result.Items = append(result.Items, item)
}
return nil, err
}

return result, nil
}
138 changes: 138 additions & 0 deletions pkg/action/custom_resource_lister.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package action

import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/operator-framework/api/pkg/operators/v1alpha1"
"github.com/operator-framework/api/pkg/operators/v2alpha1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// FindOperator finds an operator object on-cluster provided a package and namespace.
func FindOperator(ctx context.Context, client client.Client, key types.NamespacedName) (*v2alpha1.Operator, error) {
operator := v2alpha1.Operator{}

err := client.Get(ctx, key, &operator)
if err != nil {
if k8serrors.IsNotFound(err) {
return nil, fmt.Errorf("package %s not found", key.Name)
}
return nil, err
}
return &operator, nil
}

// Unzip finds the CSV referenced by the provided operator and then inspects the spec.customresourcedefinitions.owned
// section of the CSV to return a list of APIs that are owned by the CSV.
func Unzip(ctx context.Context, client client.Client, operator *v2alpha1.Operator) ([]v1alpha1.CRDDescription, error) {
csv := v1alpha1.ClusterServiceVersion{}
csvKey := types.NamespacedName{}

if operator.Status.Components == nil {
return nil, fmt.Errorf("could not find underlying components for operator %s", operator.Name)
}
for _, resource := range operator.Status.Components.Refs {
if resource.Kind == v1alpha1.ClusterServiceVersionKind {
csvKey.Name = resource.Name
csvKey.Namespace = resource.Namespace
break
}
}

if csvKey.Name == "" && csvKey.Namespace == "" {
return nil, fmt.Errorf("could not find underlying CSV for operator %s", operator.Name)
}

err := client.Get(ctx, csvKey, &csv)
if err != nil {
return nil, fmt.Errorf("could not get %s CSV on cluster: %s", csvKey.String(), err)
}

// check if owned CRDs are defined on the csv
if len(csv.Spec.CustomResourceDefinitions.Owned) == 0 {
return nil, fmt.Errorf("no owned CustomResourceDefinitions specified on CSV %s, no custom resources to display", csvKey.String())
}

return csv.Spec.CustomResourceDefinitions.Owned, nil
}

// List takes in a CRD description and finds the associated CRs on-cluster.
// List can return a potentially unbounded list that callers may need to paginate.
func List(ctx context.Context, crClient client.Client, crdDesc v1alpha1.CRDDescription, namespace string) (*unstructured.UnstructuredList, error) {
result := &unstructured.UnstructuredList{}

// find CRD on-cluster to determine CRD scope (not included in description)
crd := apiextensionsv1.CustomResourceDefinition{}
crdKey := types.NamespacedName{
Name: crdDesc.Name,
}
err := crClient.Get(ctx, crdKey, &crd)
if err != nil {
return nil, nil
}
scope := crd.Spec.Scope

if scope == apiextensionsv1.ClusterScoped {
// get all CRs across the cluster for the given CRD since namespace is not relevant for cluster-scoped CRs
result := unstructured.UnstructuredList{}
gvk := schema.GroupVersionKind{
Group: crdDesc.Name,
Version: crdDesc.Version,
Kind: crdDesc.Kind,
}
result.SetGroupVersionKind(gvk)
if err := crClient.List(ctx, &result); err != nil {
return nil, err
}
return &result, nil

} else if scope == apiextensionsv1.NamespaceScoped {
// get CRs in the cluster for the given namespace only
result := unstructured.UnstructuredList{}
gvk := schema.GroupVersionKind{
Group: crdDesc.Name,
Version: crdDesc.Version,
Kind: crdDesc.Kind,
}
result.SetGroupVersionKind(gvk)

options := client.ListOptions{Namespace: namespace}
if err := crClient.List(ctx, &result, &options); err != nil {
return nil, err
}
return &result, nil
}

return result, nil
}

// ListAll wraps the above functions to provide a convenient command to go from package/namespace to custom resources.
func ListAll(ctx context.Context, client client.Client, opKey types.NamespacedName) (*unstructured.UnstructuredList, error) {
operator, err := FindOperator(ctx, client, opKey)
if err != nil {
return nil, err
}

crdDescs, err := Unzip(ctx, client, operator)
if err != nil {
return nil, err
}

var result unstructured.UnstructuredList
for _, crd := range crdDescs {
list, err := List(ctx, client, crd, opKey.Namespace)
if err != nil {
return nil, err
}
for _, cr := range list.Items {
result.Items = append(result.Items, cr)
}
}
return &result, nil
}

0 comments on commit d004c0a

Please sign in to comment.