Introduce service accounts and bootstrap tokens (#22)

This commit is contained in:
Nikolay Edigaryev
2023-02-21 11:34:12 -05:00
committed by GitHub
parent edb9b3d693
commit 8df31f7c2d
37 changed files with 1245 additions and 40 deletions
+31 -5
View File
@@ -7,6 +7,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"github.com/cirruslabs/orchard/internal/bootstraptoken"
"github.com/cirruslabs/orchard/internal/config"
"github.com/cirruslabs/orchard/internal/controller"
"github.com/cirruslabs/orchard/pkg/client"
@@ -19,6 +20,9 @@ import (
var ErrCreateFailed = errors.New("failed to create context")
var bootstrapTokenRaw string
var serviceAccountName string
var serviceAccountToken string
var force bool
func newCreateCommand() *cobra.Command {
@@ -31,6 +35,12 @@ func newCreateCommand() *cobra.Command {
command.PersistentFlags().StringVar(&contextName, "name", "default",
"context name to use")
command.PersistentFlags().StringVar(&bootstrapTokenRaw, "bootstrap-token", "",
"bootstrap token to use")
command.PersistentFlags().StringVar(&serviceAccountName, "service-account-name", "",
"service account name to use (alternative to --bootstrap-token)")
command.PersistentFlags().StringVar(&serviceAccountToken, "service-account-token", "",
"service account token to use (alternative to --bootstrap-token)")
command.PersistentFlags().BoolVar(&force, "force", false,
"create the context even if a context with the same name already exists")
@@ -54,9 +64,22 @@ func runCreate(cmd *cobra.Command, args []string) error {
}
// Establish trust
trustedControllerCertificate, err := probeControllerCertificate(controllerURL)
if err != nil {
return err
var trustedControllerCertificate *x509.Certificate
if bootstrapTokenRaw != "" {
bootstrapToken, err := bootstraptoken.NewFromString(bootstrapTokenRaw)
if err != nil {
return err
}
serviceAccountName = bootstrapToken.ServiceAccountName()
serviceAccountToken = bootstrapToken.ServiceAccountToken()
trustedControllerCertificate = bootstrapToken.Certificate()
} else {
trustedControllerCertificate, err = probeControllerCertificate(controllerURL)
if err != nil {
return err
}
}
// Check that the API is accessible
@@ -72,6 +95,7 @@ func runCreate(cmd *cobra.Command, args []string) error {
client, err := client.New(
client.WithAddress(controllerURL.String()),
client.WithTLSConfig(tlsConfig),
client.WithCredentials(serviceAccountName, serviceAccountToken),
)
if err != nil {
return err
@@ -92,8 +116,10 @@ func runCreate(cmd *cobra.Command, args []string) error {
})
return configHandle.CreateContext(contextName, config.Context{
URL: controllerURL.String(),
Certificate: certificatePEMBytes,
URL: controllerURL.String(),
Certificate: certificatePEMBytes,
ServiceAccountName: serviceAccountName,
ServiceAccountToken: serviceAccountToken,
}, force)
}
+7 -2
View File
@@ -30,10 +30,15 @@ func runList(cmd *cobra.Command, args []string) error {
table := uitable.New()
table.AddRow("Name", "URL")
table.AddRow("Name", "URL", "Default")
for name, context := range config.Contexts {
table.AddRow(name, context.URL)
var defaultMark string
if name == config.DefaultContext {
defaultMark = "*"
}
table.AddRow(name, context.URL, defaultMark)
}
fmt.Println(table)
+27 -4
View File
@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"github.com/cirruslabs/orchard/internal/controller"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/spf13/cobra"
"math/big"
"time"
@@ -20,6 +21,8 @@ var ErrInitFailed = errors.New("controller initialization failed")
var controllerCertPath string
var controllerKeyPath string
var serviceAccountName string
var serviceAccountToken string
var force bool
func newInitCommand() *cobra.Command {
@@ -35,6 +38,10 @@ func newInitCommand() *cobra.Command {
command.PersistentFlags().StringVar(&controllerKeyPath, "controller-key", "",
"do not auto-generate the controller certificate key, import it from the specified path instead"+
" (requires --controller-cert)")
command.PersistentFlags().StringVar(&serviceAccountName, "service-account-name", "admin",
"name of the service account with maximum privileges to create")
command.PersistentFlags().StringVar(&serviceAccountToken, "service-account-token", "",
"token to use when creating the service account with maximum privileges")
command.PersistentFlags().BoolVar(&force, "force", false,
"force re-initialization if the controller is already initialized")
@@ -42,7 +49,9 @@ func newInitCommand() *cobra.Command {
}
func runInit(cmd *cobra.Command, args []string) (err error) {
var controllerCert tls.Certificate
if serviceAccountToken == "" {
return fmt.Errorf("%w: --service-account-token is required", ErrInitFailed)
}
dataDir, err := controller.NewDataDir(dataDirPath)
if err != nil {
@@ -59,6 +68,8 @@ func runInit(cmd *cobra.Command, args []string) (err error) {
"please specify \"--force\" to re-initialize", ErrInitFailed)
}
var controllerCert tls.Certificate
if controllerCertPath != "" || controllerKeyPath != "" {
if err := checkBothCertAndKeyAreSpecified(); err != nil {
return err
@@ -69,7 +80,7 @@ func runInit(cmd *cobra.Command, args []string) (err error) {
return err
}
} else {
controllerCert, err = generateSelfSignedControllerCertificate()
controllerCert, err = GenerateSelfSignedControllerCertificate()
if err != nil {
return err
}
@@ -79,7 +90,19 @@ func runInit(cmd *cobra.Command, args []string) (err error) {
return err
}
return nil
// Run the controller to create the service account with maximum privileges
controller, err := controller.New(controller.WithDataDir(dataDir))
if err != nil {
return err
}
return controller.EnsureServiceAccount(&v1.ServiceAccount{
Meta: v1.Meta{
Name: serviceAccountName,
},
Token: serviceAccountToken,
Roles: v1.AllServiceAccountRoles(),
})
}
func checkBothCertAndKeyAreSpecified() error {
@@ -96,7 +119,7 @@ func checkBothCertAndKeyAreSpecified() error {
return nil
}
func generateSelfSignedControllerCertificate() (tls.Certificate, error) {
func GenerateSelfSignedControllerCertificate() (tls.Certificate, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), cryptorand.Reader)
if err != nil {
return tls.Certificate{}, err
+2 -2
View File
@@ -7,10 +7,10 @@ import (
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "create",
Short: "Create resources on the controller (VMs)",
Short: "Create resources on the controller",
}
command.AddCommand(newCreateVMCommand())
command.AddCommand(newCreateVMCommand(), newCreateServiceAccount())
return command
}
@@ -0,0 +1,58 @@
package create
import (
"fmt"
"github.com/cirruslabs/orchard/pkg/client"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/spf13/cobra"
"strings"
)
var token string
var roles []string
func newCreateServiceAccount() *cobra.Command {
command := &cobra.Command{
Use: "service-account",
RunE: runCreateServiceAccount,
Args: cobra.ExactArgs(1),
}
command.PersistentFlags().StringVar(&token, "token", "",
"token to use for this service account (autogenerated by the API server if left empty)")
var serviceAccountRoleList []string
for _, role := range v1.AllServiceAccountRoles() {
serviceAccountRoleList = append(serviceAccountRoleList, string(role))
}
command.PersistentFlags().StringArrayVar(&roles, "roles", []string{},
fmt.Sprintf("roles to grant to this service account (supported roles: %s)",
strings.Join(serviceAccountRoleList, ", ")))
return command
}
func runCreateServiceAccount(cmd *cobra.Command, args []string) error {
name := args[0]
client, err := client.New()
if err != nil {
return err
}
var serviceAccountRoles []v1.ServiceAccountRole
for _, role := range roles {
// Don't bother checking if the role name is valid
// since this will be checked by the API server anyway
serviceAccountRoles = append(serviceAccountRoles, v1.ServiceAccountRole(role))
}
return client.ServiceAccounts().Create(cmd.Context(), &v1.ServiceAccount{
Meta: v1.Meta{
Name: name,
},
Token: token,
Roles: serviceAccountRoles,
})
}
+2 -2
View File
@@ -7,10 +7,10 @@ import (
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "delete",
Short: "Delete resources from the controller (VMs)",
Short: "Delete resources from the controller",
}
command.AddCommand(newDeleteVMCommand())
command.AddCommand(newDeleteVMCommand(), newDeleteServiceComandCommand())
return command
}
@@ -0,0 +1,25 @@
package deletecmd
import (
"github.com/cirruslabs/orchard/pkg/client"
"github.com/spf13/cobra"
)
func newDeleteServiceComandCommand() *cobra.Command {
return &cobra.Command{
Use: "service-account",
Args: cobra.ExactArgs(1),
RunE: runDeleteServiceAccountCommand,
}
}
func runDeleteServiceAccountCommand(cmd *cobra.Command, args []string) error {
name := args[0]
client, err := client.New()
if err != nil {
return err
}
return client.ServiceAccounts().Delete(cmd.Context(), name, false)
}
+2 -1
View File
@@ -40,7 +40,8 @@ func runDev(cmd *cobra.Command, args []string) error {
return err
}
controller, err := controller.New(controller.WithDataDir(dataDir), controller.WithLogger(logger))
controller, err := controller.New(controller.WithDataDir(dataDir),
controller.WithInsecureAuthDisabled(), controller.WithLogger(logger))
if err != nil {
return err
}
+53
View File
@@ -0,0 +1,53 @@
package get
import (
"fmt"
"github.com/cirruslabs/orchard/internal/bootstraptoken"
"github.com/cirruslabs/orchard/internal/config"
"github.com/cirruslabs/orchard/pkg/client"
"github.com/spf13/cobra"
)
func newGetBootstrapTokenCommand() *cobra.Command {
command := &cobra.Command{
Use: "bootstrap-token",
Short: "Retrieve a bootstrap token for the specified service account",
RunE: runGetBootstrapToken,
Args: cobra.ExactArgs(1),
}
return command
}
func runGetBootstrapToken(cmd *cobra.Command, args []string) error {
name := args[0]
configHandle, err := config.NewHandle()
if err != nil {
return err
}
defaultContext, err := configHandle.DefaultContext()
if err != nil {
return err
}
client, err := client.New()
if err != nil {
return err
}
serviceAccount, err := client.ServiceAccounts().Get(cmd.Context(), name)
if err != nil {
return err
}
bootstrapToken, err := bootstraptoken.New(defaultContext.Certificate, serviceAccount.Name, serviceAccount.Token)
if err != nil {
return err
}
fmt.Println(bootstrapToken)
return nil
}
+19
View File
@@ -0,0 +1,19 @@
package get
import (
"errors"
"github.com/spf13/cobra"
)
var ErrGetFailed = errors.New("get command failed")
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "get",
Short: "Retrieve resources from the controller",
}
command.AddCommand(newGetServiceAccountCommand(), newGetBootstrapTokenCommand())
return command
}
+70
View File
@@ -0,0 +1,70 @@
package get
import (
"fmt"
"github.com/cirruslabs/orchard/internal/structpath"
"github.com/cirruslabs/orchard/pkg/client"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"strings"
)
func newGetServiceAccountCommand() *cobra.Command {
command := &cobra.Command{
Use: "service-account",
Short: "Retrieve service account and it's fields",
RunE: runGetServiceAccount,
Args: cobra.ExactArgs(1),
}
return command
}
func runGetServiceAccount(cmd *cobra.Command, args []string) error {
name := args[0]
client, err := client.New()
if err != nil {
return err
}
// Ability to retrieve resource fields (e.g. "orchard get service-account workers/token")
splits := strings.Split(name, "/")
var path []string
if len(splits) > 1 {
name = splits[0]
path = splits[1:]
}
serviceAccount, err := client.ServiceAccounts().Get(cmd.Context(), name)
if err != nil {
return err
}
// Ability to retrieve resource fields (e.g. "orchard get service-account workers/token")
if len(path) != 0 {
result, ok := structpath.Lookup(*serviceAccount, path)
if !ok {
return fmt.Errorf("%w: failed to find the specified field \"%s\" or the field is not a string",
ErrGetFailed, strings.Join(path, "/"))
}
fmt.Println(result)
return nil
}
table := uitable.New()
table.AddRow("name", serviceAccount.Name)
var scopeList []string
for _, scope := range serviceAccount.Roles {
scopeList = append(scopeList, string(scope))
}
table.AddRow("roles", strings.Join(scopeList, ", "))
fmt.Println(table)
return nil
}
+2 -2
View File
@@ -9,10 +9,10 @@ var quiet bool
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "list",
Short: "List resources on the controller (workers, VMs)",
Short: "List resources on the controller",
}
command.AddCommand(newListWorkersCommand(), newListVMsCommand())
command.AddCommand(newListWorkersCommand(), newListVMsCommand(), newListServiceAccountsCommand())
command.PersistentFlags().BoolVarP(&quiet, "", "q", false, "only show resource names")
+57
View File
@@ -0,0 +1,57 @@
package list
import (
"fmt"
"github.com/cirruslabs/orchard/pkg/client"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"strings"
)
func newListServiceAccountsCommand() *cobra.Command {
command := &cobra.Command{
Use: "service-accounts",
Short: "List service accounts",
RunE: runListServiceAccounts,
}
return command
}
func runListServiceAccounts(cmd *cobra.Command, args []string) error {
client, err := client.New()
if err != nil {
return err
}
serviceAccounts, err := client.ServiceAccounts().List(cmd.Context())
if err != nil {
return err
}
if quiet {
for _, serviceAccount := range serviceAccounts {
fmt.Println(serviceAccount.Name)
}
return nil
}
table := uitable.New()
table.AddRow("Name", "Roles")
for _, serviceAccount := range serviceAccounts {
var scopeList []string
for _, scope := range serviceAccount.Roles {
scopeList = append(scopeList, string(scope))
}
table.AddRow(serviceAccount.Name, strings.Join(scopeList, ", "))
}
fmt.Println(table)
return nil
}
+3 -2
View File
@@ -9,8 +9,9 @@ import (
func newListVMsCommand() *cobra.Command {
command := &cobra.Command{
Use: "vms",
RunE: runListVMs,
Use: "vms",
Short: "List VMs",
RunE: runListVMs,
}
return command
+2
View File
@@ -6,6 +6,7 @@ import (
"github.com/cirruslabs/orchard/internal/command/create"
deletepkg "github.com/cirruslabs/orchard/internal/command/deletecmd"
"github.com/cirruslabs/orchard/internal/command/dev"
"github.com/cirruslabs/orchard/internal/command/get"
"github.com/cirruslabs/orchard/internal/command/list"
"github.com/cirruslabs/orchard/internal/command/worker"
"github.com/spf13/cobra"
@@ -20,6 +21,7 @@ func NewRootCmd() *cobra.Command {
addGroupedCommands(command, "Working With Resources:",
create.NewCommand(),
get.NewCommand(),
list.NewCommand(),
deletepkg.NewCommand(),
)