Display list of commands from the local devfile in odo describe component output (#6944)

* Add integration tests highlighting the expectations

* Add and fill a 'Commands' field from the DevfileData struct returned by `describe`

* Display commands in the human-readable output of 'odo describe'

* Add documentation and sample outputs
This commit is contained in:
Armel Soro
2023-07-03 16:19:06 +02:00
committed by GitHub
parent 97644aac7c
commit 4479c24dfe
11 changed files with 411 additions and 6 deletions

View File

@@ -33,6 +33,32 @@ Supported odo features:
• Deploy: true
• Debug: true
Commands:
• my-install
Type: exec
Group: build
Command Line: "npm install"
Component: runtime
Component Type: container
• my-run
Type: exec
Group: run
Command Line: "npm start"
Component: runtime
Component Type: container
• build-image
Type: apply
Component: prod-image
Component Type: image
Image Name: devfile-nodejs-deploy:latest
• deploy-deployment
Type: apply
Component: outerloop-deploy
Component Type: kubernetes
• deploy
Type: composite
Group: deploy
Container components:
• runtime
@@ -55,6 +81,7 @@ Kubernetes Routes:
This command returns information extracted from the Devfile:
- metadata (name, display name, project type, language, version, description and tags)
- supported odo features, indicating if the Devfile defines necessary information to run `odo dev`, `odo dev --debug` and `odo deploy`
- the list of commands, if any, along with some useful information about each command
- the list of container components,
- the list of Kubernetes components.
- the list of forwarded ports if the component is running in Dev mode.

View File

@@ -139,6 +139,7 @@ When the `describe component` command is executed without parameter from a direc
- the path of the Devfile,
- the content of the Devfile,
- supported `odo` features, indicating if the Devfile defines necessary information to run `odo dev`, `odo dev --debug` and `odo deploy`
- the list of commands, if any, along with some useful information about each command
- ingress or routes created in Deploy mode
- the status of the component
- the forwarded ports if odo is currently running in Dev mode,
@@ -155,6 +156,45 @@ odo describe component -o json
"schemaVersion": "2.0.0",
[ devfile.yaml file content ]
},
"commands": [
{
"name": "my-install",
"type": "exec",
"group": "build",
"isDefault": true,
"commandLine": "npm install",
"component": "runtime",
"componentType": "container"
},
{
"name": "my-run",
"type": "exec",
"group": "run",
"isDefault": true,
"commandLine": "npm start",
"component": "runtime",
"componentType": "container"
},
{
"name": "build-image",
"type": "apply",
"component": "prod-image",
"componentType": "image",
"imageName": "devfile-nodejs-deploy"
},
{
"name": "deploy-deployment",
"type": "apply",
"component": "outerloop-deploy",
"componentType": "kubernetes"
},
{
"name": "deploy",
"type": "composite",
"group": "deploy",
"isDefault": true
}
],
"supportedOdoFeatures": {
"dev": true,
"deploy": false,

View File

@@ -5,6 +5,7 @@ import "github.com/devfile/library/v2/pkg/devfile/parser/data"
// DevfileData describes a devfile content
type DevfileData struct {
Devfile data.DevfileData `json:"devfile"`
Commands []DevfileCommand `json:"commands,omitempty"`
SupportedOdoFeatures *SupportedOdoFeatures `json:"supportedOdoFeatures,omitempty"`
}
@@ -14,3 +15,41 @@ type SupportedOdoFeatures struct {
Deploy bool `json:"deploy"`
Debug bool `json:"debug"`
}
type DevfileCommand struct {
Name string `json:"name,omitempty"`
Type DevfileCommandType `json:"type,omitempty"`
Group DevfileCommandGroup `json:"group,omitempty"`
IsDefault *bool `json:"isDefault,omitempty"`
CommandLine string `json:"commandLine,omitempty"`
Component string `json:"component,omitempty"`
ComponentType DevfileComponentType `json:"componentType,omitempty"`
ImageName string `json:"imageName,omitempty"`
}
type DevfileCommandType string
const (
ExecCommandType DevfileCommandType = "exec"
ApplyCommandType DevfileCommandType = "apply"
CompositeCommandType DevfileCommandType = "composite"
)
type DevfileCommandGroup string
const (
BuildCommandGroup DevfileCommandGroup = "build"
RunCommandGroup DevfileCommandGroup = "run"
TestCommandGroup DevfileCommandGroup = "test"
DebugCommandGroup DevfileCommandGroup = "debug"
DeployCommandGroup DevfileCommandGroup = "deploy"
)
type DevfileComponentType string
const (
ImageComponentType DevfileComponentType = "image"
ContainerComponentType DevfileComponentType = "container"
KubernetesComponentType DevfileComponentType = "kubernetes"
OpenshiftComponentType DevfileComponentType = "openshift"
)

View File

@@ -1,16 +1,24 @@
package api
import (
v1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/devfile/parser/data"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"github.com/redhat-developer/odo/pkg/libdevfile"
)
func GetDevfileData(devfileObj parser.DevfileObj) *DevfileData {
func GetDevfileData(devfileObj parser.DevfileObj) (*DevfileData, error) {
commands, err := getDevfileCommands(devfileObj.Data)
if err != nil {
return nil, err
}
return &DevfileData{
Devfile: devfileObj.Data,
Commands: commands,
SupportedOdoFeatures: getSupportedOdoFeatures(devfileObj.Data),
}
}, nil
}
func getSupportedOdoFeatures(devfileData data.DevfileData) *SupportedOdoFeatures {
@@ -20,3 +28,92 @@ func getSupportedOdoFeatures(devfileData data.DevfileData) *SupportedOdoFeatures
Debug: libdevfile.HasDebugCommand(devfileData),
}
}
func getDevfileCommands(devfileData data.DevfileData) ([]DevfileCommand, error) {
commands, err := devfileData.GetCommands(common.DevfileOptions{})
if err != nil {
return nil, err
}
toGroupFn := func(g *v1alpha2.CommandGroup) (group DevfileCommandGroup, isDefault *bool) {
if g == nil {
return "", nil
}
switch g.Kind {
case v1alpha2.BuildCommandGroupKind:
group = BuildCommandGroup
case v1alpha2.RunCommandGroupKind:
group = RunCommandGroup
case v1alpha2.DebugCommandGroupKind:
group = DebugCommandGroup
case v1alpha2.TestCommandGroupKind:
group = TestCommandGroup
case v1alpha2.DeployCommandGroupKind:
group = DeployCommandGroup
}
return group, g.IsDefault
}
var result []DevfileCommand
for _, cmd := range commands {
var (
cmdType DevfileCommandType
cmdComponent string
cmdCompType DevfileComponentType
cmdLine string
)
var cmdGroup *v1alpha2.CommandGroup
switch {
case cmd.Apply != nil:
cmdType = ApplyCommandType
cmdComponent = cmd.Apply.Component
cmdGroup = cmd.Apply.Group
case cmd.Exec != nil:
cmdType = ExecCommandType
cmdComponent = cmd.Exec.Component
cmdGroup = cmd.Exec.Group
cmdLine = cmd.Exec.CommandLine
case cmd.Composite != nil:
cmdType = CompositeCommandType
cmdGroup = cmd.Composite.Group
}
var imageName string
var comp v1alpha2.Component
if cmdComponent != "" {
var ok bool
comp, ok, err = libdevfile.FindComponentByName(devfileData, cmdComponent)
if err != nil {
return nil, err
}
if ok {
switch {
case comp.Kubernetes != nil:
cmdCompType = KubernetesComponentType
case comp.Openshift != nil:
cmdCompType = OpenshiftComponentType
case comp.Container != nil:
cmdCompType = ContainerComponentType
case comp.Image != nil:
cmdCompType = ImageComponentType
imageName = comp.Image.ImageName
}
}
}
g, isDefault := toGroupFn(cmdGroup)
c := DevfileCommand{
Name: cmd.Id,
Type: cmdType,
Group: g,
IsDefault: isDefault,
CommandLine: cmdLine,
Component: cmdComponent,
ComponentType: cmdCompType,
ImageName: imageName,
}
result = append(result, c)
}
return result, nil
}

View File

@@ -9,6 +9,8 @@ import (
"github.com/devfile/library/v2/pkg/devfile/generator"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"k8s.io/klog"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/kclient"
@@ -20,7 +22,6 @@ import (
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/podman"
"github.com/redhat-developer/odo/pkg/state"
"k8s.io/klog"
)
// DescribeDevfileComponent describes the component defined by the devfile in the current directory
@@ -36,6 +37,11 @@ func DescribeDevfileComponent(
componentName = odocontext.GetComponentName(ctx)
)
devfileData, err := api.GetDevfileData(*devfileObj)
if err != nil {
return api.Component{}, nil, err
}
isPlatformFeatureEnabled := feature.IsEnabled(ctx, feature.GenericPlatformFlag)
platform := fcontext.GetPlatform(ctx, "")
switch platform {
@@ -115,7 +121,7 @@ func DescribeDevfileComponent(
cmp := api.Component{
DevfilePath: devfilePath,
DevfileData: api.GetDevfileData(*devfileObj),
DevfileData: devfileData,
DevForwardedPorts: forwardedPorts,
RunningIn: api.MergeRunningModes(runningOn),
RunningOn: runningOn,

View File

@@ -451,6 +451,20 @@ func GetContainerComponentsForCommand(devfileObj parser.DevfileObj, cmd v1alpha2
}
}
// FindComponentByName returns the Devfile component that matches the specified name.
func FindComponentByName(d data.DevfileData, n string) (v1alpha2.Component, bool, error) {
comps, err := d.GetComponents(common.DevfileOptions{})
if err != nil {
return v1alpha2.Component{}, false, err
}
for _, c := range comps {
if c.Name == n {
return c, true, nil
}
}
return v1alpha2.Component{}, false, nil
}
// GetK8sManifestsWithVariablesSubstituted returns the full content of either a Kubernetes or an Openshift
// Devfile component, either Inlined or referenced via a URI.
// No matter how the component is defined, it returns the content with all variables substituted

View File

@@ -11,6 +11,7 @@ import (
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
"k8s.io/utils/pointer"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/component/describe"
@@ -192,6 +193,36 @@ func printHumanReadableOutput(ctx context.Context, cmp api.Component, devfileObj
}
fmt.Println()
if cmp.DevfileData != nil && len(cmp.DevfileData.Commands) != 0 {
log.Info("Commands:")
for _, cmd := range cmp.DevfileData.Commands {
item := cmd.Name
if pointer.BoolDeref(cmd.IsDefault, false) {
item = log.Sbold(cmd.Name)
}
if cmd.Type != "" {
item += fmt.Sprintf("\n Type: %s", cmd.Type)
}
if cmd.Group != "" {
item += fmt.Sprintf("\n Group: %s", cmd.Group)
}
if cmd.CommandLine != "" {
item += fmt.Sprintf("\n Command Line: %q", cmd.CommandLine)
}
if cmd.Component != "" {
item += fmt.Sprintf("\n Component: %s", cmd.Component)
}
if cmd.ComponentType != "" {
item += fmt.Sprintf("\n Component Type: %s", cmd.ComponentType)
}
if cmd.ImageName != "" {
item += fmt.Sprintf("\n Image Name: %s", cmd.ImageName)
}
log.Printf(item)
}
}
fmt.Println()
err := listComponentsNames("Container components:", devfileObj, v1alpha2.ContainerComponentType)
if err != nil {
return err

View File

@@ -161,9 +161,13 @@ func (o *InitOptions) RunForJsonOutput(ctx context.Context) (out interface{}, er
if err != nil {
return nil, err
}
devfileData, err := api.GetDevfileData(devfileObj)
if err != nil {
return nil, err
}
return api.Component{
DevfilePath: devfilePath,
DevfileData: api.GetDevfileData(devfileObj),
DevfileData: devfileData,
DevForwardedPorts: []api.ForwardedPort{},
RunningIn: api.NewRunningModes(),
ManagedBy: "odo",

View File

@@ -445,5 +445,10 @@ func (o RegistryClient) retrieveDevfileDataFromRegistry(ctx context.Context, reg
// Convert DevfileObj to DevfileData
// use api.GetDevfileData to get supported features
return *api.GetDevfileData(devfileObj), nil
devfileData, err := api.GetDevfileData(devfileObj)
if err != nil {
return api.DevfileData{}, err
}
return *devfileData, nil
}

View File

@@ -355,6 +355,13 @@ func JsonPathContentIsValidUserPort(json string, path string) {
))
}
func JsonPathContentHasLen(json string, path string, len int) {
result := gjson.Get(json, path+".#")
intVal, err := strconv.Atoi(result.String())
Expect(err).ToNot(HaveOccurred())
Expect(intVal).To(Equal(len), fmt.Sprintf("%q should contain exactly %d elements", path, len))
}
// GetProjectName sets projectNames based on the name of the test file name (without path and replacing _ with -), line number of current ginkgo execution, and a random string of 3 letters
func GetProjectName() string {
//Get current test filename and remove file path, file extension and replace undescores with hyphens

View File

@@ -131,6 +131,26 @@ var _ = Describe("odo describe component command tests", func() {
helper.JsonPathContentIs(jsonContent, "devfileData.supportedOdoFeatures.deploy", "false")
helper.JsonPathContentIs(jsonContent, "devfileData.supportedOdoFeatures.debug", "true")
helper.JsonPathContentIs(jsonContent, "managedBy", "odo")
helper.JsonPathContentHasLen(jsonContent, "devfileData.commands", 4)
helper.JsonPathContentIs(jsonContent, "devfileData.commands.0.name", "install")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.0.group", "build")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.0.commandLine", "npm install")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.1.name", "run")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.1.group", "run")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.1.commandLine", "npm start")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.2.name", "debug")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.2.group", "debug")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.2.commandLine", "npm run debug")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.3.name", "test")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.3.group", "test")
helper.JsonPathContentIs(jsonContent, "devfileData.commands.3.commandLine", "npm test")
for i := 0; i <= 3; i++ {
helper.JsonPathContentIs(jsonContent, fmt.Sprintf("devfileData.commands.%d.type", i), "exec")
helper.JsonPathContentIs(jsonContent, fmt.Sprintf("devfileData.commands.%d.isDefault", i), "true")
helper.JsonPathContentIs(jsonContent, fmt.Sprintf("devfileData.commands.%d.component", i), "runtime")
helper.JsonPathContentIs(jsonContent, fmt.Sprintf("devfileData.commands.%d.componentType", i), "container")
}
}
checkDevfileDescription := func(content string, withUnknown bool) {
@@ -144,6 +164,7 @@ var _ = Describe("odo describe component command tests", func() {
Expect(content).To(ContainSubstring("Dev: Unknown"))
Expect(content).To(ContainSubstring("Debug: Unknown"))
Expect(content).To(ContainSubstring("Deploy: Unknown"))
Expect(content).ShouldNot(ContainSubstring("Commands:"))
} else {
Expect(content).To(ContainSubstring("Display Name: "))
Expect(content).To(ContainSubstring("Language: "))
@@ -153,6 +174,26 @@ var _ = Describe("odo describe component command tests", func() {
Expect(content).To(ContainSubstring("Dev: true"))
Expect(content).To(ContainSubstring("Debug: true"))
Expect(content).To(ContainSubstring("Deploy: false"))
Expect(content).To(ContainSubstring("Commands:"))
for _, c := range []string{"exec"} {
Expect(content).To(ContainSubstring("Type: " + c))
}
for _, c := range []string{"runtime"} {
Expect(content).To(ContainSubstring("Component: " + c))
}
for _, c := range []string{"container"} {
Expect(content).To(ContainSubstring("Component Type: " + c))
}
for _, c := range []string{"install", "run", "debug", "test"} {
Expect(content).To(ContainSubstring(c))
}
for _, c := range []string{"build", "run", "debug", "test"} {
Expect(content).To(ContainSubstring("Group: %s", c))
}
for _, c := range []string{"npm install", "npm start", "npm run debug", "npm test"} {
Expect(content).To(ContainSubstring("Command Line: %q", c))
}
}
}
@@ -440,6 +481,7 @@ var _ = Describe("odo describe component command tests", func() {
helper.JsonPathContentIs(stdout, "runningOn.cluster.deploy", "false")
helper.JsonPathDoesNotExist(stdout, "runningOn.podman")
}
helper.JsonPathDoesNotExist(stdout, "devfileData.commands")
})
})
})
@@ -651,4 +693,97 @@ var _ = Describe("odo describe component command tests", func() {
}
})
})
Context("describe commands in Devfile", Label(helper.LabelUnauth), Label(helper.LabelNoCluster), func() {
When("initializing a component with different types of commands", func() {
BeforeEach(func() {
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context)
helper.CopyExampleDevFile(
filepath.Join("source", "devfiles", "nodejs", "devfile-deploy-functional-pods.yaml"),
path.Join(commonVar.Context, "devfile.yaml"),
cmpName)
})
It("should describe the Devfile commands in human-readable form", func() {
stdout := helper.Cmd("odo", "describe", "component").ShouldPass().Out()
Expect(stdout).To(ContainSubstring("Commands:"))
for _, c := range []string{"exec", "composite", "apply"} {
Expect(stdout).To(ContainSubstring("Type: " + c))
}
for _, c := range []string{"runtime", "innerloop-pod", "prod-image", "outerloop-deploy"} {
Expect(stdout).To(ContainSubstring("Component: " + c))
}
for _, c := range []string{"container", "kubernetes", "image"} {
Expect(stdout).To(ContainSubstring("Component Type: " + c))
}
for _, c := range []string{
"install",
"innerloop-pod-command",
"start",
"run",
"build-image",
"deploy-deployment",
"deploy-another-deployment",
"outerloop-pod-command",
"deploy",
} {
Expect(stdout).To(ContainSubstring(c))
}
for _, c := range []string{"build", "run", "deploy"} {
Expect(stdout).To(ContainSubstring("Group: %s", c))
}
for _, c := range []string{"npm install", "npm start"} {
Expect(stdout).To(ContainSubstring("Command Line: %q", c))
}
for _, c := range []string{"quay.io/tkral/devfile-nodejs-deploy:latest"} {
Expect(stdout).To(ContainSubstring("Image Name: %s", c))
}
})
It("should describe the Devfile commands in JSON output", func() {
stdout := helper.Cmd("odo", "describe", "component", "-o", "json").ShouldPass().Out()
Expect(helper.IsJSON(stdout)).To(BeTrue(), fmt.Sprintf("invalid JSON output: %q", stdout))
helper.JsonPathContentHasLen(stdout, "devfileData.commands", 9)
helper.JsonPathContentIs(stdout, "devfileData.commands.0.name", "install")
helper.JsonPathContentIs(stdout, "devfileData.commands.0.group", "build")
helper.JsonPathContentIs(stdout, "devfileData.commands.0.commandLine", "npm install")
helper.JsonPathContentIs(stdout, "devfileData.commands.0.type", "exec")
helper.JsonPathContentIs(stdout, "devfileData.commands.0.isDefault", "true")
helper.JsonPathContentIs(stdout, "devfileData.commands.0.component", "runtime")
helper.JsonPathContentIs(stdout, "devfileData.commands.0.componentType", "container")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.0.imageName")
helper.JsonPathContentIs(stdout, "devfileData.commands.1.name", "innerloop-pod-command")
helper.JsonPathContentIs(stdout, "devfileData.commands.1.type", "apply")
helper.JsonPathContentIs(stdout, "devfileData.commands.1.component", "innerloop-pod")
helper.JsonPathContentIs(stdout, "devfileData.commands.1.componentType", "kubernetes")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.1.group")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.1.commandLine")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.1.isDefault")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.1.imageName")
helper.JsonPathContentIs(stdout, "devfileData.commands.4.name", "build-image")
helper.JsonPathContentIs(stdout, "devfileData.commands.4.type", "apply")
helper.JsonPathContentIs(stdout, "devfileData.commands.4.component", "prod-image")
helper.JsonPathContentIs(stdout, "devfileData.commands.4.componentType", "image")
helper.JsonPathContentIs(stdout, "devfileData.commands.4.imageName", "quay.io/tkral/devfile-nodejs-deploy:latest")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.4.group")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.4.commandLine")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.4.isDefault")
helper.JsonPathContentIs(stdout, "devfileData.commands.8.name", "deploy")
helper.JsonPathContentIs(stdout, "devfileData.commands.8.group", "deploy")
helper.JsonPathContentIs(stdout, "devfileData.commands.8.type", "composite")
helper.JsonPathContentIs(stdout, "devfileData.commands.8.isDefault", "true")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.8.imageName")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.8.commandLine")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.8.component")
helper.JsonPathDoesNotExist(stdout, "devfileData.commands.8.componentType")
})
})
})
})