Skip to content

Commit

Permalink
Merge pull request #67 from jetstack/sa-output
Browse files Browse the repository at this point in the history
Add command to get a new cluster service account
  • Loading branch information
charlieegan3 authored Nov 18, 2022
2 parents b2dc875 + 94cab1e commit af80ee7
Show file tree
Hide file tree
Showing 23 changed files with 464 additions and 229 deletions.
1 change: 1 addition & 0 deletions docs/reference/jsctl_auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Subcommands for authentication
### SEE ALSO

* [jsctl](jsctl.md) - Command-line tool for the Jetstack Secure Control Plane
* [jsctl auth clusters](jsctl_auth_clusters.md) -
* [jsctl auth login](jsctl_auth_login.md) - Performs the authentication flow to allow access to other commands
* [jsctl auth logout](jsctl_auth_logout.md) -
* [jsctl auth status](jsctl_auth_status.md) - Print the logged in account and token location
Expand Down
24 changes: 24 additions & 0 deletions docs/reference/jsctl_auth_clusters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## jsctl auth clusters



### Options

```
-h, --help help for clusters
```

### Options inherited from parent commands

```
--api-url string Base URL of the control-plane API (default "https://platform.jetstack.io")
--config string Location of the user's jsctl config directory (default "HOME or USERPROFILE/.jsctl")
--kubeconfig string Location of the user's kubeconfig file for applying directly to the cluster (default "~/.kube/config")
--stdout If provided, manifests are written to stdout rather than applied to the current cluster
```

### SEE ALSO

* [jsctl auth](jsctl_auth.md) - Subcommands for authentication
* [jsctl auth clusters create-service-account](jsctl_auth_clusters_create-service-account.md) - Create a new Jetstack Secure service account for a cluster agent

37 changes: 37 additions & 0 deletions docs/reference/jsctl_auth_clusters_create-service-account.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## jsctl auth clusters create-service-account

Create a new Jetstack Secure service account for a cluster agent

### Synopsis

Generate a new service account for a Jetstack Secure cluster agent
This is only needed if you are not deploying the agent with jsctl.
Output can be json formatted or as Kubernetes Secret.


```
jsctl auth clusters create-service-account name [flags]
```

### Options

```
--format string The desired output format, valid options: [jsonKeyData, secret] (default "jsonKeyData")
-h, --help help for create-service-account
--secret-name string If using the 'secret' format, the name of the secret to create (default "agent-credentials")
--secret-namespace string If using the 'secret' format, the namespace of the secret to create (default "jetstack-secure")
```

### Options inherited from parent commands

```
--api-url string Base URL of the control-plane API (default "https://platform.jetstack.io")
--config string Location of the user's jsctl config directory (default "HOME or USERPROFILE/.jsctl")
--kubeconfig string Location of the user's kubeconfig file for applying directly to the cluster (default "~/.kube/config")
--stdout If provided, manifests are written to stdout rather than applied to the current cluster
```

### SEE ALSO

* [jsctl auth clusters](jsctl_auth_clusters.md) -

2 changes: 1 addition & 1 deletion docs/reference/jsctl_clusters_connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Creates a new cluster in the control plane and deploys the agent in your current kubenetes context

```
jsctl clusters connect [name] [flags]
jsctl clusters connect name [flags]
```

### Options
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/jsctl_clusters_delete.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Deletes a cluster from the organization

```
jsctl clusters delete [name] [flags]
jsctl clusters delete name [flags]
```

### Options
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/jsctl_clusters_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Opens a browser window to the cluster's dashboard

```
jsctl clusters view [name] [flags]
jsctl clusters view name [flags]
```

### Options
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/jsctl_configuration_set_organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Set your current organization

```
jsctl configuration set organization [value] [flags]
jsctl configuration set organization name [flags]
```

### Options
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/jsctl_users_add.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Add a user to the current organization

```
jsctl users add [email] [flags]
jsctl users add email [flags]
```

### Options
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/jsctl_users_remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Remove a user from the current organization

```
jsctl users remove [email] [flags]
jsctl users remove email [flags]
```

### Options
Expand Down
24 changes: 24 additions & 0 deletions internal/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import (
"text/template"
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/jetstack/jsctl/internal/client"
)

Expand Down Expand Up @@ -158,3 +161,24 @@ func marshalBase64(in interface{}) ([]byte, error) {

return buffer.Bytes(), nil
}

// AgentServiceAccount secret takes a service account json and formats it as a
// k8s secret.
func AgentServiceAccountSecret(keyData []byte, name, namespace string) *corev1.Secret {
secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"credentials.json": keyData,
},
}

return secret
}
213 changes: 5 additions & 208 deletions internal/command/auth.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
package command

import (
"context"
"errors"
"fmt"
"os"

"github.com/golang-jwt/jwt/v4"
"github.com/spf13/cobra"
"github.com/toqueteos/webbrowser"
"golang.org/x/oauth2"

"github.com/jetstack/jsctl/internal/auth"
"github.com/jetstack/jsctl/internal/config"
"github.com/jetstack/jsctl/internal/command/auth"
)

// Auth returns a cobra.Command instance that is the root for all "jsctl auth" subcommands.
Expand All @@ -23,205 +14,11 @@ func Auth() *cobra.Command {
}

cmd.AddCommand(
authLogin(),
authLogout(),
authStatus(),
)

return cmd
}

func authStatus() *cobra.Command {
var credentials string

cmd := &cobra.Command{
Use: "status",
Short: "Print the logged in account and token location",
Args: cobra.ExactArgs(0),
Run: run(func(ctx context.Context, args []string) error {
var token *oauth2.Token
var err error
var tokenPath string
if credentials != "" {
tokenPath = credentials
token, err = loginWithCredentials(ctx, auth.GetOAuthConfig(), credentials)
if err != nil {
return fmt.Errorf("failed to login with credentials file %q: %w", credentials, err)
}
} else {
tokenPath, err = auth.DetermineTokenFilePath(ctx)
if err != nil {
return fmt.Errorf("failed to determine token path: %w", err)
}
if _, err := os.Stat(tokenPath); errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("token missing at %s", tokenPath)
}

fmt.Println("Token path:", tokenPath)

token, err = auth.LoadOAuthToken(ctx)
if err != nil {
fmt.Println("Not logged in")
return nil
}
}

claims := jwt.MapClaims{}
_, err = jwt.ParseWithClaims(token.AccessToken, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(""), nil
})

email, ok := claims["https://jetstack.io/claims/name"].(string)
if ok {
fmt.Println("Logged in as:", email)
}

cnf, ok := config.FromContext(ctx)
if !ok || cnf.Organization == "" {
fmt.Println("You do not have an organization selected, select one using: \n\n\tjsctl config set organization [name]\n\n" +
"To view organizations you have access to, list them using: \n\n\tjsctl organizations list")
return nil
}
fmt.Println("Current Organization:", cnf.Organization)

return nil
}),
}

flags := cmd.PersistentFlags()
flags.StringVar(
&credentials,
"credentials",
os.Getenv("JSCTL_CREDENTIALS"),
"The location of a credentials file to use instead of the normal oauth login flow",
)

return cmd
}

func authLogin() *cobra.Command {
var credentials string
var disconnected bool

cmd := &cobra.Command{
Use: "login",
Short: "Performs the authentication flow to allow access to other commands",
Args: cobra.ExactArgs(0),
Run: run(func(ctx context.Context, args []string) error {
oAuthConfig := auth.GetOAuthConfig()

var err error
var token *oauth2.Token
if credentials != "" {
token, err = loginWithCredentials(ctx, oAuthConfig, credentials)
} else {
token, err = loginWithOAuth(ctx, oAuthConfig, disconnected)
}

if err != nil {
return fmt.Errorf("failed to obtain token: %w", err)
}

if err = auth.SaveOAuthToken(ctx, token); err != nil {
return fmt.Errorf("failed to save token: %w", err)
}

fmt.Println("Login succeeded")

err = config.Save(ctx, &config.Config{})
if err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}

cnf, ok := config.FromContext(ctx)
if !ok || cnf.Organization == "" {
fmt.Println("You do not have an organization selected, select one using: \n\n\tjsctl config set organization [name]\n\n" +
"To view organizations you have access to, list them using: \n\n\tjsctl organizations list")
}

return nil
}),
}

flags := cmd.PersistentFlags()
flags.StringVar(
&credentials,
"credentials",
os.Getenv("JSCTL_CREDENTIALS"),
"The location of service account credentials file to use instead of the normal oauth login flow",
)
flags.BoolVar(
&disconnected,
"disconnected",
false,
"Use a disconnected login flow where browser and terminal are not running on the same machine",
auth.Login(run),
auth.Logout(run),
auth.Status(run),
auth.Clusters(run, apiURL),
)

return cmd
}

func authLogout() *cobra.Command {
return &cobra.Command{
Use: "logout",
Args: cobra.ExactArgs(0),
Run: run(func(ctx context.Context, args []string) error {
err := auth.DeleteOAuthToken(ctx)
switch {
case errors.Is(err, auth.ErrNoToken):
return fmt.Errorf("host contains no authentication data")
case err != nil:
return fmt.Errorf("failed to remove authentication data: %w", err)
default:
fmt.Println("You were logged out successfully")
return nil
}
}),
}
}

func loginWithOAuth(ctx context.Context, oAuthConfig *oauth2.Config, disconnected bool) (*oauth2.Token, error) {
url, state := auth.GetOAuthURLAndState(oAuthConfig)

// disconnected can be set to true when the browser and terminal are not running
// on the same machine.
if disconnected {
fmt.Printf("Navigate to the URL below to login:\n%s\n", url)
token, err := auth.WaitForOAuthTokenCommandLine(ctx, oAuthConfig, state)
if err != nil {
return nil, fmt.Errorf("failed to obtain token: %w", err)
}
return token, nil
}

fmt.Println("Opening browser to:", url)

if err := webbrowser.Open(url); err != nil {
fmt.Printf("Navigate to the URL below to login:\n%s\n", url)
} else {
fmt.Println("You will be taken to your browser for authentication")
}

token, err := auth.WaitForOAuthTokenCallback(ctx, oAuthConfig, state)
if err != nil {
return nil, err
}

return token, nil
}

func loginWithCredentials(ctx context.Context, oAuthConfig *oauth2.Config, location string) (*oauth2.Token, error) {
credentials, err := auth.LoadCredentials(location)
switch {
case errors.Is(err, auth.ErrNoCredentials):
return nil, fmt.Errorf("no service account was found at: %s", location)
case err != nil:
return nil, fmt.Errorf("failed to read service account key: %w", err)
}

token, err := auth.GetOAuthTokenForCredentials(ctx, oAuthConfig, credentials)
if err != nil {
return nil, err
}

return token, nil
}
Loading

0 comments on commit af80ee7

Please sign in to comment.