Adds list command of storage for devfile v2 (#3788)

* Adds list command of storage for devfile v2

* Fixes import names in cli/storage/list.go

* Fixes display of componentName in list output. It also converts some functions to private and adds project flag while creating components in the storage script.

* Uses default storage size from the adapter

* Changes default storage size in integration test
This commit is contained in:
Mrinal Das
2020-08-31 17:16:24 +05:30
committed by GitHub
parent 2a4bafc151
commit d160b0097f
13 changed files with 1111 additions and 28 deletions

View File

@@ -61,7 +61,7 @@ const (
BinBash = "/bin/sh"
// Default volume size for volumes defined in a devfile
volumeSize = "5Gi"
DefaultVolumeSize = "1Gi"
// EnvProjectsRoot is the env defined for /projects where component mountSources=true
EnvProjectsRoot = "PROJECTS_ROOT"
@@ -180,7 +180,7 @@ func GetVolumes(devfileObj devfileParser.DevfileObj) map[string][]DevfileVolume
containerNameToVolumes := make(map[string][]DevfileVolume)
for _, containerComp := range containerComponents {
for _, volumeMount := range containerComp.Container.VolumeMounts {
size := volumeSize
size := DefaultVolumeSize
// check if there is a volume component name against the container component volume mount name
if volumeComp, ok := volumeNameToVolumeComponent[volumeMount.Name]; ok {

View File

@@ -186,7 +186,7 @@ func TestGetVolumes(t *testing.T) {
"comp1": {
{
Name: "myvolume1",
Size: "5Gi",
Size: "1Gi",
ContainerPath: "/my/volume/mount/path1",
},
},

View File

@@ -3,6 +3,7 @@ package storage
import (
"fmt"
"github.com/openshift/odo/pkg/devfile"
adapterCommon "github.com/openshift/odo/pkg/devfile/adapters/common"
"github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/machineoutput"
@@ -27,8 +28,6 @@ var (
`)
)
const defaultStorageSize = "1Gi"
type StorageCreateOptions struct {
storageName string
storageSize string
@@ -55,7 +54,7 @@ func (o *StorageCreateOptions) Complete(name string, cmd *cobra.Command, args []
o.componentName = o.EnvSpecificInfo.GetName()
if o.storageSize == "" {
o.storageSize = defaultStorageSize
o.storageSize = adapterCommon.DefaultVolumeSize
}
} else {
o.Context = genericclioptions.NewContext(cmd)

View File

@@ -2,7 +2,13 @@ package storage
import (
"fmt"
"github.com/openshift/odo/pkg/devfile"
"github.com/openshift/odo/pkg/devfile/parser"
"github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/odo/cli/component"
odoutil "github.com/openshift/odo/pkg/util"
"os"
"path/filepath"
"text/tabwriter"
"github.com/openshift/odo/pkg/log"
@@ -31,6 +37,9 @@ var (
type StorageListOptions struct {
componentContext string
*genericclioptions.Context
isDevfile bool
parser.DevfileObj
}
// NewStorageListOptions creates a new StorageListOptions instance
@@ -40,8 +49,19 @@ func NewStorageListOptions() *StorageListOptions {
// Complete completes StorageListOptions after they've been created
func (o *StorageListOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
// this also initializes the context as well
o.Context = genericclioptions.NewContext(cmd)
devFilePath := filepath.Join(o.componentContext, component.DevfilePath)
o.isDevfile = odoutil.CheckPathExists(devFilePath)
if o.isDevfile {
o.Context = genericclioptions.NewDevfileContext(cmd)
o.DevfileObj, err = devfile.ParseAndValidate(devFilePath)
if err != nil {
return err
}
} else {
// this also initializes the context as well
o.Context = genericclioptions.NewContext(cmd)
}
return
}
@@ -51,32 +71,53 @@ func (o *StorageListOptions) Validate() (err error) {
}
func (o *StorageListOptions) Run() (err error) {
storageList, err := storage.ListStorageWithState(o.Client, o.LocalConfigInfo, o.Component(), o.Application)
if err != nil {
return err
var storageList storage.StorageList
var componentName string
if o.isDevfile {
componentName = o.EnvSpecificInfo.GetName()
storageList, err = storage.DevfileList(o.KClient, o.DevfileObj.Data, o.EnvSpecificInfo.GetName())
if err != nil {
return err
}
} else {
componentName = o.LocalConfigInfo.GetName()
storageList, err = storage.ListStorageWithState(o.Client, o.LocalConfigInfo, o.Component(), o.Application)
if err != nil {
return err
}
}
if log.IsJSON() {
machineoutput.OutputSuccess(storageList)
} else {
printStorage(storageList, o.LocalConfigInfo.GetName())
if o.isDevfile && isContainerDisplay(storageList, o.DevfileObj.Data.GetComponents()) {
printStorageWithContainer(storageList, componentName)
} else {
printStorage(storageList, componentName)
}
}
return
}
// printStorage prints the given storageList
func printStorage(storageList storage.StorageList, compName string) {
if len(storageList.Items) > 0 {
tabWriterMounted := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent)
storageMap := make(map[string]bool)
// create headers of mounted storage table
fmt.Fprintln(tabWriterMounted, "NAME", "\t", "SIZE", "\t", "PATH", "\t", "STATE")
// iterating over all mounted storage and put in the mount storage table
for _, mStorage := range storageList.Items {
fmt.Fprintln(tabWriterMounted, mStorage.Name, "\t", mStorage.Spec.Size, "\t", mStorage.Spec.Path, "\t", mStorage.Status)
_, ok := storageMap[mStorage.Name]
if !ok {
storageMap[mStorage.Name] = true
fmt.Fprintln(tabWriterMounted, mStorage.Name, "\t", mStorage.Spec.Size, "\t", mStorage.Spec.Path, "\t", mStorage.Status)
}
}
// print all mounted storage of the given component
@@ -89,6 +130,83 @@ func printStorage(storageList storage.StorageList, compName string) {
fmt.Println("")
}
// printStorageWithContainer prints the given storageList with the corresponding container name
func printStorageWithContainer(storageList storage.StorageList, compName string) {
if len(storageList.Items) > 0 {
tabWriterMounted := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent)
// create headers of mounted storage table
fmt.Fprintln(tabWriterMounted, "NAME", "\t", "SIZE", "\t", "PATH", "\t", "CONTAINER", "\t", "STATE")
// iterating over all mounted storage and put in the mount storage table
for _, mStorage := range storageList.Items {
fmt.Fprintln(tabWriterMounted, mStorage.Name, "\t", mStorage.Spec.Size, "\t", mStorage.Spec.Path, "\t", mStorage.Spec.ContainerName, "\t", mStorage.Status)
}
// print all mounted storage of the given component
log.Infof("The component '%v' has the following storage attached:", compName)
tabWriterMounted.Flush()
} else {
log.Infof("The component '%v' has no storage attached", compName)
}
fmt.Println("")
}
// isContainerDisplay checks whether the container name should be included in the output
func isContainerDisplay(storageList storage.StorageList, components []common.DevfileComponent) bool {
// get all the container names
componentsMap := make(map[string]bool)
for _, comp := range components {
if comp.Container != nil {
componentsMap[comp.Container.Name] = true
}
}
storageCompMap := make(map[string][]string)
pathMap := make(map[string]string)
storageMap := make(map[string]storage.StorageStatus)
for _, storageItem := range storageList.Items {
if pathMap[storageItem.Name] == "" {
pathMap[storageItem.Name] = storageItem.Spec.Path
}
if storageMap[storageItem.Name] == "" {
storageMap[storageItem.Name] = storageItem.Status
}
// check if the storage is mounted on the same path in all the containers
if pathMap[storageItem.Name] != storageItem.Spec.Path {
return true
}
// check if the storage is in the same state for all the containers
if storageMap[storageItem.Name] != storageItem.Status {
return true
}
// check if the storage is mounted on a valid devfile container
// this situation can arrive when a container is removed from the devfile
// but the state is not pushed thus it exists on the cluster
_, ok := componentsMap[storageItem.Spec.ContainerName]
if !ok {
return true
}
storageCompMap[storageItem.Name] = append(storageCompMap[storageItem.Name], storageItem.Spec.ContainerName)
}
for _, containerNames := range storageCompMap {
// check if the storage is mounted on all the devfile containers
if len(containerNames) != len(componentsMap) {
return true
}
}
return false
}
// NewCmdStorageList implements the odo storage list command.
func NewCmdStorageList(name, fullName string) *cobra.Command {
o := NewStorageListOptions()

View File

@@ -0,0 +1,112 @@
package storage
import (
"github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/storage"
"github.com/openshift/odo/pkg/testingutil"
"testing"
)
func Test_isContainerDisplay(t *testing.T) {
generateStorage := func(storage storage.Storage, status storage.StorageStatus, containerName string) storage.Storage {
storage.Status = status
storage.Spec.ContainerName = containerName
return storage
}
type args struct {
storageList storage.StorageList
obj []common.DevfileComponent
}
tests := []struct {
name string
args args
want bool
}{
{
name: "case 1: storage is mounted on all the containers on the same path",
args: args{
storageList: storage.StorageList{
Items: []storage.Storage{
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/data"), storage.StateTypePushed, "container-0"),
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/data"), storage.StateTypePushed, "container-1"),
},
},
obj: []common.DevfileComponent{
testingutil.GetFakeContainerComponent("container-0"),
testingutil.GetFakeContainerComponent("container-1"),
},
},
want: false,
},
{
name: "case 2: storage is mounted on different paths",
args: args{
storageList: storage.StorageList{
Items: []storage.Storage{
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/data"), storage.StateTypePushed, "container-0"),
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/path"), storage.StateTypePushed, "container-1"),
},
},
obj: []common.DevfileComponent{
testingutil.GetFakeContainerComponent("container-0"),
testingutil.GetFakeContainerComponent("container-1"),
},
},
want: true,
},
{
name: "case 3: storage is mounted to the same path on all the containers but states are different",
args: args{
storageList: storage.StorageList{
Items: []storage.Storage{
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/data"), storage.StateTypePushed, "container-0"),
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/data"), storage.StateTypeNotPushed, "container-1"),
},
},
obj: []common.DevfileComponent{
testingutil.GetFakeContainerComponent("container-0"),
testingutil.GetFakeContainerComponent("container-1"),
},
},
want: true,
},
{
name: "case 4: storage is not mounted on all the containers",
args: args{
storageList: storage.StorageList{
Items: []storage.Storage{
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/data"), storage.StateTypePushed, "container-0"),
},
},
obj: []common.DevfileComponent{
testingutil.GetFakeContainerComponent("container-0"),
testingutil.GetFakeContainerComponent("container-1"),
},
},
want: true,
},
{
name: "case 5: storage is mounted on a container deleted locally from the devfile",
args: args{
storageList: storage.StorageList{
Items: []storage.Storage{
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/data"), storage.StateTypePushed, "container-0"),
generateStorage(storage.GetMachineReadableFormat("pvc-1", "1Gi", "/data"), storage.StateTypePushed, "container-1"),
},
},
obj: []common.DevfileComponent{
testingutil.GetFakeContainerComponent("container-0"),
},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isContainerDisplay(tt.args.storageList, tt.args.obj); got != tt.want {
t.Errorf("isContainerDisplay() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -2,20 +2,23 @@ package storage
import (
"fmt"
"github.com/openshift/odo/pkg/machineoutput"
"github.com/openshift/odo/pkg/devfile/adapters/common"
"reflect"
"github.com/openshift/odo/pkg/config"
"github.com/openshift/odo/pkg/devfile/parser/data"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/machineoutput"
applabels "github.com/openshift/odo/pkg/application/labels"
componentlabels "github.com/openshift/odo/pkg/component/labels"
"github.com/openshift/odo/pkg/kclient"
"github.com/openshift/odo/pkg/occlient"
storagelabels "github.com/openshift/odo/pkg/storage/labels"
"github.com/openshift/odo/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
)
@@ -465,6 +468,21 @@ func GetMachineReadableFormat(storageName, storageSize, storagePath string) Stor
}
}
// GetMachineFormatWithContainer gives machine readable Storage definition
// storagePath indicates the path to which the storage is mounted to, "" if not mounted
func GetMachineFormatWithContainer(storageName, storageSize, storagePath string, container string) Storage {
storage := Storage{
TypeMeta: metav1.TypeMeta{Kind: "storage", APIVersion: apiVersion},
ObjectMeta: metav1.ObjectMeta{Name: storageName},
Spec: StorageSpec{
Size: storageSize,
Path: storagePath,
},
}
storage.Spec.ContainerName = container
return storage
}
func ListStorageWithState(client *occlient.Client, localConfig *config.LocalConfigInfo, componentName string, applicationName string) (StorageList, error) {
storageConfig, err := localConfig.StorageList()
@@ -550,3 +568,127 @@ 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) {
pod, err := kClient.GetPodUsingComponentName(componentName)
if err != nil {
if _, ok := err.(*kclient.PodNotFoundError); ok {
return StorageList{}, nil
}
return StorageList{}, err
}
var storage []Storage
var volumeMounts []Storage
for _, container := range pod.Spec.Containers {
for _, volumeMount := range container.VolumeMounts {
volumeMounts = append(volumeMounts, Storage{
ObjectMeta: metav1.ObjectMeta{Name: volumeMount.Name},
Spec: StorageSpec{
Path: volumeMount.MountPath,
ContainerName: container.Name,
},
})
}
}
if len(volumeMounts) <= 0 {
return StorageList{}, nil
}
label := fmt.Sprintf("component=%s", componentName)
pvcs, err := kClient.GetPVCsFromSelector(label)
if err != nil {
return StorageList{}, errors.Wrapf(err, "unable to get PVC using selector %v", storagelabels.StorageLabel)
}
for _, pvc := range pvcs {
found := false
for _, volumeMount := range volumeMounts {
if volumeMount.Name == pvc.Name+"-vol" {
found = true
size := pvc.Spec.Resources.Requests[corev1.ResourceStorage]
storage = append(storage, GetMachineFormatWithContainer(pvc.Labels[storagelabels.DevfileStorageLabel], size.String(), volumeMount.Spec.Path, volumeMount.Spec.ContainerName))
}
}
if !found {
return StorageList{}, fmt.Errorf("mount path for pvc %s not found", pvc.Name)
}
}
return StorageList{Items: storage}, nil
}
// getLocalDevfileStorage lists the storage from the devfile
func getLocalDevfileStorage(devfileData data.DevfileData) StorageList {
volumeSizeMap := make(map[string]string)
for _, component := range devfileData.GetComponents() {
if component.Volume == nil {
continue
}
if component.Volume.Size == "" {
component.Volume.Size = common.DefaultVolumeSize
}
volumeSizeMap[component.Volume.Name] = component.Volume.Size
}
components := devfileData.GetComponents()
var storage []Storage
for _, component := range components {
if component.Container == nil {
continue
}
for _, volumeMount := range component.Container.VolumeMounts {
size, ok := volumeSizeMap[volumeMount.Name]
if ok {
storage = append(storage, GetMachineFormatWithContainer(volumeMount.Name, size, volumeMount.Path, component.Container.Name))
}
}
}
return StorageList{Items: storage}
}
// DevfileList lists the storage from the local devfile and cluster with their respective state
func DevfileList(kClient *kclient.Client, devfileData data.DevfileData, componentName string) (StorageList, error) {
localStorage := getLocalDevfileStorage(devfileData)
clusterStorage, err := devfileListMounted(kClient, componentName)
if err != nil {
return StorageList{}, err
}
var storageList []Storage
// find the local storage which are in a pushed and not pushed state
for _, localStore := range localStorage.Items {
found := false
for _, clusterStore := range clusterStorage.Items {
if reflect.DeepEqual(localStore, clusterStore) {
found = true
}
}
if found {
localStore.Status = StateTypePushed
} else {
localStore.Status = StateTypeNotPushed
}
storageList = append(storageList, localStore)
}
// find the cluster storage which have been deleted locally
for _, clusterStore := range clusterStorage.Items {
found := false
for _, localStore := range localStorage.Items {
if reflect.DeepEqual(localStore, clusterStore) {
found = true
}
}
if !found {
clusterStore.Status = StateTypeLocallyDeleted
storageList = append(storageList, clusterStore)
}
}
return GetMachineReadableFormatForList(storageList), nil
}

View File

@@ -1,6 +1,10 @@
package storage
import (
"github.com/kylelemons/godebug/pretty"
"github.com/openshift/odo/pkg/devfile/parser/data"
"github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/kclient"
"reflect"
"testing"
@@ -937,3 +941,612 @@ func TestListStorageWithState(t *testing.T) {
})
}
}
func generateStorage(storage Storage, status StorageStatus, containerName string) Storage {
storage.Status = status
storage.Spec.ContainerName = containerName
return storage
}
func TestGetLocalDevfileStorage(t *testing.T) {
type args struct {
devfileData data.DevfileData
}
tests := []struct {
name string
args args
want StorageList
}{
{
name: "case 1: list all the volumes in the devfile along with their respective size and containers",
args: args{
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: "container-0",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-0",
Path: "/path",
},
{
Name: "volume-1",
Path: "/data",
},
},
},
},
{
Container: &common.Container{
Name: "container-1",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-1",
Path: "/data",
},
},
},
},
testingutil.GetFakeVolumeComponent("volume-0", "5Gi"),
testingutil.GetFakeVolumeComponent("volume-1", "10Gi"),
},
},
},
want: StorageList{
Items: []Storage{
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/path"), "", "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/data"), "", "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/data"), "", "container-1"),
},
},
},
{
name: "case 2: list all the volumes in the devfile with the default size when no size is mentioned",
args: args{
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: "container-0",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-0",
Path: "/path",
},
{
Name: "volume-1",
Path: "/data",
},
},
},
},
testingutil.GetFakeVolumeComponent("volume-0", ""),
testingutil.GetFakeVolumeComponent("volume-1", "10Gi"),
},
},
},
want: StorageList{
Items: []Storage{
generateStorage(GetMachineReadableFormat("volume-0", "1Gi", "/path"), "", "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/data"), "", "container-0"),
},
},
},
{
name: "case 3: return empty when no volumes is mounted",
args: args{
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: "container-0",
},
},
testingutil.GetFakeVolumeComponent("volume-0", ""),
testingutil.GetFakeVolumeComponent("volume-1", "10Gi"),
},
},
},
want: StorageList{
Items: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getLocalDevfileStorage(tt.args.devfileData); !reflect.DeepEqual(got, tt.want) {
t.Errorf("getLocalDevfileStorage() difference between got and want : %v", pretty.Compare(got, tt.want))
}
})
}
}
func TestDevfileListMounted(t *testing.T) {
type args struct {
componentName string
}
tests := []struct {
name string
args args
returnedPods *corev1.PodList
returnedPVCs *corev1.PersistentVolumeClaimList
want StorageList
wantErr bool
}{
{
name: "case 1: should error out for multiple pods returned",
args: args{
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePod("nodejs", "pod-0"),
*testingutil.CreateFakePod("nodejs", "pod-1"),
},
},
wantErr: true,
},
{
name: "case 2: pod not found",
args: args{
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{},
},
want: StorageList{},
wantErr: false,
},
{
name: "case 3: no volume mounts on pod",
args: args{
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePod("nodejs", "pod-0"),
},
},
want: StorageList{},
wantErr: false,
},
{
name: "case 4: two volumes mounted on a single container",
args: args{
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{
testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
{Name: "volume-0-vol", MountPath: "/data"},
{Name: "volume-1-vol", MountPath: "/path"},
}),
}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
*testingutil.FakePVC("volume-0", "5Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-0"}),
*testingutil.FakePVC("volume-1", "10Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-1"}),
},
},
want: StorageList{
Items: []Storage{
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/data"), "", "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/path"), "", "container-0"),
},
},
wantErr: false,
},
{
name: "case 5: one volume is mounted on a single container and another on both",
args: args{
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{
testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
{Name: "volume-0-vol", MountPath: "/data"},
{Name: "volume-1-vol", MountPath: "/path"},
}),
testingutil.CreateFakeContainerWithVolumeMounts("container-1", []corev1.VolumeMount{
{Name: "volume-1-vol", MountPath: "/path"},
}),
}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
*testingutil.FakePVC("volume-0", "5Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-0"}),
*testingutil.FakePVC("volume-1", "10Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-1"}),
},
},
want: StorageList{
Items: []Storage{
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/data"), "", "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/path"), "", "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/path"), "", "container-1"),
},
},
wantErr: false,
},
{
name: "case 6: pvc for volumeMount not found",
args: args{
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{
testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
{Name: "volume-0-vol", MountPath: "/data"},
}),
testingutil.CreateFakeContainer("container-1"),
}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
*testingutil.FakePVC("volume-0", "5Gi", map[string]string{"component": "nodejs"}),
*testingutil.FakePVC("volume-1", "5Gi", map[string]string{"component": "nodejs"}),
},
},
wantErr: true,
},
{
name: "case 7: the storage label should be used as the name of the storage",
args: args{
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{
testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
{Name: "volume-0-nodejs-vol", MountPath: "/data"},
}),
}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
*testingutil.FakePVC("volume-0-nodejs", "5Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-0"}),
},
},
want: StorageList{
Items: []Storage{
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/data"), "", "container-0"),
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient, fakeClientSet := kclient.FakeNew()
fakeClientSet.Kubernetes.PrependReactor("list", "persistentvolumeclaims", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, tt.returnedPVCs, nil
})
fakeClientSet.Kubernetes.PrependReactor("list", "pods", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, tt.returnedPods, nil
})
got, err := devfileListMounted(fakeClient, tt.args.componentName)
if (err != nil) != tt.wantErr {
t.Errorf("devfileListMounted() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("devfileListMounted() result is different: %v", pretty.Compare(got, tt.want))
}
})
}
}
func TestDevfileList(t *testing.T) {
type args struct {
devfileData data.DevfileData
componentName string
}
tests := []struct {
name string
args args
returnedPods *corev1.PodList
returnedPVCs *corev1.PersistentVolumeClaimList
want StorageList
wantErr bool
}{
{
name: "case 1: no volume on devfile and no pod on cluster",
args: args{
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
testingutil.GetFakeContainerComponent("runtime"),
},
},
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{},
},
want: GetMachineReadableFormatForList(nil),
wantErr: false,
},
{
name: "case 2: no volume on devfile and pod",
args: args{
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
testingutil.GetFakeContainerComponent("runtime"),
},
},
componentName: "nodejs",
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{testingutil.CreateFakeContainer("container-0")}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{},
},
want: GetMachineReadableFormatForList(nil),
wantErr: false,
},
{
name: "case 3: same two volumes on cluster and devFile",
args: args{
componentName: "nodejs",
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: "container-0",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-0",
Path: "/data",
},
{
Name: "volume-1",
Path: "/path",
},
},
},
},
testingutil.GetFakeVolumeComponent("volume-0", "5Gi"),
testingutil.GetFakeVolumeComponent("volume-1", "10Gi"),
},
},
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{
testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
{Name: "volume-0-vol", MountPath: "/data"},
{Name: "volume-1-vol", MountPath: "/path"},
}),
}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
*testingutil.FakePVC("volume-0", "5Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-0"}),
*testingutil.FakePVC("volume-1", "10Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-1"}),
},
},
want: GetMachineReadableFormatForList([]Storage{
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/data"), StateTypePushed, "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/path"), StateTypePushed, "container-0"),
}),
wantErr: false,
},
{
name: "case 4: both volumes, present on the cluster and devFile, are different",
args: args{
componentName: "nodejs",
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: "container-0",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-0",
Path: "/data",
},
{
Name: "volume-1",
Path: "/path",
},
},
},
},
testingutil.GetFakeVolumeComponent("volume-0", "5Gi"),
testingutil.GetFakeVolumeComponent("volume-1", "10Gi"),
},
},
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{
testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
{Name: "volume-00-vol", MountPath: "/data"},
{Name: "volume-11-vol", MountPath: "/path"},
}),
}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
*testingutil.FakePVC("volume-00", "5Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-00"}),
*testingutil.FakePVC("volume-11", "10Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-11"}),
},
},
want: GetMachineReadableFormatForList([]Storage{
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/data"), StateTypeNotPushed, "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/path"), StateTypeNotPushed, "container-0"),
generateStorage(GetMachineReadableFormat("volume-00", "5Gi", "/data"), StateTypeLocallyDeleted, "container-0"),
generateStorage(GetMachineReadableFormat("volume-11", "10Gi", "/path"), StateTypeLocallyDeleted, "container-0"),
}),
wantErr: false,
},
{
name: "case 5: two containers with different volumes but one container is not pushed",
args: args{
componentName: "nodejs",
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: "container-0",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-0",
Path: "/data",
},
},
},
},
{
Container: &common.Container{
Name: "container-1",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-1",
Path: "/data",
},
},
},
},
testingutil.GetFakeVolumeComponent("volume-0", "5Gi"),
testingutil.GetFakeVolumeComponent("volume-1", "10Gi"),
},
},
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{
testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
{Name: "volume-0-vol", MountPath: "/data"},
}),
}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
*testingutil.FakePVC("volume-0", "5Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-0"}),
},
},
want: GetMachineReadableFormatForList([]Storage{
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/data"), StateTypePushed, "container-0"),
generateStorage(GetMachineReadableFormat("volume-1", "10Gi", "/data"), StateTypeNotPushed, "container-1"),
}),
wantErr: false,
},
{
name: "case 6: two containers with different volumes on the cluster but one container is deleted locally",
args: args{
componentName: "nodejs",
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: "container-0",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-0",
Path: "/data",
},
},
},
},
testingutil.GetFakeVolumeComponent("volume-0", "5Gi"),
},
},
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePodWithContainers("nodejs", "pod-0", []corev1.Container{
testingutil.CreateFakeContainerWithVolumeMounts("container-0", []corev1.VolumeMount{
{Name: "volume-0-vol", MountPath: "/data"},
}),
testingutil.CreateFakeContainerWithVolumeMounts("container-1", []corev1.VolumeMount{
{Name: "volume-0-vol", MountPath: "/data"}},
),
}),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
*testingutil.FakePVC("volume-0", "5Gi", map[string]string{"component": "nodejs", storageLabels.DevfileStorageLabel: "volume-0"}),
},
},
want: GetMachineReadableFormatForList([]Storage{
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/data"), StateTypePushed, "container-0"),
generateStorage(GetMachineReadableFormat("volume-0", "5Gi", "/data"), StateTypeLocallyDeleted, "container-1"),
}),
wantErr: false,
},
{
name: "case 7: multiple pods are present on the cluster",
args: args{
componentName: "nodejs",
devfileData: &testingutil.TestDevfileData{
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: "container-0",
VolumeMounts: []common.VolumeMount{
{
Name: "volume-0",
Path: "/data",
},
},
},
},
testingutil.GetFakeVolumeComponent("volume-0", "5Gi"),
},
},
},
returnedPods: &corev1.PodList{
Items: []corev1.Pod{
*testingutil.CreateFakePod("nodejs", "pod-0"),
*testingutil.CreateFakePod("nodejs", "pod-1"),
},
},
returnedPVCs: &corev1.PersistentVolumeClaimList{},
want: StorageList{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient, fakeClientSet := kclient.FakeNew()
fakeClientSet.Kubernetes.PrependReactor("list", "persistentvolumeclaims", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, tt.returnedPVCs, nil
})
fakeClientSet.Kubernetes.PrependReactor("list", "pods", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, tt.returnedPods, nil
})
got, err := DevfileList(fakeClient, tt.args.devfileData, tt.args.componentName)
if (err != nil) != tt.wantErr {
t.Errorf("DevfileList() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("DevfileList() result is different: %v", pretty.Compare(tt.want, got))
}
})
}
}

View File

@@ -29,6 +29,8 @@ type StorageSpec struct {
Size string `json:"size,omitempty"`
// if path is empty, it indicates that the storage is not mounted in any component
Path string `json:"path,omitempty"`
ContainerName string `json:"containerName,omitempty"`
}
// StorageList is a list of storages

View File

@@ -0,0 +1,17 @@
package testingutil
import corev1 "k8s.io/api/core/v1"
// CreateFakeContainer creates a container with the given containerName
func CreateFakeContainer(containerName string) corev1.Container {
return corev1.Container{
Name: containerName,
}
}
// CreateFakeContainerWithVolumeMounts creates a container with the given containerName and volumeMounts
func CreateFakeContainerWithVolumeMounts(containerName string, volumeMounts []corev1.VolumeMount) corev1.Container {
container := CreateFakeContainer(containerName)
container.VolumeMounts = volumeMounts
return container
}

View File

@@ -143,7 +143,9 @@ func (d TestDevfileData) AddProjects(projects []common.DevfileProject) error { r
func (d TestDevfileData) UpdateProject(project common.DevfileProject) {}
func (d TestDevfileData) AddStarterProjects(projects []common.DevfileStarterProject) error { return nil }
func (d TestDevfileData) AddStarterProjects(projects []common.DevfileStarterProject) error {
return nil
}
func (d TestDevfileData) UpdateStarterProject(project common.DevfileStarterProject) {}

View File

@@ -20,3 +20,10 @@ func CreateFakePod(componentName, podName string) *corev1.Pod {
}
return fakePod
}
// CreateFakePodWithContainers creates a fake pod with the given pod name, container name and containers
func CreateFakePodWithContainers(componentName, podName string, containers []corev1.Container) *corev1.Pod {
fakePod := CreateFakePod(componentName, podName)
fakePod.Spec.Containers = containers
return fakePod
}

View File

@@ -643,7 +643,7 @@ var _ = Describe("odo devfile push command tests", func() {
// Verify the pvc size for firstvol
storageSize := cliRunner.GetPVCSize(cmpName, "firstvol", namespace)
// should be the default size
Expect(storageSize).To(ContainSubstring("5Gi"))
Expect(storageSize).To(ContainSubstring("1Gi"))
// Verify the pvc size for secondvol
storageSize = cliRunner.GetPVCSize(cmpName, "secondvol", namespace)

View File

@@ -49,7 +49,7 @@ var _ = Describe("odo devfile storage command tests", func() {
Context("When devfile storage create command is executed", func() {
It("should create the storage and mount it on the container", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
@@ -90,7 +90,7 @@ var _ = Describe("odo devfile storage command tests", func() {
})
It("should create a storage with default size when --size is not provided", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
@@ -109,7 +109,7 @@ var _ = Describe("odo devfile storage command tests", func() {
})
It("should create a storage when storage is not provided", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
@@ -126,7 +126,7 @@ var _ = Describe("odo devfile storage command tests", func() {
})
It("should create and output in json format", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
@@ -140,7 +140,7 @@ var _ = Describe("odo devfile storage command tests", func() {
Context("When devfile storage delete command is executed", func() {
It("should delete the storage and unmount it on the container", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
@@ -184,7 +184,7 @@ var _ = Describe("odo devfile storage command tests", func() {
})
It("should delete the storage and output in json format", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
@@ -198,9 +198,80 @@ var _ = Describe("odo devfile storage command tests", func() {
})
})
Context("When devfile storage list command is executed", func() {
It("should list the storage with the proper states", func() {
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml"))
storageNames := []string{helper.RandString(5), helper.RandString(5)}
pathNames := []string{"/data", "/data-1"}
sizes := []string{"5Gi", "1Gi"}
helper.CmdShouldPass("odo", "storage", "create", storageNames[0], "--path", pathNames[0], "--size", sizes[0], "--context", context)
stdOut := helper.CmdShouldPass("odo", "storage", "list", "--context", context)
helper.MatchAllInOutput(stdOut, []string{storageNames[0], pathNames[0], sizes[0], "Not Pushed", cmpName})
helper.DontMatchAllInOutput(stdOut, []string{"CONTAINER", "runtime"})
helper.CmdShouldPass("odo", "push", "--context", context)
stdOut = helper.CmdShouldPass("odo", "storage", "list", "--context", context)
helper.MatchAllInOutput(stdOut, []string{storageNames[0], pathNames[0], sizes[0], "Pushed"})
helper.DontMatchAllInOutput(stdOut, []string{"CONTAINER", "runtime"})
helper.CmdShouldPass("odo", "storage", "create", storageNames[1], "--path", pathNames[1], "--size", sizes[1], "--context", context)
stdOut = helper.CmdShouldPass("odo", "storage", "list", "--context", context)
helper.MatchAllInOutput(stdOut, []string{storageNames[0], pathNames[0], sizes[0], "Pushed"})
helper.MatchAllInOutput(stdOut, []string{storageNames[1], pathNames[1], sizes[1], "Not Pushed"})
helper.DontMatchAllInOutput(stdOut, []string{"CONTAINER", "runtime"})
helper.CmdShouldPass("odo", "storage", "delete", storageNames[0], "-f", "--context", context)
stdOut = helper.CmdShouldPass("odo", "storage", "list", "--context", context)
helper.MatchAllInOutput(stdOut, []string{storageNames[0], pathNames[0], sizes[0], "Locally Deleted"})
helper.DontMatchAllInOutput(stdOut, []string{"CONTAINER", "runtime"})
})
It("should list the storage with the proper states and container names", func() {
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-with-volume-components.yaml"), filepath.Join(context, "devfile.yaml"))
stdOut := helper.CmdShouldPass("odo", "storage", "list", "--context", context)
helper.MatchAllInOutput(stdOut, []string{"firstvol", "secondvol", "Not Pushed", "CONTAINER", "runtime", "runtime2"})
helper.CmdShouldPass("odo", "push", "--context", context)
stdOut = helper.CmdShouldPass("odo", "storage", "list", "--context", context)
helper.MatchAllInOutput(stdOut, []string{"firstvol", "secondvol", "Pushed", "CONTAINER", "runtime", "runtime2"})
helper.CmdShouldPass("odo", "storage", "delete", "firstvol", "-f", "--context", context)
stdOut = helper.CmdShouldPass("odo", "storage", "list", "--context", context)
helper.MatchAllInOutput(stdOut, []string{"firstvol", "secondvol", "Pushed", "Locally Deleted", "CONTAINER", "runtime", "runtime2"})
})
It("should list output in json format", func() {
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml"))
helper.CmdShouldPass("odo", "storage", "create", "mystorage", "--path=/opt/app-root/src/storage/", "--size=1Gi", "--context", context)
actualStorageList := helper.CmdShouldPass("odo", "storage", "list", "--context", context, "-o", "json")
desiredStorageList := `{"kind":"List","apiVersion":"odo.dev/v1alpha1","metadata":{},"items":[{"kind":"storage","apiVersion":"odo.dev/v1alpha1","metadata":{"name":"mystorage","creationTimestamp":null},"spec":{"size":"1Gi","path":"/opt/app-root/src/storage/","containerName":"runtime"},"status":"Not Pushed"}]}`
Expect(desiredStorageList).Should(MatchJSON(actualStorageList))
})
})
Context("When devfile storage commands are invalid", func() {
It("should error if same storage name is provided again", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
@@ -215,7 +286,7 @@ var _ = Describe("odo devfile storage command tests", func() {
})
It("should error if same path is provided again", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)
@@ -230,7 +301,7 @@ var _ = Describe("odo devfile storage command tests", func() {
})
It("should throw error if no storage is present", func() {
args := []string{"create", "nodejs", cmpName, "--context", context}
args := []string{"create", "nodejs", cmpName, "--context", context, "--project", namespace}
helper.CmdShouldPass("odo", args...)
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context)