Skip to content

[public-api] Simple CLI tool to interact with Public API #9588

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

Merged
merged 2 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
/components/ws-manager @gitpod-io/engineering-workspace
/components/ws-proxy @gitpod-io/engineering-workspace
/dev/gpctl @gitpod-io/engineering-workspace
/dev/gpctl/api/ @gitpod-io/engineering-webapp
/dev/loadgen @gitpod-io/engineering-workspace
/dev/preview @gitpod-io/platform
/operations/observability/mixins @gitpod-io/platform
Expand Down
1 change: 1 addition & 0 deletions dev/gpctl/BUILD.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ packages:
- components/ws-daemon-api/go:lib
- components/ws-manager-api/go:lib
- components/ws-manager-bridge-api/go:lib
- components/public-api/go:lib
env:
- CGO_ENABLED=0
config:
Expand Down
80 changes: 80 additions & 0 deletions dev/gpctl/cmd/api/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package api

import (
"context"
"crypto/tls"
"fmt"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)

var (
connOpts = &connectionOptions{}
)

func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "api",
Short: "Interact with Public API services",
}

cmd.PersistentFlags().StringVar(&connOpts.address, "address", "api.main.preview.gitpod-dev.com:443", "Address of the API endpoint. Must be in the form <host>:<port>.")
cmd.PersistentFlags().BoolVar(&connOpts.insecure, "insecure", false, "Disable TLS when making requests against the API. For testing purposes only.")

cmd.PersistentFlags().StringVar(&connOpts.token, "token", "", "Authentication token to interact with the API")

cmd.AddCommand(newWorkspacesCommand())

return cmd
}

func newConn(connOpts *connectionOptions) (*grpc.ClientConn, error) {
log.Log.Infof("Estabilishing gRPC connection against %s", connOpts.address)

opts := []grpc.DialOption{
// attach token to requests to auth
grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
withAuth := metadata.AppendToOutgoingContext(ctx, "authorization", connOpts.token)
return invoker(withAuth, method, req, reply, cc, opts...)
}),
}

if connOpts.insecure {
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
} else {
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
}

conn, err := grpc.Dial(connOpts.address, opts...)
if err != nil {
return nil, fmt.Errorf("failed to dial %s: %w", connOpts.address, err)
}

return conn, nil
}

type connectionOptions struct {
address string
insecure bool
token string
}

func (o *connectionOptions) Validate() error {
if o.address == "" {
return fmt.Errorf("empty connection address")
}

if o.token == "" {
return fmt.Errorf("empty connection token")
}

return nil
}
94 changes: 94 additions & 0 deletions dev/gpctl/cmd/api/workspaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package api

import (
"context"
"fmt"
"github.com/gitpod-io/gitpod/common-go/log"
v1 "github.com/gitpod-io/gitpod/public-api/v1"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"io"
"os"
)

func newWorkspacesCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "workspaces",
Short: "Retrieve information about workspaces",
}

cmd.AddCommand(newWorkspacesGetCommand())

return cmd
}

func newWorkspacesGetCommand() *cobra.Command {
var workspaceID string

cmd := &cobra.Command{
Use: "get",
Short: "Retrieve details about a workspace by ID",
Example: " get --id 1234",
Run: func(cmd *cobra.Command, args []string) {
if workspaceID == "" {
log.Log.Fatal("no workspace id specified, use --id to set it")
}

if err := connOpts.Validate(); err != nil {
log.Log.WithError(err).Fatal("Invalid connections options.")
}

conn, err := newConn(connOpts)
if err != nil {
log.Log.WithError(err).Fatal("Failed to establish gRPC connection")
}

workspace, err := getWorkspace(cmd.Context(), conn, workspaceID)

log.Log.WithError(err).WithField("workspace", workspace.String()).Debugf("Workspace response")
if err != nil {
log.Log.WithError(err).Fatal("Failed to retrieve workspace.")
return
}

if err := printProtoMsg(os.Stdout, workspace); err != nil {
log.Log.WithError(err).Fatal("Failed to serialize proto message and print it.")
}
},
}

cmd.Flags().StringVar(&workspaceID, "id", "", "Workspace ID")

return cmd
}

func getWorkspace(ctx context.Context, conn *grpc.ClientConn, workspaceID string) (*v1.GetWorkspaceResponse, error) {
service := v1.NewWorkspacesServiceClient(conn)

log.Log.Debugf("Retrieving workspace ID: %s", workspaceID)
resp, err := service.GetWorkspace(ctx, &v1.GetWorkspaceRequest{WorkspaceId: workspaceID})
if err != nil {
return nil, fmt.Errorf("failed to retrieve workspace (ID: %s): %w", workspaceID, err)
}

return resp, nil
}

func printProtoMsg(w io.Writer, m proto.Message) error {
b, err := protojson.Marshal(m)
if err != nil {
return fmt.Errorf("failed to marshal proto object: %w", err)
}

if _, err := fmt.Fprint(w, string(b)); err != nil {
return fmt.Errorf("failed to write proto object to writer: %w", err)
}

return nil
}
3 changes: 3 additions & 0 deletions dev/gpctl/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd

import (
"fmt"
"github.com/gitpod-io/gitpod/gpctl/cmd/api"
"os"
"path/filepath"

Expand Down Expand Up @@ -42,6 +43,8 @@ func init() {

rootCmd.PersistentFlags().StringP("output-format", "o", "template", "Output format. One of: string|json|jsonpath|template")
rootCmd.PersistentFlags().String("output-template", "", "Output format Go template or jsonpath. Use with -o template or -o jsonpath")

rootCmd.AddCommand(api.NewCommand())
}

func getKubeconfig() (*rest.Config, string, error) {
Expand Down
4 changes: 4 additions & 0 deletions dev/gpctl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ require (
k8s.io/client-go v0.0.0
)

require github.com/gitpod-io/gitpod/public-api v0.0.0-00010101000000-000000000000

require (
cloud.google.com/go v0.81.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
Expand Down Expand Up @@ -87,6 +89,8 @@ replace github.com/gitpod-io/gitpod/content-service/api => ../../components/cont

replace github.com/gitpod-io/gitpod/image-builder/api => ../../components/image-builder-api/go // leeway

replace github.com/gitpod-io/gitpod/public-api => ../../components/public-api/go // leeway

replace github.com/gitpod-io/gitpod/registry-facade/api => ../../components/registry-facade-api/go // leeway

replace github.com/gitpod-io/gitpod/ws-daemon/api => ../../components/ws-daemon-api/go // leeway
Expand Down