Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

22 changes: 19 additions & 3 deletions pkg/api/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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"`
Expand Down
84 changes: 57 additions & 27 deletions pkg/component/describe/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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, false)

// TODO(feloy) Pass PID with `--pid` flag
allFwdPorts, err := stateClient.GetForwardedPorts(ctx)
if err != nil {
Expand All @@ -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, true)

runningOn, err := GetRunningOn(ctx, componentName, kubeClient, podmanClient)
if err != nil {
Expand All @@ -122,6 +116,7 @@ func DescribeDevfileComponent(
cmp := api.Component{
DevfilePath: devfilePath,
DevfileData: devfileData,
DevControlPlane: devControlPlaneData,
DevForwardedPorts: forwardedPorts,
RunningIn: api.MergeRunningModes(runningOn),
RunningOn: runningOn,
Expand Down Expand Up @@ -234,6 +229,41 @@ func GetRunningOn(ctx context.Context, n string, kubeClient kclient.ClientInterf
return runningOn, nil
}

func filterByPlatform[T platformDependent](ctx context.Context, isFeatEnabled bool, all []T, includeIfFeatDisabled bool) (result []T) {
plt := fcontext.GetPlatform(ctx, "")
switch plt {
case "":
if isFeatEnabled {
// Read from all platforms
result = all
} else if includeIfFeatDisabled {
// Limit to cluster only
for _, p := range all {
if p.GetPlatform() == "" || p.GetPlatform() == commonflags.PlatformCluster {
result = append(result, p)
}
}
}
case commonflags.PlatformCluster:
if isFeatEnabled || includeIfFeatDisabled {
for _, p := range all {
if p.GetPlatform() == "" || p.GetPlatform() == commonflags.PlatformCluster {
result = append(result, p)
}
}
}
case commonflags.PlatformPodman:
if isFeatEnabled || includeIfFeatDisabled {
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},
Expand Down
13 changes: 13 additions & 0 deletions pkg/odo/cli/describe/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions pkg/state/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
34 changes: 34 additions & 0 deletions pkg/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {

Expand Down
2 changes: 1 addition & 1 deletion pkg/state/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
4 changes: 3 additions & 1 deletion tests/helper/helper_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
return DevSession{}, err
}

env := append(make([]string, 0, len(options.EnvVars)), options.EnvVars...)
args := []string{"dev"}
if options.NoCommands {
args = append(args, "--no-commands")
Expand All @@ -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))
Expand All @@ -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
Expand Down
72 changes: 64 additions & 8 deletions tests/integration/cmd_dev_api_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand All @@ -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", "/")
})
})
}))
}
Expand All @@ -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)
Expand Down Expand Up @@ -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())
})
Expand Down