Adds app commands for devfile components (#4007)

* Adds app commands for devfile components

* Fixes log messages and comments

* Fixes s2i app test script

* Fixes s2i local config components

* Removes devfile test for help

* Adds unit test cases for List()

* Updates integration tests

* Fixed alias in import
This commit is contained in:
Mrinal Das
2020-10-12 18:06:00 +05:30
committed by GitHub
parent 49853dc69d
commit b1245a5337
28 changed files with 694 additions and 219 deletions

2
.gitignore vendored
View File

@@ -87,4 +87,4 @@ tags
_site/
.sass-cache/
.jekyll-cache/
.jekyll-metadata
.jekyll-metadata

View File

@@ -240,6 +240,11 @@ test-cmd-docker-devfile-exec:
test-cmd-devfile-watch:
ginkgo $(GINKGO_FLAGS) -focus="odo devfile watch command tests" tests/integration/devfile/
# Run odo devfile app command tests
.PHONY: test-cmd-devfile-app
test-cmd-devfile-app:
ginkgo $(GINKGO_FLAGS) -focus="odo devfile app command tests" tests/integration/devfile/
# Run odo devfile delete command tests
.PHONY: test-cmd-devfile-delete
test-cmd-devfile-delete:

View File

@@ -1,6 +1,7 @@
package application
import (
"github.com/openshift/odo/pkg/kclient"
"github.com/pkg/errors"
"k8s.io/klog"
@@ -33,13 +34,27 @@ func ListInProject(client *occlient.Client) ([]string, error) {
return appNames, nil
}
// Get all DeploymentConfigs with the "app" label
deploymentConfigAppNames, err := client.GetDeploymentConfigLabelValues(applabels.ApplicationLabel, applabels.ApplicationLabel)
deploymentSupported, err := client.IsDeploymentConfigSupported()
if err != nil {
return nil, errors.Wrap(err, "unable to list applications from deployment config")
return nil, err
}
appNames = append(appNames, deploymentConfigAppNames...)
// Get all DeploymentConfigs with the "app" label
deploymentAppNames, err := client.GetKubeClient().GetDeploymentLabelValues(applabels.ApplicationLabel, applabels.ApplicationLabel)
if err != nil {
return nil, errors.Wrap(err, "unable to list applications from deployments")
}
appNames = append(appNames, deploymentAppNames...)
if deploymentSupported {
// Get all DeploymentConfigs with the "app" label
deploymentConfigAppNames, err := client.GetDeploymentConfigLabelValues(applabels.ApplicationLabel, applabels.ApplicationLabel)
if err != nil {
return nil, errors.Wrap(err, "unable to list applications from deployment config")
}
appNames = append(appNames, deploymentConfigAppNames...)
}
// Get all ServiceInstances with the "app" label
// Okay, so there is an edge-case here.. if Service Catalog is *not* enabled in the cluster, we shouldn't error out..
@@ -48,7 +63,7 @@ func ListInProject(client *occlient.Client) ([]string, error) {
if err != nil {
klog.V(4).Infof("Unable to list Service Catalog instances: %s", err)
} else {
appNames = append(deploymentConfigAppNames, serviceInstanceAppNames...)
appNames = append(appNames, serviceInstanceAppNames...)
}
// Filter out any names, as there could be multiple components but within the same application
@@ -56,9 +71,10 @@ func ListInProject(client *occlient.Client) ([]string, error) {
}
// Exists checks whether the given app exist or not
func Exists(app string, client *occlient.Client) (bool, error) {
func Exists(app string, client *occlient.Client, kClient *kclient.Client) (bool, error) {
appList, err := List(client)
if err != nil {
return false, err
}
@@ -90,8 +106,21 @@ func Delete(client *occlient.Client, name string) error {
}
}
}
supported, err := client.IsDeploymentConfigSupported()
if err != nil {
return err
}
if supported {
// delete application from cluster
err = client.Delete(labels, false)
if err != nil {
return errors.Wrapf(err, "unable to delete application %s", name)
}
}
// delete application from cluster
err = client.Delete(labels, false)
err = client.GetKubeClient().Delete(labels, false)
if err != nil {
return errors.Wrapf(err, "unable to delete application %s", name)
}

View File

@@ -5,6 +5,7 @@ import (
"bytes"
"fmt"
"io"
v1 "k8s.io/api/apps/v1"
"os"
"path/filepath"
"sort"
@@ -612,6 +613,7 @@ func ApplyConfig(client *occlient.Client, kClient *kclient.Client, componentConf
applicationName = componentConfig.GetApplication()
} else {
componentName = envSpecificInfo.GetName()
applicationName = envSpecificInfo.GetApplication()
}
isRouteSupported := false
@@ -861,33 +863,52 @@ func List(client *occlient.Client, applicationName string, localConfigInfo *conf
applicationSelector = fmt.Sprintf("%s=%s", applabels.ApplicationLabel, applicationName)
}
deploymentConfigSupported := false
var err error
var deploymentList []v1.Deployment
var components []Component
componentNamesMap := make(map[string]bool)
if client != nil {
project, err := client.GetProject(client.Namespace)
deploymentConfigSupported, err = client.IsDeploymentConfigSupported()
if err != nil {
return ComponentList{}, err
}
if project != nil {
// retrieve all the deployment configs that are associated with this application
dcList, err := client.GetDeploymentConfigsFromSelector(applicationSelector)
if err != nil {
return ComponentList{}, errors.Wrapf(err, "unable to list components")
}
// retrieve all the deployments that are associated with this application
deploymentList, err = client.GetKubeClient().GetDeploymentFromSelector(applicationSelector)
if err != nil {
return ComponentList{}, errors.Wrapf(err, "unable to list components")
}
}
// extract the labels we care about from each component
for _, elem := range dcList {
component, err := GetComponent(client, elem.Labels[componentlabels.ComponentLabel], applicationName, client.Namespace)
if err != nil {
return ComponentList{}, errors.Wrap(err, "Unable to get component")
}
components = append(components, component)
componentNamesMap[component.Name] = true
}
// extract the labels we care about from each component
for _, elem := range deploymentList {
component, err := GetComponent(client, elem.Labels[componentlabels.ComponentLabel], applicationName, client.Namespace)
if err != nil {
return ComponentList{}, errors.Wrap(err, "Unable to get component")
}
components = append(components, component)
componentNamesMap[component.Name] = true
}
if deploymentConfigSupported && client != nil {
// retrieve all the deployment configs that are associated with this application
dcList, err := client.GetDeploymentConfigsFromSelector(applicationSelector)
if err != nil {
return ComponentList{}, errors.Wrapf(err, "unable to list components")
}
// extract the labels we care about from each component
for _, elem := range dcList {
component, err := GetComponent(client, elem.Labels[componentlabels.ComponentLabel], applicationName, client.Namespace)
if err != nil {
return ComponentList{}, errors.Wrap(err, "Unable to get component")
}
components = append(components, component)
componentNamesMap[component.Name] = true
}
}
if localConfigInfo != nil {
@@ -1437,6 +1458,7 @@ func getRemoteComponentMetadata(client *occlient.Client, componentName string, a
if err != nil {
return Component{}, err
}
component.Spec.URLSpec = urls
urlsNb := len(urls)
if urlsNb > 0 {
res := make([]string, 0, urlsNb)
@@ -1449,15 +1471,17 @@ func getRemoteComponentMetadata(client *occlient.Client, componentName string, a
// Storage
if getStorage {
appStore, err := storage.List(client, componentName, applicationName)
appStore, err := fromCluster.GetStorage()
if err != nil {
return Component{}, errors.Wrap(err, "unable to get storage list")
}
var storage []string
for _, store := range appStore.Items {
storage = append(storage, store.Name)
component.Spec.StorageSpec = appStore
var storageList []string
for _, store := range appStore {
storageList = append(storageList, store.Name)
}
component.Spec.Storage = storage
component.Spec.Storage = storageList
}
// Environment Variables

View File

@@ -3,6 +3,7 @@ package component
import (
"fmt"
"io/ioutil"
v1 "k8s.io/api/apps/v1"
"os"
"path/filepath"
"reflect"
@@ -268,20 +269,37 @@ func TestList(t *testing.T) {
getFakeDC("test", "test", "otherApp", "python"),
}}
const caseName = "Case 5: List component when openshift cluster not reachable"
deploymentList := v1.DeploymentList{Items: []v1.Deployment{
*testingutil.CreateFakeDeployment("comp0"),
*testingutil.CreateFakeDeployment("comp1"),
}}
deploymentList.Items[0].Labels[componentlabels.ComponentTypeLabel] = "nodejs"
deploymentList.Items[0].Annotations = map[string]string{
ComponentSourceTypeAnnotation: "local",
}
deploymentList.Items[1].Labels[componentlabels.ComponentTypeLabel] = "wildfly"
deploymentList.Items[1].Annotations = map[string]string{
ComponentSourceTypeAnnotation: "local",
}
const caseName = "Case 4: List component when openshift cluster not reachable"
tests := []struct {
name string
dcList appsv1.DeploymentConfigList
projectExists bool
wantErr bool
existingLocalConfigInfo *LocalConfigInfo
output ComponentList
name string
dcList appsv1.DeploymentConfigList
deploymentConfigSupported bool
deploymentList v1.DeploymentList
projectExists bool
wantErr bool
existingLocalConfigInfo *LocalConfigInfo
output ComponentList
}{
{
name: "Case 1: Components are returned",
dcList: dcList,
wantErr: false,
projectExists: true,
name: "Case 1: Components are returned",
dcList: dcList,
deploymentConfigSupported: true,
wantErr: false,
projectExists: true,
output: ComponentList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
@@ -295,23 +313,18 @@ func TestList(t *testing.T) {
},
},
{
name: "Case 2: projects doesn't exist",
wantErr: false,
projectExists: false,
output: GetMachineReadableFormatForList([]Component{}),
},
{
name: "Case 3: no component and no config exists",
name: "Case 2: no component and no config exists",
wantErr: false,
projectExists: true,
output: GetMachineReadableFormatForList([]Component{}),
},
{
name: "Case 4: Components are returned from the config plus and cluster",
dcList: dcList,
wantErr: false,
projectExists: true,
existingLocalConfigInfo: &existingSampleLocalConfig,
name: "Case 3: Components are returned from the config plus and cluster",
dcList: dcList,
deploymentConfigSupported: true,
wantErr: false,
projectExists: true,
existingLocalConfigInfo: &existingSampleLocalConfig,
output: ComponentList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
@@ -332,25 +345,68 @@ func TestList(t *testing.T) {
existingLocalConfigInfo: &existingSampleLocalConfig,
output: GetMachineReadableFormatForList([]Component{componentConfig2}),
},
{
name: "Case 5: Components are returned from deployments on a kubernetes cluster",
dcList: dcList,
deploymentList: deploymentList,
wantErr: false,
projectExists: true,
deploymentConfigSupported: false,
output: ComponentList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "odo.dev/v1alpha1",
},
ListMeta: metav1.ListMeta{},
Items: []Component{
getFakeComponent("comp0", "test", "app", "nodejs", StateTypePushed),
getFakeComponent("comp1", "test", "app", "wildfly", StateTypePushed),
},
},
},
{
name: "Case 6: Components are returned from both",
dcList: dcList,
deploymentList: deploymentList,
wantErr: false,
projectExists: true,
deploymentConfigSupported: true,
output: ComponentList{
TypeMeta: metav1.TypeMeta{
Kind: "List",
APIVersion: "odo.dev/v1alpha1",
},
ListMeta: metav1.ListMeta{},
Items: []Component{
getFakeComponent("comp0", "test", "app", "nodejs", StateTypePushed),
getFakeComponent("comp1", "test", "app", "wildfly", StateTypePushed),
getFakeComponent("frontend", "test", "app", "nodejs", StateTypePushed),
getFakeComponent("backend", "test", "app", "java", StateTypePushed),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.deploymentConfigSupported {
os.Setenv("KUBERNETES", "true")
defer os.Unsetenv("KUBERNETES")
}
client, fakeClientSet := occlient.FakeNew()
client.Namespace = "test"
fakeClientSet.ProjClientset.PrependReactor("get", "projects", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
if !tt.projectExists {
return true, nil, nil
}
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
})
//fake the deployments
fakeClientSet.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.deploymentList, nil
})
// Prepend reactor returns the last matched reactor added
// We need to return errorNotFound for localconfig only component
fakeClientSet.AppsClientset.PrependReactor("get", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) {
@@ -376,8 +432,18 @@ func TestList(t *testing.T) {
// simulate unavailable cluster
return true, nil, errors.NewUnauthorized("user unauthorized")
}
// the only other time this is called is when attempting to retrieve the state of the local component that is not pushed yet (case 4)
return true, nil, errors.NewNotFound(schema.GroupResource{Resource: "deployments"}, "comp")
getAction, ok := action.(ktesting.GetAction)
if !ok {
return false, nil, fmt.Errorf("expected a GetAction, got %v", action)
}
switch getAction.GetName() {
case "comp0":
return true, &tt.deploymentList.Items[0], nil
case "comp1":
return true, &tt.deploymentList.Items[1], nil
default:
return true, nil, errors.NewNotFound(schema.GroupResource{Resource: "deploymentconfigs"}, "")
}
})
results, err := List(client, "app", tt.existingLocalConfigInfo)

View File

@@ -8,6 +8,7 @@ import (
componentlabels "github.com/openshift/odo/pkg/component/labels"
"github.com/openshift/odo/pkg/config"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/storage"
"github.com/openshift/odo/pkg/url"
"github.com/openshift/odo/pkg/util"
"github.com/pkg/errors"
@@ -32,11 +33,13 @@ type PushedComponent interface {
GetApplication() string
GetType() (string, error)
GetSource() (string, string, error)
GetStorage() ([]storage.Storage, error)
}
type defaultPushedComponent struct {
application string
urls []url.URL
storage []storage.Storage
provider provider
client *occlient.Client
}
@@ -84,14 +87,33 @@ func (d defaultPushedComponent) GetURLs() ([]url.URL, error) {
if err != nil && !isIgnorableError(err) {
return []url.URL{}, err
}
urls := make([]url.URL, 0, len(routes.Items)+len(ingresses.Items))
urls = append(urls, routes.Items...)
urls = append(urls, ingresses.Items...)
d.urls = urls
d.urls = append(routes.Items, ingresses.Items...)
}
return d.urls, nil
}
func (d defaultPushedComponent) GetStorage() ([]storage.Storage, error) {
if d.storage == nil {
var storageItems []storage.Storage
if _, ok := d.provider.(*s2iComponent); ok {
storageList, err := storage.ListMounted(d.client, d.GetName(), d.GetApplication())
if err != nil {
return nil, err
}
storageItems = append(storageItems, storageList.Items...)
}
if _, ok := d.provider.(*devfileComponent); ok {
storageList, err := storage.DevfileListMounted(d.client.GetKubeClient(), d.GetName())
if err != nil {
return nil, err
}
storageItems = append(storageItems, storageList.Items...)
}
d.storage = storageItems
}
return d.storage, nil
}
func (d defaultPushedComponent) GetApplication() string {
return d.application
}
@@ -211,27 +233,29 @@ func getType(component provider) (string, error) {
// GetPushedComponents retrieves a map of PushedComponents from the cluster, keyed by their name
func GetPushedComponents(c *occlient.Client, applicationName string) (map[string]PushedComponent, error) {
applicationSelector := fmt.Sprintf("%s=%s", applabels.ApplicationLabel, applicationName)
dcList, err := c.GetDeploymentConfigsFromSelector(applicationSelector)
if err != nil {
if isIgnorableError(err) {
dList, err := c.GetKubeClient().ListDeployments(applicationSelector)
if err != nil {
return nil, errors.Wrapf(err, "unable to list components")
}
res := make(map[string]PushedComponent, len(dList.Items))
for _, d := range dList.Items {
comp := newPushedComponent(applicationName, &devfileComponent{d: d}, c)
res[comp.GetName()] = comp
}
return res, nil
if !isIgnorableError(err) {
return nil, err
}
return nil, err
}
res := make(map[string]PushedComponent, len(dcList))
for _, dc := range dcList {
comp := newPushedComponent(applicationName, &s2iComponent{dc: dc}, c)
res[comp.GetName()] = comp
}
deploymentList, err := c.GetKubeClient().ListDeployments(applicationSelector)
if err != nil {
return nil, errors.Wrapf(err, "unable to list components")
}
for _, d := range deploymentList.Items {
comp := newPushedComponent(applicationName, &devfileComponent{d: d}, c)
res[comp.GetName()] = comp
}
return res, nil
}

View File

@@ -1,6 +1,8 @@
package component
import (
"github.com/openshift/odo/pkg/storage"
"github.com/openshift/odo/pkg/url"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -15,14 +17,16 @@ type Component struct {
// ComponentSpec is spec of components
type ComponentSpec struct {
App string `json:"app,omitempty"`
Type string `json:"type,omitempty"`
Source string `json:"source,omitempty"`
SourceType string `json:"sourceType,omitempty"`
URL []string `json:"url,omitempty"`
Storage []string `json:"storage,omitempty"`
Env []corev1.EnvVar `json:"env,omitempty"`
Ports []string `json:"ports,omitempty"`
App string `json:"app,omitempty"`
Type string `json:"type,omitempty"`
Source string `json:"source,omitempty"`
SourceType string `json:"sourceType,omitempty"`
URL []string `json:"url,omitempty"`
URLSpec []url.URL `json:"-"`
Storage []string `json:"storage,omitempty"`
StorageSpec []storage.Storage `json:"-"`
Env []corev1.EnvVar `json:"env,omitempty"`
Ports []string `json:"ports,omitempty"`
}
// ComponentList is list of components

View File

@@ -3,6 +3,7 @@ package kclient
import (
"encoding/json"
"fmt"
"sort"
"time"
"github.com/openshift/odo/pkg/log"
@@ -327,3 +328,50 @@ func (c *Client) patchDeploymentOfComponent(componentName, applicationName strin
return nil
}
// GetDeploymentLabelValues get label values of given label from objects in project that are matching selector
// returns slice of unique label values
func (c *Client) GetDeploymentLabelValues(label string, selector string) ([]string, error) {
// List DeploymentConfig according to selectors
dcList, err := c.appsClient.Deployments(c.Namespace).List(metav1.ListOptions{LabelSelector: selector})
if err != nil {
return nil, errors.Wrap(err, "unable to list DeploymentConfigs")
}
// Grab all the matched strings
var values []string
for _, elem := range dcList.Items {
for key, val := range elem.Labels {
if key == label {
values = append(values, val)
}
}
}
// Sort alphabetically
sort.Strings(values)
return values, nil
}
// GetDeploymentConfigsFromSelector returns an array of Deployment Config
// resources which match the given selector
func (c *Client) GetDeploymentConfigsFromSelector(selector string) ([]appsv1.Deployment, error) {
var dcList *appsv1.DeploymentList
var err error
if selector != "" {
dcList, err = c.appsClient.Deployments(c.Namespace).List(metav1.ListOptions{
LabelSelector: selector,
})
} else {
dcList, err = c.appsClient.Deployments(c.Namespace).List(metav1.ListOptions{
FieldSelector: fields.Set{"metadata.namespace": c.Namespace}.AsSelector().String(),
})
}
if err != nil {
return nil, errors.Wrap(err, "unable to list DeploymentConfigs")
}
return dcList.Items, nil
}

View File

@@ -13,18 +13,19 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
)
func GetIngressListWithMultiple(componentName string) *extensionsv1.IngressList {
func GetIngressListWithMultiple(componentName, appName string) *extensionsv1.IngressList {
return &extensionsv1.IngressList{
Items: []extensionsv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
Name: "example-0",
Labels: map[string]string{
applabels.ApplicationLabel: "",
applabels.ApplicationLabel: appName,
componentlabels.ComponentLabel: componentName,
applabels.OdoManagedBy: "odo",
applabels.OdoVersion: version.VERSION,
labels.URLLabel: "example-0",
applabels.App: appName,
},
},
Spec: *kclient.GenerateIngressSpec(kclient.IngressParameter{IngressDomain: "example-0.com", ServiceName: "example-0", PortNumber: intstr.FromInt(8080)}),
@@ -33,11 +34,12 @@ func GetIngressListWithMultiple(componentName string) *extensionsv1.IngressList
ObjectMeta: metav1.ObjectMeta{
Name: "example-1",
Labels: map[string]string{
applabels.ApplicationLabel: "",
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: componentName,
applabels.OdoManagedBy: "odo",
applabels.OdoVersion: version.VERSION,
labels.URLLabel: "example-1",
applabels.App: "app",
},
},
Spec: *kclient.GenerateIngressSpec(kclient.IngressParameter{IngressDomain: "example-1.com", ServiceName: "example-1", PortNumber: intstr.FromInt(9090)}),
@@ -46,17 +48,17 @@ func GetIngressListWithMultiple(componentName string) *extensionsv1.IngressList
}
}
func GetSingleIngress(urlName, componentName string) *extensionsv1.Ingress {
func GetSingleIngress(urlName, componentName, appName string) *extensionsv1.Ingress {
return &extensionsv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: urlName,
Labels: map[string]string{
applabels.ApplicationLabel: "",
applabels.ApplicationLabel: appName,
componentlabels.ComponentLabel: componentName,
applabels.OdoManagedBy: "odo",
applabels.OdoVersion: version.VERSION,
labels.URLLabel: urlName,
applabels.App: "",
applabels.App: appName,
},
},
Spec: *kclient.GenerateIngressSpec(kclient.IngressParameter{IngressDomain: fmt.Sprintf("%s.com", urlName), ServiceName: urlName, PortNumber: intstr.FromInt(8080)}),

View File

@@ -1,7 +1,14 @@
package kclient
import (
"github.com/openshift/odo/pkg/util"
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/watch"
appsclientset "k8s.io/client-go/kubernetes/typed/apps/v1"
"k8s.io/klog"
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
@@ -20,6 +27,7 @@ const (
Please ensure you have an active kubernetes context to your cluster.
Consult your Kubernetes distribution's documentation for more details
`
waitForComponentDeletionTimeout = 120 * time.Second
)
// Client is a collection of fields used for client configuration and interaction
@@ -29,6 +37,7 @@ type Client struct {
KubeClientConfig *rest.Config
Namespace string
OperatorClient *operatorsclientset.OperatorsV1alpha1Client
appsClient appsclientset.AppsV1Interface
// DynamicClient interacts with client-go's `dynamic` package. It is used
// to dynamically create service from an operator. It can take an arbitrary
// yaml and create k8s/OpenShift resource from it.
@@ -77,6 +86,12 @@ func NewForConfig(config clientcmd.ClientConfig) (client *Client, err error) {
return nil, err
}
appsClient, err := appsclientset.NewForConfig(client.KubeClientConfig)
if err != nil {
return nil, err
}
client.appsClient = appsClient
return client, nil
}
@@ -92,3 +107,76 @@ func CreateObjectMeta(name, namespace string, labels, annotations map[string]str
return objectMeta
}
// Delete takes labels as a input and based on it, deletes respective resource
func (c *Client) Delete(labels map[string]string, wait bool) error {
// convert labels to selector
selector := util.ConvertLabelsToSelector(labels)
klog.V(3).Infof("Selectors used for deletion: %s", selector)
var errorList []string
var deletionPolicy = metav1.DeletePropagationBackground
// for --wait flag, it deletes component dependents first and then delete component
if wait {
deletionPolicy = metav1.DeletePropagationForeground
}
// Delete Deployments
klog.V(3).Info("Deleting Deployments")
err := c.appsClient.Deployments(c.Namespace).DeleteCollection(&metav1.DeleteOptions{PropagationPolicy: &deletionPolicy}, metav1.ListOptions{LabelSelector: selector})
if err != nil {
errorList = append(errorList, "unable to delete deployments")
}
// for --wait it waits for component to be deleted
// TODO: Need to modify for `odo app delete`, currently wait flag is added only in `odo component delete`
// so only one component gets passed in selector
if wait {
err = c.WaitForComponentDeletion(selector)
if err != nil {
errorList = append(errorList, err.Error())
}
}
// Error string
errString := strings.Join(errorList, ",")
if len(errString) != 0 {
return errors.New(errString)
}
return nil
}
// WaitForComponentDeletion waits for component to be deleted
func (c *Client) WaitForComponentDeletion(selector string) error {
klog.V(3).Infof("Waiting for component to get deleted")
watcher, err := c.appsClient.Deployments(c.Namespace).Watch(metav1.ListOptions{LabelSelector: selector})
if err != nil {
return err
}
defer watcher.Stop()
eventCh := watcher.ResultChan()
for {
select {
case event, ok := <-eventCh:
_, typeOk := event.Object.(*appsv1.Deployment)
if !ok || !typeOk {
return errors.New("Unable to watch deployments")
}
if event.Type == watch.Deleted {
klog.V(3).Infof("WaitForComponentDeletion, Event Recieved:Deleted")
return nil
} else if event.Type == watch.Error {
klog.V(3).Infof("WaitForComponentDeletion, Event Recieved:Deleted ")
return errors.New("Unable to watch deployments")
}
case <-time.After(waitForComponentDeletionTimeout):
klog.V(3).Infof("WaitForComponentDeletion, Timeout")
return errors.New("Time out waiting for component to get deleted")
}
}
}

View File

@@ -27,6 +27,7 @@ type FakeClientset struct {
RouteClientset *fakeRouteClientset.Clientset
ProjClientset *fakeProjClientset.Clientset
ServiceCatalogClientSet *fakeServiceCatalogClientSet.Clientset
DiscoveryClientSet *fake.FakeDiscovery
}
// FakeNew creates new fake client for testing
@@ -63,8 +64,11 @@ func FakeNew() (*Client, *FakeClientset) {
if os.Getenv("KUBERNETES") != "true" {
client.SetDiscoveryInterface(fakeDiscoveryWithRoute)
client.SetDiscoveryInterface(fakeDiscoveryWithDeploymentConfig)
} else {
client.SetDiscoveryInterface(&fake.FakeDiscovery{})
client.SetDiscoveryInterface(&fakeDiscovery{
resourceMap: map[string]*resourceMapEntry{},
})
}
return client, fkclientset
@@ -98,6 +102,22 @@ var fakeDiscoveryWithRoute = &fakeDiscovery{
},
}
var fakeDiscoveryWithDeploymentConfig = &fakeDiscovery{
resourceMap: map[string]*resourceMapEntry{
"apps.openshift.io/v1": {
list: &metav1.APIResourceList{
GroupVersion: "apps.openshift.io/v1",
APIResources: []metav1.APIResource{{
Name: "deploymentconfigs",
SingularName: "deploymentconfigs",
Namespaced: true,
Kind: "deploymentconfigs",
}},
},
},
},
}
func (c *fakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
c.lock.Lock()
defer c.lock.Unlock()

View File

@@ -2,12 +2,10 @@ package application
import (
"fmt"
"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/openshift/odo/pkg/storage"
"github.com/openshift/odo/pkg/url"
"github.com/pkg/errors"
"k8s.io/klog"
@@ -29,7 +27,7 @@ func NewCmdApplication(name, fullName string) *cobra.Command {
applicationCmd := &cobra.Command{
Use: name,
Short: "Perform application operations",
Long: `Performs application operations related to your OpenShift project.`,
Long: `Performs application operations related to your project.`,
Example: fmt.Sprintf("%s\n\n%s\n\n%s",
delete.Example,
describe.Example,
@@ -55,8 +53,9 @@ func AddApplicationFlag(cmd *cobra.Command) {
completion.RegisterCommandFlagHandler(cmd, "app", completion.AppCompletionHandler)
}
// printDeleteAppInfo will print things which will be deleted
func printDeleteAppInfo(client *occlient.Client, appName string, projectName string) error {
// printAppInfo will print things which will be deleted
func printAppInfo(client *occlient.Client, kClient *kclient.Client, appName string, projectName string) error {
componentList, err := component.List(client, appName, nil)
if err != nil {
return errors.Wrap(err, "failed to get Component list")
@@ -65,29 +64,19 @@ func printDeleteAppInfo(client *occlient.Client, appName string, projectName str
if len(componentList.Items) != 0 {
log.Info("This application has following components that will be deleted")
for _, currentComponent := range componentList.Items {
componentDesc, err := component.GetComponent(client, currentComponent.Name, appName, projectName)
if err != nil {
return errors.Wrap(err, "unable to get component description")
}
log.Info("component named", currentComponent.Name)
if len(componentDesc.Spec.URL) != 0 {
ul, err := url.ListPushed(client, componentDesc.Name, appName)
if err != nil {
return errors.Wrap(err, "Could not get url list")
}
if len(currentComponent.Spec.URL) != 0 {
log.Info("This component has following urls that will be deleted with component")
for _, u := range ul.Items {
for _, u := range currentComponent.Spec.URLSpec {
log.Info("URL named", u.GetName(), "with host", u.Spec.Host, "having protocol", u.Spec.Protocol, "at port", u.Spec.Port)
}
}
storages, err := storage.List(client, currentComponent.Name, appName)
odoutil.LogErrorAndExit(err, "")
if len(storages.Items) != 0 {
if len(currentComponent.Spec.Storage) != 0 {
log.Info("The component has following storages which will be deleted with the component")
for _, storageName := range componentDesc.Spec.Storage {
store := storages.Get(storageName)
for _, storage := range currentComponent.Spec.StorageSpec {
store := storage
log.Info("Storage named", store.GetName(), "of size", store.Spec.Size)
}
}

View File

@@ -2,6 +2,7 @@ package application
import (
"fmt"
"path/filepath"
odoUtil "github.com/openshift/odo/pkg/odo/util"
@@ -37,7 +38,11 @@ func NewDeleteOptions() *DeleteOptions {
// Complete completes DeleteOptions after they've been created
func (o *DeleteOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
o.Context = genericclioptions.NewContext(cmd)
if util.CheckPathExists(filepath.Join(".odo", "config.yaml")) {
o.Context = genericclioptions.NewContext(cmd)
} else {
o.Context = genericclioptions.NewDevfileContext(cmd)
}
o.appName = o.Application
if len(args) == 1 {
// If app name passed, consider it for deletion
@@ -56,7 +61,7 @@ func (o *DeleteOptions) Validate() (err error) {
return fmt.Errorf("given output format %s is not supported", o.OutputFlag)
}
exist, err := application.Exists(o.appName, o.Client)
exist, err := application.Exists(o.appName, o.Client, o.KClient)
if !exist {
return fmt.Errorf("%s app does not exists", o.appName)
}
@@ -74,7 +79,7 @@ func (o *DeleteOptions) Run() (err error) {
}
// Print App Information which will be deleted
err = printDeleteAppInfo(o.Client, o.appName, o.Project)
err = printAppInfo(o.Client, o.KClient, o.appName, o.Project)
if err != nil {
return err
}

View File

@@ -2,6 +2,8 @@ package application
import (
"fmt"
odoutil "github.com/openshift/odo/pkg/util"
"path/filepath"
"github.com/openshift/odo/pkg/application"
"github.com/openshift/odo/pkg/component"
@@ -37,7 +39,11 @@ func NewDescribeOptions() *DescribeOptions {
// Complete completes DescribeOptions after they've been created
func (o *DescribeOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
o.Context = genericclioptions.NewContext(cmd)
if odoutil.CheckPathExists(filepath.Join(".odo", "config.yaml")) {
o.Context = genericclioptions.NewContext(cmd)
} else {
o.Context = genericclioptions.NewDevfileContext(cmd)
}
o.appName = o.Application
if len(args) == 1 {
o.appName = args[0]
@@ -58,7 +64,7 @@ func (o *DescribeOptions) Validate() (err error) {
return fmt.Errorf("There's no active application in project: %v", o.Project)
}
exist, err := application.Exists(o.appName, o.Client)
exist, err := application.Exists(o.appName, o.Client, o.KClient)
if !exist {
return fmt.Errorf("%s app does not exists", o.appName)
}
@@ -71,7 +77,6 @@ func (o *DescribeOptions) Run() (err error) {
appDef := application.GetMachineReadableFormat(o.Client, o.appName, o.Project)
machineoutput.OutputSuccess(appDef)
} else {
// List of Component
componentList, err := component.List(o.Client, o.appName, nil)
if err != nil {
return err
@@ -87,9 +92,7 @@ func (o *DescribeOptions) Run() (err error) {
o.appName, len(componentList.Items), len(serviceList.Items))
if len(componentList.Items) > 0 {
for _, currentComponent := range componentList.Items {
componentDesc, err := component.GetComponent(o.Client, currentComponent.Name, o.appName, o.Project)
util.LogErrorAndExit(err, "")
util.PrintComponentInfo(o.Client, currentComponent.Name, componentDesc, o.appName, o.Project)
util.PrintComponentInfo(o.Client, currentComponent.Name, currentComponent, o.appName, o.Project)
fmt.Println("--------------------------------------")
}
}

View File

@@ -38,7 +38,7 @@ func NewListOptions() *ListOptions {
// Complete completes ListOptions after they've been created
func (o *ListOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
o.Context = genericclioptions.NewContext(cmd)
o.Context = genericclioptions.NewDevfileContext(cmd)
return
}

View File

@@ -63,7 +63,6 @@ func (lo *ListOptions) Complete(name string, cmd *cobra.Command, args []string)
if util.CheckPathExists(lo.devfilePath) {
lo.Context = genericclioptions.NewDevfileContext(cmd)
lo.Client = genericclioptions.Client(cmd)
lo.hasDCSupport, err = lo.Client.IsDeploymentConfigSupported()
if err != nil {
return err

View File

@@ -10,7 +10,6 @@ import (
routev1 "github.com/openshift/api/route/v1"
"github.com/openshift/odo/pkg/devfile"
"github.com/openshift/odo/pkg/envinfo"
"github.com/openshift/odo/pkg/occlient"
pkgutil "github.com/openshift/odo/pkg/util"
clicomponent "github.com/openshift/odo/pkg/odo/cli/component"
@@ -123,12 +122,8 @@ func (o *URLListOptions) Run() (err error) {
}
} else {
componentName := o.EnvSpecificInfo.GetName()
oclient, err := occlient.New()
if err != nil {
return err
}
oclient.Namespace = o.KClient.Namespace
routeSupported, err := oclient.IsRouteSupported()
routeSupported, err := o.Context.Client.IsRouteSupported()
if err != nil {
return err
}
@@ -138,7 +133,7 @@ func (o *URLListOptions) Run() (err error) {
}
containerComponents := adaptersCommon.GetDevfileContainerComponents(devObj.Data)
urls, err := url.ListIngressAndRoute(oclient, o.EnvSpecificInfo, containerComponents, componentName, routeSupported)
urls, err := url.ListIngressAndRoute(o.Context.Client, o.EnvSpecificInfo, containerComponents, componentName, routeSupported)
if err != nil {
return err
}

View File

@@ -41,8 +41,8 @@ func NewContext(command *cobra.Command, toggles ...bool) *Context {
}
// NewDevfileContext creates a new Context struct populated with the current state based on flags specified for the provided command
func NewDevfileContext(command *cobra.Command, ignoreMissingConfiguration ...bool) *Context {
return newDevfileContext(command)
func NewDevfileContext(command *cobra.Command) *Context {
return newDevfileContext(command, false)
}
// NewContextCreatingAppIfNeeded creates a new Context struct populated with the current state based on flags specified for the
@@ -156,11 +156,15 @@ func getValidEnvInfo(command *cobra.Command) (*envinfo.EnvSpecificInfo, error) {
// Get details from the env file
componentContext := FlagValueIfSet(command, ContextFlagName)
// Grab the absolute path of the eenv file
// Grab the absolute path of the env file
if componentContext != "" {
fAbs, err := pkgUtil.GetAbsPath(componentContext)
util.LogErrorAndExit(err, "")
componentContext = fAbs
} else {
fAbs, err := pkgUtil.GetAbsPath(".")
util.LogErrorAndExit(err, "")
componentContext = fAbs
}
// Access the env file
@@ -273,6 +277,7 @@ func (o *internalCxt) resolveProject(localConfiguration envinfo.LocalConfigProvi
checkProjectCreateOrDeleteOnlyOnInvalidNamespace(command, errFormat)
}
}
o.Client.GetKubeClient().Namespace = namespace
o.Client.Namespace = namespace
o.Project = namespace
if o.KClient != nil {
@@ -311,6 +316,8 @@ func (o *internalCxt) resolveNamespace(configProvider envinfo.LocalConfigProvide
checkProjectCreateOrDeleteOnlyOnInvalidNamespaceNoFmt(command, errFormat)
}
}
o.Client.Namespace = namespace
o.Client.GetKubeClient().Namespace = namespace
o.KClient.Namespace = namespace
o.Project = namespace
}
@@ -408,7 +415,7 @@ func newContext(command *cobra.Command, createAppIfNeeded bool, ignoreMissingCon
}
// newDevfileContext creates a new context based on command flags for devfile components
func newDevfileContext(command *cobra.Command) *Context {
func newDevfileContext(command *cobra.Command, createAppIfNeeded bool) *Context {
// Resolve output flag
outputFlag := FlagValueIfSet(command, OutputFlagName)
@@ -428,7 +435,7 @@ func newDevfileContext(command *cobra.Command) *Context {
}
internalCxt.EnvSpecificInfo = envInfo
internalCxt.resolveApp(true, envInfo)
internalCxt.resolveApp(createAppIfNeeded, envInfo)
// If the push target is NOT Docker we will set the client to Kubernetes.
if !pushtarget.IsPushTargetDocker() {
@@ -438,6 +445,8 @@ func newDevfileContext(command *cobra.Command) *Context {
internalCxt.Client = client(command)
// Gather the environment information
internalCxt.EnvSpecificInfo = envInfo
internalCxt.resolveNamespace(envInfo)
}

View File

@@ -102,13 +102,10 @@ func PrintComponentInfo(client *occlient.Client, currentComponentName string, co
if len(componentDesc.Spec.Storage) > 0 {
var storages storage.StorageList
var err error
if componentDesc.Status.State == "Pushed" {
// Retrieve the storage list
storages, err = storage.List(client, currentComponentName, applicationName)
LogErrorAndExit(err, "")
storages = storage.StorageList{Items: componentDesc.Spec.StorageSpec}
} else {
localConfig, err := config.New()
LogErrorAndExit(err, "")
@@ -145,8 +142,7 @@ func PrintComponentInfo(client *occlient.Client, currentComponentName string, co
}
} else {
// Retrieve the URLs
urls, err := urlPkg.ListPushed(client, currentComponentName, applicationName)
LogErrorAndExit(err, "")
urls := urlPkg.URLList{Items: componentDesc.Spec.URLSpec}
// Gather the output
for _, componentURL := range componentDesc.Spec.URL {

View File

@@ -569,8 +569,8 @@ func MachineReadableSuccessOutput(storageName string, message string) {
machineoutput.OutputSuccess(machineOutput)
}
// devfileListMounted lists the storage which are mounted on a container
func devfileListMounted(kClient *kclient.Client, componentName string) (StorageList, error) {
// DevfileListMounted lists the storage which are mounted on a container
func DevfileListMounted(kClient *kclient.Client, componentName string) (StorageList, error) {
pod, err := kClient.GetPodUsingComponentName(componentName)
if err != nil {
if _, ok := err.(*kclient.PodNotFoundError); ok {
@@ -654,7 +654,7 @@ func GetLocalDevfileStorage(devfileData data.DevfileData) StorageList {
func DevfileList(kClient *kclient.Client, devfileData data.DevfileData, componentName string) (StorageList, error) {
localStorage := GetLocalDevfileStorage(devfileData)
clusterStorage, err := devfileListMounted(kClient, componentName)
clusterStorage, err := DevfileListMounted(kClient, componentName)
if err != nil {
return StorageList{}, err
}

View File

@@ -1234,7 +1234,7 @@ func TestDevfileListMounted(t *testing.T) {
return true, tt.returnedPods, nil
})
got, err := devfileListMounted(fakeClient, tt.args.componentName)
got, err := DevfileListMounted(fakeClient, tt.args.componentName)
if (err != nil) != tt.wantErr {
t.Errorf("devfileListMounted() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@@ -1,6 +1,8 @@
package testingutil
import (
applabels "github.com/openshift/odo/pkg/application/labels"
componentlabels "github.com/openshift/odo/pkg/component/labels"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -14,6 +16,11 @@ func CreateFakeDeployment(podName string) *appsv1.Deployment {
ObjectMeta: metav1.ObjectMeta{
Name: podName,
UID: fakeUID,
Labels: map[string]string{
applabels.App: "app",
applabels.ApplicationLabel: "app",
componentlabels.ComponentLabel: podName,
},
},
}
return &deployment

View File

@@ -151,11 +151,11 @@ func GetIngressOrRoute(client *occlient.Client, kClient *kclient.Client, envSpec
}
// Delete deletes a URL
func Delete(client *occlient.Client, kClient *kclient.Client, urlName string, applicationName string, urlType envinfo.URLKind) error {
func Delete(client *occlient.Client, kClient *kclient.Client, urlName string, applicationName string, urlType envinfo.URLKind, isS2i bool) error {
if urlType == envinfo.INGRESS {
return kClient.DeleteIngress(urlName)
} else if urlType == envinfo.ROUTE {
if applicationName != "" {
if isS2i {
// Namespace the URL name
var err error
urlName, err = util.NamespaceOpenShiftObject(urlName, applicationName)
@@ -1022,7 +1022,7 @@ func Push(client *occlient.Client, kClient *kclient.Client, parameters PushParam
// to avoid error due to duplicate ingress name defined in different devfile components
deleteURLName = fmt.Sprintf("%s-%s", urlName, parameters.ComponentName)
}
err := Delete(client, kClient, deleteURLName, parameters.ApplicationName, urlSpec.Spec.Kind)
err := Delete(client, kClient, deleteURLName, parameters.ApplicationName, urlSpec.Spec.Kind, parameters.IsS2I)
if err != nil {
return err
}

View File

@@ -178,13 +178,14 @@ func TestCreate(t *testing.T) {
name: "Case 4: Create a ingress, with same name as component,instead of route on openshift cluster",
args: args{
componentName: "nodejs",
applicationName: "app",
urlName: "nodejs",
portNumber: 8080,
host: "com",
isRouteSupported: true,
urlKind: envinfo.INGRESS,
},
returnedIngress: fake.GetSingleIngress("nodejs-nodejs", "nodejs"),
returnedIngress: fake.GetSingleIngress("nodejs-nodejs", "nodejs", "app"),
want: "http://nodejs.com",
wantErr: false,
},
@@ -192,6 +193,7 @@ func TestCreate(t *testing.T) {
name: "Case 5: Create a ingress, with different name as component,instead of route on openshift cluster",
args: args{
componentName: "nodejs",
applicationName: "app",
urlName: "example",
portNumber: 8080,
host: "com",
@@ -220,7 +222,7 @@ func TestCreate(t *testing.T) {
},
},
},
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs"),
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs", "app"),
want: "http://example.com",
wantErr: false,
},
@@ -228,6 +230,7 @@ func TestCreate(t *testing.T) {
name: "Case 6: Create a secure ingress, instead of route on openshift cluster, default tls exists",
args: args{
componentName: "nodejs",
applicationName: "app",
urlName: "example",
portNumber: 8080,
host: "com",
@@ -235,7 +238,7 @@ func TestCreate(t *testing.T) {
secure: true,
urlKind: envinfo.INGRESS,
},
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs"),
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs", "app"),
defaultTLSExists: true,
want: "https://example.com",
wantErr: false,
@@ -244,6 +247,7 @@ func TestCreate(t *testing.T) {
name: "Case 7: Create a secure ingress, instead of route on openshift cluster and default tls doesn't exist",
args: args{
componentName: "nodejs",
applicationName: "app",
urlName: "example",
portNumber: 8080,
host: "com",
@@ -251,7 +255,7 @@ func TestCreate(t *testing.T) {
secure: true,
urlKind: envinfo.INGRESS,
},
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs"),
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs", "app"),
defaultTLSExists: false,
want: "https://example.com",
wantErr: false,
@@ -259,6 +263,7 @@ func TestCreate(t *testing.T) {
{
name: "Case 8: Fail when while creating ingress when user given tls secret doesn't exists",
args: args{
applicationName: "app",
componentName: "nodejs",
urlName: "example",
portNumber: 8080,
@@ -268,7 +273,7 @@ func TestCreate(t *testing.T) {
tlsSecret: "user-secret",
urlKind: envinfo.INGRESS,
},
returnedIngress: fake.GetSingleIngress("example", "nodejs"),
returnedIngress: fake.GetSingleIngress("example", "nodejs", "app"),
defaultTLSExists: false,
userGivenTLSExists: false,
want: "http://example.com",
@@ -277,6 +282,7 @@ func TestCreate(t *testing.T) {
{
name: "Case 9: Create a secure ingress, instead of route on openshift cluster, user tls secret does exists",
args: args{
applicationName: "app",
componentName: "nodejs",
urlName: "example",
portNumber: 8080,
@@ -286,7 +292,7 @@ func TestCreate(t *testing.T) {
tlsSecret: "user-secret",
urlKind: envinfo.INGRESS,
},
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs"),
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs", "app"),
defaultTLSExists: false,
userGivenTLSExists: true,
want: "https://example.com",
@@ -296,6 +302,7 @@ func TestCreate(t *testing.T) {
{
name: "Case 10: invalid url kind",
args: args{
applicationName: "app",
componentName: "nodejs",
urlName: "example",
portNumber: 8080,
@@ -305,7 +312,7 @@ func TestCreate(t *testing.T) {
tlsSecret: "user-secret",
urlKind: "blah",
},
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs"),
returnedIngress: fake.GetSingleIngress("example-nodejs", "nodejs", "app"),
defaultTLSExists: false,
userGivenTLSExists: true,
want: "",
@@ -320,7 +327,7 @@ func TestCreate(t *testing.T) {
isRouteSupported: false,
urlKind: envinfo.ROUTE,
},
returnedIngress: fake.GetSingleIngress("example", "nodejs"),
returnedIngress: fake.GetSingleIngress("example", "nodejs", "app"),
defaultTLSExists: false,
userGivenTLSExists: true,
want: "",
@@ -336,7 +343,7 @@ func TestCreate(t *testing.T) {
tlsSecret: "secret",
urlKind: envinfo.ROUTE,
},
returnedIngress: fake.GetSingleIngress("example", "nodejs"),
returnedIngress: fake.GetSingleIngress("example", "nodejs", "app"),
defaultTLSExists: false,
userGivenTLSExists: true,
want: "",
@@ -890,15 +897,17 @@ func TestPush(t *testing.T) {
{
name: "0 urls on env file and cluster",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{},
returnedRoutes: &routev1.RouteList{},
returnedIngress: &extensionsv1.IngressList{},
},
{
name: "2 urls on env file and 0 on openshift cluster",
componentName: "nodejs",
args: args{isRouteSupported: true},
name: "2 urls on env file and 0 on openshift cluster",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example",
@@ -960,10 +969,11 @@ func TestPush(t *testing.T) {
{
name: "0 urls on env file and 2 on openshift cluster",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{},
returnedRoutes: &routev1.RouteList{},
returnedIngress: fake.GetIngressListWithMultiple("nodejs"),
returnedIngress: fake.GetIngressListWithMultiple("nodejs", "app"),
deletedURLs: []URL{
{
ObjectMeta: metav1.ObjectMeta{
@@ -978,9 +988,10 @@ func TestPush(t *testing.T) {
},
},
{
name: "2 urls on env file and 2 on openshift cluster, but they are different",
componentName: "wildfly",
args: args{isRouteSupported: true},
name: "2 urls on env file and 2 on openshift cluster, but they are different",
componentName: "wildfly",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example-local-0",
@@ -1013,7 +1024,7 @@ func TestPush(t *testing.T) {
},
},
returnedRoutes: &routev1.RouteList{},
returnedIngress: fake.GetIngressListWithMultiple("wildfly"),
returnedIngress: fake.GetIngressListWithMultiple("wildfly", "app"),
createdURLs: []URL{
{
ObjectMeta: metav1.ObjectMeta{
@@ -1052,9 +1063,10 @@ func TestPush(t *testing.T) {
},
},
{
name: "2 urls on env file and openshift cluster are in sync",
componentName: "wildfly",
args: args{isRouteSupported: true},
name: "2 urls on env file and openshift cluster are in sync",
componentName: "wildfly",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example-0",
@@ -1087,14 +1099,15 @@ func TestPush(t *testing.T) {
},
},
returnedRoutes: &routev1.RouteList{},
returnedIngress: fake.GetIngressListWithMultiple("wildfly"),
returnedIngress: fake.GetIngressListWithMultiple("wildfly", "app"),
createdURLs: []URL{},
deletedURLs: []URL{},
},
{
name: "2 (1 ingress,1 route) urls on env file and 2 on openshift cluster (1 ingress,1 route), but they are different",
componentName: "nodejs",
args: args{isRouteSupported: true},
name: "2 (1 ingress,1 route) urls on env file and 2 on openshift cluster (1 ingress,1 route), but they are different",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example-local-0",
@@ -1126,7 +1139,7 @@ func TestPush(t *testing.T) {
},
},
returnedRoutes: &routev1.RouteList{},
returnedIngress: fake.GetIngressListWithMultiple("nodejs"),
returnedIngress: fake.GetIngressListWithMultiple("nodejs", "app"),
createdURLs: []URL{
{
ObjectMeta: metav1.ObjectMeta{
@@ -1164,9 +1177,10 @@ func TestPush(t *testing.T) {
},
},
{
name: "create a ingress on a kubernetes cluster",
componentName: "nodejs",
args: args{isRouteSupported: false},
name: "create a ingress on a kubernetes cluster",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: false},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example",
@@ -1207,8 +1221,9 @@ func TestPush(t *testing.T) {
},
},
{
name: "url with same name exists on env and cluster but with different specs",
componentName: "nodejs",
name: "url with same name exists on env and cluster but with different specs",
componentName: "nodejs",
applicationName: "app",
args: args{
isRouteSupported: true,
},
@@ -1235,7 +1250,7 @@ func TestPush(t *testing.T) {
returnedRoutes: &routev1.RouteList{},
returnedIngress: &extensionsv1.IngressList{
Items: []extensionsv1.Ingress{
*fake.GetSingleIngress("example-local-0", "nodejs"),
*fake.GetSingleIngress("example-local-0", "nodejs", "app"),
},
},
createdURLs: []URL{
@@ -1299,9 +1314,10 @@ func TestPush(t *testing.T) {
wantErr: false,
},
{
name: "create a secure route url",
componentName: "nodejs",
args: args{isRouteSupported: true},
name: "create a secure route url",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example",
@@ -1338,9 +1354,10 @@ func TestPush(t *testing.T) {
},
},
{
name: "create a secure ingress url with empty user given tls secret",
componentName: "nodejs",
args: args{isRouteSupported: true},
name: "create a secure ingress url with empty user given tls secret",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example",
@@ -1379,9 +1396,10 @@ func TestPush(t *testing.T) {
},
},
{
name: "create a secure ingress url with user given tls secret",
componentName: "nodejs",
args: args{isRouteSupported: true},
name: "create a secure ingress url with user given tls secret",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example",
@@ -1570,6 +1588,7 @@ func TestPush(t *testing.T) {
{
name: "should create route in openshift cluster if endpoint is defined in devfile",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{},
containerComponents: []versionsCommon.DevfileComponent{
@@ -1604,9 +1623,10 @@ func TestPush(t *testing.T) {
},
},
{
name: "should create ingress if endpoint is defined in devfile",
componentName: "nodejs",
args: args{isRouteSupported: true},
name: "should create ingress if endpoint is defined in devfile",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example",
@@ -1649,6 +1669,7 @@ func TestPush(t *testing.T) {
{
name: "should create route in openshift cluster with path defined in devfile",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{},
containerComponents: []versionsCommon.DevfileComponent{
@@ -1684,9 +1705,10 @@ func TestPush(t *testing.T) {
},
},
{
name: "should create ingress with path defined in devfile",
componentName: "nodejs",
args: args{isRouteSupported: true},
name: "should create ingress with path defined in devfile",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true},
existingEnvInfoURLs: []envinfo.EnvInfoURL{
{
Name: "example",
@@ -2041,7 +2063,7 @@ func TestListIngressAndRoute(t *testing.T) {
},
},
routeSupported: true,
ingressList: fake.GetIngressListWithMultiple(componentName),
ingressList: fake.GetIngressListWithMultiple(componentName, "app"),
routeList: &routev1.RouteList{
Items: []routev1.Route{
testingutil.GetSingleRoute(testURL4.Name, int(exampleEndpoint.TargetPort), componentName, ""),
@@ -2114,7 +2136,7 @@ func TestListIngressAndRoute(t *testing.T) {
},
},
routeList: &routev1.RouteList{},
ingressList: fake.GetIngressListWithMultiple(componentName),
ingressList: fake.GetIngressListWithMultiple(componentName, "app"),
routeSupported: false,
wantURLs: []URL{
URL{
@@ -2159,7 +2181,7 @@ func TestListIngressAndRoute(t *testing.T) {
},
routeSupported: true,
routeList: &routev1.RouteList{},
ingressList: fake.GetIngressListWithMultiple(componentName),
ingressList: fake.GetIngressListWithMultiple(componentName, "app"),
wantURLs: []URL{
URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
@@ -2366,7 +2388,7 @@ func TestGetIngressOrRoute(t *testing.T) {
component: componentName,
urlName: testURL1.Name,
routeSupported: true,
pushedIngress: fake.GetSingleIngress(testURL1.Name, componentName),
pushedIngress: fake.GetSingleIngress(testURL1.Name, componentName, "app"),
pushedRoute: routev1.Route{},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
@@ -2383,7 +2405,7 @@ func TestGetIngressOrRoute(t *testing.T) {
component: componentName,
urlName: testURL2.Name,
routeSupported: true,
pushedIngress: fake.GetSingleIngress(testURL2.Name, componentName),
pushedIngress: fake.GetSingleIngress(testURL2.Name, componentName, "app"),
pushedRoute: routev1.Route{},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
@@ -2504,7 +2526,7 @@ func TestGetIngressOrRoute(t *testing.T) {
component: componentName,
urlName: testURL2.Name,
routeSupported: false,
pushedIngress: fake.GetSingleIngress(testURL2.Name, componentName),
pushedIngress: fake.GetSingleIngress(testURL2.Name, componentName, "app"),
pushedRoute: routev1.Route{},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
@@ -2521,7 +2543,7 @@ func TestGetIngressOrRoute(t *testing.T) {
component: componentName,
urlName: testURL1.Name,
routeSupported: false,
pushedIngress: fake.GetSingleIngress(testURL1.Name, componentName),
pushedIngress: fake.GetSingleIngress(testURL1.Name, componentName, "app"),
pushedRoute: routev1.Route{},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},

View File

@@ -260,6 +260,12 @@ func Suffocate(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "\t", "")
}
// IsJson returns true if a string is in json format
func IsJSON(s string) bool {
var js map[string]interface{}
return json.Unmarshal([]byte(s), &js) == nil
}
type CommonVar struct {
// Project is new clean project/namespace for each test
Project string

View File

@@ -30,7 +30,7 @@ var _ = Describe("odo app command tests", func() {
Context("when running help for app command", func() {
It("should display the help", func() {
appHelp := helper.CmdShouldPass("odo", "app", "-h")
Expect(appHelp).To(ContainSubstring("Performs application operations related to your OpenShift project."))
Expect(appHelp).To(ContainSubstring("Performs application operations related to your project."))
})
})
@@ -58,19 +58,19 @@ var _ = Describe("odo app command tests", func() {
// changing directory to the context directory
helper.Chdir(commonVar.Context)
appListOutput := helper.CmdShouldPass("odo", "app", "list")
appListOutput := helper.CmdShouldPass("odo", "app", "list", "--project", commonVar.Project)
Expect(appListOutput).To(ContainSubstring(appName))
actualCompListJSON := helper.CmdShouldPass("odo", "list", "-o", "json")
actualCompListJSON := helper.CmdShouldPass("odo", "list", "-o", "json", "--project", commonVar.Project)
desiredCompListJSON := fmt.Sprintf(`{"kind":"List","apiVersion":"odo.dev/v1alpha1","metadata":{},"s2iComponents":[{"kind":"Component","apiVersion":"odo.dev/v1alpha1","metadata":{"name":"nodejs","namespace":"%s","creationTimestamp":null},"spec":{"app":"app","type":"nodejs","sourceType":"local","env":[{"name":"DEBUG_PORT","value":"5858"}]},"status":{"state":"Pushed"}}],"devfileComponents":[]}`, commonVar.Project)
Expect(desiredCompListJSON).Should(MatchJSON(actualCompListJSON))
helper.CmdShouldPass("odo", "app", "describe")
helper.CmdShouldPass("odo", "app", "describe", "--project", commonVar.Project)
desiredDesAppJSON := fmt.Sprintf(`{"kind":"Application","apiVersion":"odo.dev/v1alpha1","metadata":{"name":"app","namespace":"%s","creationTimestamp":null},"spec":{"components": ["nodejs"]}}`, commonVar.Project)
actualDesAppJSON := helper.CmdShouldPass("odo", "app", "describe", "app", "-o", "json")
actualDesAppJSON := helper.CmdShouldPass("odo", "app", "describe", "app", "-o", "json", "--project", commonVar.Project)
Expect(desiredDesAppJSON).Should(MatchJSON(actualDesAppJSON))
helper.CmdShouldPass("odo", "app", "delete", "-f")
helper.CmdShouldPass("odo", "app", "delete", "-f", "--project", commonVar.Project)
})
})
@@ -83,8 +83,8 @@ var _ = Describe("odo app command tests", func() {
// list should pass as the project exists
appListOutput := helper.CmdShouldPass("odo", "app", "list", "--project", commonVar.Project)
Expect(appListOutput).To(ContainSubstring(appName))
helper.CmdShouldFail("odo", "app", "describe")
helper.CmdShouldFail("odo", "app", "delete", "-f")
helper.CmdShouldFail("odo", "app", "describe", "--project", commonVar.Project)
helper.CmdShouldFail("odo", "app", "delete", "-f", "--project", commonVar.Project)
})
})

View File

@@ -187,15 +187,24 @@ func componentTests(args ...string) {
helper.CmdShouldPass("odo", append(args, "create", "--s2i", "nodejs", "cmp-git", "--project", commonVar.Project, "--git", "https://github.com/openshift/nodejs-ex", "--context", commonVar.Context, "--app", "testing")...)
helper.ValidateLocalCmpExist(commonVar.Context, "Type,nodejs", "Name,cmp-git", "Application,testing")
kubeconfigOrig := os.Getenv("KUBECONFIG")
os.Setenv("KUBECONFIG", "/no/such/path")
cmpList := helper.CmdShouldPass("odo", append(args, "list", "--context", commonVar.Context, "--v", "9")...)
helper.MatchAllInOutput(cmpList, []string{"cmp-git", "Unknown"})
// KUBECONFIG defaults to ~/.kube/config so it can be empty in some cases.
if kubeconfigOrig != "" {
os.Setenv("KUBECONFIG", kubeconfigOrig)
} else {
os.Unsetenv("KUBECONFIG")
unset := func() {
// KUBECONFIG defaults to ~/.kube/config so it can be empty in some cases.
if kubeconfigOrig != "" {
os.Setenv("KUBECONFIG", kubeconfigOrig)
} else {
os.Unsetenv("KUBECONFIG")
}
}
os.Setenv("KUBECONFIG", "/no/such/path")
defer unset()
cmpList := helper.CmdShouldPass("odo", append(args, "list", "--context", commonVar.Context, "--v", "9")...)
helper.MatchAllInOutput(cmpList, []string{"cmp-git", "Unknown"})
unset()
fmt.Printf("kubeconfig before delete %v", os.Getenv("KUBECONFIG"))
helper.CmdShouldPass("odo", append(args, "delete", "-f", "--all", "--context", commonVar.Context)...)
})

View File

@@ -0,0 +1,125 @@
package devfile
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/openshift/odo/tests/helper"
"os"
"path/filepath"
"time"
)
var _ = Describe("odo devfile app command tests", func() {
var namespace, context, currentWorkingDirectory, originalKubeconfig string
// Using program command according to cliRunner in devfile
cliRunner := helper.GetCliRunner()
// This is run before every Spec (It)
var _ = BeforeEach(func() {
SetDefaultEventuallyTimeout(10 * time.Minute)
context = helper.CreateNewContext()
os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml"))
originalKubeconfig = os.Getenv("KUBECONFIG")
helper.LocalKubeconfigSet(context)
namespace = cliRunner.CreateRandNamespaceProject()
currentWorkingDirectory = helper.Getwd()
})
// This is run after every Spec (It)
var _ = AfterEach(func() {
cliRunner.DeleteNamespaceProject(namespace)
helper.Chdir(currentWorkingDirectory)
err := os.Setenv("KUBECONFIG", originalKubeconfig)
Expect(err).NotTo(HaveOccurred())
helper.DeleteDir(context)
os.Unsetenv("GLOBALODOCONFIG")
})
Context("listing apps", func() {
It("it should list, describe and delete the apps", func() {
runner(namespace, false)
})
})
Context("Testing URLs for OpenShift specific scenarios", func() {
JustBeforeEach(func() {
if os.Getenv("KUBERNETES") == "true" {
Skip("This is a OpenShift specific scenario, skipping")
}
})
It("it should list, describe and delete the apps", func() {
runner(namespace, true)
})
})
})
func runner(namespace string, s2i bool) {
context0 := helper.CreateNewContext()
context00 := helper.CreateNewContext()
context1 := helper.CreateNewContext()
defer func() {
helper.DeleteDir(context0)
helper.DeleteDir(context00)
helper.DeleteDir(context1)
}()
app0 := helper.RandString(4)
app1 := helper.RandString(4)
component0 := helper.RandString(4)
component00 := helper.RandString(4)
component1 := helper.RandString(4)
storage00 := helper.RandString(4)
url00 := helper.RandString(4)
helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, component0, "--context", context0, "--app", app0)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context0)
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile.yaml"), filepath.Join(context0, "devfile.yaml"))
helper.CmdShouldPass("odo", "push", "--context", context0)
if s2i {
helper.CopyExample(filepath.Join("source", "nodejs"), context00)
helper.CmdShouldPass("odo", "component", "create", "--s2i", "nodejs", component00, "--app", app0, "--project", namespace, "--context", context00)
helper.CmdShouldPass("odo", "storage", "create", storage00, "--path", "/data", "--size", "1Gi", "--context", context00)
helper.CmdShouldPass("odo", "url", "create", url00, "--port", "8080", "--context", context00)
} else {
helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, component00, "--context", context00, "--app", app0)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context00)
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile.yaml"), filepath.Join(context00, "devfile.yaml"))
helper.CmdShouldPass("odo", "storage", "create", storage00, "--path", "/data", "--size", "1Gi", "--context", context00)
helper.CmdShouldPass("odo", "url", "create", url00, "--port", "3000", "--context", context00, "--host", "com", "--ingress")
}
helper.CmdShouldPass("odo", "push", "--context", context00)
helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, component1, "--context", context1, "--app", app1)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context1)
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile.yaml"), filepath.Join(context1, "devfile.yaml"))
helper.CmdShouldPass("odo", "push", "--context", context1)
stdOut := helper.CmdShouldPass("odo", "app", "list", "--project", namespace)
helper.MatchAllInOutput(stdOut, []string{app0, app1})
// test the json output
stdOut = helper.CmdShouldPass("odo", "app", "list", "--project", namespace, "-o", "json")
helper.MatchAllInOutput(stdOut, []string{app0, app1})
Expect(helper.IsJSON(stdOut)).To(BeTrue())
stdOut = helper.CmdShouldPass("odo", "app", "describe", app0, "--project", namespace)
helper.MatchAllInOutput(stdOut, []string{app0, component0, component00, storage00, url00})
// test the json output
stdOut = helper.CmdShouldPass("odo", "app", "describe", app0, "--project", namespace, "-o", "json")
helper.MatchAllInOutput(stdOut, []string{app0, component0, component00})
Expect(helper.IsJSON(stdOut)).To(BeTrue())
stdOut = helper.CmdShouldPass("odo", "app", "delete", app0, "--project", namespace, "-f")
helper.MatchAllInOutput(stdOut, []string{app0, component0, component00, url00, storage00})
stdOut = helper.CmdShouldPass("odo", "app", "list", "--project", namespace)
helper.MatchAllInOutput(stdOut, []string{app1})
}