Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/danger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ jobs:
pull-requests: write # to be able to comment on released pull requests
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-tags: true
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: "lts/*"
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.24.x'
go-version: '1.25.5'
- name: Install quill CLI
run: curl -sSfL https://raw.githubusercontent.com/anchore/quill/main/install.sh | sh -s -- -b /usr/local/bin
- name: Check if snapshot build
Expand All @@ -50,7 +50,7 @@ jobs:
QUILL_NOTARY_KEY_ID: ${{ secrets.QUILL_NOTARY_KEY_ID }}
QUILL_NOTARY_ISSUER: ${{ secrets.QUILL_NOTARY_ISSUER }}
- name: Upload dist artifacts to GitHub when not on main
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: "!contains(github.ref, 'main')"
with:
name: gpcore
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.22.x'
go-version: '1.25.5'

- name: Install dependencies
run: go mod download
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
*_gen.go
/gpc
/id_ed25519*
/tools/
/tools/
/dist/
27 changes: 14 additions & 13 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,20 @@ builds:
ldflags:
- "-s -w -X '{{ .ModulePath }}/cmd.version={{.Tag}}' -X '{{ .ModulePath }}/cmd.commit={{.Commit}}' -X '{{ .ModulePath }}/cmd.date={{.Date}}'"

- id: gpcore-macos
goos:
- darwin
ldflags:
- "-s -w -X '{{ .ModulePath }}/cmd.version={{.Tag}}' -X '{{ .ModulePath }}/cmd.commit={{.Commit}}' -X '{{ .ModulePath }}/cmd.date={{.Date}}'"
goarch:
- amd64
- arm64
hooks:
post:
- cmd: quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
env:
- QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log
# TODO: Disabled for now due to a missing agreement from apple developer account.
# - id: gpcore-macos
# goos:
# - darwin
# ldflags:
# - "-s -w -X '{{ .ModulePath }}/cmd.version={{.Tag}}' -X '{{ .ModulePath }}/cmd.commit={{.Commit}}' -X '{{ .ModulePath }}/cmd.date={{.Date}}'"
# goarch:
# - amd64
# - arm64
# hooks:
# post:
# - cmd: quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
# env:
# - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log

archives:
- formats:
Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ between client and server is secured, and no other ssh client can connect to it.
If you messed up your config, the sensitive data in the keyring or the public/private
key, you can reset everything with the ```gpcore agent setup``` command.

As the client ID and client secret, you can use a service account, created in
the GPCORE panel under https://panel.gpcore.io/user/settings/clients.

A special case is the ```user impersonate``` command, which allows you to impersonate
another user. This command needs admin permissions, and you need to use the
```gpcore-cli``` client ID for that (see Bitwarden or Keycloak for the client
secret). This client is preconfigured to have the impersonate role. Further, your
admin user need the "impersonate-user" permission set in Keycloak. To stop
impersonating a user, use the `user logout` command.

### Admin permissions

The GPCORE API has a concept of admin permissions. Several actions can only
Expand Down Expand Up @@ -75,6 +85,22 @@ The client itself is a simple SSH client. It connects to the agent and sends
commands to it. The result is printed to stdout. You can use the standard
SSH command (ssh) to connect through it, but it is not that convenient.

### Using different environments

It is possible to use the CLI with different GPCORE environments, like
staging or development. For that, you can set the API base URL and the
auth realm when the agent starts (`agent start` or first command).

* `--endpoint` (or `GPCORE_ENDPOINT` environment variable):
Set the API base URL, efault is the production API.
* `--auth-realm` (or `GPCORE_AUTH_REALM` environment variable)
Set the auth realm, efault is set to the production realm (master).

Remember, this will be set once the agent starts and will be used for all
subsequent commands. If you want to change the environment, you need to
stop the agent with `gpcore agent stop` and start it again with the new
parameters.

## Usage

The commandline tool is separated into subcommands. To get a list of all
Expand Down Expand Up @@ -109,7 +135,7 @@ You can always add custom subcommands without generating it. Just add a new
file to ```cmd/```. The file name will be the name of the subcommand.

With hooks, you can inject code at some points in auto generated code. For
example, if you want to remove some colums, format certain colums or validate
example, if you want to remove some columns, format certain columns or validate
input, you can do this with hooks. Create a file in the same package with the
same name of the action (auto generated file), but with the prefix ```_pre.go```
to execute code before the action and ```_post.go``` to execute code after the
Expand Down
15 changes: 7 additions & 8 deletions cmd/agent/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package agent

import (
"fmt"
"net"
"strconv"
"time"

"github.com/G-PORTAL/gpcore-cli/cmd"
"github.com/G-PORTAL/gpcore-cli/cmd/help"
"github.com/G-PORTAL/gpcore-cli/pkg/config"
"github.com/G-PORTAL/gpcore-cli/pkg/consts"
"github.com/G-PORTAL/gpcore-go/pkg/gpcore/client"
"github.com/spf13/cobra"
"net"
"strconv"
"time"
)

var printVersion = false
Expand Down Expand Up @@ -44,9 +45,10 @@ func New() *cobra.Command {
// Application information
rootCmd.Flags().BoolVarP(&printVersion, "version", "V", false, "print version information and quit")

// GPCORE API
// GPCORE API + Authentication/Authorization
// TODO: Will set on first run (when agent starts),the following client calls will ignore these, so, move this to the agent only or reconnect the API on every change
rootCmd.PersistentFlags().StringVarP(&config.Endpoint, "endpoint", "e", client.DefaultEndpoint, "set API endpoint")
rootCmd.PersistentFlags().StringVarP(&config.AuthRealm, "auth-realm", "r", "master", "set auth realm to use")

// Output formats and verbosity
rootCmd.PersistentFlags().BoolVarP(&config.Verbose, "verbose", "v", false, "verbose mode")
Expand All @@ -56,10 +58,7 @@ func New() *cobra.Command {
// Special client commands
cmd.SelfupdateCommand(&rootCmd)
cmd.SetLogLevelCommand(&rootCmd)

if config.HasAdminConfig() {
cmd.LiveLogCommand(&rootCmd)
}
cmd.LiveLogCommand(&rootCmd)
//InteractiveCLICommand(&rootCmd)

// Autogenerated commands
Expand Down
104 changes: 16 additions & 88 deletions cmd/agent/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,98 +4,25 @@ import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"

"github.com/G-PORTAL/gpcore-cli/pkg/api"
"github.com/G-PORTAL/gpcore-cli/pkg/config"
"github.com/G-PORTAL/gpcore-cli/pkg/consts"
"github.com/G-PORTAL/gpcore-go/pkg/gpcore/client"
"github.com/G-PORTAL/gpcore-go/pkg/gpcore/client/auth"
"github.com/charmbracelet/log"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"math"
"net/http"
"os"
"os/signal"
"syscall"
)

var rootCmd *cobra.Command

type Session struct {
config *config.SessionConfig
conn *grpc.ClientConn
ssh *ssh.Session
}

var session Session

var DoneChan = make(chan os.Signal, 1)
var IsRunning = false

func (s *Session) ContextWithSession(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, "config", s.config)
ctx = context.WithValue(ctx, "ssh", s.ssh)
ctx = context.WithValue(ctx, "conn", s.conn)
return ctx
}

// ConnectToAPI connects to the API with the given credentials, depending
// on what credentials we have.
func ConnectToAPI(session *Session) (*grpc.ClientConn, error) {
// Endpoint
endpoint := config.Endpoint
if os.Getenv("GPCORE_ENDPOINT") != "" {
endpoint = os.Getenv("GPCORE_ENDPOINT")
}

var credentials client.AuthProviderOption

// We have two different connection methods available, depending on the
// type of credentials we get. For "normal" usage, we need the ClientID
// and the ClientSecret, which can be used by every user.
// Some endpoints need admin privileges, tho. For that, we need the
// username and password of the user. We can not use the same connection
// for that, so we need to reconnect with admin credentials.

// First, we check if we have user/pass for admin login. If we have the
// credentials, we use it for login.
if config.HasAdminConfig() {
log.Info("Using admin credentials")
credentials = &auth.ProviderKeycloakUserPassword{
ClientID: session.config.ClientID,
ClientSecret: session.config.ClientSecret,
Username: *session.config.Username,
Password: *session.config.Password,
}

return api.NewGRPCConnection(
credentials,
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(math.MaxInt32),
grpc.MaxCallSendMsgSize(math.MaxInt32),
),
client.EndpointOverrideOption(endpoint),
)
}

// Otherwise, we just use the client credentials. With this login, the
// admin endpoints will not work and result in an error.
credentials = &auth.ProviderKeycloakClientAuth{
ClientID: session.config.ClientID,
ClientSecret: session.config.ClientSecret,
}
return api.NewGRPCConnection(
credentials,
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(math.MaxInt32),
grpc.MaxCallSendMsgSize(math.MaxInt32),
),
client.EndpointOverrideOption(endpoint),
)
}

var startCmd = &cobra.Command{
Use: "start",
Short: "Start the agent",
Expand All @@ -108,18 +35,18 @@ var startCmd = &cobra.Command{
panic(err)
}

session = Session{
config: sessionConfig,
}

// Open new connection
session.conn, err = ConnectToAPI(&session)
// If we have impersonated a user before the agent was stopped, we remove
// the access token and the expiry time, so the user start with his own
// user.
sessionConfig.ImpersonateAccessToken = nil
sessionConfig.ImpersonateExpiresIn = nil
err = sessionConfig.Write()
if err != nil {
log.Errorf("Can not connect to GPCORE API: %v", err)
log.Fatal("Check your config file and/or reset it with \"gpcore agent setup\"")
panic(err)
}

api.RenewAPISession()

server, err := wish.NewServer(
wish.WithAddress(fmt.Sprintf("%s:%d", consts.AgentHost, consts.AgentPort)),
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
Expand All @@ -139,8 +66,9 @@ var startCmd = &cobra.Command{
rootCmd.SetErr(s.Stderr())
rootCmd.CompletionOptions.DisableDefaultCmd = true

session.ssh = &s
ctx := session.ContextWithSession(context.Background())
api.ActiveSession.SSH = &s
ctx := api.ActiveSession.ContextWithSession(context.Background())

if err := rootCmd.ExecuteContext(ctx); err != nil {
log.Errorf("Error executing command on agent: %v", err)
rootCmd.Printf("Error executing command on agent: %v\n", err)
Expand Down
4 changes: 1 addition & 3 deletions cmd/project/_network_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,5 @@ func init() {
networkCreateCmd.MarkFlagRequired("type")
networkCreateCmd.MarkFlagRequired("subnets")

if config.HasAdminConfig() {
RootProjectCommand.AddCommand(networkCreateCmd)
}
RootProjectCommand.AddCommand(networkCreateCmd)
}
2 changes: 1 addition & 1 deletion cmd/project/list_post.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func ListHookPost(resp *cloudv1.ListProjectsResponse, cobraCmd *cobra.Command) (

ctx := client.ExtractContext(cobraCmd)
cfg := ctx.Value("config").(*config.SessionConfig)
user := client.GetUser(ctx)
user := client.GetUserFromContext(ctx)

for i := range resp.Projects {
name := resp.Projects[i].Name
Expand Down
Loading