Files
odo/pkg/odo/cli/service/operator_backend.go
Philippe Martin c58e4c7e6b Create a cmdline interface for testing Context creation + Complete function (#5256)
* Create a cmdline interface
The interface abstracts the cobra.Command

* Remove cobra relation from Context

* Remove cmd parameter to Run method

* Remove direct use of cobra.Command

* Add mock for cmdline interface

* First test

* Fix typo in rebase

* Restore SetClusterType

* Move constants to specific package to avoid import cycle

* Fix rebase
2021-12-15 00:42:55 +01:00

386 lines
11 KiB
Go

/*
This file contains code for various service backends supported by odo. Different backends have different logics for
Complete, Validate and Run functions. These are covered in this file.
*/
package service
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"text/tabwriter"
"github.com/ghodss/yaml"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/pkg/errors"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
svc "github.com/redhat-developer/odo/pkg/service"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
var (
ErrNoMetadataName = errors.New("invalid \"metadata\" in the yaml; provide a valid \"metadata.name\"")
)
// CompleteServiceCreate contains logic to complete the "odo service create" call for the case of Operator backend
func (b *OperatorBackend) CompleteServiceCreate(o *CreateOptions, args []string) (err error) {
// since interactive mode is not supported for Operators yet, set it to false
o.interactive = false
// if user has just used "odo service create", simply return
if o.fromFileFlag == "" && len(args) == 0 {
return
}
// if user wants to create service from file and use a name given on CLI
if o.fromFileFlag != "" {
if len(args) == 1 {
o.ServiceName = args[0]
}
return
}
// split the name provided on CLI and populate servicetype & customresource
o.ServiceType, b.CustomResource, err = svc.SplitServiceKindName(args[0])
if err != nil {
return fmt.Errorf("invalid service name, use the format <operator-type>/<crd-name>")
}
// if two args are given, first is service type and second one is service name
if len(args) == 2 {
o.ServiceName = args[1]
}
return nil
}
func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) error {
u := unstructured.Unstructured{}
// if the user wants to create service from a file, we check for
// existence of file and validate if the requested operator and CR
// exist on the cluster
if o.fromFileFlag != "" {
if _, err := os.Stat(o.fromFileFlag); err != nil {
return errors.Wrap(err, "unable to find specified file")
}
// Parse the file to find Operator and CR info
fileContents, err := ioutil.ReadFile(o.fromFileFlag)
if err != nil {
return err
}
err = yaml.Unmarshal(fileContents, &u.Object)
if err != nil {
return err
}
gvk := u.GroupVersionKind()
b.group, b.version, b.kind = gvk.Group, gvk.Version, gvk.Kind
if u.GetName() == "" {
return ErrNoMetadataName
}
if o.ServiceName != "" && !o.DryRunFlag {
// First check if service with provided name already exists
svcFullName := strings.Join([]string{b.kind, o.ServiceName}, "/")
exists, e := svc.OperatorSvcExists(o.KClient, svcFullName)
if e != nil {
return e
}
if exists {
return fmt.Errorf("service %q already exists; please provide a different name or delete the existing service first", svcFullName)
}
u.SetName(o.ServiceName)
} else {
o.ServiceName = u.GetName()
}
csvPtr, err := o.KClient.GetCSVWithCR(u.GetKind())
if err != nil {
// error only occurs when OperatorHub is not installed.
// k8s doesn't have it installed by default but OCP does
return err
}
csv := *csvPtr
// CRD is valid. We can use it further to create a service from it.
b.CustomResourceDefinition = u.Object
// Validate spec
hasCR, cr := o.KClient.CheckCustomResourceInCSV(b.kind, &csv)
if !hasCR {
return fmt.Errorf("the %q resource doesn't exist in specified %q operator", b.CustomResource, b.group)
}
crd, err := o.KClient.GetCRDSpec(cr, b.group, b.kind)
if err != nil {
return err
}
err = validate.AgainstSchema(crd, u.Object["spec"], strfmt.Default)
if err != nil {
return err
}
} else if b.CustomResource != "" {
// make sure that CSV of the specified ServiceType exists
csv, err := o.KClient.GetClusterServiceVersion(o.ServiceType)
if err != nil {
// error only occurs when OperatorHub is not installed.
// k8s doesn't have it installed by default but OCP does
return err
}
b.group, b.version, b.resource, err = svc.GetGVRFromOperator(csv, b.CustomResource)
if err != nil {
return err
}
// if the service name is blank then we set it to custom resource name
if o.ServiceName == "" {
o.ServiceName = strings.ToLower(b.CustomResource)
}
hasCR, cr := o.KClient.CheckCustomResourceInCSV(b.CustomResource, &csv)
if !hasCR {
return fmt.Errorf("the %q resource doesn't exist in specified %q operator", b.CustomResource, b.group)
}
crd, err := o.KClient.GetCRDSpec(cr, b.group, b.CustomResource)
if err != nil {
return err
}
if len(o.parametersFlag) != 0 {
builtCRD, e := svc.BuildCRDFromParams(o.ParametersMap, crd, b.group, b.version, b.CustomResource)
if e != nil {
return e
}
u.Object = builtCRD
} else {
almExample, e := svc.GetAlmExample(csv, b.CustomResource, o.ServiceType)
if e != nil {
return e
}
u.Object = almExample
}
if o.ServiceName != "" && !o.DryRunFlag {
// First check if service with provided name already exists
svcFullName := strings.Join([]string{b.CustomResource, o.ServiceName}, "/")
exists, e := svc.OperatorSvcExists(o.KClient, svcFullName)
if e != nil {
return e
}
if exists {
return fmt.Errorf("service %q already exists; please provide a different name or delete the existing service first", svcFullName)
}
}
u.SetName(o.ServiceName)
if u.GetName() == "" {
return ErrNoMetadataName
}
// CRD is valid. We can use it further to create a service from it.
b.CustomResourceDefinition = u.Object
if o.ServiceName == "" {
o.ServiceName = u.GetName()
}
// Validate spec
err = validate.AgainstSchema(crd, u.Object["spec"], strfmt.Default)
if err != nil {
return err
}
} else {
// This block is executed only when user has neither provided a
// file nor a valid `odo service create <operator-name>` to start
// the service from an Operator. So we raise an error because the
// correct way is to execute:
// `odo service create <operator-name>/<crd-name>`
return fmt.Errorf("please use a valid command to start an Operator backed service; desired format: %q", "odo service create <operator-name>/<crd-name>")
}
return nil
}
func (b *OperatorBackend) RunServiceCreate(o *CreateOptions) (err error) {
s := &log.Status{}
// if cluster has resources of type CSV and o.CustomResource is not
// empty, we're expected to create an Operator backed service
if o.DryRunFlag {
// if it's dry run, only print the alm-example (o.CustomResourceDefinition) and exit
jsonCR, err := json.MarshalIndent(b.CustomResourceDefinition, "", " ")
if err != nil {
return err
}
// convert json to yaml
yamlCR, err := yaml.JSONToYAML(jsonCR)
if err != nil {
return err
}
log.Info(string(yamlCR))
return nil
} else {
if o.inlinedFlag {
crdYaml, err := yaml.Marshal(b.CustomResourceDefinition)
if err != nil {
return err
}
err = devfile.AddKubernetesComponentToDevfile(string(crdYaml), o.ServiceName, o.EnvSpecificInfo.GetDevfileObj())
if err != nil {
return err
}
} else {
crdYaml, err := yaml.Marshal(b.CustomResourceDefinition)
if err != nil {
return err
}
err = devfile.AddKubernetesComponent(string(crdYaml), o.ServiceName, o.contextFlag, o.EnvSpecificInfo.GetDevfileObj())
if err != nil {
return err
}
}
if log.IsJSON() {
svcFullName := strings.Join([]string{b.CustomResource, o.ServiceName}, "/")
svc := NewServiceItem(svcFullName)
svc.Manifest = b.CustomResourceDefinition
svc.InDevfile = true
machineoutput.OutputSuccess(svc)
}
}
s.End(true)
return
}
// ServiceDefined returns true if the service is defined in the devfile
func (b *OperatorBackend) ServiceDefined(ctx *genericclioptions.Context, name string) (bool, error) {
_, instanceName, err := svc.SplitServiceKindName(name)
if err != nil {
return false, err
}
return devfile.IsComponentDefined(instanceName, ctx.EnvSpecificInfo.GetDevfileObj())
}
func (b *OperatorBackend) DeleteService(o *DeleteOptions, name string, application string) error {
// "name" is of the form CR-Name/Instance-Name so we split it
_, instanceName, err := svc.SplitServiceKindName(name)
if err != nil {
return err
}
err = devfile.DeleteKubernetesComponentFromDevfile(instanceName, o.EnvSpecificInfo.GetDevfileObj(), o.contextFlag)
if err != nil {
return errors.Wrap(err, "failed to delete service from the devfile")
}
return nil
}
func (b *OperatorBackend) DescribeService(o *DescribeOptions, serviceName, app string) error {
clusterList, _, err := svc.ListOperatorServices(o.KClient)
if err != nil {
return err
}
var clusterFound *unstructured.Unstructured
for i, clusterInstance := range clusterList {
fullName := strings.Join([]string{clusterInstance.GetKind(), clusterInstance.GetName()}, "/")
if fullName == serviceName {
clusterFound = &clusterList[i]
break
}
}
devfileList, err := svc.ListDevfileServices(o.KClient, o.EnvSpecificInfo.GetDevfileObj(), o.contextFlag)
if err != nil {
return err
}
devfileService, inDevfile := devfileList[serviceName]
item := NewServiceItem(serviceName)
item.InDevfile = inDevfile
item.Deployed = clusterFound != nil
if item.Deployed {
item.Manifest = clusterFound.Object
} else if item.InDevfile {
item.Manifest = devfileService.Object
}
if log.IsJSON() {
machineoutput.OutputSuccess(item)
return nil
}
return PrintHumanReadableOutput(item)
}
// PrintHumanReadableOutput outputs the description of a service in a human readable format
func PrintHumanReadableOutput(item *serviceItem) error {
log.Describef("Version: ", "%s", item.Manifest["apiVersion"])
log.Describef("Kind: ", "%s", item.Manifest["kind"])
metadata, ok := item.Manifest["metadata"].(map[string]interface{})
if !ok {
return errors.New("unable to get name from manifest")
}
log.Describef("Name: ", "%s", metadata["name"])
spec, ok := item.Manifest["spec"].(map[string]interface{})
if !ok {
return errors.New("unable to get specifications from manifest")
}
var tab bytes.Buffer
wr := tabwriter.NewWriter(&tab, 5, 2, 3, ' ', tabwriter.TabIndent)
fmt.Fprint(wr, "NAME", "\t", "VALUE", "\n")
displayParameters(wr, spec, "")
wr.Flush()
log.Describef("Parameters:\n", tab.String())
return nil
}
// displayParameters adds lines describing fields of a given map
func displayParameters(wr *tabwriter.Writer, spec map[string]interface{}, prefix string) {
keys := make([]string, len(spec))
i := 0
for key := range spec {
keys[i] = key
i++
}
for _, k := range keys {
v := spec[k]
switch val := v.(type) {
case map[string]interface{}:
displayParameters(wr, val, prefix+k+".")
default:
fmt.Fprintf(wr, "%s%s\t%v\n", prefix, k, val)
}
}
}