Display information about the running API Server and web UI in odo describe component output (#6964)

* refactor: Set the experimental mode env var in a single place for the '--api-server' related tests

* Add integration tests highlighting the expectations

* Make the state client return the API server ports per platform

'odo dev' might be running on both cluster and podman,
so we might end up with several API servers.

* Make 'odo describe component' return information about the API Server and web UI

This is viewable only when running 'odo describe component'
with the experimental mode enabled.

* fixup! refactor: Set the experimental mode env var in a single place for the '--api-server' related tests

* Unit-test describe#filterByPlatform logic

* Simplify logic for 'describe#filterByPlatform', as suggested in review

Co-authored-by: Philippe Martin <phmartin@redhat.com>

---------

Co-authored-by: Philippe Martin <phmartin@redhat.com>
This commit is contained in:
Armel Soro
2023-07-18 16:31:14 +02:00
committed by GitHub
parent 53cb806a65
commit 6e725952bd
9 changed files with 274 additions and 40 deletions

View File

@@ -2,9 +2,10 @@ package api
// Component describes the state of a devfile component
type Component struct {
DevfilePath string `json:"devfilePath,omitempty"`
DevfileData *DevfileData `json:"devfileData,omitempty"`
DevForwardedPorts []ForwardedPort `json:"devForwardedPorts,omitempty"`
DevfilePath string `json:"devfilePath,omitempty"`
DevfileData *DevfileData `json:"devfileData,omitempty"`
DevControlPlane []DevControlPlane `json:"devControlPlane,omitempty"`
DevForwardedPorts []ForwardedPort `json:"devForwardedPorts,omitempty"`
// RunningIn is the overall running mode map of the component;
// this is computing as a merge of RunningOn (all the different running modes
// for each platform the component is running on).
@@ -29,6 +30,21 @@ type ForwardedPort struct {
Protocol string `json:"protocol,omitempty"`
}
func (o ForwardedPort) GetPlatform() string {
return o.Platform
}
type DevControlPlane struct {
Platform string `json:"platform,omitempty"`
LocalPort int `json:"localPort"`
APIServerPath string `json:"apiServerPath"`
WebInterfacePath string `json:"webInterfacePath"`
}
func (o DevControlPlane) GetPlatform() string {
return o.Platform
}
type ConnectionData struct {
Name string `json:"name"`
Rules []Rules `json:"rules,omitempty"`

View File

@@ -24,6 +24,10 @@ import (
"github.com/redhat-developer/odo/pkg/state"
)
type platformDependent interface {
GetPlatform() string
}
// DescribeDevfileComponent describes the component defined by the devfile in the current directory
func DescribeDevfileComponent(
ctx context.Context,
@@ -64,6 +68,22 @@ func DescribeDevfileComponent(
kubeClient = nil
}
isApiServerFeatureEnabled := feature.IsEnabled(ctx, feature.APIServerFlag)
// TODO(feloy) Pass PID with `--pid` flag
allControlPlaneData, err := stateClient.GetAPIServerPorts(ctx)
if err != nil {
return api.Component{}, nil, err
}
if isApiServerFeatureEnabled {
for i := range allControlPlaneData {
if allControlPlaneData[i].Platform == "" {
allControlPlaneData[i].Platform = commonflags.PlatformCluster
}
}
}
devControlPlaneData := filterByPlatform(ctx, isApiServerFeatureEnabled, allControlPlaneData)
// TODO(feloy) Pass PID with `--pid` flag
allFwdPorts, err := stateClient.GetForwardedPorts(ctx)
if err != nil {
@@ -76,33 +96,7 @@ func DescribeDevfileComponent(
}
}
}
var forwardedPorts []api.ForwardedPort
switch platform {
case "":
if isPlatformFeatureEnabled {
// Read ports from all platforms
forwardedPorts = allFwdPorts
} else {
// Limit to cluster ports only
for _, p := range allFwdPorts {
if p.Platform == "" || p.Platform == commonflags.PlatformCluster {
forwardedPorts = append(forwardedPorts, p)
}
}
}
case commonflags.PlatformCluster:
for _, p := range allFwdPorts {
if p.Platform == "" || p.Platform == commonflags.PlatformCluster {
forwardedPorts = append(forwardedPorts, p)
}
}
case commonflags.PlatformPodman:
for _, p := range allFwdPorts {
if p.Platform == commonflags.PlatformPodman {
forwardedPorts = append(forwardedPorts, p)
}
}
}
forwardedPorts := filterByPlatform(ctx, isPlatformFeatureEnabled, allFwdPorts)
runningOn, err := GetRunningOn(ctx, componentName, kubeClient, podmanClient)
if err != nil {
@@ -122,6 +116,7 @@ func DescribeDevfileComponent(
cmp := api.Component{
DevfilePath: devfilePath,
DevfileData: devfileData,
DevControlPlane: devControlPlaneData,
DevForwardedPorts: forwardedPorts,
RunningIn: api.MergeRunningModes(runningOn),
RunningOn: runningOn,
@@ -234,6 +229,32 @@ func GetRunningOn(ctx context.Context, n string, kubeClient kclient.ClientInterf
return runningOn, nil
}
func filterByPlatform[T platformDependent](ctx context.Context, isFeatEnabled bool, all []T) (result []T) {
if !isFeatEnabled {
return nil
}
plt := fcontext.GetPlatform(ctx, "")
switch plt {
case "":
// Read from all platforms
result = all
case commonflags.PlatformCluster:
for _, p := range all {
if p.GetPlatform() == "" || p.GetPlatform() == commonflags.PlatformCluster {
result = append(result, p)
}
}
case commonflags.PlatformPodman:
for _, p := range all {
if p.GetPlatform() == commonflags.PlatformPodman {
result = append(result, p)
}
}
}
return result
}
func updateWithRemoteSourceLocation(cmp *api.Component) {
components, err := cmp.DevfileData.Devfile.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1alpha2.ContainerComponentType},

View File

@@ -0,0 +1,89 @@
package describe
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
fcontext "github.com/redhat-developer/odo/pkg/odo/commonflags/context"
)
type testType struct {
value string
platform string
}
var _ platformDependent = testType{}
func (c testType) GetPlatform() string {
return c.platform
}
func Test_filterByPlatform(t *testing.T) {
type args struct {
ctx context.Context
isFeatEnabled bool
}
type testCase struct {
name string
args args
wantResult []testType
}
allValues := []testType{
{value: "value without platform"},
{value: "value11 (cluster)", platform: "cluster"},
{value: "value12 (cluster)", platform: "cluster"},
{value: "value21 (podman)", platform: "podman"},
{value: "value22 (podman)", platform: "podman"},
}
tests := []testCase{
{
name: "feature disabled",
args: args{
ctx: context.Background(),
isFeatEnabled: false,
},
wantResult: nil,
},
{
name: "feature enabled and platform unset in context",
args: args{
ctx: context.Background(),
isFeatEnabled: true,
},
wantResult: allValues,
},
{
name: "feature enabled and platform set to cluster in context",
args: args{
ctx: fcontext.WithPlatform(context.Background(), "cluster"),
isFeatEnabled: true,
},
wantResult: []testType{
{"value without platform", ""},
{"value11 (cluster)", "cluster"},
{"value12 (cluster)", "cluster"},
},
},
{
name: "feature enabled and platform set to podman in context",
args: args{
ctx: fcontext.WithPlatform(context.Background(), "podman"),
isFeatEnabled: true,
},
wantResult: []testType{
{"value21 (podman)", "podman"},
{"value22 (podman)", "podman"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotResult := filterByPlatform(tt.args.ctx, tt.args.isFeatEnabled, allValues)
if diff := cmp.Diff(tt.wantResult, gotResult, cmp.AllowUnexported(testType{})); diff != "" {
t.Errorf("filterByPlatform() mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -156,6 +156,19 @@ func printHumanReadableOutput(ctx context.Context, cmp api.Component, devfileObj
fmt.Println()
}
if feature.IsEnabled(ctx, feature.APIServerFlag) && len(cmp.DevControlPlane) != 0 {
const ctrlPlaneHost = "localhost"
log.Info("Dev Control Plane:")
for _, dcp := range cmp.DevControlPlane {
log.Printf(`%[1]s
API: http://%[2]s:%[3]d/%[4]s
Web UI: http://%[2]s:%[3]d/`,
log.Sbold(dcp.Platform),
ctrlPlaneHost, dcp.LocalPort, strings.TrimPrefix(dcp.APIServerPath, "/"))
}
fmt.Println()
}
if len(cmp.DevForwardedPorts) > 0 {
log.Info("Forwarded ports:")
for _, port := range cmp.DevForwardedPorts {

View File

@@ -21,4 +21,7 @@ type Client interface {
// SetAPIServerPort sets the port where API server is listening in the state file and saves it to the file, updating the metadata
SetAPIServerPort(ctx context.Context, port int) error
// GetAPIServerPorts returns the port where the API servers are listening, possibly per platform.
GetAPIServerPorts(ctx context.Context) ([]api.DevControlPlane, error)
}

View File

@@ -85,6 +85,7 @@ func (o *State) SaveExit(ctx context.Context) error {
o.content.ForwardedPorts = nil
o.content.PID = 0
o.content.Platform = ""
o.content.APIServerPort = 0
err := o.delete(pid)
if err != nil {
return err
@@ -103,6 +104,39 @@ func (o *State) SetAPIServerPort(ctx context.Context, port int) error {
return o.save(ctx, pid)
}
func (o *State) GetAPIServerPorts(ctx context.Context) ([]api.DevControlPlane, error) {
var (
result []api.DevControlPlane
platforms []string
platform = fcontext.GetPlatform(ctx, "")
)
if platform == "" {
platforms = []string{commonflags.PlatformCluster, commonflags.PlatformPodman}
} else {
platforms = []string{platform}
}
for _, platform = range platforms {
content, err := o.read(platform)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue // if the state file does not exist, no API Servers are listening
}
return nil, err
}
if content.APIServerPort == 0 {
continue
}
result = append(result, api.DevControlPlane{
Platform: platform,
LocalPort: content.APIServerPort,
APIServerPath: "/api/v1/",
WebInterfacePath: "/",
})
}
return result, nil
}
// save writes the content structure in json format in file
func (o *State) save(ctx context.Context, pid int) error {

View File

@@ -11,5 +11,5 @@ type Content struct {
Platform string `json:"platform"`
// ForwardedPorts are the ports forwarded during odo dev session
ForwardedPorts []api.ForwardedPort `json:"forwardedPorts"`
APIServerPort int `json:"apiServerPort"`
APIServerPort int `json:"apiServerPort,omitempty"`
}

View File

@@ -149,6 +149,7 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
return DevSession{}, err
}
env := append([]string{}, options.EnvVars...)
args := []string{"dev"}
if options.NoCommands {
args = append(args, "--no-commands")
@@ -163,6 +164,7 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
args = append(args, "--address", options.CustomAddress)
}
if options.StartAPIServer {
env = append(env, "ODO_EXPERIMENTAL_MODE=true")
args = append(args, "--api-server")
if options.APIServerPort != 0 {
args = append(args, "--api-server-port", fmt.Sprintf("%d", options.APIServerPort))
@@ -180,7 +182,7 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
cmd.Cmd.Stdout = c.Tty()
cmd.Cmd.Stderr = c.Tty()
session := cmd.AddEnv(options.EnvVars...).Runner().session
session := cmd.AddEnv(env...).Runner().session
timeoutInSeconds := 420
if options.TimeoutInSeconds != 0 {
timeoutInSeconds = options.TimeoutInSeconds

View File

@@ -6,13 +6,15 @@ import (
"io"
"net/http"
"path/filepath"
"strconv"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/utils/pointer"
"github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/tests/helper"
"k8s.io/utils/pointer"
)
var _ = Describe("odo dev command with api server tests", func() {
@@ -41,17 +43,15 @@ var _ = Describe("odo dev command with api server tests", func() {
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile.yaml"), filepath.Join(commonVar.Context, "devfile.yaml"), cmpName)
})
When(fmt.Sprintf("odo dev is run with --api-server flag (custom api server port=%v)", customPort), func() {
var (
devSession helper.DevSession
localPort = helper.GetCustomStartPort()
)
var devSession helper.DevSession
var localPort int
BeforeEach(func() {
opts := helper.DevSessionOpts{
RunOnPodman: podman,
StartAPIServer: true,
EnvVars: []string{"ODO_EXPERIMENTAL_MODE=true"},
}
if customPort {
localPort = helper.GetCustomStartPort()
opts.APIServerPort = localPort
}
var err error
@@ -71,6 +71,64 @@ var _ = Describe("odo dev command with api server tests", func() {
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusOK))
})
It("should not describe the API Server in non-experimental mode", func() {
args := []string{"describe", "component"}
if podman {
args = append(args, "--platform", "podman")
}
stdout := helper.Cmd("odo", args...).ShouldPass().Out()
for _, s := range []string{"Dev Control Plane", "API Server"} {
Expect(stdout).ShouldNot(ContainSubstring(s))
}
})
It("should not describe the API Server in non-experimental mode (JSON)", func() {
args := []string{"describe", "component", "-o", "json"}
if podman {
args = append(args, "--platform", "podman")
}
stdout := helper.Cmd("odo", args...).ShouldPass().Out()
helper.JsonPathDoesNotExist(stdout, "devControlPlane")
})
It("should describe the API Server port in the experimental mode", func() {
args := []string{"describe", "component"}
if podman {
args = append(args, "--platform", "podman")
}
stdout := helper.Cmd("odo", args...).AddEnv("ODO_EXPERIMENTAL_MODE=true").ShouldPass().Out()
Expect(stdout).To(ContainSubstring("Dev Control Plane"))
Expect(stdout).To(ContainSubstring("API: http://%s", devSession.APIServerEndpoint))
if customPort {
Expect(stdout).To(ContainSubstring("Web UI: http://localhost:%d/", localPort))
} else {
Expect(stdout).To(MatchRegexp("Web UI: http:\\/\\/localhost:[0-9]+\\/"))
}
})
It("should describe the API Server port in the experimental mode (JSON)", func() {
args := []string{"describe", "component", "-o", "json"}
if podman {
args = append(args, "--platform", "podman")
}
stdout := helper.Cmd("odo", args...).AddEnv("ODO_EXPERIMENTAL_MODE=true").ShouldPass().Out()
helper.IsJSON(stdout)
helper.JsonPathExist(stdout, "devControlPlane")
plt := "cluster"
if podman {
plt = "podman"
}
helper.JsonPathContentHasLen(stdout, "devControlPlane", 1)
helper.JsonPathContentIs(stdout, "devControlPlane.0.platform", plt)
if customPort {
helper.JsonPathContentIs(stdout, "devControlPlane.0.localPort", strconv.Itoa(localPort))
} else {
helper.JsonPathContentIsValidUserPort(stdout, "devControlPlane.0.localPort")
}
helper.JsonPathContentIs(stdout, "devControlPlane.0.apiServerPath", "/api/v1/")
helper.JsonPathContentIs(stdout, "devControlPlane.0.webInterfacePath", "/")
})
})
}))
}
@@ -88,7 +146,6 @@ var _ = Describe("odo dev command with api server tests", func() {
opts := helper.DevSessionOpts{
RunOnPodman: podman,
StartAPIServer: true,
EnvVars: []string{"ODO_EXPERIMENTAL_MODE=true"},
}
var err error
devSession, err = helper.StartDevMode(opts)
@@ -184,7 +241,6 @@ var _ = Describe("odo dev command with api server tests", func() {
CmdlineArgs: args,
RunOnPodman: podman,
StartAPIServer: true,
EnvVars: []string{"ODO_EXPERIMENTAL_MODE=true"},
})
Expect(err).ToNot(HaveOccurred())
})