mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
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:
@@ -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"`
|
||||
|
||||
@@ -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},
|
||||
|
||||
89
pkg/component/describe/describe_test.go
Normal file
89
pkg/component/describe/describe_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user