Add new --run-port flag to odo init to set ports non-interactively (#6953)

* Add new `--run-port` flag to `odo init` to set ports non-interactively

As depicted in [1], this leverages the default (or single non-default) run command to find the linked container component.
As such, it assumes that the command found is an exec command,
and that the linked component is a container component.

[1] https://github.com/redhat-developer/odo/issues/6925

* Add unit and integration tests highlighting the expectations

* Document the new `--run-port` flag

* Fix some typos and language correctness issues in the `odo init` doc

* Add doc automation test for the output of `odo init --run-port`

This ensures the output and sample in the doc are kept in sync with the code base.
This commit is contained in:
Armel Soro
2023-07-06 16:35:24 +02:00
committed by GitHub
parent f6bb4e2689
commit c4b103d9c4
11 changed files with 926 additions and 14 deletions

View File

@@ -0,0 +1,14 @@
```console
$ odo init --devfile go --name my-go-app --run-port 3456 --run-port 9876
__
/ \__ Initializing a new component
\__/ \
/ \__/ odo version: v3.12.0
\__/
✓ Downloading devfile "go" [48ms]
Your new component 'my-go-app' is ready in the current directory.
To start editing your component, use 'odo dev' and open this folder in your favorite IDE.
Changes will be directly reflected on the cluster.
```

View File

@@ -67,15 +67,20 @@ import NonEmptyDirectoryOutput from './docs-mdx/init/interactive_mode_directory_
In non-interactive mode, you will have to specify from the command-line the information needed to get a devfile.
If you want to download a devfile from a registry, you must specify the devfile name with the `--devfile` flag. The devfile with the specified name will be searched in the registries referenced (using `odo preference view`), and the first one matching will be downloaded.
If you want to download the devfile from a specific registry in the list or referenced registries, you can use the `--devfile-registry` flag to specify the name of this registry. By default odo uses official devfile registry [registry.devfile.io](https://registry.devfile.io). You can use registry's [web interface](https://registry.devfile.io/viewer) to view its content.
If you want to download a version devfile, you must specify the version with `--devfile-version` flag.
If you want to download the devfile from a specific registry in the list or referenced registries, you can use the `--devfile-registry` flag to specify the name of this registry. By default, `odo` uses the official devfile registry [registry.devfile.io](https://registry.devfile.io). You can use the registry [web interface](https://registry.devfile.io/viewer) to view its content.
If you want to download a specific version of a devfile, you can specify the version with the `--devfile-version` flag.
If you prefer to download a devfile from an URL or from the local filesystem, you can use the `--devfile-path` instead.
If you prefer to download a devfile from a URL or from the local filesystem, you can use the `--devfile-path` instead.
The `--starter` flag indicates the name of the starter project (as referenced in the selected devfile), that you want to use to start your development. To see the available starter projects for devfile stacks in the official devfile registry use its [web interface](https://registry.devfile.io/viewer) to view its content.
The required `--name` flag indicates how the component initialized by this command should be named. The name must follow the [Kubernetes naming convention](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names) and not be all-numeric.
If you know what ports your application uses, you can specify the `--run-port` flag to initialize the Devfile with the specified ports, instead of the default ones set in the registry.
The `--run-port` flag is a repeatable flag that will make `odo` read the downloaded Devfile and look for the container component referenced by the default `run` command.
It will then overwrite the container component endpoints with the ports specified.
As such, it requires the default `run` command to be an `exec` command pointing to a `container` component.
#### Fetch Devfile from any registry of the list
In this example, the devfile will be downloaded from the **StagingRegistry** registry, which is the first one in the list containing the `nodejs-react` devfile.
@@ -156,3 +161,26 @@ Use "latest" as the version name to fetch the latest version of a given Devfile.
</details>
:::
#### Specify the application ports
```console
odo init \
--devfile <devfile-name> \
--name <component-name> \
--run-port <port> [--run-port ANOTHER_PORT] \
[--starter STARTER]
```
In this example, `odo` will download the Devfile from the registry and overwrite the container endpoints with the ones specified in `--run-port`.
This works because the Devfile downloaded from the registry defines a default `run` command of type `exec` and referencing a `container` component.
<details>
<summary>Example</summary>
import DevfileWithRunPortOutput from './docs-mdx/init/devfile_with_run-port_output.mdx';
<DevfileWithRunPortOutput />
</details>

View File

@@ -5,6 +5,7 @@ import (
"io"
"strconv"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"k8s.io/klog"
@@ -40,27 +41,39 @@ func handleApplicationPorts(w io.Writer, devfileobj parser.DevfileObj, ports []i
component := components[0]
err = setPortsInContainerComponent(&devfileobj, &component, ports, true)
if err != nil {
return parser.DevfileObj{}, err
}
return devfileobj, nil
}
func setPortsInContainerComponent(devfileobj *parser.DevfileObj, component *v1alpha2.Component, ports []int, withDebug bool) error {
// Add the new ports at the beginning of the list (that is before any Debug endpoints).
// This way, application ports will be port-forwarded first.
portsToSet := make([]string, 0, len(ports))
for _, p := range ports {
portsToSet = append(portsToSet, strconv.Itoa(p))
}
debugEndpoints, err := libdevfile.GetDebugEndpointsForComponent(component)
debugEndpoints, err := libdevfile.GetDebugEndpointsForComponent(*component)
if err != nil {
return parser.DevfileObj{}, err
return err
}
// Clear the existing endpoint list
component.Container.Endpoints = nil
// Add the new application ports first
err = devfileobj.Data.SetPorts(map[string][]string{component.Name: portsToSet})
if err != nil {
return parser.DevfileObj{}, err
return err
}
// Append debug endpoints to the end of the list
component.Container.Endpoints = append(component.Container.Endpoints, debugEndpoints...)
if withDebug {
component.Container.Endpoints = append(component.Container.Endpoints, debugEndpoints...)
}
return devfileobj, nil
return nil
}

View File

@@ -4,7 +4,12 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"k8s.io/klog"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/registry"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
@@ -24,6 +29,7 @@ const (
FLAG_STARTER = "starter"
FLAG_DEVFILE_PATH = "devfile-path"
FLAG_DEVFILE_VERSION = "devfile-version"
FLAG_RUN_PORT = "run-port"
)
// FlagsBackend is a backend that will extract all needed information from flags passed to the command
@@ -133,7 +139,80 @@ func (o FlagsBackend) PersonalizeDevfileConfig(devfileobj parser.DevfileObj) (pa
return devfileobj, nil
}
func (o FlagsBackend) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string) (parser.DevfileObj, error) {
// Currently not supported, but this will be done in a separate issue: https://github.com/redhat-developer/odo/issues/6211
func (o FlagsBackend) HandleApplicationPorts(devfileobj parser.DevfileObj, _ []int, flags map[string]string) (parser.DevfileObj, error) {
d, err := setPortsForFlag(devfileobj, flags, FLAG_RUN_PORT)
if err != nil {
return parser.DevfileObj{}, err
}
return d, nil
}
func setPortsForFlag(devfileobj parser.DevfileObj, flags map[string]string, flagName string) (parser.DevfileObj, error) {
flagVal := flags[flagName]
// Repeatable flags are formatted as "[val1,val2]"
if !(strings.HasPrefix(flagVal, "[") && strings.HasSuffix(flagVal, "]")) {
return devfileobj, nil
}
portsStr := flagVal[1 : len(flagVal)-1]
var ports []int
split := strings.Split(portsStr, ",")
for _, s := range split {
p, err := strconv.Atoi(s)
if err != nil {
return parser.DevfileObj{}, fmt.Errorf("invalid value for %s (%q): %w", flagName, s, err)
}
ports = append(ports, p)
}
var kind v1alpha2.CommandGroupKind
switch flagName {
case FLAG_RUN_PORT:
kind = v1alpha2.RunCommandGroupKind
default:
return parser.DevfileObj{}, fmt.Errorf("unknown flag: %q", flagName)
}
cmd, ok, err := libdevfile.GetCommand(devfileobj, "", kind)
if err != nil {
return parser.DevfileObj{}, err
}
if !ok {
klog.V(3).Infof("Specified %s flag will not be applied - no default (or single non-default) command found for kind %v", flagName, kind)
return devfileobj, nil
}
// command must be an exec command to determine the right container component endpoints to update.
cmdType, err := common.GetCommandType(cmd)
if err != nil {
return parser.DevfileObj{}, err
}
if cmdType != v1alpha2.ExecCommandType {
return parser.DevfileObj{},
fmt.Errorf("%v cannot be used with non-exec commands. Found out that command (id: %s) for kind %v is of type %q instead",
flagName, cmd.Id, kind, cmdType)
}
cmp, ok, err := libdevfile.FindComponentByName(devfileobj.Data, cmd.Exec.Component)
if err != nil {
return parser.DevfileObj{}, err
}
if !ok {
return parser.DevfileObj{}, fmt.Errorf("component not found in Devfile for exec command %q", cmd.Id)
}
cmpType, err := common.GetComponentType(cmp)
if err != nil {
return parser.DevfileObj{}, err
}
if cmpType != v1alpha2.ContainerComponentType {
return parser.DevfileObj{},
fmt.Errorf("%v cannot be used with non-container components. Found out that command (id: %s) for kind %v points to a compoenent of type %q instead",
flagName, cmd.Id, kind, cmpType)
}
err = setPortsInContainerComponent(&devfileobj, &cmp, ports, false)
if err != nil {
return parser.DevfileObj{}, err
}
return devfileobj, nil
}

View File

@@ -6,6 +6,8 @@ import (
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"k8s.io/utils/pointer"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
@@ -15,6 +17,7 @@ import (
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/registry"
"github.com/redhat-developer/odo/pkg/testingutil"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
)
@@ -468,3 +471,685 @@ func TestFlagsBackend_PersonalizeName(t *testing.T) {
})
}
}
func TestFlagsBackend_HandleApplicationPorts(t *testing.T) {
type devfileProvider func(fs dffilesystem.Filesystem) (parser.DevfileObj, error)
zeroDevfileProvider := func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
return parser.DevfileObj{}, nil
}
fakeDevfileProvider := func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220))
obj := parser.DevfileObj{
Ctx: parsercontext.FakeContext(fs, "/tmp/devfile.yaml"),
Data: devfileData,
}
return obj, nil
}
type fields struct {
registryClient registry.Client
}
type args struct {
devfileObjProvider devfileProvider
flags map[string]string
}
tests := []struct {
name string
fields fields
args args
wantProvider devfileProvider
wantErr bool
}{
{
name: "no run-port flag",
args: args{
devfileObjProvider: fakeDevfileProvider,
flags: map[string]string{
"opt1": "val1",
FLAG_NAME: "my-name",
},
},
wantProvider: fakeDevfileProvider,
},
{
name: "flag string value not enclosed within []",
args: args{
devfileObjProvider: fakeDevfileProvider,
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "aaa,bbb",
},
},
wantProvider: fakeDevfileProvider,
},
{
name: "invalid port type",
args: args{
devfileObjProvider: fakeDevfileProvider,
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,abcde]",
},
},
wantErr: true,
wantProvider: zeroDevfileProvider,
},
{
name: "devfile with no command, but --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
},
{
name: "devfile with more than one default run commands, --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
},
},
},
{
Id: "devrun2",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantErr: true,
wantProvider: zeroDevfileProvider,
},
{
name: "devfile with more than one non-default run commands, --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(false),
},
},
},
},
},
},
{
Id: "devrun2",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(false),
},
},
},
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(false),
},
},
},
},
},
},
{
Id: "devrun2",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(false),
},
},
},
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
},
{
name: "devfile with no run command, --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devdebug",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{Kind: v1alpha2.DebugCommandGroupKind},
},
},
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devdebug",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{Kind: v1alpha2.DebugCommandGroupKind},
},
},
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
},
{
name: "devfile with a default non-exec (apply) run command, --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Apply: &v1alpha2.ApplyCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantErr: true,
wantProvider: zeroDevfileProvider,
},
{
name: "devfile with a default non-exec (composite) run command, --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Composite: &v1alpha2.CompositeCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantErr: true,
wantProvider: zeroDevfileProvider,
},
{
name: "devfile with an exec run command with non-container component, --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
Component: "some-random-name",
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantErr: true,
wantProvider: zeroDevfileProvider,
},
{
name: "devfile with an exec run command with non-container component, --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
{
Name: "k8s-comp1",
ComponentUnion: v1alpha2.ComponentUnion{
Kubernetes: &v1alpha2.KubernetesComponent{
K8sLikeComponent: v1alpha2.K8sLikeComponent{
K8sLikeComponentLocation: v1alpha2.K8sLikeComponentLocation{
Inlined: "some-k8s-def",
},
},
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
Component: "k8s-comp1",
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantErr: true,
wantProvider: zeroDevfileProvider,
},
{
name: "devfile with default exec run command with container component, --run-port set",
args: args{
devfileObjProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
testingutil.GetFakeContainerComponent("my-cont1", 1234, 2345),
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
Component: "my-cont1",
},
},
},
{
Id: "devdebug1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.DebugCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
Component: "my-cont2",
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
flags: map[string]string{
FLAG_NAME: "my-name",
FLAG_RUN_PORT: "[8080,8081]",
},
},
wantErr: false,
wantProvider: func(fs dffilesystem.Filesystem) (parser.DevfileObj, error) {
devfileObj, err := fakeDevfileProvider(fs)
if err != nil {
return parser.DevfileObj{}, err
}
// only my-cont1 (referenced by the default run command) should change
cont1 := testingutil.GetFakeContainerComponent("my-cont1")
cont1.Container.Endpoints = append(cont1.Container.Endpoints,
v1alpha2.Endpoint{
Name: "port-8080-tcp",
TargetPort: 8080,
Protocol: "tcp",
},
v1alpha2.Endpoint{
Name: "port-8081-tcp",
TargetPort: 8081,
Protocol: "tcp",
},
)
err = devfileObj.Data.AddComponents([]v1alpha2.Component{
cont1,
testingutil.GetFakeContainerComponent("my-cont2", 4321, 5432),
})
if err != nil {
return parser.DevfileObj{}, err
}
err = devfileObj.Data.AddCommands([]v1alpha2.Command{
{
Id: "devrun1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.RunCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
Component: "my-cont1",
},
},
},
{
Id: "devdebug1",
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
LabeledCommand: v1alpha2.LabeledCommand{
BaseCommand: v1alpha2.BaseCommand{
Group: &v1alpha2.CommandGroup{
Kind: v1alpha2.DebugCommandGroupKind,
IsDefault: pointer.Bool(true),
},
},
},
Component: "my-cont2",
},
},
},
})
if err != nil {
return parser.DevfileObj{}, err
}
return devfileObj, nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &FlagsBackend{
registryClient: tt.fields.registryClient,
}
fs := dffilesystem.NewFakeFs()
devfileObj, err := tt.args.devfileObjProvider(fs)
if err != nil {
t.Errorf("error building input DevfileObj: %v", err)
return
}
want, err := tt.wantProvider(fs)
if err != nil {
t.Errorf("error building expected DevfileObj: %v", err)
return
}
got, err := o.HandleApplicationPorts(devfileObj, nil, tt.args.flags)
if (err != nil) != tt.wantErr {
t.Errorf("HandleApplicationPorts() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(parsercontext.DevfileCtx{})); diff != "" {
t.Errorf("HandleApplicationPorts() mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -4,11 +4,12 @@ import (
"context"
"errors"
"fmt"
"github.com/AlecAivazis/survey/v2/terminal"
"net/url"
"path/filepath"
"strings"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
dfutil "github.com/devfile/library/v2/pkg/util"
@@ -40,6 +41,17 @@ type InitClient struct {
var _ Client = (*InitClient)(nil)
// Explicit list of tags useful to select the right backend
var _initFlags = []string{
backend.FLAG_NAME,
backend.FLAG_DEVFILE,
backend.FLAG_DEVFILE_REGISTRY,
backend.FLAG_STARTER,
backend.FLAG_DEVFILE_PATH,
backend.FLAG_DEVFILE_VERSION,
backend.FLAG_RUN_PORT,
}
func NewInitClient(fsys filesystem.Filesystem, preferenceClient preference.Client, registryClient registry.Client, alizerClient alizer.Client) *InitClient {
// We create the asker client and the backends here and not at the CLI level, as we want to hide these details to the CLI
askerClient := asker.NewSurveyAsker()
@@ -57,9 +69,13 @@ func NewInitClient(fsys filesystem.Filesystem, preferenceClient preference.Clien
// It ignores all the flags except the ones specific to init operation, for e.g. verbosity flag
func (o *InitClient) GetFlags(flags map[string]string) map[string]string {
initFlags := map[string]string{}
outer:
for flag, value := range flags {
if flag == backend.FLAG_NAME || flag == backend.FLAG_DEVFILE || flag == backend.FLAG_DEVFILE_REGISTRY || flag == backend.FLAG_STARTER || flag == backend.FLAG_DEVFILE_PATH || flag == backend.FLAG_DEVFILE_VERSION {
initFlags[flag] = value
for _, f := range _initFlags {
if flag == f {
initFlags[flag] = value
continue outer
}
}
}
return initFlags

View File

@@ -95,7 +95,9 @@ func executeCommand(ctx context.Context, devfileObj parser.DevfileObj, command v
}
// GetCommand iterates through the devfile commands and returns the devfile command with the specified name and group kind.
// If commandName is empty, it returns the default command for the group kind or returns an error if there is no default command.
// If commandName is empty, it returns the default command for the group kind; or, if there is only one command for the specified kind, it will return that
// (even if it is not marked as the default).
// It returns an error if there is more than one default command.
func GetCommand(
devfileObj parser.DevfileObj,
commandName string,

View File

@@ -285,6 +285,7 @@ func NewCmdInit(name, fullName string, testClientset clientset.Clientset) *cobra
initCmd.Flags().String(backend.FLAG_STARTER, "", "name of the starter project")
initCmd.Flags().String(backend.FLAG_DEVFILE_PATH, "", "path to a devfile. This is an alternative to using devfile from Devfile registry. It can be local filesystem path or http(s) URL")
initCmd.Flags().String(backend.FLAG_DEVFILE_VERSION, "", "version of the devfile stack; use \"latest\" to dowload the latest stack")
initCmd.Flags().StringArray(backend.FLAG_RUN_PORT, []string{}, "ports used by the application (via the 'run' command)")
commonflags.UseOutputFlag(initCmd)
// Add a defined annotation in order to appear in the help menu

View File

@@ -212,6 +212,16 @@ var _ = Describe("doc command reference odo init", Label(helper.LabelNoCluster),
diff := cmp.Diff(want, got)
Expect(diff).To(BeEmpty(), file)
})
It("set application ports after fetching Devfile", func() {
args := []string{"init", "--devfile", "go", "--name", "my-go-app", "--run-port", "3456", "--run-port", "9876"}
out := helper.Cmd("odo", args...).ShouldPass().Out()
got := fmt.Sprintf(outputStringFormat, strings.Join(args, " "), helper.StripSpinner(out))
file := "devfile_with_run-port_output.mdx"
want := helper.GetMDXContent(filepath.Join(commonPath, file))
diff := cmp.Diff(want, got)
Expect(diff).To(BeEmpty(), file)
})
})
})

View File

@@ -10,6 +10,8 @@ import (
. "github.com/onsi/gomega"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/utils/pointer"
"github.com/redhat-developer/odo/pkg/devfile"
)
// DevfileUpdater is a helper type that can mutate a Devfile object.
@@ -105,3 +107,10 @@ func SetFsGroup(containerName string, fsGroup int) DevfileUpdater {
return nil
}
}
// ReadRawDevfile parses and validates the Devfile specified and returns its raw content.
func ReadRawDevfile(devfilePath string) parser.DevfileObj {
d, err := devfile.ParseAndValidateFromFile(devfilePath, "", false)
Expect(err).ToNot(HaveOccurred())
return d
}

View File

@@ -6,6 +6,8 @@ import (
"os"
"path/filepath"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/tidwall/gjson"
@@ -616,4 +618,57 @@ spec:
})
})
Context("setting application ports", func() {
When("running odo init --run-port with a Devfile with no commands", func() {
BeforeEach(func() {
helper.Cmd("odo", "init", "--name", "aname", "--devfile-path",
filepath.Join(helper.GetExamplePath(), "source", "devfiles", "nodejs", "devfile-without-commands.yaml"),
"--run-port", "1234", "--run-port", "2345", "--run-port", "3456").ShouldPass().Out()
})
It("should ignore the run ports", func() {
d := helper.ReadRawDevfile(filepath.Join(commonVar.Context, "devfile.yaml"))
components, err := d.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{
ComponentType: v1alpha2.ContainerComponentType,
},
})
Expect(err).ShouldNot(HaveOccurred())
Expect(components).To(HaveLen(1))
Expect(components[0].Name).Should(Equal("runtime"))
Expect(components[0].Container).ShouldNot(BeNil())
Expect(components[0].Container.Endpoints).Should(HaveLen(2))
Expect(components[0].Container.Endpoints[0].Name).Should(Equal("http-node"))
Expect(components[0].Container.Endpoints[0].TargetPort).Should(Equal(3000))
Expect(components[0].Container.Endpoints[1].Name).Should(Equal("debug"))
Expect(components[0].Container.Endpoints[1].TargetPort).Should(Equal(5858))
Expect(components[0].Container.Endpoints[1].Exposure).Should(Equal(v1alpha2.NoneEndpointExposure))
})
})
When("running odo init --run-port with a Devfile with no commands", func() {
BeforeEach(func() {
helper.Cmd("odo", "init", "--name", "aname", "--devfile-path",
filepath.Join(helper.GetExamplePath(), "source", "devfiles", "nodejs", "devfile-with-debugrun.yaml"),
"--run-port", "1234", "--run-port", "2345", "--run-port", "3456").ShouldPass().Out()
})
It("should overwrite the ports into the container component referenced by the default run command", func() {
d := helper.ReadRawDevfile(filepath.Join(commonVar.Context, "devfile.yaml"))
components, err := d.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{
ComponentType: v1alpha2.ContainerComponentType,
},
})
Expect(err).ShouldNot(HaveOccurred())
Expect(components).To(HaveLen(1))
Expect(components[0].Name).Should(Equal("runtime"))
Expect(components[0].Container).ShouldNot(BeNil())
Expect(components[0].Container.Endpoints).Should(HaveLen(3))
for i, p := range []int{1234, 2345, 3456} {
Expect(components[0].Container.Endpoints[i].Name).Should(Equal(fmt.Sprintf("port-%d-tcp", p)))
Expect(components[0].Container.Endpoints[i].TargetPort).Should(Equal(p))
Expect(components[0].Container.Endpoints[i].Protocol).Should(Equal(v1alpha2.TCPEndpointProtocol))
}
})
})
})
})