Dropping support for service catalog based services (#4906)

* Removing cli layer and integration tests related to service catalog

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Fixing missing error msg

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Fixing golint errors

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Fixing error for interactive mode

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Moving interactive mode error to top

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Fixing

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Fixing interactive mode error condition

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Removing some more service related code

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Fixing golint

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Removing service catalog backend part 1

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Updating changelogs

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Removing some more of the service catalog related code

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Updating as per comments in review

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Update pkg/odo/cli/service/create.go

Co-authored-by: Parthvi Vala <pvala@redhat.com>

* Fixing gofmt

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Adding kube to cli docs

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Updating changelog as per comments by @dharmit

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Removing some unnessasary stuff

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Updating docs based changes as per review

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Fixing test

Signed-off-by: Mohammed Zeeshan Ahmed <mohammed.zee1000@gmail.com>

* Update pkg/odo/cli/catalog/describe/service.go

Co-authored-by: Philippe Martin <contact@elol.fr>

* Update pkg/odo/cli/service/list.go

Co-authored-by: Philippe Martin <contact@elol.fr>

Co-authored-by: Parthvi Vala <pvala@redhat.com>
Co-authored-by: Philippe Martin <contact@elol.fr>
This commit is contained in:
Mohammed Ahmed
2021-08-18 17:11:43 +05:30
committed by GitHub
parent 9c4a3f7f35
commit 0b8b712a99
31 changed files with 39 additions and 3963 deletions

View File

@@ -7,6 +7,7 @@
- Added a column in odo list output to indicate components created by odo ([#4962](https://github.com/openshift/odo/pull/4962))
- Fix description for "odo link" command ([#4930](https://github.com/openshift/odo/pull/4930))
- Add deprecation warnings for s2i components([#4967](https://github.com/openshift/odo/issues/4967))
- Dropping support for service catalog based services ([#4906](https://github.com/openshift/odo/pull/4906))
### Bug Fixes

View File

@@ -166,10 +166,6 @@ test-cmd-login-logout: ## Run odo login and logout tests
test-cmd-link-unlink-4-cluster: ## Run link and unlink commnad tests against 4.x cluster
$(RUN_GINKGO) $(GINKGO_FLAGS) -focus="odo link and unlink commnad tests" tests/integration/
.PHONY: test-cmd-link-unlink-311-cluster
test-cmd-link-unlink-311-cluster: ## Run link and unlink command tests against 3.11 cluster
$(RUN_GINKGO) $(GINKGO_FLAGS) -focus="odo link and unlink command tests" tests/integration/servicecatalog/
.PHONY: test-cmd-service
test-cmd-service: ## Run odo service command tests
$(RUN_GINKGO) $(GINKGO_FLAGS) -focus="odo service command tests" tests/integration/servicecatalog/
@@ -299,11 +295,6 @@ test-integration-devfile: ## Run devfile integration tests
$(RUN_GINKGO) $(GINKGO_FLAGS) tests/integration/devfile/
$(RUN_GINKGO) $(GINKGO_FLAGS_SERIAL) tests/integration/devfile/debug/
# Only service and link command tests are the part of this test run
.PHONY: test-integration-service-catalog
test-integration-service-catalog: ## Run command's integration tests which are dependent on service catalog enabled cluster.
$(RUN_GINKGO) $(GINKGO_FLAGS) tests/integration/servicecatalog/
.PHONY: test-e2e-beta
test-e2e-beta: ## Run core beta flow e2e tests
$(RUN_GINKGO) $(GINKGO_FLAGS) -focus="odo core beta flow" tests/e2escenarios/

View File

@@ -8,7 +8,6 @@ import (
applabels "github.com/openshift/odo/pkg/application/labels"
"github.com/openshift/odo/pkg/component"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/service"
"github.com/openshift/odo/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -86,21 +85,6 @@ func Delete(client *occlient.Client, name string) error {
labels := applabels.GetLabels(name, false)
// first delete the services (ServiceInstance in OpenShift terminology)
// belonging to the app
svcList, err := service.List(client, name)
if err != nil {
// error is returned when there's no Service Catalog enabled in the service
klog.V(4).Infof("Service catalog is not enabled in the cluster, skipping service deletion")
} else {
for _, svc := range svcList.Items {
err = service.DeleteServiceAndUnlinkComponents(client, svc.Name, name)
if err != nil {
return errors.Wrapf(err, "unable to delete the application %s due to failure in deleting service(s) in the application", name)
}
}
}
supported, err := client.IsDeploymentConfigSupported()
if err != nil {
return err

View File

@@ -1,5 +1,5 @@
// Package application provides functions to list, check existence of, delete and get machine readable description of applications.
// An application is a set of components and services.
// An application is materialized by the `app:` label in `deployments`, `deploymentconfigs`,
// or service instances (service instances from Service Catalog or from Operator Backed Services).
// or service instances (service instances from Operator Backed Services).
package application

View File

@@ -4,17 +4,14 @@ import (
"fmt"
applabels "github.com/openshift/odo/pkg/application/labels"
"github.com/openshift/odo/pkg/component"
"github.com/openshift/odo/pkg/kclient"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/pkg/errors"
"k8s.io/klog"
"github.com/openshift/odo/pkg/component"
odoutil "github.com/openshift/odo/pkg/odo/util"
"github.com/openshift/odo/pkg/odo/util/completion"
"github.com/openshift/odo/pkg/service"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -87,18 +84,5 @@ func printAppInfo(client *occlient.Client, kClient *kclient.Client, appName stri
}
}
}
// List services that will be removed
serviceList, err := service.List(client, appName)
if err != nil {
log.Info("No services / could not get services")
klog.V(4).Info(err.Error())
}
if len(serviceList.Items) != 0 {
log.Info("This application has following service(s) that will be deleted")
for _, ser := range serviceList.Items {
log.Info("service named", ser.ObjectMeta.Name, "of type", ser.Spec.Type)
}
}
return nil
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/service"
svc "github.com/openshift/odo/pkg/service"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
)
@@ -13,29 +12,18 @@ import (
const serviceRecommendedCommandName = "service"
var (
serviceExample = ktemplates.Examples(` # Describe a service catalog service
%[1]s mysql-persistent
# Describe a Operator backed service
serviceExample = ktemplates.Examples(`# Describe a Operator backed service
%[1]s
`)
serviceLongDesc = ktemplates.LongDesc(`Describes a service type.
This command supports both Service Catalog services and Operator backed services.
This command supports Operator backed services.
A user can describe an Operator backed service by providing the full identifier for an Operand i.e. <operator_type>/<cr_name> which they can find by running "odo catalog list services".
If the format doesn't match <operator_type>/<cr_name> then service catalog services would be searched.
`)
)
// DescribeServiceOptions encapsulates the options for the odo catalog describe service command
type DescribeServiceOptions struct {
// name of the service to describe, from command arguments
serviceName string
// resolved service
service svc.ServiceClass
plans []svc.ServicePlan
// generic context options common to all commands
*genericclioptions.Context
@@ -59,7 +47,7 @@ func (o *DescribeServiceOptions) Complete(name string, cmd *cobra.Command, args
if _, _, err := service.SplitServiceKindName(args[0]); err == nil {
o.backend = NewOperatorBackend()
} else {
o.backend = NewServiceCatalogBackend()
return fmt.Errorf("no deployable operators found")
}
return o.backend.CompleteDescribeService(o, args)

View File

@@ -1,99 +0,0 @@
package describe
import (
"fmt"
"os"
"strings"
"github.com/olekukonko/tablewriter"
svc "github.com/openshift/odo/pkg/service"
)
type serviceCatalogBackend struct {
}
func NewServiceCatalogBackend() *serviceCatalogBackend {
return &serviceCatalogBackend{}
}
func (ohb *serviceCatalogBackend) CompleteDescribeService(dso *DescribeServiceOptions, args []string) error {
dso.serviceName = args[0]
return nil
}
func (ohb *serviceCatalogBackend) ValidateDescribeService(dso *DescribeServiceOptions) error {
var err error
dso.service, dso.plans, err = svc.GetServiceClassAndPlans(dso.Client, dso.serviceName)
return err
}
func (ohb *serviceCatalogBackend) RunDescribeService(dso *DescribeServiceOptions) error {
table := tablewriter.NewWriter(os.Stdout)
table.SetBorder(false)
table.SetAlignment(tablewriter.ALIGN_LEFT)
serviceData := [][]string{
{"Name", dso.service.Name},
{"Bindable", fmt.Sprint(dso.service.Bindable)},
{"Operated by the broker", dso.service.ServiceBrokerName},
{"Short Description", dso.service.ShortDescription},
{"Long Description", dso.service.LongDescription},
{"Versions Available", strings.Join(dso.service.VersionsAvailable, ",")},
{"Tags", strings.Join(dso.service.Tags, ",")},
}
table.AppendBulk(serviceData)
table.Append([]string{""})
if len(dso.plans) > 0 {
table.Append([]string{"PLANS"})
for _, plan := range dso.plans {
// create the display values for required and optional parameters
requiredWithMandatoryUserInputParameterNames := []string{}
requiredWithOptionalUserInputParameterNames := []string{}
optionalParameterDisplay := []string{}
for _, parameter := range plan.Parameters {
if parameter.Required {
// until we have a better solution for displaying the plan data (like a separate table perhaps)
// this is simplest thing to do
if len(parameter.Default) > 0 {
requiredWithOptionalUserInputParameterNames = append(
requiredWithOptionalUserInputParameterNames,
fmt.Sprintf("%s (default: '%s')", parameter.Name, parameter.Default))
} else {
requiredWithMandatoryUserInputParameterNames = append(requiredWithMandatoryUserInputParameterNames, parameter.Name)
}
} else {
optionalParameterDisplay = append(optionalParameterDisplay, parameter.Name)
}
}
table.Append([]string{"***********************", "*****************************************************"})
planLineSeparator := []string{"-----------------", "-----------------"}
planData := [][]string{
{"Name", plan.Name},
planLineSeparator,
{"Display Name", plan.DisplayName},
planLineSeparator,
{"Short Description", plan.Description},
planLineSeparator,
{"Required Params without a default value", strings.Join(requiredWithMandatoryUserInputParameterNames, ", ")},
planLineSeparator,
{"Required Params with a default value", strings.Join(requiredWithOptionalUserInputParameterNames, ", ")},
planLineSeparator,
{"Optional Params", strings.Join(optionalParameterDisplay, ", ")},
{"", ""},
}
table.AppendBulk(planData)
}
table.Render()
} else {
return fmt.Errorf("no plans found for service %s", dso.serviceName)
}
return nil
}

View File

@@ -17,13 +17,11 @@ import (
const servicesRecommendedCommandName = "services"
var servicesExample = ` # Get the supported services from service catalog
var servicesExample = ` # Get the supported services
%[1]s`
// ServiceOptions encapsulates the options for the odo catalog list services command
type ServiceOptions struct {
// list of known services
services catalog.ServiceTypeList
// list of clusterserviceversions (installed by Operators)
csvs *olm.ClusterServiceVersionList
// generic context options common to all commands
@@ -49,11 +47,6 @@ func (o *ServiceOptions) Complete(name string, cmd *cobra.Command, args []string
return err
}
}
o.services, _ = catalog.ListSvcCatServices(o.Client)
o.services = util.FilterHiddenServices(o.services)
return
}
@@ -65,20 +58,16 @@ func (o *ServiceOptions) Validate() (err error) {
// Run contains the logic for the command associated with ListServicesOptions
func (o *ServiceOptions) Run(cmd *cobra.Command) (err error) {
if log.IsJSON() {
machineoutput.OutputSuccess(newCatalogListOutput(&o.services, o.csvs))
machineoutput.OutputSuccess(newCatalogListOutput(o.csvs))
} else {
if len(o.csvs.Items) == 0 && len(o.services.Items) == 0 {
log.Info("no deployable services/operators found")
if len(o.csvs.Items) == 0 {
log.Info("no deployable operators found")
return
}
if len(o.csvs.Items) > 0 {
util.DisplayClusterServiceVersions(o.csvs)
}
if len(o.services.Items) > 0 {
util.DisplayServices(o.services)
}
}
return
}
@@ -102,18 +91,16 @@ func NewCmdCatalogListServices(name, fullName string) *cobra.Command {
type catalogListOutput struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Services *catalog.ServiceTypeList `json:"services,omitempty"`
// list of clusterserviceversions (installed by Operators)
Operators *olm.ClusterServiceVersionList `json:"operators,omitempty"`
}
func newCatalogListOutput(services *catalog.ServiceTypeList, operators *olm.ClusterServiceVersionList) catalogListOutput {
func newCatalogListOutput(operators *olm.ClusterServiceVersionList) catalogListOutput {
return catalogListOutput{
TypeMeta: v1.TypeMeta{
Kind: "List",
APIVersion: machineoutput.APIVersion,
},
Services: services,
Operators: operators,
}
}

View File

@@ -96,7 +96,7 @@ func NewCmdCatalogSearchService(name, fullName string) *cobra.Command {
Long: `Search service type in catalog.
This searches for a partial match for the given search term in all the available
services from service catalog.
services from operator hub services.
`,
Example: fmt.Sprintf(serviceExample, fullName),
Args: cobra.ExactArgs(1),

View File

@@ -1,184 +0,0 @@
package util
import (
"reflect"
"testing"
"github.com/openshift/odo/pkg/catalog"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestFilterHiddenServices(t *testing.T) {
tests := []struct {
name string
input catalog.ServiceTypeList
expected catalog.ServiceTypeList
}{
/*
This test is not needed.. Also fails using DeepEqual anyways..
--- FAIL: TestFilterHiddenServices/Case_1:_empty_input (0.00s)
util_test.go:101: got: [], wanted: []
{
name: "Case 1: empty input",
input: catalog.ServiceTypeList{},
expected: catalog.ServiceTypeList{},
},
*/
{
name: "Case 2: non empty input",
input: catalog.ServiceTypeList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "odo.dev/v1alpha1",
},
ListMeta: metav1.ListMeta{},
Items: []catalog.ServiceType{
{
ObjectMeta: metav1.ObjectMeta{
Name: "n1",
},
Spec: catalog.ServiceSpec{
Hidden: true,
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "n2",
},
Spec: catalog.ServiceSpec{
Hidden: false,
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "n3",
},
Spec: catalog.ServiceSpec{
Hidden: true,
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "n4",
},
Spec: catalog.ServiceSpec{
Hidden: false,
},
},
},
},
expected: catalog.ServiceTypeList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "odo.dev/v1alpha1",
},
ListMeta: metav1.ListMeta{},
Items: []catalog.ServiceType{
{
ObjectMeta: metav1.ObjectMeta{
Name: "n2",
},
Spec: catalog.ServiceSpec{
Hidden: false,
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "n4",
},
Spec: catalog.ServiceSpec{
Hidden: false,
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := FilterHiddenServices(tt.input)
if !reflect.DeepEqual(tt.expected, output) {
t.Errorf("got: %+v, wanted: %+v", output.Items, tt.expected.Items)
}
})
}
}
func TestFilterHiddenComponents(t *testing.T) {
tests := []struct {
name string
input []catalog.ComponentType
expected []catalog.ComponentType
}{
{
name: "Case 1: empty input",
input: []catalog.ComponentType{},
expected: []catalog.ComponentType{},
},
{
name: "Case 2: non empty input",
input: []catalog.ComponentType{
{
ObjectMeta: metav1.ObjectMeta{
Name: "n1",
},
Spec: catalog.ComponentSpec{
NonHiddenTags: []string{"1", "latest"},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "n2",
},
Spec: catalog.ComponentSpec{
NonHiddenTags: []string{},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "n3",
},
Spec: catalog.ComponentSpec{
NonHiddenTags: []string{},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "n4",
},
Spec: catalog.ComponentSpec{
NonHiddenTags: []string{"10"},
},
},
},
expected: []catalog.ComponentType{
{
ObjectMeta: metav1.ObjectMeta{
Name: "n1",
},
Spec: catalog.ComponentSpec{
NonHiddenTags: []string{"1", "latest"},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "n4",
},
Spec: catalog.ComponentSpec{
NonHiddenTags: []string{"10"},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := FilterHiddenComponents(tt.input)
if !reflect.DeepEqual(tt.expected, output) {
t.Errorf("got: %+v, wanted: %+v", output, tt.expected)
}
})
}
}

View File

@@ -12,7 +12,6 @@ import (
projectCmd "github.com/openshift/odo/pkg/odo/cli/project"
"github.com/openshift/odo/pkg/odo/genericclioptions"
odoutil "github.com/openshift/odo/pkg/odo/util"
"github.com/openshift/odo/pkg/odo/util/completion"
svc "github.com/openshift/odo/pkg/service"
ktemplates "k8s.io/kubectl/pkg/util/templates"
@@ -44,7 +43,7 @@ var (
# and make the secrets accessible as files in the '/bindings/etcd/' directory
%[1]s EtcdCluster/myetcd --bind-as-files --name etcd`)
linkLongDesc = `Link component to a service (backed by an Operator or Service Catalog) or component
linkLongDesc = `Link component to a service (backed by an Operator) or component
If the source component is not provided, the current active component is assumed.
In both use cases, link adds the appropriate secret to the environment of the source component.
@@ -72,14 +71,7 @@ odo link backend --component frontend
Now the frontend has 2 ENV variables it can use:
COMPONENT_BACKEND_HOST=backend-app
COMPONENT_BACKEND_PORT=8080
If you wish to use a database, we can use the Service Catalog and link it to our backend:
odo service create dh-postgresql-apb --plan dev -p postgresql_user=luke -p postgresql_password=secret
odo link dh-postgresql-apb
Now backend has 2 ENV variables it can use:
DB_USER=luke
DB_PASSWORD=secret`
`
)
// LinkOptions encapsulates the options for the odo link command
@@ -182,7 +174,5 @@ func NewCmdLink(name, fullName string) *cobra.Command {
//Adding context flag
genericclioptions.AddContextFlag(linkCmd, &o.componentContext)
completion.RegisterCommandHandler(linkCmd, completion.LinkCompletionHandler)
return linkCmd
}

View File

@@ -7,7 +7,6 @@ import (
appCmd "github.com/openshift/odo/pkg/odo/cli/application"
projectCmd "github.com/openshift/odo/pkg/odo/cli/project"
"github.com/openshift/odo/pkg/odo/util/completion"
svc "github.com/openshift/odo/pkg/service"
"github.com/openshift/odo/pkg/odo/util"
@@ -112,7 +111,5 @@ func NewCmdUnlink(name, fullName string) *cobra.Command {
// Adding context flag
genericclioptions.AddContextFlag(unlinkCmd, &o.componentContext)
completion.RegisterCommandHandler(unlinkCmd, completion.UnlinkCompletionHandler)
return unlinkCmd
}

View File

@@ -1,44 +1,29 @@
package service
import (
"bytes"
"errors"
"fmt"
"strings"
"text/template"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/odo/cli/component"
"github.com/openshift/odo/pkg/odo/cli/service/ui"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/odo/util/completion"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
"strings"
)
const (
createRecommendedCommandName = "create"
equivalentTemplate = "{{.CmdFullName}} {{.ServiceType}}" +
"{{if .ServiceName}} {{.ServiceName}}{{end}}" +
" --app {{.Application}}" +
" --project {{.Project}}" +
"{{if .Plan}} --plan {{.Plan}}{{end}}" +
"{{range $key, $value := .ParametersMap}} -p {{$key}}={{$value}}{{end}}"
)
var (
createExample = ktemplates.Examples(`
# Create new postgresql service from service catalog using dev plan and name my-postgresql-db.
%[1]s dh-postgresql-apb my-postgresql-db --plan dev -p postgresql_user=luke -p postgresql_password=secret`)
createOperatorExample = ktemplates.Examples(`
# Create new EtcdCluster service from etcdoperator.v0.9.4 operator.
%[1]s etcdoperator.v0.9.4/EtcdCluster`)
createShortDesc = `Create a new service from Operator Hub or Service Catalog and deploy it on OpenShift.`
createShortDesc = `Create a new service from Operator Hub and deploy it on Kubernetes or OpenShift.`
createLongDesc = ktemplates.LongDesc(`
Create a new service from Operator Hub or Service Catalog and deploy it on OpenShift.
Create a new service from Operator Hub and deploy it on Kubernetes or OpenShift.
Service creation can be performed from a valid component directory (one containing a devfile.yaml) only.
@@ -46,8 +31,6 @@ To create the service from outside a component directory, specify path to a vali
When creating a service using Operator Hub, provide a service name along with Operator name.
When creating a service using Service Catalog, a --plan must be passed along with the service type. Parameters to configure the service are passed as key=value pairs.
For a full list of service types, use: 'odo catalog list services'`)
)
@@ -55,8 +38,6 @@ For a full list of service types, use: 'odo catalog list services'`)
type CreateOptions struct {
// parameters hold the user-provided values for service class parameters via flags (populated by cobra)
parameters []string
// Plan is the selected service plan
Plan string
// ServiceType corresponds to the service class name
ServiceType string
// ServiceName is how the service will be named and known by odo
@@ -65,8 +46,6 @@ type CreateOptions struct {
ParametersMap map[string]string
// interactive specifies whether the command operates in interactive mode or not
interactive bool
// outputCLI specifies whether to output the non-interactive version of the command or not
outputCLI bool
// CmdFullName records the command's full name
CmdFullName string
// whether or not to wait for the service to be ready
@@ -79,7 +58,7 @@ type CreateOptions struct {
DryRun bool
// Location of the file in which yaml specification of CR is stored.
fromFile string
// Backend is the service provider backend (Operator Hub or Service Catalog) providing the service requested by the user
// Backend is the service provider backend providing the service requested by the user
Backend ServiceProviderBackend
}
@@ -114,36 +93,17 @@ func (o *CreateOptions) Complete(name string, cmd *cobra.Command, args []string)
if err != nil {
return err
}
// decide which service backend to use
if o.fromFile != "" {
// fromFile is supported only for Operator backend
o.Backend = NewOperatorBackend()
// since interactive mode is not supported for Operators yet, set it to false
o.interactive = false
return o.Backend.CompleteServiceCreate(o, cmd, args)
//if no args are provided and if request is not from file, user wants interactive mode
if o.fromFile == "" && len(args) == 0 {
return fmt.Errorf("odo doesn't support interactive mode for creating Operator backed service")
}
// check if interactive mode is requested
if len(args) == 0 {
o.interactive = true
// only Service Catalog backend supports interactive mode for service creation
o.Backend = NewServiceCatalogBackend()
} else {
o.Backend = decideBackend(args[0])
}
o.Backend = NewOperatorBackend()
o.interactive = false
return o.Backend.CompleteServiceCreate(o, cmd, args)
}
// Validate validates the CreateOptions based on completed values
func (o *CreateOptions) Validate() (err error) {
// if we are in interactive mode, all values are already valid
if o.interactive {
return nil
}
return o.Backend.ValidateServiceCreate(o)
}
@@ -159,54 +119,30 @@ func (o *CreateOptions) Run(cmd *cobra.Command) (err error) {
log.Info("Successfully added service to the configuration; do 'odo push' to create service on the cluster")
}
equivalent := o.outputNonInteractiveEquivalent()
if len(equivalent) > 0 {
log.Info("Equivalent command:\n" + ui.StyledOutput(equivalent, "cyan"))
}
return
}
// outputNonInteractiveEquivalent outputs the populated options as the equivalent command that would be used in non-interactive mode
func (o *CreateOptions) outputNonInteractiveEquivalent() string {
if o.outputCLI {
var tpl bytes.Buffer
t := template.Must(template.New("service-create-cli").Parse(equivalentTemplate))
e := t.Execute(&tpl, o)
if e != nil {
panic(e) // shouldn't happen
}
return strings.TrimSpace(tpl.String())
}
return ""
}
// NewCmdServiceCreate implements the odo service create command.
func NewCmdServiceCreate(name, fullName string) *cobra.Command {
o := NewCreateOptions()
o.CmdFullName = fullName
serviceCreateCmd := &cobra.Command{
Use: name + " <service_type> --plan <plan_name> [service_name]",
Use: name + " <operator_type>/<crd_name> [service_name] [flags]",
Short: createShortDesc,
Long: createLongDesc,
Example: fmt.Sprintf(createExample, fullName),
Example: fmt.Sprintf(createOperatorExample, fullName),
Args: cobra.RangeArgs(0, 2),
Run: func(cmd *cobra.Command, args []string) {
genericclioptions.GenericRun(o, cmd, args)
},
}
serviceCreateCmd.Use += fmt.Sprintf(" [flags]\n %s <operator_type>/<crd_name> [service_name] [flags]", o.CmdFullName)
serviceCreateCmd.Example += "\n\n" + fmt.Sprintf(createOperatorExample, fullName)
serviceCreateCmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "Print the yaml specificiation that will be used to create the operator backed service")
// remove this feature after enabling service create interactive mode for operator backed services
serviceCreateCmd.Flags().StringVar(&o.fromFile, "from-file", "", "Path to the file containing yaml specification to use to start operator backed service")
serviceCreateCmd.Flags().StringVar(&o.Plan, "plan", "", "The name of the plan of the service to be created")
serviceCreateCmd.Flags().StringArrayVarP(&o.parameters, "parameters", "p", []string{}, "Parameters of the plan where a parameter is expressed as <key>=<value")
serviceCreateCmd.Flags().BoolVarP(&o.wait, "wait", "w", false, "Wait until the service is ready")
genericclioptions.AddContextFlag(serviceCreateCmd, &o.componentContext)
completion.RegisterCommandHandler(serviceCreateCmd, completion.ServiceClassCompletionHandler)
completion.RegisterCommandFlagHandler(serviceCreateCmd, "plan", completion.ServicePlanCompletionHandler)
completion.RegisterCommandFlagHandler(serviceCreateCmd, "parameters", completion.ServiceParameterCompletionHandler)
return serviceCreateCmd
}

View File

@@ -1,124 +0,0 @@
package service
import (
"testing"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/odo/genericclioptions"
)
func TestOutputNonInteractiveEquivalent(t *testing.T) {
t.Parallel()
client, _ := occlient.FakeNew()
tests := []struct {
name string
options CreateOptions
expected string
}{
{
name: "when output is not requested, should return empty string",
options: CreateOptions{
Context: genericclioptions.NewFakeContext("testproject", "app", "", client, nil),
CmdFullName: RecommendedCommandName,
outputCLI: false,
ServiceType: "foo",
},
expected: "",
},
{
name: "just service class",
options: CreateOptions{
Context: genericclioptions.NewFakeContext("testproject", "app", "", client, nil),
CmdFullName: RecommendedCommandName,
outputCLI: true,
ServiceType: "foo",
},
expected: RecommendedCommandName + " foo --app app --project testproject",
},
{
name: "just service class and name",
options: CreateOptions{
Context: genericclioptions.NewFakeContext("testproject", "app", "", client, nil),
CmdFullName: RecommendedCommandName,
outputCLI: true,
ServiceType: "foo",
ServiceName: "myservice",
},
expected: RecommendedCommandName + " foo myservice --app app --project testproject",
},
{
name: "service class, name and plan",
options: CreateOptions{
Context: genericclioptions.NewFakeContext("testproject", "app", "", client, nil),
CmdFullName: RecommendedCommandName,
outputCLI: true,
ServiceType: "foo",
ServiceName: "myservice",
Plan: "dev",
},
expected: RecommendedCommandName + " foo myservice --app app --project testproject --plan dev",
},
{
name: "service class and plan",
options: CreateOptions{
Context: genericclioptions.NewFakeContext("testproject", "app", "", client, nil),
CmdFullName: RecommendedCommandName,
outputCLI: true,
ServiceType: "foo",
Plan: "dev",
},
expected: RecommendedCommandName + " foo --app app --project testproject --plan dev",
},
{
name: "service class and empty params",
options: CreateOptions{
Context: genericclioptions.NewFakeContext("testproject", "app", "", client, nil),
CmdFullName: RecommendedCommandName,
outputCLI: true,
ServiceType: "foo",
ParametersMap: map[string]string{},
},
expected: RecommendedCommandName + " foo --app app --project testproject",
},
{
name: "service class and params",
options: CreateOptions{
Context: genericclioptions.NewFakeContext("testproject", "app", "", client, nil),
CmdFullName: RecommendedCommandName,
outputCLI: true,
ServiceType: "foo",
ParametersMap: map[string]string{"param1": "value1", "param2": "value2"},
},
expected: RecommendedCommandName + " foo --app app --project testproject -p param1=value1 -p param2=value2",
},
{
name: "all",
options: CreateOptions{
Context: genericclioptions.NewFakeContext("testproject", "app", "", client, nil),
CmdFullName: RecommendedCommandName,
outputCLI: true,
ServiceType: "foo",
ServiceName: "name",
Plan: "plan",
ParametersMap: map[string]string{"param1": "value1", "param2": "value2"},
},
expected: RecommendedCommandName + " foo name --app app --project testproject --plan plan -p param1=value1 -p param2=value2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := tt.options.outputNonInteractiveEquivalent()
if tt.expected != actual {
t.Errorf("expected '%s', got '%s'", tt.expected, actual)
}
})
}
}

View File

@@ -2,13 +2,13 @@ package service
import (
"fmt"
"github.com/openshift/odo/pkg/service"
"strings"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/odo/cli/component"
"github.com/openshift/odo/pkg/odo/cli/ui"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/odo/util/completion"
"github.com/spf13/cobra"
"k8s.io/klog"
ktemplates "k8s.io/kubectl/pkg/util/templates"
@@ -32,7 +32,7 @@ type DeleteOptions struct {
*genericclioptions.Context
// Context to use when listing service. This will use app and project values from the context
componentContext string
// Backend is the service provider backend (Operator Hub or Service Catalog) that was used to create the service
// Backend is the service provider backend that was used to create the service
Backend ServiceProviderBackend
}
@@ -57,9 +57,12 @@ func (o *DeleteOptions) Complete(name string, cmd *cobra.Command, args []string)
return err
}
// decide which service backend to use
o.Backend = decideBackend(args[0])
o.serviceName = args[0]
_, _, err = service.SplitServiceKindName(o.serviceName)
if err != nil {
return fmt.Errorf("invalid service name")
}
o.Backend = NewOperatorBackend()
return
}
@@ -107,6 +110,5 @@ func NewCmdServiceDelete(name, fullName string) *cobra.Command {
}
serviceDeleteCmd.Flags().BoolVarP(&o.serviceForceDeleteFlag, "force", "f", false, "Delete service without prompting")
genericclioptions.AddContextFlag(serviceDeleteCmd, &o.componentContext)
completion.RegisterCommandHandler(serviceDeleteCmd, completion.ServiceCompletionHandler)
return serviceDeleteCmd
}

View File

@@ -2,14 +2,8 @@ package service
import (
"fmt"
"os"
"text/tabwriter"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/machineoutput"
"github.com/openshift/odo/pkg/odo/cli/component"
"github.com/openshift/odo/pkg/odo/genericclioptions"
odoutil "github.com/openshift/odo/pkg/odo/util"
svc "github.com/openshift/odo/pkg/service"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
@@ -31,7 +25,7 @@ type ServiceListOptions struct {
*genericclioptions.Context
// Context to use when listing service. This will use app and project values from the context
componentContext string
// choose between Operator Hub and Service Catalog. If true, Operator Hub
// If true, Operator Hub is installed on the cluster
csvSupport bool
}
@@ -54,56 +48,19 @@ func (o *ServiceListOptions) Complete(name string, cmd *cobra.Command, args []st
return err
}
} else {
o.Context, err = genericclioptions.NewContext(cmd)
return fmt.Errorf("failed to list operator backed services, have you installed operators on the cluseter?")
}
return
}
// Validate validates the ServiceListOptions based on completed values
func (o *ServiceListOptions) Validate() (err error) {
if !o.csvSupport {
// Throw error if project and application values are not available.
// This will most likely be the case when user does odo service list from outside a component directory and
// doesn't provide --app and/or --project flags
if o.Context.Project == "" || o.Context.Application == "" {
return odoutil.ThrowContextError()
}
}
return
}
// Run contains the logic for the odo service list command
func (o *ServiceListOptions) Run(cmd *cobra.Command) (err error) {
if o.csvSupport {
// if cluster supports Operators, we list only operator backed services
// and not service catalog ones
return o.listOperatorServices()
}
return o.listServiceCatalogServices()
}
func (o *ServiceListOptions) listServiceCatalogServices() (err error) {
services, err := svc.ListWithDetailedStatus(o.Client, o.Application)
if err != nil {
return fmt.Errorf("Service catalog is not enabled within your cluster: %v", err)
}
if len(services.Items) == 0 {
return fmt.Errorf("There are no services deployed for this application")
}
if log.IsJSON() {
machineoutput.OutputSuccess(services)
} else {
w := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent)
fmt.Fprintln(w, "NAME", "\t", "TYPE", "\t", "PLAN", "\t", "STATUS")
for _, comp := range services.Items {
fmt.Fprintln(w, comp.ObjectMeta.Name, "\t", comp.Spec.Type, "\t", comp.Spec.Plan, "\t", comp.Status.Status)
}
w.Flush()
}
return
return o.listOperatorServices()
}
// NewCmdServiceList implements the odo service list command.

View File

@@ -14,7 +14,7 @@ import (
// RecommendedCommandName is the recommended service command name
const RecommendedCommandName = "service"
var serviceLongDesc = ktemplates.LongDesc(`Perform service catalog operations`)
var serviceLongDesc = ktemplates.LongDesc(`Perform service related operations`)
// NewCmdService implements the odo service command
func NewCmdService(name, fullName string) *cobra.Command {
@@ -23,7 +23,7 @@ func NewCmdService(name, fullName string) *cobra.Command {
serviceDeleteCmd := NewCmdServiceDelete(deleteRecommendedCommandName, util.GetFullName(fullName, deleteRecommendedCommandName))
serviceCmd := &cobra.Command{
Use: name,
Short: "Perform service catalog operations",
Short: "Perform service related operations",
Long: serviceLongDesc,
Example: fmt.Sprintf("%s\n\n%s\n\n%s",
serviceCreateCmd.Example,

View File

@@ -1,181 +0,0 @@
package service
import (
"fmt"
"strings"
"github.com/openshift/odo/pkg/odo/util/validation"
svc "github.com/openshift/odo/pkg/service"
scv1beta1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/odo/cli/service/ui"
commonui "github.com/openshift/odo/pkg/odo/cli/ui"
"github.com/spf13/cobra"
"k8s.io/klog"
)
// This CompleteServiceCreate contains logic to complete the "odo service create" call for the case of Service Catalog backend
func (b *ServiceCatalogBackend) CompleteServiceCreate(o *CreateOptions, cmd *cobra.Command, args []string) (err error) {
var class scv1beta1.ClusterServiceClass
if o.interactive {
classesByCategory, err := o.Client.GetKubeClient().ListServiceClassesByCategory()
if err != nil {
// this error indicates that Service Catalog is not properly setup
// we inform the user that if they're trying interactive mode for Operators, it's not yet supported.
// TODO: remove the warning when interactive mode for Operators is supported
log.Warning("odo doesn't support interactive mode for creating Operator backed service yet; refer \"odo service create -h\"")
return fmt.Errorf("unable to retrieve service classes: %v", err)
}
if len(classesByCategory) == 0 {
return fmt.Errorf("no available service classes")
}
class, o.ServiceType = ui.SelectClassInteractively(classesByCategory)
plans, err := o.Client.GetKubeClient().ListMatchingPlans(class)
if err != nil {
return fmt.Errorf("couldn't retrieve plans for class %s: %v", class.GetExternalName(), err)
}
var svcPlan scv1beta1.ClusterServicePlan
// if there is only one available plan, we select it
if len(plans) == 1 {
for k, v := range plans {
o.Plan = k
svcPlan = v
}
klog.V(4).Infof("Plan %s was automatically selected since it's the only one available for service %s", o.Plan, o.ServiceType)
} else {
// otherwise select the plan interactively
o.Plan = ui.SelectPlanNameInteractively(plans, "Which service plan should we use ")
svcPlan = plans[o.Plan]
}
o.ParametersMap = ui.EnterServicePropertiesInteractively(svcPlan)
o.ServiceName = ui.EnterServiceNameInteractively(o.ServiceType, "How should we name your service ", o.validateServiceName)
o.outputCLI = commonui.Proceed("Output the non-interactive version of the selected options")
o.wait = commonui.Proceed("Wait for the service to be ready")
} else {
o.ServiceType = args[0]
// if two args are given, first is service type and second one is service name
if len(args) == 2 {
o.ServiceName = args[1]
} else {
o.ServiceName = o.ServiceType
}
}
return nil
}
func (b *ServiceCatalogBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
// make sure the service type exists
classPtr, err := o.Client.GetKubeClient().GetClusterServiceClass(o.ServiceType)
if err != nil {
return fmt.Errorf("unable to create service because Service Catalog is not enabled in your cluster")
}
if classPtr == nil {
return fmt.Errorf("service %v doesn't exist\nRun 'odo catalog list services' to see a list of supported services.\n", o.ServiceType)
}
// check plan
plans, err := o.Client.GetKubeClient().ListMatchingPlans(*classPtr)
if err != nil {
return err
}
if len(o.Plan) == 0 {
// when the plan has not been supplied, if there is only one available plan, we select it
if len(plans) == 1 {
for k := range plans {
o.Plan = k
}
klog.V(4).Infof("Plan %s was automatically selected since it's the only one available for service %s", o.Plan, o.ServiceType)
} else {
return fmt.Errorf("no plan was supplied for service %v.\nPlease select one of: %v\n", o.ServiceType, strings.Join(ui.GetServicePlanNames(plans), ","))
}
} else {
// when the plan has been supplied, we need to make sure it exists
if _, ok := plans[o.Plan]; !ok {
return fmt.Errorf("plan %s is invalid for service %v.\nPlease select one of: %v\n", o.Plan, o.ServiceType, strings.Join(ui.GetServicePlanNames(plans), ","))
}
}
//validate service name
return o.validateServiceName(o.ServiceName)
}
// validateServiceName adopts the Validator interface and checks that the name of the service being created is valid
func (o *CreateOptions) validateServiceName(i interface{}) (err error) {
s := i.(string)
err = validation.ValidateName(s)
if err != nil {
return err
}
exists, err := svc.SvcExists(o.Client, s, o.Application)
if err != nil {
return err
}
if exists {
return fmt.Errorf("%s service already exists in the current application", o.ServiceName)
}
return
}
func (b *ServiceCatalogBackend) RunServiceCreate(o *CreateOptions) (err error) {
s := &log.Status{}
log.Infof("Deploying service %q of type: %q", o.ServiceName, o.ServiceType)
// create a ServiceInstance
serviceInstance, err := svc.CreateService(o.Client, o.ServiceName, o.ServiceType, o.Plan, o.ParametersMap, o.Application)
if err != nil {
return err
}
err = svc.AddKubernetesComponentToDevfile(serviceInstance, o.ServiceName, o.EnvSpecificInfo.GetDevfileObj())
if err != nil {
return err
}
s.End(true)
if o.wait {
s = log.Spinner("Waiting for service to come up")
_, err = o.Client.GetKubeClient().WaitAndGetSecret(o.ServiceName, o.Project)
if err == nil {
s.End(true)
log.Successf(`Service %q is ready for use`, o.ServiceName)
}
} else {
log.Successf(`Service %q was created`, o.ServiceName)
log.Italic("\nProgress of the provisioning will not be reported and might take a long time\nYou can see the current status by executing 'odo service list'")
}
return
}
// ServiceDefined returns true if the service is defined in the devfile
func (b *ServiceCatalogBackend) ServiceDefined(o *DeleteOptions) (bool, error) {
return svc.IsDefined(o.serviceName, o.EnvSpecificInfo.GetDevfileObj())
}
func (b *ServiceCatalogBackend) ServiceExists(o *DeleteOptions) (bool, error) {
return svc.SvcExists(o.Client, o.serviceName, o.Application)
}
func (b *ServiceCatalogBackend) DeleteService(o *DeleteOptions, name string, application string) error {
err := svc.DeleteServiceAndUnlinkComponents(o.Client, o.serviceName, o.Application)
if err != nil {
return err
}
err = svc.DeleteKubernetesComponentFromDevfile(o.serviceName, o.EnvSpecificInfo.GetDevfileObj())
if err != nil {
return err
}
return nil
}

View File

@@ -1,99 +0,0 @@
package service
import (
"sort"
"testing"
"github.com/openshift/odo/pkg/testingutil"
"github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1"
componentlabels "github.com/openshift/odo/pkg/component/labels"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/odo/util/completion"
"github.com/posener/complete"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ktesting "k8s.io/client-go/testing"
)
func TestCompletions(t *testing.T) {
t.Parallel()
tests := []struct {
name string
handler completion.ContextualizedPredictor
cmd *cobra.Command
last string
want []string
}{
{
name: "Completing service create without input returns all available service class external names",
handler: completion.ServiceClassCompletionHandler,
cmd: NewCmdServiceCreate(createRecommendedCommandName, createRecommendedCommandName),
want: []string{"foo", "bar", "boo"},
},
{
name: "Completing service delete without input returns all available service instances",
handler: completion.ServiceCompletionHandler,
cmd: NewCmdServiceDelete(deleteRecommendedCommandName, deleteRecommendedCommandName),
want: []string{"foo"},
},
}
client, fakeClientSet := occlient.FakeNew()
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "clusterserviceclasses", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &v1beta1.ClusterServiceClassList{
Items: []v1beta1.ClusterServiceClass{
testingutil.FakeClusterServiceClass("foo"),
testingutil.FakeClusterServiceClass("bar"),
testingutil.FakeClusterServiceClass("boo"),
},
}, nil
})
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "serviceinstances", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &v1beta1.ServiceInstanceList{
Items: []v1beta1.ServiceInstance{
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app.kubernetes.io/part-of": "foo", componentlabels.ComponentLabel: "foo", componentlabels.ComponentTypeLabel: "service"},
},
Status: v1beta1.ServiceInstanceStatus{
Conditions: []v1beta1.ServiceInstanceCondition{
{
Reason: "some reason",
},
},
},
},
},
}, nil
})
context := genericclioptions.NewFakeContext("", "", "", client, nil)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := complete.Args{Last: tt.last}
got := tt.handler(tt.cmd, completion.NewParsedArgs(a, tt.cmd), context)
if !equal(got, tt.want) {
t.Errorf("Failed %s: got: %q, want: %q", t.Name(), got, tt.want)
}
})
}
}
func equal(s1, s2 []string) bool {
sort.Strings(s1)
sort.Strings(s2)
if len(s1) != len(s2) {
return false
}
for i := range s1 {
if s1[i] != s2[i] {
return false
}
}
return true
}

View File

@@ -17,11 +17,3 @@ type OperatorBackend struct {
func NewOperatorBackend() *OperatorBackend {
return &OperatorBackend{}
}
// ServiceCatalogBackend implements the interface ServiceProviderBackend and contains methods that help create a service from Service Catalog
type ServiceCatalogBackend struct {
}
func NewServiceCatalogBackend() *ServiceCatalogBackend {
return &ServiceCatalogBackend{}
}

View File

@@ -1,312 +0,0 @@
package ui
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/openshift/odo/pkg/odo/cli/ui"
"github.com/openshift/odo/pkg/odo/util/validation"
"github.com/openshift/odo/pkg/service"
"k8s.io/klog"
"github.com/mgutz/ansi"
terminal2 "golang.org/x/term"
"gopkg.in/AlecAivazis/survey.v1"
"gopkg.in/AlecAivazis/survey.v1/core"
"gopkg.in/AlecAivazis/survey.v1/terminal"
scv1beta1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1"
)
// Retrieve the list of existing service class categories
func getServiceClassesCategories(categories map[string][]scv1beta1.ClusterServiceClass) (keys []string) {
keys = make([]string, len(categories))
i := 0
for k := range categories {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
}
// GetServicePlanNames returns the service plan names included in the specified map
func GetServicePlanNames(stringMap map[string]scv1beta1.ClusterServicePlan) (keys []string) {
keys = make([]string, len(stringMap))
i := 0
for k := range stringMap {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
}
// getServiceClassMap converts the specified array of service classes to a name-service class map
func getServiceClassMap(classes []scv1beta1.ClusterServiceClass) (classMap map[string]scv1beta1.ClusterServiceClass) {
classMap = make(map[string]scv1beta1.ClusterServiceClass, len(classes))
for _, v := range classes {
classMap[v.Spec.ExternalName] = v
}
return classMap
}
// getServiceClassNames retrieves the keys (service class names) of the specified name-service class mappings
func getServiceClassNames(stringMap map[string]scv1beta1.ClusterServiceClass) (keys []string) {
keys = make([]string, len(stringMap))
i := 0
for k := range stringMap {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
}
// SelectPlanNameInteractively lets the user to select the plan name from possible options, specifying which text should appear
// in the prompt
func SelectPlanNameInteractively(plans map[string]scv1beta1.ClusterServicePlan, promptText string) (plan string) {
prompt := &survey.Select{
Message: promptText,
Options: GetServicePlanNames(plans),
}
err := survey.AskOne(prompt, &plan, nil)
ui.HandleError(err)
return plan
}
// EnterServiceNameInteractively lets the user enter the name of the service instance to create, defaulting to the provided
// default value and specifying both the prompt text and validation function for the name
func EnterServiceNameInteractively(defaultValue, promptText string, validateName validation.Validator) (serviceName string) {
// if only one arg is given, ask to Name the service providing the class Name as default
instancePrompt := &survey.Input{
Message: promptText,
Default: defaultValue,
}
err := survey.AskOne(instancePrompt, &serviceName, survey.Validator(validateName))
ui.HandleError(err)
return serviceName
}
// SelectClassInteractively lets the user select target service class from possible options, first filtering by categories then
// by class name
func SelectClassInteractively(classesByCategory map[string][]scv1beta1.ClusterServiceClass) (class scv1beta1.ClusterServiceClass, serviceType string) {
var category string
prompt := &survey.Select{
Message: "Which kind of service do you wish to create",
Options: getServiceClassesCategories(classesByCategory),
}
err := survey.AskOne(prompt, &category, survey.Required)
ui.HandleError(err)
classes := getServiceClassMap(classesByCategory[category])
// make a new displayClassInfo function available to survey templates to be able to add class information to the display
displayClassInfo := "displayClassInfo"
core.TemplateFuncs[displayClassInfo] = func(index int, pageEntries []string) string {
if index >= 0 && len(pageEntries) > index {
selected := pageEntries[index]
class := classes[selected]
return ansi.ColorCode("default+bu") + "Service class details" + ansi.ColorCode("reset") + ":\n" +
classInfoItem("Name", class.GetExternalName()) +
classInfoItem("Description", class.GetDescription()) +
classInfoItem("Long", getLongDescription(class))
}
return "No matching entry"
}
defer delete(core.TemplateFuncs, displayClassInfo)
// record original template and defer restoring it once done
original := survey.SelectQuestionTemplate
defer restoreOriginalTemplate(original)
// add more information about the currently selected class
survey.SelectQuestionTemplate = original + `
{{- if not .ShowAnswer}}
{{$classInfo:=(displayClassInfo .SelectedIndex .PageEntries)}}
{{- if $classInfo}}
{{$classInfo}}
{{- end}}
{{- end}}`
prompt = &survey.Select{
Message: "Which " + category + " service class should we use",
Options: getServiceClassNames(classes),
}
err = survey.AskOne(prompt, &serviceType, survey.Required)
ui.HandleError(err)
return classes[serviceType], serviceType
}
// classInfoItem computes how a given service class information item should be displayed
func classInfoItem(name, value string) string {
// wrap value if needed accounting for size of value "header" (its name)
value = wrapIfNeeded(value, len(name)+3)
if len(value) > 0 {
// display the name using the default color, in bold and then reset style right after
return StyledOutput(name, "default+b") + ": " + value + "\n"
}
return ""
}
// StyledOutput returns an ANSI color code to style the specified text accordingly, issuing a reset code when done using the
// https://github.com/mgutz/ansi#style-format format
func StyledOutput(text, style string) string {
return ansi.ColorCode(style) + text + ansi.ColorCode("reset")
}
const defaultColumnNumberBeforeWrap = 80
// wrapIfNeeded wraps the given string taking the given prefix size into account based on the width of the terminal (or
// defaultColumnNumberBeforeWrap if terminal size cannot be determined).
func wrapIfNeeded(value string, prefixSize int) string {
// get the width of the terminal
width, _, err := terminal2.GetSize(0)
if width == 0 || err != nil {
// if for some reason we couldn't get the size use default value
width = defaultColumnNumberBeforeWrap
}
// if the value length is greater than the width, wrap it
// note that we need to account for the size of the name of the value being displayed before the value (i.e. its name)
valueSize := len(value)
if valueSize+prefixSize >= width {
// look at each line of the value
split := strings.Split(value, "\n")
for index, line := range split {
// for each line, trim it and split it in space-separated clusters ("words")
line = strings.TrimSpace(line)
words := strings.Split(line, " ")
newLine := ""
lineSize := 0
for _, word := range words {
if lineSize+len(word)+1+prefixSize < width {
// concatenate word to the new computed line only if adding it to the line won't make it larger than acceptable
newLine = newLine + " " + word
lineSize = lineSize + 1 + len(word) // accumulate the line size
} else {
// otherwise, break the line and add the word on a new "line"
newLine = newLine + "\n" + word
lineSize = len(word) // reset the line size
}
}
// replace the initial line with the new computed version
split[index] = strings.TrimSpace(newLine)
}
// compute the new value by joining all the modified lines
value = strings.Join(split, "\n")
}
return value
}
// restoreOriginalTemplate restores the original survey template once we're done with the display
func restoreOriginalTemplate(original string) {
survey.SelectQuestionTemplate = original
}
// Convert the provided ClusterServiceClass to its UI representation
func getLongDescription(class scv1beta1.ClusterServiceClass) (longDescription string) {
extension := class.Spec.ExternalMetadata
if extension != nil {
var meta map[string]interface{}
err := json.Unmarshal(extension.Raw, &meta)
if err != nil {
klog.V(4).Infof("Unable unmarshal Extension metadata for ClusterServiceClass '%v'", class.Spec.ExternalName)
}
if val, ok := meta["longDescription"]; ok {
longDescription = val.(string)
}
}
return
}
// EnterServicePropertiesInteractively lets the user enter the properties specified by the provided plan if not already
// specified by the passed values
func EnterServicePropertiesInteractively(svcPlan scv1beta1.ClusterServicePlan) (values map[string]string) {
return enterServicePropertiesInteractively(svcPlan)
}
// enterServicePropertiesInteractively lets user enter the properties interactively using the specified Stdio instance (useful
// for testing purposes)
func enterServicePropertiesInteractively(svcPlan scv1beta1.ClusterServicePlan, stdio ...terminal.Stdio) (values map[string]string) {
planDetails, _ := service.NewServicePlan(svcPlan)
properties := make(map[string]service.ServicePlanParameter, len(planDetails.Parameters))
for _, v := range planDetails.Parameters {
properties[v.Name] = v
}
values = make(map[string]string, len(properties))
sort.Sort(planDetails.Parameters)
// first deal with required properties
for _, prop := range planDetails.Parameters {
if prop.Required {
addValueFor(prop, values, stdio...)
// remove property from list of properties to consider
delete(properties, prop.Name)
}
}
// finally check if we still have plan properties that have not been considered
if len(properties) > 0 && ui.Proceed("Provide values for non-required properties", stdio...) {
for _, prop := range properties {
addValueFor(prop, values, stdio...)
}
}
return values
}
func addValueFor(prop service.ServicePlanParameter, values map[string]string, stdio ...terminal.Stdio) {
var result string
prompt := &survey.Input{
Message: fmt.Sprintf("Enter a value for %s property %s:", prop.Type, propDesc(prop)),
}
if len(stdio) == 1 {
prompt.WithStdio(stdio[0])
}
if len(prop.Default) > 0 {
prompt.Default = prop.Default
}
err := survey.AskOne(prompt, &result, ui.GetValidatorFor(prop.AsValidatable()))
ui.HandleError(err)
values[prop.Name] = result
}
// propDesc computes a human-readable description of the specified property
func propDesc(prop service.ServicePlanParameter) string {
msg := ""
if len(prop.Title) > 0 {
msg = prop.Title
} else if len(prop.Description) > 0 {
msg = prop.Description
}
if len(msg) > 0 {
msg = " (" + strings.TrimSpace(msg) + ")"
}
return prop.Name + msg
}

View File

@@ -1,238 +0,0 @@
package ui
import (
"reflect"
"testing"
"github.com/Netflix/go-expect"
beta1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1"
"github.com/openshift/odo/pkg/service"
"github.com/openshift/odo/pkg/testingutil"
"github.com/stretchr/testify/require"
"gopkg.in/AlecAivazis/survey.v1/core"
"gopkg.in/AlecAivazis/survey.v1/terminal"
)
func init() {
// disable color output for all prompts to simplify testing
core.DisableColor = true
}
func TestGetCategories(t *testing.T) {
t.Run("getServiceClassesCategories should work", func(t *testing.T) {
foo := testingutil.FakeClusterServiceClass("foo", "footag", "footag2")
bar := testingutil.FakeClusterServiceClass("bar", "")
boo := testingutil.FakeClusterServiceClass("boo")
classes := map[string][]beta1.ClusterServiceClass{"footag": {foo}, "other": {bar, boo}}
categories := getServiceClassesCategories(classes)
expected := []string{"footag", "other"}
if !reflect.DeepEqual(expected, categories) {
t.Errorf("test failed, expected %v, got %v", expected, categories)
}
})
}
func TestGetServicePlanNames(t *testing.T) {
t.Run("GetServicePlanNames should work", func(t *testing.T) {
foo := testingutil.FakeClusterServicePlan("foo", 1)
bar := testingutil.FakeClusterServicePlan("bar", 2)
boo := testingutil.FakeClusterServicePlan("boo", 3)
plans := GetServicePlanNames(map[string]beta1.ClusterServicePlan{"foo": foo, "bar": bar, "boo": boo})
expected := []string{"bar", "boo", "foo"}
if !reflect.DeepEqual(expected, plans) {
t.Errorf("test failed, expected %v, got %v", expected, plans)
}
})
}
func TestWrapIfNeeded(t *testing.T) {
tests := []struct {
name string
input string
prefixSize int
expected string
}{
{
name: "empty string, empty prefix",
input: "",
prefixSize: 0,
expected: "",
},
{
name: "short string, empty prefix should not be wrapped",
input: "foo bar baz",
prefixSize: 0,
expected: "foo bar baz",
},
{
name: "short string, empty prefix should not be wrapped with default width",
input: "foo bar baz",
prefixSize: 2,
expected: "foo bar baz",
},
{
name: "short string, long prefix should wrap",
input: "foo bar baz",
prefixSize: 78,
expected: "foo\nbar\nbaz",
},
{
name: "long string, empty prefix should wrap",
input: "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789",
prefixSize: 0,
expected: "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789\n0123456789",
},
{
name: "long string, short prefix should wrap",
input: "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789",
prefixSize: 2,
expected: "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789\n0123456789",
},
{
name: "long string, longer prefix should wrap more",
input: "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789",
prefixSize: 10,
expected: "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789\n0123456789 0123456789",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := wrapIfNeeded(tt.input, tt.prefixSize)
if tt.expected != output {
t.Errorf("test failed, expected %s, got %s", tt.expected, output)
}
})
}
}
func init() {
// disable color output for all prompts to simplify testing
core.DisableColor = true
}
func TestEnterServicePropertiesInteractively(t *testing.T) {
// TODO: this test is currently skipped because it is not currently working properly. :(
t.Skip("TODO: Skip this test until we can figure out what is wrong with it")
tests := []struct {
name string
servicePlan beta1.ClusterServicePlan
expectedValues map[string]string
}{
{
name: "test 1 : with correct values",
servicePlan: testingutil.FakeClusterServicePlan("dev", 1),
expectedValues: map[string]string{
"PLAN_DATABASE_URI": "someuri",
"PLAN_DATABASE_USERNAME": "default",
"PLAN_DATABASE_PASSWORD": "foo",
},
},
}
for _, tt := range tests {
plan := tt.servicePlan
valuesPtr := new(map[string]string)
testingutil.RunTest(t, func(c *expect.Console) {
_, _ = c.ExpectString("Enter a value for string property PLAN_DATABASE_PASSWORD:")
_, _ = c.SendLine("foo")
_, _ = c.ExpectString("Enter a value for string property PLAN_DATABASE_URI:")
_, _ = c.SendLine("")
_, _ = c.ExpectString("Enter a value for string property PLAN_DATABASE_USERNAME:")
_, _ = c.SendLine("")
_, _ = c.ExpectString("Provide values for non-required properties")
_, _ = c.SendLine("")
_, _ = c.ExpectEOF()
}, func(stdio terminal.Stdio) error {
values := enterServicePropertiesInteractively(plan, stdio)
valuesPtr = &values
return nil
})
require.Equal(t, tt.expectedValues, *valuesPtr)
}
}
func TestGetLongDescription(t *testing.T) {
desc := testingutil.FakeClusterServiceClass("foo")
desc.Spec.ExternalMetadata = testingutil.SingleValuedRawExtension("longDescription", "description")
empty := testingutil.FakeClusterServiceClass("foo")
empty.Spec.ExternalMetadata = testingutil.SingleValuedRawExtension("longDescription", "")
tests := []struct {
name string
input beta1.ClusterServiceClass
expected string
}{
{
name: "no metadata",
input: testingutil.FakeClusterServiceClass("foo"),
expected: "",
},
{
name: "description",
input: desc,
expected: "description",
},
{
name: "empty description",
input: empty,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := getLongDescription(tt.input)
if tt.expected != output {
t.Errorf("test failed, expected %s, got %s", tt.expected, output)
}
})
}
}
func TestPropDesc(t *testing.T) {
tests := []struct {
name string
prop service.ServicePlanParameter
expected string
}{
{
name: "empty",
prop: service.ServicePlanParameter{},
expected: "",
},
{
name: "name only",
prop: service.ServicePlanParameter{Name: "foo"},
expected: "foo",
},
{
name: "with title",
prop: service.ServicePlanParameter{Name: "foo", Title: "title"},
expected: "foo (title)",
},
{
name: "with description",
prop: service.ServicePlanParameter{Name: "foo", Description: "desc"},
expected: "foo (desc)",
},
{
name: "with title and description",
prop: service.ServicePlanParameter{Name: "foo", Description: "desc", Title: "title"},
expected: "foo (title)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := propDesc(tt.prop)
if tt.expected != output {
t.Errorf("test failed, expected %v, got %v", tt.expected, output)
}
})
}
}

View File

@@ -4,8 +4,6 @@ import (
"fmt"
"path/filepath"
svc "github.com/openshift/odo/pkg/service"
"github.com/openshift/odo/pkg/odo/cli/component"
"github.com/openshift/odo/pkg/util"
)
@@ -22,15 +20,3 @@ func validDevfileDirectory(componentContext string) error {
}
return nil
}
// decideBackend returns the type of service provider backend to be used
func decideBackend(arg string) ServiceProviderBackend {
_, _, err := svc.SplitServiceKindName(arg)
if err != nil {
// failure to split provided name into two; hence ServiceCatalogBackend
return NewServiceCatalogBackend()
} else {
// provided name adheres to the format <operator-type>/<crd-name>; hence OperatorBackend
return NewOperatorBackend()
}
}

View File

@@ -1,9 +1,6 @@
package completion
import (
"fmt"
"strings"
applabels "github.com/openshift/odo/pkg/application/labels"
"github.com/openshift/odo/pkg/application"
@@ -11,7 +8,6 @@ import (
"github.com/openshift/odo/pkg/component"
"github.com/openshift/odo/pkg/config"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/service"
"github.com/openshift/odo/pkg/storage"
"github.com/openshift/odo/pkg/url"
"github.com/openshift/odo/pkg/util"
@@ -19,128 +15,6 @@ import (
"github.com/spf13/cobra"
)
// ServiceCompletionHandler provides service name completion for the current project and application
var ServiceCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
completions = make([]string, 0)
services, err := service.List(context.Client, context.Application)
if err != nil {
return completions
}
for _, class := range services.Items {
if args.commands[class.ObjectMeta.Name] {
return nil
}
completions = append(completions, class.ObjectMeta.Name)
}
return
}
// ServiceClassCompletionHandler provides catalog service class name completion
var ServiceClassCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
completions = make([]string, 0)
services, err := context.Client.GetKubeClient().ListClusterServiceClasses()
if err != nil {
complete.Log("error retrieving services")
return completions
}
complete.Log(fmt.Sprintf("found %d services", len(services)))
for _, class := range services {
if args.commands[class.Spec.ExternalName] {
return nil
}
completions = append(completions, class.Spec.ExternalName)
}
return
}
// ServicePlanCompletionHandler provides completion for the the plan of a selected service
var ServicePlanCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
completions = make([]string, 0)
// if we have less than two arguments, it means the user didn't specify the name of the service
// meaning that there is no point in providing suggestions
if len(args.original.Completed) < 2 {
complete.Log("Couldn't extract the service name")
return completions
}
inputServiceName := args.original.Completed[1]
complete.Log(fmt.Sprintf("Using input: serviceName = %s", inputServiceName))
clusterServiceClass, err := context.Client.GetKubeClient().GetClusterServiceClass(inputServiceName)
if err != nil {
complete.Log("Error retrieving details of service")
return completions
}
servicePlans, err := context.Client.GetKubeClient().ListClusterServicePlansByServiceName(clusterServiceClass.Name)
if err != nil {
complete.Log("Error retrieving details of plans of service")
return completions
}
for _, servicePlan := range servicePlans {
completions = append(completions, servicePlan.Spec.ExternalName)
}
return completions
}
// ServiceParameterCompletionHandler provides completion for the parameter names of a selected service and plan
var ServiceParameterCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
completions = make([]string, 0)
if len(args.original.Completed) < 2 {
complete.Log("Couldn't extract the service name")
return completions
}
inputServiceName := args.original.Completed[1]
inputPlanName := args.flagValues["plan"]
complete.Log(fmt.Sprintf("Using input: serviceName = %s, servicePlan = %s", inputServiceName, inputPlanName))
_, servicePlans, err := service.GetServiceClassAndPlans(context.Client, inputServiceName)
if err != nil {
complete.Log("Error retrieving details of service")
return completions
}
var matchingServicePlan *service.ServicePlan = nil
if len(servicePlans) == 0 {
complete.Log("Service has no plans so no parameters can be found")
return completions
} else if len(servicePlans) == 1 && inputPlanName == "" {
matchingServicePlan = &servicePlans[0]
} else {
for _, sp := range servicePlans {
servicePlan := sp
if servicePlan.Name == inputPlanName {
matchingServicePlan = &servicePlan
break
}
}
if matchingServicePlan == nil {
complete.Log("No service plan for the service matched the supplied plan name")
return completions
}
}
alreadyAddedParameters := args.flagValues["parameters"]
for _, servicePlanParameter := range matchingServicePlan.Parameters {
// don't add the parameter if it's already on the command line
if !strings.Contains(alreadyAddedParameters, servicePlanParameter.Name) {
completions = append(completions, servicePlanParameter.Name)
}
}
return completions
}
// AppCompletionHandler provides completion for the app commands
var AppCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
completions = make([]string, 0)
@@ -304,99 +178,6 @@ var CreateCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context
return completions
}
// LinkCompletionHandler provides completion for the odo link command
// The function returns both components and services
var LinkCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
components, err := component.GetComponentNames(context.Client, context.Application)
if err != nil {
return completions
}
services, err := service.List(context.Client, context.Application)
if err != nil {
return completions
}
completions = make([]string, 0, len(components)+len(services.Items))
for _, component := range components {
// we found the name in the list which means
// that the name has been already selected by the user so no need to suggest more
if val, ok := args.commands[component]; ok && val {
return nil
}
// we don't want to show the selected component as a target for linking, so we remove it from the suggestions
if component != context.Component() {
completions = append(completions, component)
}
}
for _, service := range services.Items {
// we found the name in the list which means
// that the name has been already selected by the user so no need to suggest more
if val, ok := args.commands[service.ObjectMeta.Name]; ok && val {
return nil
}
completions = append(completions, service.ObjectMeta.Name)
}
return completions
}
// LinkCompletionHandler provides completion for the odo unlink command
// The function returns both components and services
var UnlinkCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
// first we need to retrieve the current component
comp, err := component.GetPushedComponent(context.Client, context.Component(), context.Application)
if err != nil {
return completions
}
components, err := component.GetComponentNames(context.Client, context.Application)
if err != nil {
return completions
}
services, err := service.List(context.Client, context.Application)
if err != nil {
return completions
}
completions = make([]string, 0, len(components)+len(services.Items))
secretMounts := comp.GetLinkedSecrets()
for _, component := range components {
// we found the name in the list which means
// that the name has been already selected by the user so no need to suggest more
if val, ok := args.commands[component]; ok && val {
return nil
}
// we don't want to show the selected component as a target for linking, so we remove it from the suggestions
if component != context.Component() {
// we also need to make sure that this component has been linked to the current component
for _, secret := range secretMounts {
if strings.Contains(secret.SecretName, component) {
completions = append(completions, component)
}
}
}
}
for _, service := range services.Items {
// we found the name in the list which means
// that the name has been already selected by the user so no need to suggest more
if val, ok := args.commands[service.Name]; ok && val {
return nil
}
// we also need to make sure that this component has been linked to the current component
for _, secret := range secretMounts {
if strings.Contains(secret.SecretName, service.Name) {
completions = append(completions, service.Name)
}
}
}
return completions
}
// ComponentNameCompletionHandler provides component name completion
var ComponentNameCompletionHandler = func(cmd *cobra.Command, args parsedArgs, context *genericclioptions.Context) (completions []string) {
completions = make([]string, 0)

View File

@@ -1,748 +0,0 @@
package completion
import (
"reflect"
"sort"
"testing"
"github.com/openshift/odo/pkg/component"
"github.com/openshift/odo/pkg/testingutil"
"github.com/posener/complete"
scv1beta1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1"
appsv1 "github.com/openshift/api/apps/v1"
applabels "github.com/openshift/odo/pkg/application/labels"
componentlabels "github.com/openshift/odo/pkg/component/labels"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/odo/genericclioptions"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ktesting "k8s.io/client-go/testing"
)
func TestServicePlanCompletionHandler(t *testing.T) {
serviceClassList := &scv1beta1.ClusterServiceClassList{
Items: []scv1beta1.ClusterServiceClass{testingutil.FakeClusterServiceClass("class name", "dummy")},
}
tests := []struct {
name string
returnedServiceClass *scv1beta1.ClusterServiceClassList
returnedServicePlan []scv1beta1.ClusterServicePlan
output []string
parsedArgs parsedArgs
}{
{
name: "Case 0: no service name supplied",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create"},
},
},
output: []string{},
},
{
name: "Case 1: single plan exists",
returnedServiceClass: serviceClassList,
returnedServicePlan: []scv1beta1.ClusterServicePlan{testingutil.FakeClusterServicePlan("default", 1)},
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create", "class name"},
},
},
output: []string{"default"},
},
{
name: "Case 2: multiple plans exist",
returnedServiceClass: serviceClassList,
returnedServicePlan: []scv1beta1.ClusterServicePlan{
testingutil.FakeClusterServicePlan("plan1", 1),
testingutil.FakeClusterServicePlan("plan2", 2),
},
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create", "class name"},
},
},
output: []string{"plan1", "plan2"},
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
context := genericclioptions.NewFakeContext("project", "app", "component", client, nil)
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "clusterserviceclasses", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, tt.returnedServiceClass, nil
})
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "clusterserviceplans", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &scv1beta1.ClusterServicePlanList{Items: tt.returnedServicePlan}, nil
})
completions := ServicePlanCompletionHandler(nil, tt.parsedArgs, context)
// Sort the output and expected output in order to avoid false negatives (since ordering of the results is not important)
sort.Strings(completions)
sort.Strings(tt.output)
if !reflect.DeepEqual(tt.output, completions) {
t.Errorf("expected output: %#v,got: %#v", tt.output, completions)
}
}
}
func TestServiceParameterCompletionHandler(t *testing.T) {
serviceClassList := &scv1beta1.ClusterServiceClassList{
Items: []scv1beta1.ClusterServiceClass{testingutil.FakeClusterServiceClass("class name", "dummy")},
}
tests := []struct {
name string
returnedServiceClass *scv1beta1.ClusterServiceClassList
returnedServicePlan []scv1beta1.ClusterServicePlan
output []string
parsedArgs parsedArgs
}{
{
name: "Case 0: no service name supplied",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create"},
},
},
output: []string{},
},
{
name: "Case 1: no plan supplied and single plan exists",
returnedServiceClass: serviceClassList,
returnedServicePlan: []scv1beta1.ClusterServicePlan{testingutil.FakeClusterServicePlan("default", 1)},
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create", "class name"},
},
},
output: []string{"PLAN_DATABASE_URI", "PLAN_DATABASE_USERNAME", "PLAN_DATABASE_PASSWORD", "SOME_OTHER"},
},
{
name: "Case 2: no plan supplied and multiple plans exists",
returnedServiceClass: serviceClassList,
returnedServicePlan: []scv1beta1.ClusterServicePlan{
testingutil.FakeClusterServicePlan("plan1", 1),
testingutil.FakeClusterServicePlan("plan2", 2),
},
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create", "class name"},
},
},
output: []string{},
},
{
name: "Case 3: plan supplied but doesn't match",
returnedServiceClass: serviceClassList,
returnedServicePlan: []scv1beta1.ClusterServicePlan{testingutil.FakeClusterServicePlan("default", 1)},
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create", "class name"},
},
flagValues: map[string]string{"plan": "other"},
},
output: []string{},
},
{
name: "Case 4: matching plan supplied and no other parameters supplied",
returnedServiceClass: serviceClassList,
returnedServicePlan: []scv1beta1.ClusterServicePlan{
testingutil.FakeClusterServicePlan("plan2", 2),
testingutil.FakeClusterServicePlan("plan1", 1),
},
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create", "class name"},
},
flagValues: map[string]string{"plan": "plan1"},
},
output: []string{"PLAN_DATABASE_URI", "PLAN_DATABASE_USERNAME", "PLAN_DATABASE_PASSWORD", "SOME_OTHER"},
},
{
name: "Case 5: no plan supplied but some other parameters supplied",
returnedServiceClass: serviceClassList,
returnedServicePlan: []scv1beta1.ClusterServicePlan{testingutil.FakeClusterServicePlan("default", 1)},
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create", "class name"},
},
flagValues: map[string]string{"parameters": "[PLAN_DATABASE_USERNAME, SOME_OTHER]"},
},
output: []string{"PLAN_DATABASE_URI", "PLAN_DATABASE_PASSWORD"},
},
{
name: "Case 6: matching plan supplied but some other parameters supplied",
returnedServiceClass: serviceClassList,
returnedServicePlan: []scv1beta1.ClusterServicePlan{testingutil.FakeClusterServicePlan("default", 1)},
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create", "class name"},
},
flagValues: map[string]string{"plan": "default", "parameters": "[PLAN_DATABASE_USERNAME]"},
},
output: []string{"PLAN_DATABASE_URI", "PLAN_DATABASE_PASSWORD", "SOME_OTHER"},
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
context := genericclioptions.NewFakeContext("project", "app", "component", client, nil)
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "clusterserviceclasses", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, tt.returnedServiceClass, nil
})
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "clusterserviceplans", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &scv1beta1.ClusterServicePlanList{Items: tt.returnedServicePlan}, nil
})
completions := ServiceParameterCompletionHandler(nil, tt.parsedArgs, context)
// Sort the output and expected output in order to avoid false negatives (since ordering of the results is not important)
sort.Strings(completions)
sort.Strings(tt.output)
if !reflect.DeepEqual(tt.output, completions) {
t.Errorf("expected output: %#v,got: %#v", tt.output, completions)
}
}
}
func TestLinkCompletionHandler(t *testing.T) {
tests := []struct {
name string
component string
dcList appsv1.DeploymentConfigList
serviceList scv1beta1.ServiceInstanceList
output []string
}{
{
name: "Case 1: both components and services are present",
component: "frontend",
serviceList: scv1beta1.ServiceInstanceList{
Items: []scv1beta1.ServiceInstance{
{
ObjectMeta: metav1.ObjectMeta{
Name: "mysql-persistent",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "mysql-persistent",
componentlabels.ComponentTypeLabel: "mysql-persistent",
},
Annotations: map[string]string{
component.ComponentSourceTypeAnnotation: "local",
},
},
Spec: scv1beta1.ServiceInstanceSpec{
PlanReference: scv1beta1.PlanReference{
ClusterServiceClassExternalName: "mysql-persistent",
ClusterServicePlanExternalName: "default",
},
},
Status: scv1beta1.ServiceInstanceStatus{
Conditions: []scv1beta1.ServiceInstanceCondition{
{
Reason: "ProvisionedSuccessfully",
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "postgresql-ephemeral",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "postgresql-ephemeral",
componentlabels.ComponentTypeLabel: "postgresql-ephemeral",
},
Annotations: map[string]string{
component.ComponentSourceTypeAnnotation: "local",
},
},
Spec: scv1beta1.ServiceInstanceSpec{
PlanReference: scv1beta1.PlanReference{
ClusterServiceClassExternalName: "postgresql-ephemeral",
ClusterServicePlanExternalName: "default",
},
},
Status: scv1beta1.ServiceInstanceStatus{
Conditions: []scv1beta1.ServiceInstanceCondition{
{
Reason: "Provisioning",
},
},
},
},
},
},
dcList: appsv1.DeploymentConfigList{
Items: []appsv1.DeploymentConfig{
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "backend",
componentlabels.ComponentTypeLabel: "java",
},
Annotations: map[string]string{
component.ComponentSourceTypeAnnotation: "local",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "frontend",
componentlabels.ComponentTypeLabel: "nodejs",
},
Annotations: map[string]string{
component.ComponentSourceTypeAnnotation: "local",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
},
},
// make sure that the 'component' is not part of the suggestions
output: []string{"backend", "mysql-persistent", "postgresql-ephemeral"},
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
parsedArgs := parsedArgs{
commands: make(map[string]bool),
}
context := genericclioptions.NewFakeContext("project", "app", tt.component, client, nil)
fakeClientSet.ProjClientset.PrependReactor("get", "projects", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &testingutil.FakeOnlyOneExistingProjects().Items[0], nil
})
//fake the services
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "serviceinstances", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.serviceList, nil
})
//fake the dcs
fakeClientSet.AppsClientset.PrependReactor("list", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.dcList, nil
})
for i := range tt.dcList.Items {
fakeClientSet.AppsClientset.PrependReactor("get", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.dcList.Items[i], nil
})
}
completions := LinkCompletionHandler(nil, parsedArgs, context)
sort.Strings(completions)
if !reflect.DeepEqual(tt.output, completions) {
t.Errorf("expected output: %#v,got: %#v", tt.output, completions)
}
}
}
func TestUnlinkCompletionHandler(t *testing.T) {
tests := []struct {
name string
component string
dcList appsv1.DeploymentConfigList
serviceList scv1beta1.ServiceInstanceList
output []string
}{
{
name: "Case 1: both components and services are present",
component: "frontend",
serviceList: scv1beta1.ServiceInstanceList{
Items: []scv1beta1.ServiceInstance{
{
ObjectMeta: metav1.ObjectMeta{
Name: "mysql-persistent",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "mysql-persistent",
componentlabels.ComponentTypeLabel: "mysql-persistent",
},
},
Spec: scv1beta1.ServiceInstanceSpec{
PlanReference: scv1beta1.PlanReference{
ClusterServiceClassExternalName: "mysql-persistent",
ClusterServicePlanExternalName: "default",
},
},
Status: scv1beta1.ServiceInstanceStatus{
Conditions: []scv1beta1.ServiceInstanceCondition{
{
Reason: "ProvisionedSuccessfully",
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "postgresql-ephemeral",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "postgresql-ephemeral",
componentlabels.ComponentTypeLabel: "postgresql-ephemeral",
},
},
Spec: scv1beta1.ServiceInstanceSpec{
PlanReference: scv1beta1.PlanReference{
ClusterServiceClassExternalName: "postgresql-ephemeral",
ClusterServicePlanExternalName: "default",
},
},
Status: scv1beta1.ServiceInstanceStatus{
Conditions: []scv1beta1.ServiceInstanceCondition{
{
Reason: "ProvisionedSuccessfully",
},
},
},
},
},
},
dcList: appsv1.DeploymentConfigList{
Items: []appsv1.DeploymentConfig{
{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-app",
Namespace: "project",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "backend",
componentlabels.ComponentTypeLabel: "java",
},
Annotations: map[string]string{
component.ComponentSourceTypeAnnotation: "local",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "backend2-app",
Namespace: "project",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "backend2",
componentlabels.ComponentTypeLabel: "java",
},
Annotations: map[string]string{
component.ComponentSourceTypeAnnotation: "local",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "frontend-app",
Namespace: "project",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "frontend",
componentlabels.ComponentTypeLabel: "nodejs",
},
Annotations: map[string]string{
component.ComponentSourceTypeAnnotation: "local",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
EnvFrom: []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "postgresql-ephemeral"},
},
},
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: "backend-8080"},
},
},
},
},
},
},
},
},
},
},
},
// make sure that the 'component' is not part of the suggestions and that only actually linked components/services show up
output: []string{"backend", "postgresql-ephemeral"},
},
}
p := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "postgresql-ephemeral",
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
parsedArgs := parsedArgs{
commands: make(map[string]bool),
}
context := genericclioptions.NewFakeContext("project", "app", tt.component, client, nil)
//fake the services
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "serviceinstances", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.serviceList, nil
})
fakeClientSet.ProjClientset.PrependReactor("get", "projects", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &testingutil.FakeOnlyOneExistingProjects().Items[0], nil
})
//fake the dcs
fakeClientSet.AppsClientset.PrependReactor("list", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.dcList, nil
})
for i := range tt.dcList.Items {
fakeClientSet.AppsClientset.PrependReactor("get", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.dcList.Items[i], nil
})
}
fakeClientSet.Kubernetes.PrependReactor("get", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &p, nil
})
completions := UnlinkCompletionHandler(nil, parsedArgs, context)
sort.Strings(completions)
if !reflect.DeepEqual(tt.output, completions) {
t.Errorf("expected output: %#v,got: %#v", tt.output, completions)
}
}
}
func TestServiceCompletionHandler(t *testing.T) {
tests := []struct {
name string
returnedServiceClassInstances *scv1beta1.ServiceInstanceList
output []string
parsedArgs parsedArgs
}{
{
name: "test case 1: no service instance exists and name not typed",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"delete"},
},
},
returnedServiceClassInstances: &scv1beta1.ServiceInstanceList{},
output: []string{},
},
{
name: "test case 2: one service class instance exists and name not typed",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"delete"},
},
},
returnedServiceClassInstances: &scv1beta1.ServiceInstanceList{
Items: []scv1beta1.ServiceInstance{
testingutil.FakeServiceClassInstance("service-1", "mariadb-apb", "default", "ProvisionedSuccessfully"),
},
},
output: []string{"service-1"},
},
{
name: "test case 3: multiple service class instance exists and name not typed",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"delete"},
},
},
returnedServiceClassInstances: &scv1beta1.ServiceInstanceList{
Items: []scv1beta1.ServiceInstance{
testingutil.FakeServiceClassInstance("service-1", "mariadb-apb", "default", "ProvisionedSuccessfully"),
testingutil.FakeServiceClassInstance("service-2", "mariadb-apb", "prod", "ProvisionedSuccessfully"),
},
},
output: []string{"service-1", "service-2"},
},
{
name: "test case 4: multiple service class instance exists and name fully typed",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"delete"},
},
commands: map[string]bool{"service-1": true},
},
returnedServiceClassInstances: &scv1beta1.ServiceInstanceList{
Items: []scv1beta1.ServiceInstance{
testingutil.FakeServiceClassInstance("service-1", "mariadb-apb", "default", "ProvisionedSuccessfully"),
testingutil.FakeServiceClassInstance("service-2", "mariadb-apb", "prod", "ProvisionedSuccessfully"),
},
},
output: nil,
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
context := genericclioptions.NewFakeContext("project", "app", "component", client, nil)
//fake the services
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "serviceinstances", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, tt.returnedServiceClassInstances, nil
})
completions := ServiceCompletionHandler(nil, tt.parsedArgs, context)
// Sort the output and expected output in order to avoid false negatives (since ordering of the results is not important)
sort.Strings(completions)
sort.Strings(tt.output)
if !reflect.DeepEqual(tt.output, completions) {
t.Errorf("expected output: %#v,got: %#v", tt.output, completions)
}
}
}
func TestServiceClassCompletionHandler(t *testing.T) {
tests := []struct {
name string
returnedServiceClasses *scv1beta1.ClusterServiceClassList
output []string
parsedArgs parsedArgs
}{
{
name: "test case 1: no service class exists and name not typed",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create"},
},
},
returnedServiceClasses: &scv1beta1.ClusterServiceClassList{},
output: []string{},
},
{
name: "test case 2: one service class exists and name not typed",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create"},
},
},
returnedServiceClasses: &scv1beta1.ClusterServiceClassList{
Items: []scv1beta1.ClusterServiceClass{
testingutil.FakeClusterServiceClass("foo"),
},
},
output: []string{"foo"},
},
{
name: "test case 3: multiple service classes exist and name not typed",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"create"},
},
},
returnedServiceClasses: &scv1beta1.ClusterServiceClassList{
Items: []scv1beta1.ClusterServiceClass{
testingutil.FakeClusterServiceClass("foo"),
testingutil.FakeClusterServiceClass("bar"),
},
},
output: []string{"foo", "bar"},
},
{
name: "test case 4: multiple service classes exist and name fully typed",
parsedArgs: parsedArgs{
original: complete.Args{
Completed: []string{"delete"},
},
commands: map[string]bool{"foo": true},
},
returnedServiceClasses: &scv1beta1.ClusterServiceClassList{
Items: []scv1beta1.ClusterServiceClass{
testingutil.FakeClusterServiceClass("foo"),
testingutil.FakeClusterServiceClass("bar"),
},
},
output: nil,
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
context := genericclioptions.NewFakeContext("project", "app", "component", client, nil)
//fake the services
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "clusterserviceclasses", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, tt.returnedServiceClasses, nil
})
completions := ServiceClassCompletionHandler(nil, tt.parsedArgs, context)
// Sort the output and expected output in order to avoid false negatives (since ordering of the results is not important)
sort.Strings(completions)
sort.Strings(tt.output)
if !reflect.DeepEqual(tt.output, completions) {
t.Errorf("expected output: %#v,got: %#v", tt.output, completions)
}
}
}

View File

@@ -10,12 +10,10 @@ import (
parsercommon "github.com/devfile/library/pkg/devfile/parser/data/v2/common"
"github.com/openshift/odo/pkg/kclient"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/odo/util/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog"
scv1beta1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1"
appsv1 "github.com/openshift/api/apps/v1"
olm "github.com/operator-framework/api/pkg/operators/v1alpha1"
@@ -44,45 +42,8 @@ const ServiceLabel = "app.kubernetes.io/service-name"
// ServiceKind is the kind of the service in the service binding object
const ServiceKind = "app.kubernetes.io/service-kind"
// NewServicePlanParameter creates a new ServicePlanParameter instance with the specified state
func NewServicePlanParameter(name, typeName, defaultValue string, required bool) ServicePlanParameter {
return ServicePlanParameter{
Name: name,
Default: defaultValue,
Validatable: validation.Validatable{
Type: typeName,
Required: required,
},
}
}
type servicePlanParameters []ServicePlanParameter
func (params servicePlanParameters) Len() int {
return len(params)
}
func (params servicePlanParameters) Less(i, j int) bool {
return params[i].Name < params[j].Name
}
func (params servicePlanParameters) Swap(i, j int) {
params[i], params[j] = params[j], params[i]
}
// CreateService creates new service from serviceCatalog
// It returns string representation of service instance created on the cluster and error (if any).
func CreateService(client *occlient.Client, serviceName, serviceType, servicePlan string, parameters map[string]string, applicationName string) (string, error) {
labels := componentlabels.GetLabels(serviceName, applicationName, true)
// save service type as label
labels[componentlabels.ComponentTypeLabel] = serviceType
serviceInstance, err := client.GetKubeClient().CreateServiceInstance(serviceName, serviceType, servicePlan, parameters, labels)
if err != nil {
return "", errors.Wrap(err, "unable to create service instance")
}
return serviceInstance, nil
}
// GetCSV checks if the CR provided by the user in the YAML file exists in the namesapce
// It returns a CR (string representation) and CSV (Operator) upon successfully
// able to find them, an error otherwise.
@@ -583,115 +544,11 @@ func SplitServiceKindName(serviceName string) (string, string, error) {
return kind, name, nil
}
// GetServiceClassAndPlans returns the service class details with the associated plans
// serviceName is the name of the service class
// the first parameter returned is the ServiceClass object
// the second parameter returned is the array of ServicePlan associated with the service class
func GetServiceClassAndPlans(client *occlient.Client, serviceName string) (ServiceClass, []ServicePlan, error) {
result, err := client.GetKubeClient().GetClusterServiceClass(serviceName)
if err != nil {
return ServiceClass{}, nil, errors.Wrap(err, "unable to get the given service")
}
var meta map[string]interface{}
err = json.Unmarshal(result.Spec.ExternalMetadata.Raw, &meta)
if err != nil {
return ServiceClass{}, nil, errors.Wrap(err, "unable to unmarshal data the given service")
}
service := ServiceClass{
Name: result.Spec.ExternalName,
Bindable: result.Spec.Bindable,
ShortDescription: result.Spec.Description,
Tags: result.Spec.Tags,
ServiceBrokerName: result.Spec.ClusterServiceBrokerName,
}
if val, ok := meta["longDescription"]; ok {
service.LongDescription = val.(string)
}
if val, ok := meta["dependencies"]; ok {
versions := fmt.Sprint(val)
versions = strings.Replace(versions, "[", "", -1)
versions = strings.Replace(versions, "]", "", -1)
service.VersionsAvailable = strings.Split(versions, " ")
}
// get the plans according to the service name
planResults, err := client.GetKubeClient().ListClusterServicePlansByServiceName(result.Name)
if err != nil {
return ServiceClass{}, nil, errors.Wrap(err, "unable to get plans for the given service")
}
var plans []ServicePlan
for _, result := range planResults {
plan, err := NewServicePlan(result)
if err != nil {
return ServiceClass{}, nil, err
}
plans = append(plans, plan)
}
return service, plans, nil
}
type InstanceCreateParameterSchema struct {
Required []string
Properties map[string]ServicePlanParameter
}
// NewServicePlan creates a new ServicePlan based on the specified ClusterServicePlan
func NewServicePlan(result scv1beta1.ClusterServicePlan) (plan ServicePlan, err error) {
plan = ServicePlan{
Name: result.Spec.ExternalName,
Description: result.Spec.Description,
}
// get the display name from the external meta data
var externalMetaData map[string]interface{}
err = json.Unmarshal(result.Spec.ExternalMetadata.Raw, &externalMetaData)
if err != nil {
return plan, errors.Wrap(err, "unable to unmarshal data the given service")
}
if val, ok := externalMetaData["displayName"]; ok {
plan.DisplayName = val.(string)
}
// get the create parameters
schema := InstanceCreateParameterSchema{}
paramBytes := result.Spec.InstanceCreateParameterSchema.Raw
err = json.Unmarshal(paramBytes, &schema)
if err != nil {
return plan, errors.Wrapf(err, "unable to unmarshal data the given service: %s", string(paramBytes[:]))
}
plan.Parameters = make([]ServicePlanParameter, 0, len(schema.Properties))
for k, v := range schema.Properties {
v.Name = k
// we set the Required flag if the name of parameter
// is one of the parameters indicated as required
// these parameters are not strictly required since they might have default values
v.Required = isRequired(schema.Required, k)
plan.Parameters = append(plan.Parameters, v)
}
return
}
// isRequired checks whether the parameter with the specified name is among the given list of required ones
func isRequired(required []string, name string) bool {
for _, n := range required {
if n == name {
return true
}
}
return false
}
// IsCSVSupported checks if the cluster supports resources of type ClusterServiceVersion
func IsCSVSupported() (bool, error) {
client, err := occlient.New()

View File

@@ -1,9 +1,6 @@
package service
import (
"encoding/json"
"fmt"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
devfile "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
@@ -12,767 +9,10 @@ import (
devfileCtx "github.com/devfile/library/pkg/devfile/parser/context"
"github.com/devfile/library/pkg/devfile/parser/data"
devfileFileSystem "github.com/devfile/library/pkg/testingutil/filesystem"
"github.com/kylelemons/godebug/pretty"
"github.com/onsi/gomega/matchers"
"github.com/openshift/odo/pkg/testingutil"
"reflect"
"sort"
"testing"
scv1beta1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1"
appsv1 "github.com/openshift/api/apps/v1"
applabels "github.com/openshift/odo/pkg/application/labels"
componentlabels "github.com/openshift/odo/pkg/component/labels"
"github.com/openshift/odo/pkg/occlient"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ktesting "k8s.io/client-go/testing"
)
func TestGetServiceClassAndPlans(t *testing.T) {
classExternalMetaData := make(map[string]interface{})
classExternalMetaData["longDescription"] = "example long description"
classExternalMetaData["dependencies"] = []string{"docker.io/centos/7", "docker.io/centos/8"}
classExternalMetaDataRaw, err := json.Marshal(classExternalMetaData)
if err != nil {
fmt.Printf("error occured %v during marshalling", err)
return
}
type args struct {
ServiceName string
}
plan1 := testingutil.FakeClusterServicePlan("dev", 1)
plan2 := testingutil.FakeClusterServicePlan("prod", 2)
tests := []struct {
name string
args args
returnedClassID string
returnedServiceClass *scv1beta1.ClusterServiceClassList
returnedServicePlan []scv1beta1.ClusterServicePlan
wantedServiceClass ServiceClass
wantedServicePlans []ServicePlan
wantErr bool
}{
{
name: "test 1 : with correct values",
args: args{
ServiceName: "class name",
},
returnedClassID: "1dda1477cace09730bd8ed7a6505607e",
returnedServiceClass: &scv1beta1.ClusterServiceClassList{
Items: []scv1beta1.ClusterServiceClass{
{
ObjectMeta: metav1.ObjectMeta{Name: "1dda1477cace09730bd8ed7a6505607e"},
Spec: scv1beta1.ClusterServiceClassSpec{
CommonServiceClassSpec: scv1beta1.CommonServiceClassSpec{
ExternalName: "class name",
Bindable: false,
Description: "example description",
Tags: []string{"php", "java"},
ExternalMetadata: &runtime.RawExtension{Raw: classExternalMetaDataRaw},
},
ClusterServiceBrokerName: "broker name",
},
},
},
},
returnedServicePlan: []scv1beta1.ClusterServicePlan{plan1, plan2},
wantedServiceClass: ServiceClass{
Name: "class name",
ShortDescription: "example description",
LongDescription: "example long description",
Tags: []string{"php", "java"},
Bindable: false,
ServiceBrokerName: "broker name",
VersionsAvailable: []string{"docker.io/centos/7", "docker.io/centos/8"},
},
wantedServicePlans: []ServicePlan{
{
Name: "dev",
Description: "this is a example description 1",
DisplayName: "plan-name-1",
Parameters: []ServicePlanParameter{
NewServicePlanParameter("PLAN_DATABASE_URI", "string", "someuri", true),
NewServicePlanParameter("PLAN_DATABASE_USERNAME", "string", "name", true),
NewServicePlanParameter("PLAN_DATABASE_PASSWORD", "string", "", true),
NewServicePlanParameter("SOME_OTHER", "string", "other", false),
},
},
{
Name: "prod",
Description: "this is a example description 2",
DisplayName: "plan-name-2",
Parameters: []ServicePlanParameter{
NewServicePlanParameter("PLAN_DATABASE_USERNAME_2", "string", "user2", true),
NewServicePlanParameter("PLAN_DATABASE_PASSWORD", "string", "", true),
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "clusterserviceclasses", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
if action.(ktesting.ListAction).GetListRestrictions().Fields.String() != fmt.Sprintf("spec.externalName=%v", tt.args.ServiceName) {
t.Errorf("got a different service name got: %v , expected: %v", action.(ktesting.ListAction).GetListRestrictions().Fields.String(), fmt.Sprintf("spec.externalName=%v", tt.args.ServiceName))
}
return true, tt.returnedServiceClass, nil
})
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "clusterserviceplans", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
if action.(ktesting.ListAction).GetListRestrictions().Fields.String() != fmt.Sprintf("spec.clusterServiceClassRef.name=%v", tt.returnedClassID) {
t.Errorf("got a different service name got: %v , expected: %v", action.(ktesting.ListAction).GetListRestrictions().Fields.String(), fmt.Sprintf("spec.clusterServiceClassRef.name=%v", tt.returnedClassID))
}
return true, &scv1beta1.ClusterServicePlanList{Items: tt.returnedServicePlan}, nil
})
serviceClass, servicePlans, err := GetServiceClassAndPlans(client, tt.args.ServiceName)
if err == nil && !tt.wantErr {
if len(fakeClientSet.ServiceCatalogClientSet.Actions()) != 2 {
t.Errorf("expected 2 actions in GetServiceClassAndPlans got: %v", fakeClientSet.ServiceCatalogClientSet.Actions())
}
if !reflect.DeepEqual(tt.wantedServiceClass.Name, serviceClass.Name) {
t.Errorf("different service class name expected got: %v , expected: %v", serviceClass.Name, tt.wantedServiceClass.Name)
}
if !reflect.DeepEqual(tt.wantedServiceClass.Bindable, serviceClass.Bindable) {
t.Errorf("different service class bindable value expected got: %v , expected: %v", serviceClass.Bindable, tt.wantedServiceClass.Bindable)
}
if !reflect.DeepEqual(tt.wantedServiceClass.ShortDescription, serviceClass.ShortDescription) {
t.Errorf("different short description value expected got: %v , expected: %v", serviceClass.ShortDescription, tt.wantedServiceClass.ShortDescription)
}
if !reflect.DeepEqual(tt.wantedServiceClass.LongDescription, serviceClass.LongDescription) {
t.Errorf("different long description value expected got: %v , expected: %v", serviceClass.LongDescription, tt.wantedServiceClass.LongDescription)
}
if !reflect.DeepEqual(tt.wantedServiceClass.ServiceBrokerName, serviceClass.ServiceBrokerName) {
t.Errorf("different service broker name value expected got: %v , expected: %v", serviceClass.ServiceBrokerName, tt.wantedServiceClass.ServiceBrokerName)
}
if !reflect.DeepEqual(tt.wantedServiceClass.Tags, serviceClass.Tags) {
t.Errorf("different service class tags value expected got: %v , expected: %v", serviceClass.Tags, tt.wantedServiceClass.Tags)
}
for _, wantedServicePlan := range tt.wantedServicePlans {
// make sure that the plans are sorted so we can compare them later
sort.Slice(wantedServicePlan.Parameters, func(i, j int) bool {
return wantedServicePlan.Parameters[i].Name < wantedServicePlan.Parameters[j].Name
})
found := false
for _, gotServicePlan := range servicePlans {
if reflect.DeepEqual(wantedServicePlan.Name, gotServicePlan.Name) {
found = true
} else {
continue
}
// make sure that the plans are sorted so we can compare them
sort.Slice(gotServicePlan.Parameters, func(i, j int) bool {
return gotServicePlan.Parameters[i].Name < gotServicePlan.Parameters[j].Name
})
if !reflect.DeepEqual(wantedServicePlan.Parameters, gotServicePlan.Parameters) {
t.Errorf("Different plan parameters value. Expected: %v , got: %v", wantedServicePlan.Parameters, gotServicePlan.Parameters)
}
if !reflect.DeepEqual(wantedServicePlan.DisplayName, gotServicePlan.DisplayName) {
t.Errorf("Different plan display name value. Expected: %v , got: %v", wantedServicePlan.DisplayName, gotServicePlan.DisplayName)
}
if !reflect.DeepEqual(wantedServicePlan.Description, gotServicePlan.Description) {
t.Errorf("Different plan description value. Expected: %v , got: %v", wantedServicePlan.Description, gotServicePlan.Description)
}
}
if !found {
t.Errorf("service plan %v not found", wantedServicePlan.Name)
}
}
} else if err == nil && tt.wantErr {
t.Error("test failed, expected: false, got true")
} else if err != nil && !tt.wantErr {
t.Errorf("test failed, expected: no error, got error: %s", err.Error())
}
}
}
func TestListWithDetailedStatus(t *testing.T) {
type args struct {
Project string
Selector string
}
tests := []struct {
name string
args args
serviceList scv1beta1.ServiceInstanceList
secretList corev1.SecretList
dcList appsv1.DeploymentConfigList
output []Service
}{
{
name: "Case 1: services with various statuses, some bound and some linked",
args: args{
Project: "myproject",
Selector: "app.kubernetes.io/instance=mysql-persistent,app.kubernetes.io/part-of=app",
},
serviceList: scv1beta1.ServiceInstanceList{
Items: []scv1beta1.ServiceInstance{
{
ObjectMeta: metav1.ObjectMeta{
Name: "mysql-persistent",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "mysql-persistent",
componentlabels.ComponentTypeLabel: "mysql-persistent",
},
Namespace: "myproject",
},
Spec: scv1beta1.ServiceInstanceSpec{
PlanReference: scv1beta1.PlanReference{
ClusterServiceClassExternalName: "mysql-persistent",
ClusterServicePlanExternalName: "default",
},
},
Status: scv1beta1.ServiceInstanceStatus{
Conditions: []scv1beta1.ServiceInstanceCondition{
{
Reason: "ProvisionedSuccessfully",
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "postgresql-ephemeral",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "postgresql-ephemeral",
componentlabels.ComponentTypeLabel: "postgresql-ephemeral",
},
Namespace: "myproject",
},
Spec: scv1beta1.ServiceInstanceSpec{
PlanReference: scv1beta1.PlanReference{
ClusterServiceClassExternalName: "postgresql-ephemeral",
ClusterServicePlanExternalName: "default",
},
},
Status: scv1beta1.ServiceInstanceStatus{
Conditions: []scv1beta1.ServiceInstanceCondition{
{
Reason: "ProvisionedSuccessfully",
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "mongodb",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "mongodb",
componentlabels.ComponentTypeLabel: "mongodb",
},
Namespace: "myproject",
},
Spec: scv1beta1.ServiceInstanceSpec{
PlanReference: scv1beta1.PlanReference{
ClusterServiceClassExternalName: "mongodb",
ClusterServicePlanExternalName: "default",
},
},
Status: scv1beta1.ServiceInstanceStatus{
Conditions: []scv1beta1.ServiceInstanceCondition{
{
Reason: "ProvisionedSuccessfully",
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "jenkins-persistent",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: "jenkins-persistent",
componentlabels.ComponentTypeLabel: "jenkins-persistent",
},
Namespace: "myproject",
},
Spec: scv1beta1.ServiceInstanceSpec{
PlanReference: scv1beta1.PlanReference{
ClusterServiceClassExternalName: "jenkins-persistent",
ClusterServicePlanExternalName: "default",
},
},
Status: scv1beta1.ServiceInstanceStatus{
Conditions: []scv1beta1.ServiceInstanceCondition{
{
Reason: "Provisioning",
},
},
},
},
},
},
secretList: corev1.SecretList{
Items: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "dummySecret",
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "postgresql-ephemeral",
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "mysql-persistent",
},
},
},
},
dcList: appsv1.DeploymentConfigList{
Items: []appsv1.DeploymentConfig{
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
applabels.ApplicationLabel: "app",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
applabels.ApplicationLabel: "app",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
EnvFrom: []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "mysql-persistent",
},
},
},
},
},
},
},
},
},
},
},
},
output: []Service{
{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "odo.dev/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "mysql-persistent",
},
Spec: ServiceSpec{
Type: "mysql-persistent",
Plan: "default",
},
Status: ServiceStatus{
Status: "ProvisionedAndLinked",
},
},
{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "odo.dev/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "postgresql-ephemeral",
},
Spec: ServiceSpec{
Type: "postgresql-ephemeral",
Plan: "default",
},
Status: ServiceStatus{
Status: "ProvisionedAndBound",
},
},
{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "odo.dev/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "mongodb",
},
Spec: ServiceSpec{
Type: "mongodb",
Plan: "default",
},
Status: ServiceStatus{
Status: "ProvisionedSuccessfully",
},
},
{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "odo.dev/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "jenkins-persistent",
},
Spec: ServiceSpec{
Type: "jenkins-persistent",
Plan: "default",
},
Status: ServiceStatus{
Status: "Provisioning",
},
},
},
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
//fake the services
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "serviceinstances", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.serviceList, nil
})
//fake the secrets
fakeClientSet.Kubernetes.PrependReactor("list", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.secretList, nil
})
//fake the dcs
fakeClientSet.AppsClientset.PrependReactor("list", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.dcList, nil
})
svcInstanceList, _ := ListWithDetailedStatus(client, "app")
if !reflect.DeepEqual(tt.output, svcInstanceList.Items) {
t.Error(fmt.Sprintf("Expected output: %v", pretty.Compare(tt.serviceList, svcInstanceList.Items)))
}
}
}
func TestDeleteServiceAndUnlinkComponents(t *testing.T) {
const appName = "app"
type args struct {
ServiceName string
}
tests := []struct {
name string
args args
serviceList scv1beta1.ServiceInstanceList
dcList appsv1.DeploymentConfigList
expectedDCNamesToBeUpdated []string
wantErr bool
}{
{
name: "Case 1: Delete service that has linked component",
args: args{
ServiceName: "mysql",
},
wantErr: false,
expectedDCNamesToBeUpdated: []string{"component-with-matching-link"},
serviceList: scv1beta1.ServiceInstanceList{
Items: []scv1beta1.ServiceInstance{
{
ObjectMeta: metav1.ObjectMeta{
Name: "mysql",
Labels: map[string]string{
applabels.ApplicationLabel: appName,
componentlabels.ComponentLabel: "mysql",
componentlabels.ComponentTypeLabel: "mysql-persistent",
},
},
},
},
},
dcList: appsv1.DeploymentConfigList{
Items: []appsv1.DeploymentConfig{
{
ObjectMeta: metav1.ObjectMeta{
Name: "component-with-no-links" + "-" + appName,
Labels: map[string]string{
applabels.ApplicationLabel: appName,
componentlabels.ComponentLabel: "component-with-no-links",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dummyContainer",
},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "component-with-matching-link" + "-" + appName,
Labels: map[string]string{
applabels.ApplicationLabel: appName,
componentlabels.ComponentLabel: "component-with-matching-link",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
EnvFrom: []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "mysql",
},
},
},
},
},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "component-with-non-matching-link" + "-" + appName,
Labels: map[string]string{
applabels.ApplicationLabel: appName,
componentlabels.ComponentLabel: "component-with-non-matching-link",
},
},
Spec: appsv1.DeploymentConfigSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
EnvFrom: []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "other",
},
},
},
},
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
client, fakeClientSet := occlient.FakeNew()
//fake the services listing
fakeClientSet.ServiceCatalogClientSet.PrependReactor("list", "serviceinstances", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.serviceList, nil
})
// Fake the servicebinding delete
fakeClientSet.ServiceCatalogClientSet.PrependReactor("delete", "servicebindings", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
// Fake the serviceinstance delete
fakeClientSet.ServiceCatalogClientSet.PrependReactor("delete", "serviceinstances", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
//fake the dc listing
fakeClientSet.AppsClientset.PrependReactor("list", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.dcList, nil
})
//fake the dc get
fakeClientSet.AppsClientset.PrependReactor("get", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) {
dcNameToFind := action.(ktesting.GetAction).GetName()
var matchingDC appsv1.DeploymentConfig
found := false
for _, dc := range tt.dcList.Items {
if dc.Name == dcNameToFind {
matchingDC = dc
found = true
break
}
}
if !found {
t.Errorf("Expected to find DeploymentConfig named %s in the dcList", dcNameToFind)
}
return true, &matchingDC, nil
})
err := DeleteServiceAndUnlinkComponents(client, tt.args.ServiceName, "app")
if !tt.wantErr == (err != nil) {
t.Errorf("service.DeleteServiceAndUnlinkComponents(...) unexpected error %v, wantErr %v", err, tt.wantErr)
}
// ensure we deleted the service
if len(fakeClientSet.ServiceCatalogClientSet.Actions()) != 3 && !tt.wantErr {
t.Errorf("service was deleted properly, got actions: %v", fakeClientSet.ServiceCatalogClientSet.Actions())
}
// ensure we updated the correct number of deployments
// there should always be a list action
// then each update to a dc is 2 actions, a get and an update
expectedNumberOfDCActions := 1 + (2 * len(tt.expectedDCNamesToBeUpdated))
if len(fakeClientSet.AppsClientset.Actions()) != 3 && !tt.wantErr {
t.Errorf("expected to see %d actions, got: %v", expectedNumberOfDCActions, fakeClientSet.AppsClientset.Actions())
}
}
}
func TestServicePlanParameterUnmarshalling(t *testing.T) {
parameter := NewServicePlanParameter("name", "string", "default", true)
parameter.Title = "title"
parameter.Description = "description"
tests := []struct {
name string
json string
expected ServicePlanParameter
}{
{
name: "full",
json: `{
"name": "name",
"title": "title",
"description": "description",
"default": "default",
"required": true,
"type": "string"
}`,
expected: parameter,
},
{
name: "not required",
json: `{
"name": "name",
"default": "default",
"type": "integer"
}`,
expected: NewServicePlanParameter("name", "integer", "default", false),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
spp := &ServicePlanParameter{}
err := json.Unmarshal([]byte(tt.json), &spp)
if err != nil {
t.Errorf("unmarshalling failed: %v", err)
}
if !reflect.DeepEqual(tt.expected, *spp) {
t.Errorf("param: %v, got: %v", tt.expected, *spp)
}
})
}
}
func TestServicePlanParameterMarshalling(t *testing.T) {
parameter := NewServicePlanParameter("name", "string", "default", true)
parameter.Title = "title"
parameter.Description = "description"
tests := []struct {
name string
json string
param ServicePlanParameter
}{
{
name: "full",
json: `{
"name": "name",
"title": "title",
"description": "description",
"default": "default",
"required": true,
"type": "string"
}`,
param: parameter,
},
{
name: "not required",
json: `{
"name": "name",
"default": "default",
"type": "integer"
}`,
param: NewServicePlanParameter("name", "integer", "default", false),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := json.Marshal(tt.param)
if err != nil {
t.Errorf("marshalling failed: %v", err)
}
s := string(actual)
matcher := matchers.MatchJSONMatcher{JSONToMatch: tt.json}
success, err := matcher.Match(s)
if err != nil {
t.Errorf("couldn't match json: %v", err)
}
if !success {
t.Errorf("param: %v, got: %v", tt.json, s)
}
})
}
}
func TestAddKubernetesComponentToDevfile(t *testing.T) {
fs := devfileFileSystem.NewFakeFs()

View File

@@ -33,7 +33,7 @@ var _ = Describe("odo link and unlink command tests", func() {
It("should display the help", func() {
By("for the link command", func() {
appHelp := helper.Cmd("odo", "link", "-h").ShouldPass().Out()
helper.MatchAllInOutput(appHelp, []string{"Link component to a service ", "backed by an Operator or Service Catalog", "or component"})
helper.MatchAllInOutput(appHelp, []string{"Link component to a service ", "backed by an Operator", "or component"})
})
By("for the unlink command", func() {
appHelp := helper.Cmd("odo", "unlink", "-h").ShouldPass().Out()

View File

@@ -1,180 +0,0 @@
package integration
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/openshift/odo/tests/helper"
)
var _ = Describe("odo link and unlink command tests", func() {
//new clean context for each test
/*
Uncomment when we uncomment the test specs
var context1, context2 string
var oc helper.OcRunner
*/
var commonVar helper.CommonVar
// This is run before every Spec (It)
var _ = BeforeEach(func() {
// oc = helper.NewOcRunner("oc")
commonVar = helper.CommonBeforeEach()
//context1 = helper.CreateNewContext()
//context2 = helper.CreateNewContext()
})
// Clean up after the test
// This is run after every Spec (It)
var _ = AfterEach(func() {
//helper.DeleteDir(context1)
//helper.DeleteDir(context2)
helper.CommonAfterEach(commonVar)
})
Context("when running help for link and unlink command", func() {
It("should display the help", func() {
appHelp := helper.Cmd("odo", "link", "-h").ShouldPass().Out()
Expect(appHelp).To(ContainSubstring("Link component to a service"))
appHelp = helper.Cmd("odo", "unlink", "-h").ShouldPass().Out()
Expect(appHelp).To(ContainSubstring("Unlink component or service from a component"))
})
})
/*
Context("When link between components using wrong port", func() {
It("should fail", func() {
helper.CopyExample(filepath.Join("source", "nodejs"), context1)
helper.CmdShouldPass("odo", "create", "nodejs", "frontend", "--context", context1, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "push", "--context", context1)
helper.CopyExample(filepath.Join("source", "python"), context2)
helper.CmdShouldPass("odo", "create", "python", "backend", "--context", context2, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "push", "--context", context2)
stdErr := helper.CmdShouldFail("odo", "link", "backend", "--context", context1, "--port", "1234")
Expect(stdErr).To(ContainSubstring("Unable to properly link to component backend using port 1234"))
})
})
Context("When handling link/unlink between components", func() {
It("should link the frontend application to the backend and then unlink successfully", func() {
helper.CopyExample(filepath.Join("source", "nodejs"), context1)
helper.CmdShouldPass("odo", "create", "nodejs", "frontend", "--context", context1, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "url", "create", "--port", "8080", "--context", context1)
helper.CmdShouldPass("odo", "push", "--context", context1)
frontendURL := helper.DetermineRouteURL(context1)
helper.CopyExample(filepath.Join("source", "python"), context2)
helper.CmdShouldPass("odo", "create", "python", "backend", "--context", context2, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "url", "create", "--context", context2)
helper.CmdShouldPass("odo", "push", "--context", context2)
helper.CmdShouldPass("odo", "link", "backend", "--context", context1)
// ensure that the proper envFrom entry was created
envFromOutput := oc.GetEnvFromEntry("frontend", "app", commonVar.Project)
Expect(envFromOutput).To(ContainSubstring("backend"))
dcName := oc.GetDcName("frontend", commonVar.Project)
// wait for DeploymentConfig rollout to finish, so we can check if application is successfully running
oc.WaitForDCRollout(dcName, commonVar.Project, 20*time.Second)
helper.HttpWaitFor(frontendURL, "Hello world from node.js!", 20, 1)
outputErr := helper.CmdShouldFail("odo", "link", "backend", "--context", context1)
Expect(outputErr).To(ContainSubstring("been linked"))
helper.CmdShouldPass("odo", "unlink", "backend", "--context", context1)
})
})
Context("When link backend between component and service", func() {
It("should link backend to service successfully", func() {
helper.CopyExample(filepath.Join("source", "nodejs"), context1)
helper.CmdShouldPass("odo", "create", "nodejs", "frontend", "--context", context1, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "push", "--context", context1)
helper.CopyExample(filepath.Join("source", "python"), context2)
helper.CmdShouldPass("odo", "create", "python", "backend", "--context", context2, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "push", "--context", context2)
helper.CmdShouldPass("odo", "link", "backend", "--context", context1) // context1 is the frontend
// Switching to context2 dir because --context flag is not supported with service command
helper.Chdir(context2)
helper.CmdShouldPass("odo", "service", "create", "mysql-persistent")
ocArgs := []string{"get", "serviceinstance", "-n", commonVar.Project, "-o", "name"}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "mysql-persistent")
})
helper.CmdShouldPass("odo", "link", "mysql-persistent", "--wait-for-target", "--component", "backend", "--project", commonVar.Project)
// ensure that the proper envFrom entry was created
envFromOutput := oc.GetEnvFromEntry("backend", "app", commonVar.Project)
Expect(envFromOutput).To(ContainSubstring("mysql-persistent"))
outputErr := helper.CmdShouldFail("odo", "link", "mysql-persistent", "--context", context2)
Expect(outputErr).To(ContainSubstring("been linked"))
})
})
Context("When deleting service and unlink the backend from the frontend", func() {
It("should pass", func() {
helper.CopyExample(filepath.Join("source", "nodejs"), context1)
helper.CmdShouldPass("odo", "create", "nodejs", "frontend", "--context", context1, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "push", "--context", context1)
helper.CopyExample(filepath.Join("source", "python"), context2)
helper.CmdShouldPass("odo", "create", "python", "backend", "--context", context2, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "push", "--context", context2)
helper.CmdShouldPass("odo", "link", "backend", "--context", context1)
helper.Chdir(context2)
helper.CmdShouldPass("odo", "service", "create", "mysql-persistent")
ocArgs := []string{"get", "serviceinstance", "-n", commonVar.Project, "-o", "name"}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "mysql-persistent")
})
helper.CmdShouldPass("odo", "service", "delete", "mysql-persistent", "-f")
// ensure that the backend no longer has an envFrom value
backendEnvFromOutput := oc.GetEnvFromEntry("backend", "app", commonVar.Project)
Expect(backendEnvFromOutput).To(Equal("''"))
// ensure that the frontend envFrom was not changed
frontEndEnvFromOutput := oc.GetEnvFromEntry("frontend", "app", commonVar.Project)
Expect(frontEndEnvFromOutput).To(ContainSubstring("backend"))
helper.CmdShouldPass("odo", "unlink", "backend", "--component", "frontend", "--project", commonVar.Project)
// ensure that the proper envFrom entry was created
envFromOutput := oc.GetEnvFromEntry("frontend", "app", commonVar.Project)
Expect(envFromOutput).To(Equal("''"))
})
})
Context("When linking or unlinking a service or component", func() {
It("should print the environment variables being linked/unlinked", func() {
helper.CopyExample(filepath.Join("source", "python"), context1)
helper.CmdShouldPass("odo", "create", "python", "component1", "--context", context1, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "push", "--context", context1)
helper.CopyExample(filepath.Join("source", "nodejs"), context2)
helper.CmdShouldPass("odo", "create", "nodejs", "component2", "--context", context2, "--project", commonVar.Project)
helper.CmdShouldPass("odo", "push", "--context", context2)
// tests for linking a component to a component
stdOut := helper.CmdShouldPass("odo", "link", "component2", "--context", context1)
helper.MatchAllInOutput(stdOut, []string{"The below secret environment variables were added", "COMPONENT_COMPONENT2_HOST", "COMPONENT_COMPONENT2_PORT"})
// tests for unlinking a component from a component
stdOut = helper.CmdShouldPass("odo", "unlink", "component2", "--context", context1)
helper.MatchAllInOutput(stdOut, []string{"The below secret environment variables were removed", "COMPONENT_COMPONENT2_HOST", "COMPONENT_COMPONENT2_PORT"})
// first create a service
helper.CmdShouldPass("odo", "service", "create", "-w", "dh-postgresql-apb", "--project", commonVar.Project, "--plan", "dev",
"-p", "postgresql_user=luke", "-p", "postgresql_password=secret",
"-p", "postgresql_database=my_data", "-p", "postgresql_version=9.6")
ocArgs := []string{"get", "serviceinstance", "-o", "name", "-n", commonVar.Project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "dh-postgresql-apb")
})
// tests for linking a service to a component
stdOut = helper.CmdShouldPass("odo", "link", "dh-postgresql-apb", "--context", context1)
helper.MatchAllInOutput(stdOut, []string{"The below secret environment variables were added", "DB_PORT", "DB_HOST"})
// tests for unlinking a service to a component
stdOut = helper.CmdShouldPass("odo", "unlink", "dh-postgresql-apb", "--context", context1)
helper.MatchAllInOutput(stdOut, []string{"The below secret environment variables were removed", "DB_PORT", "DB_HOST"})
})
})
*/
})

View File

@@ -1,421 +0,0 @@
package integration
import (
"path/filepath"
"strings"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/openshift/odo/tests/helper"
)
var _ = Describe("odo service command tests", func() {
var app, serviceName string
/*
Uncomment when we uncomment the test specs
var context1, context2 string
var oc helper.OcRunner
*/
var commonVar helper.CommonVar
// This is run before every Spec (It)
var _ = BeforeEach(func() {
//oc = helper.NewOcRunner("oc")
commonVar = helper.CommonBeforeEach()
// context1 = helper.CreateNewContext()
// context2 = helper.CreateNewContext()
})
// Clean up after the test
// This is run after every Spec (It)
var _ = AfterEach(func() {
helper.CommonAfterEach(commonVar)
// helper.DeleteDir(context1)
// helper.DeleteDir(context2)
})
Context("when running help for service command", func() {
It("should display the help", func() {
appHelp := helper.Cmd("odo", "service", "-h").ShouldPass().Out()
Expect(appHelp).To(ContainSubstring("Perform service catalog operations"))
})
})
Context("check catalog service search functionality", func() {
It("check that a service does not exist", func() {
serviceRandomName := helper.RandString(7)
output := helper.Cmd("odo", "catalog", "search", "service", serviceRandomName).ShouldFail().Err()
Expect(output).To(ContainSubstring("no service matched the query: " + serviceRandomName))
})
})
Context("checking machine readable output for service catalog", func() {
It("should succeed listing catalog components", func() {
// Since service catalog is constantly changing, we simply check to see if this command passes.. rather than checking the JSON each time.
output := helper.Cmd("odo", "catalog", "list", "services", "-o", "json").ShouldPass().Out()
Expect(output).To(ContainSubstring("List"))
})
})
Context("checking machine readable output for service catalog", func() {
It("should succeed listing catalog components", func() {
// Since service catalog is constantly changing, we simply check to see if this command passes.. rather than checking the JSON each time.
helper.Cmd("odo", "catalog", "list", "services", "-o", "json").ShouldPass()
})
})
Context("check search functionality", func() {
It("should pass with searching for part of a service name", func() {
// We just use "sql" as some catalogs only have postgresql-persistent and
// others dh-postgresql-db. So let's just see if there's "any" postgresql to begin
// with
output := helper.Cmd("odo", "catalog", "search", "service", "sql").ShouldPass().Out()
Expect(output).To(ContainSubstring("postgresql"))
})
})
Context("create service with Env non-interactively", func() {
JustBeforeEach(func() {
app = helper.RandString(7)
})
It("should be able to create postgresql with env", func() {
helper.Cmd("odo", "service", "create", "dh-postgresql-apb", "--project", commonVar.Project, "--app", app,
"--plan", "dev", "-p", "postgresql_user=lukecage", "-p", "postgresql_password=secret",
"-p", "postgresql_database=my_data", "-p", "postgresql_version=9.6", "-w").ShouldPass()
// there is only a single pod in the project
ocArgs := []string{"describe", "pod", "-n", commonVar.Project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "lukecage")
})
// Delete the service
helper.Cmd("odo", "service", "delete", "dh-postgresql-apb", "-f", "--app", app, "--project", commonVar.Project).ShouldPass()
})
It("should be able to create postgresql with env multiple times", func() {
helper.Cmd("odo", "service", "create", "dh-postgresql-apb", "--project", commonVar.Project, "--app", app,
"--plan", "dev", "-p", "postgresql_user=lukecage", "-p", "postgresql_user=testworker", "-p", "postgresql_password=secret",
"-p", "postgresql_password=universe", "-p", "postgresql_database=my_data", "-p", "postgresql_version=9.6", "-w").ShouldPass()
// there is only a single pod in the project
ocArgs := []string{"describe", "pod", "-n", commonVar.Project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "testworker")
})
// Delete the service
helper.Cmd("odo", "service", "delete", "dh-postgresql-apb", "-f", "--app", app, "--project", commonVar.Project).ShouldPass()
})
})
/*
Context("When creating with a spring boot application", func() {
JustBeforeEach(func() {
context = helper.CreateNewContext()
os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml"))
project = helper.CreateRandProject()
originalDir = helper.Getwd()
helper.Chdir(context)
})
JustAfterEach(func() {
helper.DeleteProject(project)
helper.Chdir(originalDir)
helper.DeleteDir(context)
os.Unsetenv("GLOBALODOCONFIG")
})
It("should be able to create postgresql and link it with springboot", func() {
oc.ImportJavaIS(project)
helper.CopyExample(filepath.Join("source", "openjdk-sb-postgresql"), context)
// Local config needs to be present in order to create service https://github.com/openshift/odo/issues/1602
helper.CmdShouldPass("odo", "create", "--s2i", "java:8", "sb-app", "--project", project)
// Create a URL
helper.CmdShouldPass("odo", "url", "create", "--port", "8080")
// push
helper.CmdShouldPass("odo", "push")
// create the postgres service
helper.CmdShouldPass("odo", "service", "create", "dh-postgresql-apb", "--project", project, "--plan", "dev",
"-p", "postgresql_user=luke", "-p", "postgresql_password=secret",
"-p", "postgresql_database=my_data", "-p", "postgresql_version=9.6", "-w")
// link the service
helper.CmdShouldPass("odo", "link", "dh-postgresql-apb", "--project", project, "-w", "--wait-for-target")
odoArgs := []string{"service", "list"}
helper.WaitForCmdOut("odo", odoArgs, 1, true, func(output string) bool {
return strings.Contains(output, "dh-postgresql-apb") &&
strings.Contains(output, "ProvisionedAndLinked")
})
routeURL := helper.DetermineRouteURL("")
// Ping said URL
helper.HttpWaitFor(routeURL, "Spring Boot", 90, 1)
// Delete the service
helper.CmdShouldPass("odo", "service", "delete", "dh-postgresql-apb", "-f")
// Delete the component and the config
helper.CmdShouldPass("odo", "delete", "sb-app", "-f", "--all")
})
})
*/
// TODO: auth issue, we need to find a proper way how to test it without requiring cluster admin privileges
// Context("odo hides a hidden service in service catalog", func() {
// It("not show a hidden service in the catalog", func() {
// runCmdShouldPass("oc apply -f https://github.com/openshift/library/raw/master/official/sso/templates/sso72-https.json -n openshift")
// outputErr := runCmdShouldFail("odo catalog search service sso72-https")
// Expect(outputErr).To(ContainSubstring("No service matched the query: sso72-https"))
// })
// })
Context("When working from outside a component dir", func() {
JustBeforeEach(func() {
app = helper.RandString(7)
serviceName = "odo-postgres-service"
helper.Chdir(commonVar.Context)
})
It("should be able to create, list and delete a service using a given value for --context", func() {
// create a component by copying the example
helper.CopyExample(filepath.Join("source", "python"), commonVar.Context)
helper.Cmd("odo", "create", "--s2i", "python", "--app", app, "--project", commonVar.Project).ShouldPass()
// cd to the originalDir to create service using --context
helper.Chdir(commonVar.OriginalWorkingDirectory)
helper.Cmd("odo", "service", "create", "dh-postgresql-apb", "--plan", "dev",
"-p", "postgresql_user=luke", "-p", "postgresql_password=secret",
"-p", "postgresql_database=my_data", "-p", "postgresql_version=9.6", serviceName,
"--context", commonVar.Context,
).ShouldPass()
// now check if listing the service using --context works
ocArgs := []string{"get", "serviceinstance", "-o", "name", "-n", commonVar.Project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, serviceName)
})
stdOut := helper.Cmd("odo", "service", "list", "--context", commonVar.Context).ShouldPass().Out()
Expect(stdOut).To(ContainSubstring(serviceName))
// now check if deleting the service using --context works
stdOut = helper.Cmd("odo", "service", "delete", "-f", serviceName, "--context", commonVar.Context).ShouldPass().Out()
Expect(stdOut).To(ContainSubstring(serviceName))
})
It("should be able to list services, as well as json list in a given app and project combination", func() {
// create a component by copying the example
helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context)
helper.Cmd("odo", "create", "--s2i", "nodejs", "--app", app, "--project", commonVar.Project).ShouldPass()
// create a service from within a component directory
helper.Cmd("odo", "service", "create", "dh-prometheus-apb", "--plan", "ephemeral",
"--app", app, "--project", commonVar.Project,
).ShouldPass()
ocArgs := []string{"get", "serviceinstance", "-o", "name", "-n", commonVar.Project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "dh-prometheus-apb")
})
// Listing the services should work as expected from within the component directory.
// This means, it should not require --app or --project flags
stdOut := helper.Cmd("odo", "service", "list").ShouldPass().Out()
Expect(stdOut).To(ContainSubstring("dh-prometheus-apb"))
// Check json output
stdOut = helper.Cmd("odo", "service", "list", "-o", "json").ShouldPass().Out()
helper.MatchAllInOutput(stdOut, []string{"dh-prometheus-apb", "List"})
// cd to a non-component directory and list services
helper.Chdir(commonVar.OriginalWorkingDirectory)
stdOut = helper.Cmd("odo", "service", "list", "--app", app, "--project", commonVar.Project).ShouldPass().Out()
Expect(stdOut).To(ContainSubstring("dh-prometheus-apb"))
// Check json output
helper.Chdir(commonVar.OriginalWorkingDirectory)
stdOut = helper.Cmd("odo", "service", "list", "--app", app, "--project", commonVar.Project, "-o", "json").ShouldPass().Out()
helper.MatchAllInOutput(stdOut, []string{"dh-prometheus-apb", "List"})
})
})
Context("When working from outside a component dir", func() {
It("should be able to create, list and delete services without a context and using --app and --project flags instaed", func() {
app = helper.RandString(7)
// create the service
helper.Cmd("odo", "service", "create", "dh-postgresql-apb", "--plan", "dev",
"-p", "postgresql_user=luke", "-p", "postgresql_password=secret",
"-p", "postgresql_database=my_data", "-p", "postgresql_version=9.6",
"--app", app, "--project", commonVar.Project).ShouldPass()
ocArgs := []string{"get", "serviceinstance", "-o", "name", "-n", commonVar.Project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "dh-postgresql-apb")
})
// list the service using app and project flags
stdOut := helper.Cmd("odo", "service", "list", "--app", app, "--project", commonVar.Project).ShouldPass().Out()
Expect(stdOut).To(ContainSubstring("dh-postgresql-apb"))
// delete the service using app and project flags
helper.Cmd("odo", "service", "delete", "-f", "dh-postgresql-apb", "--app", app, "--project", commonVar.Project).ShouldPass()
})
})
/*
Context("When link backend between component and service", func() {
JustBeforeEach(func() {
preSetup()
})
JustAfterEach(func() {
cleanPreSetup()
})
It("should link backend to service successfully", func() {
helper.CopyExample(filepath.Join("source", "nodejs"), context1)
helper.CmdShouldPass("odo", "create", "--s2i", "nodejs", "frontend", "--context", context1, "--project", project)
helper.CmdShouldPass("odo", "push", "--context", context1)
helper.CopyExample(filepath.Join("source", "python"), context2)
helper.CmdShouldPass("odo", "create", "--s2i", "python", "backend", "--context", context2, "--project", project)
helper.CmdShouldPass("odo", "push", "--context", context2)
helper.CmdShouldPass("odo", "link", "backend", "--context", context1)
// Switching to context2 dir because --context flag is not supported with service command
helper.Chdir(context2)
helper.CmdShouldPass("odo", "service", "create", "mysql-persistent")
ocArgs := []string{"get", "serviceinstance", "-n", project, "-o", "name"}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "mysql-persistent")
})
helper.CmdShouldPass("odo", "link", "mysql-persistent", "--wait-for-target", "--component", "backend", "--project", project)
// ensure that the proper envFrom entry was created
envFromOutput := oc.GetEnvFromEntry("backend", "app", project)
Expect(envFromOutput).To(ContainSubstring("mysql-persistent"))
outputErr := helper.CmdShouldFail("odo", "link", "mysql-persistent", "--context", context2)
Expect(outputErr).To(ContainSubstring("been linked"))
})
})
*/
/*
Context("When deleting service and unlink the backend from the frontend", func() {
JustBeforeEach(func() {
preSetup()
})
JustAfterEach(func() {
cleanPreSetup()
})
It("should pass", func() {
helper.CopyExample(filepath.Join("source", "nodejs"), context1)
helper.CmdShouldPass("odo", "create", "--s2i", "nodejs", "frontend", "--context", context1, "--project", project)
helper.CmdShouldPass("odo", "push", "--context", context1)
helper.CopyExample(filepath.Join("source", "python"), context2)
helper.CmdShouldPass("odo", "create", "--s2i", "python", "backend", "--context", context2, "--project", project)
helper.CmdShouldPass("odo", "push", "--context", context2)
helper.CmdShouldPass("odo", "link", "backend", "--context", context1)
helper.Chdir(context2)
helper.CmdShouldPass("odo", "service", "create", "mysql-persistent")
ocArgs := []string{"get", "serviceinstance", "-n", project, "-o", "name"}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "mysql-persistent")
})
helper.CmdShouldPass("odo", "service", "delete", "mysql-persistent", "-f")
// ensure that the backend no longer has an envFrom value
backendEnvFromOutput := oc.GetEnvFromEntry("backend", "app", project)
Expect(backendEnvFromOutput).To(Equal("''"))
// ensure that the frontend envFrom was not changed
frontEndEnvFromOutput := oc.GetEnvFromEntry("frontend", "app", project)
Expect(frontEndEnvFromOutput).To(ContainSubstring("backend"))
helper.CmdShouldPass("odo", "unlink", "backend", "--component", "frontend", "--project", project)
// ensure that the proper envFrom entry was created
envFromOutput := oc.GetEnvFromEntry("frontend", "app", project)
Expect(envFromOutput).To(Equal("''"))
})
})
*/
/*
Context("When linking or unlinking a service or component", func() {
JustBeforeEach(func() {
preSetup()
})
JustAfterEach(func() {
cleanPreSetup()
})
It("should print the environment variables being linked/unlinked", func() {
helper.CopyExample(filepath.Join("source", "python"), context1)
helper.CmdShouldPass("odo", "create", "--s2i", "python", "component1", "--context", context1, "--project", project)
helper.CmdShouldPass("odo", "push", "--context", context1)
helper.CopyExample(filepath.Join("source", "nodejs"), context2)
helper.CmdShouldPass("odo", "create", "--s2i", "nodejs", "component2", "--context", context2, "--project", project)
helper.CmdShouldPass("odo", "push", "--context", context2)
// tests for linking a component to a component
stdOut := helper.CmdShouldPass("odo", "link", "component2", "--context", context1)
helper.MatchAllInOutput(stdOut, []string{"The below secret environment variables were added", "COMPONENT_COMPONENT2_HOST", "COMPONENT_COMPONENT2_PORT"})
// tests for unlinking a component from a component
stdOut = helper.CmdShouldPass("odo", "unlink", "component2", "--context", context1)
helper.MatchAllInOutput(stdOut, []string{"The below secret environment variables were removed", "COMPONENT_COMPONENT2_HOST", "COMPONENT_COMPONENT2_PORT"})
// first create a service
helper.CmdShouldPass("odo", "service", "create", "-w", "dh-postgresql-apb", "--project", project, "--plan", "dev",
"-p", "postgresql_user=luke", "-p", "postgresql_password=secret",
"-p", "postgresql_database=my_data", "-p", "postgresql_version=9.6")
ocArgs := []string{"get", "serviceinstance", "-o", "name", "-n", project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "dh-postgresql-apb")
})
// tests for linking a service to a component
stdOut = helper.CmdShouldPass("odo", "link", "dh-postgresql-apb", "--context", context1)
helper.MatchAllInOutput(stdOut, []string{"The below secret environment variables were added", "DB_PORT", "DB_HOST"})
// tests for unlinking a service to a component
stdOut = helper.CmdShouldPass("odo", "unlink", "dh-postgresql-apb", "--context", context1)
helper.MatchAllInOutput(stdOut, []string{"The below secret environment variables were removed", "DB_PORT", "DB_HOST"})
})
})
*/
Context("When describing services", func() {
It("should succeed when we're describing service that could have integer value for default field", func() {
// https://github.com/openshift/odo/issues/2488
helper.CopyExample(filepath.Join("source", "python"), commonVar.Context)
helper.Cmd("odo", "create", "--s2i", "python", "component1", "--context", commonVar.Context, "--project", commonVar.Project).ShouldPass()
helper.Chdir(commonVar.Context)
helper.Cmd("odo", "catalog", "describe", "service", "dh-es-apb").ShouldPass()
helper.Cmd("odo", "catalog", "describe", "service", "dh-import-vm-apb").ShouldPass()
})
})
Context("When the application is deleted", func() {
JustBeforeEach(func() {
app = helper.RandString(6)
})
It("should delete the service(s) in the application as well", func() {
helper.Cmd("odo", "service", "create", "--app", app, "-w", "dh-postgresql-apb", "--project", commonVar.Project, "--plan", "dev",
"-p", "postgresql_user=luke", "-p", "postgresql_password=secret",
"-p", "postgresql_database=my_data", "-p", "postgresql_version=9.6").ShouldPass()
ocArgs := []string{"get", "serviceinstance", "-o", "name", "-n", commonVar.Project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "dh-postgresql-apb")
})
helper.Cmd("odo", "app", "delete", app, "--project", commonVar.Project, "-f").ShouldPass()
ocArgs = []string{"get", "serviceinstances", "-n", commonVar.Project}
helper.WaitForCmdOut("oc", ocArgs, 1, true, func(output string) bool {
return strings.Contains(output, "No resources found")
}, true)
})
})
})

View File

@@ -1,11 +0,0 @@
package integration
import (
"testing"
"github.com/openshift/odo/tests/helper"
)
func TestServicecatalog(t *testing.T) {
helper.RunTestSpecs(t, "Servicecatalog Suite")
}