Skip to content

Commit

Permalink
allow robot tokens to be synced to Vault
Browse files Browse the repository at this point in the history
  • Loading branch information
xrstf committed Oct 11, 2021
1 parent fcabd22 commit eb4cc46
Show file tree
Hide file tree
Showing 8 changed files with 673 additions and 31 deletions.
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module github.com/kubermatic-labs/aquayman
go 1.14

require (
github.com/kr/pretty v0.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
github.com/hashicorp/vault/api v1.1.1
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
)
364 changes: 361 additions & 3 deletions go.sum

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/kubermatic-labs/aquayman/pkg/config"
"github.com/kubermatic-labs/aquayman/pkg/export"
"github.com/kubermatic-labs/aquayman/pkg/publisher"
"github.com/kubermatic-labs/aquayman/pkg/quay"
"github.com/kubermatic-labs/aquayman/pkg/sync"
)
Expand All @@ -32,6 +33,11 @@ func main() {
exportMode = false
createRepositories = false
deleteRepositories = false

// Set this to enable vault integration; as the Vault API
// client uses VAULT_ADDR and VAULT_TOKEN env vars already,
// we simply do the same.
enableVault = false
)

flag.StringVar(&configFile, "config", configFile, "path to the config.yaml")
Expand All @@ -42,13 +48,20 @@ func main() {
flag.BoolVar(&exportMode, "export", exportMode, "export quay.io state and update the config file (-config flag)")
flag.BoolVar(&createRepositories, "create-repos", createRepositories, "create repositories listed in the config file but not existing on quay.io yet")
flag.BoolVar(&deleteRepositories, "delete-repos", deleteRepositories, "delete repositories on quay.io that are not listed in the config file")
flag.BoolVar(&enableVault, "enable-vault", enableVault, "enable Vault integration (VAULT_ADDR and VAULT_TOKEN env vars must be set also)")
flag.Parse()

if showVersion {
fmt.Printf("Aquayman %s (built at %s)\n", version, date)
return
}

if enableVault {
if os.Getenv("VAULT_ADDR") == "" || os.Getenv("VAULT_TOKEN") == "" {
log.Fatal("⚠ Both VAULT_ADDR and VAULT_TOKEN environment variables need to be set if -enable-vault is used.")
}
}

if configFile == "" {
log.Print("⚠ No configuration (-config) specified.\n\n")
flag.Usage()
Expand Down Expand Up @@ -107,11 +120,20 @@ func main() {
return
}

var pub publisher.Publisher
if enableVault {
pub, err = publisher.NewVaultPublisher(cfg.Organization)
if err != nil {
log.Fatalf("⚠ Failed to create Vault client: %v.", err)
}
}

log.Printf("► Updating organization %s…", cfg.Organization)

options := sync.Options{
CreateMissingRepositories: createRepositories,
DeleteDanglingRepositories: deleteRepositories,
Publisher: pub,
}

err = sync.Sync(ctx, cfg, client, options)
Expand Down
27 changes: 27 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ func (c *RepositoryConfig) IsWildcard() bool {
type RobotConfig struct {
Name string `yaml:"name"`
Description string `yaml:"description,omitempty"`

// VaultSecret is the path inside the Vault API to the
// secret where the token should be stored, for example
// "mykvstore/data/customer-xyz" (note the "/data/" bit).
// Aquayman will extend the secret with a
// "quay.io-<orgname>-token" key and store the token there.
// If this is empty, no Vault interaction happens, even
// if -enable-vault is set.
// The value can include an optional key name to override
// the default. Use a "#" to separate path from key, e.g.
// "mykvstore/data/customer-xyz#keyname".
VaultSecret string `yaml:"vaultSecret,omitempty"`

// Deleted can be used as a workaround for deleting tokens
// from Vault. If a robot was just removed from the config.yaml
// alltogether, Aquayman would have no idea where to find the
// secret in Vault in order to delete it (because the path in
// Vault is not just constructed based on the robot name, but
// based on the VaultSecret property).
// If a clean cleanup is desired, one can first set this field
// to `true` and run Aquayman, which will remove the robot
// from quay and also remove the token from Vault. Afterwards,
// the robot can be removed entirely from the configuration.
// If a robot is directly removed from the configuration,
// an orphaned (yet invalid) token will remain in Vault. Not
// nice, but not the end of the world.
Deleted bool `yaml:"deleted,omitempty"`
}

func LoadFromFile(filename string) (*Config, error) {
Expand Down
12 changes: 12 additions & 0 deletions pkg/publisher/publisher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package publisher

import (
"context"

"github.com/kubermatic-labs/aquayman/pkg/config"
)

type Publisher interface {
UpdateRobot(ctx context.Context, robot *config.RobotConfig, token string) error
DeleteRobot(ctx context.Context, robot *config.RobotConfig) error
}
140 changes: 140 additions & 0 deletions pkg/publisher/vault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package publisher

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/vault/api"
"github.com/kubermatic-labs/aquayman/pkg/config"
)

type Vault struct {
client *api.Client
org string
}

// NewVaultPublisher relies on VAULT_ADDR and VAULT_TOKEN env
// variables be set.
func NewVaultPublisher(organization string) (*Vault, error) {
client, err := api.NewClient(nil)
if err != nil {
return nil, fmt.Errorf("could not build Vault client: %w", err)
}

return &Vault{
client: client,
org: organization,
}, nil
}

func (v *Vault) UpdateRobot(ctx context.Context, robot *config.RobotConfig, token string) error {
if robot.VaultSecret == "" {
return nil
}

addr, err := v.getAddress(robot)
if err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}

// fetch current state, so we do not need to bump if there are no changes to the token
secret, err := v.client.Logical().Read(addr.path)
if err != nil {
return fmt.Errorf("failed to read from Vault: %w", err)
}

secretUpToDate := false
existingData := map[string]interface{}{}

if secret != nil {
// the secrets are wrapped in a "data" field,
// that's just how kv stores in Vault work

if data, exists := secret.Data["data"]; exists {
if m, ok := data.(map[string]interface{}); ok {
existingData = m

if value, exists := m[addr.key]; exists {
if svalue, ok := value.(string); ok {
secretUpToDate = svalue == token
}
}
}
}
}

if !secretUpToDate {
existingData[addr.key] = token
secret.Data["data"] = existingData

if _, err := v.client.Logical().Write(addr.path, secret.Data); err != nil {
return fmt.Errorf("failed to update Vault: %w", err)
}
}

return nil
}

func (v *Vault) DeleteRobot(ctx context.Context, robot *config.RobotConfig) error {
if robot.VaultSecret == "" {
return nil
}

addr, err := v.getAddress(robot)
if err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}

// fetch current state, so we do not need to bump if there are no changes to the token
secret, err := v.client.Logical().Read(addr.path)
if err != nil {
return fmt.Errorf("failed to read from Vault: %w", err)
}

if secret == nil {
return nil
}

// the secrets are wrapped in a "data" field,
// that's just how kv stores in Vault work

if data, exists := secret.Data["data"]; exists {
if m, ok := data.(map[string]interface{}); ok {
if _, exists := m[addr.key]; exists {
delete(m, addr.key)

secret.Data["data"] = m

if _, err := v.client.Logical().Write(addr.path, secret.Data); err != nil {
return fmt.Errorf("failed to update Vault: %w", err)
}
}
}
}

return nil
}

type address struct {
path string
key string
}

func (v *Vault) getAddress(robot *config.RobotConfig) (*address, error) {
parts := strings.Split(robot.VaultSecret, "#")
if len(parts) > 2 {
return nil, fmt.Errorf("invalid path %q: must not contain more than one # symbol", robot.VaultSecret)
}

a := address{
path: parts[0],
key: fmt.Sprintf("quay.io-%s-%s-token", v.org, robot.Name),
}

if len(parts) > 1 {
a.key = parts[1]
}

return &a, nil
}
8 changes: 8 additions & 0 deletions pkg/quay/robot.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ func (c *Client) GetOrganizationRobots(ctx context.Context, org string, options
return robots, err
}

func (c *Client) GetOrganizationRobot(ctx context.Context, org string, shortName string) (*Robot, error) {
response := Robot{}
path := fmt.Sprintf("/organization/%s/robots/%s", url.PathEscape(org), url.PathEscape(shortName))
err := c.call(ctx, "GET", path, nil, nil, &response)

return &response, err
}

type CreateOrganizationRobotOptions struct {
Description string `json:"description"`
}
Expand Down
Loading

0 comments on commit eb4cc46

Please sign in to comment.