Skip to content

Commit

Permalink
Add organization logic
Browse files Browse the repository at this point in the history
  • Loading branch information
biazmoreira committed Oct 6, 2023
1 parent dcf6297 commit 00ddf16
Show file tree
Hide file tree
Showing 9 changed files with 1,064 additions and 31 deletions.
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ fmtcheck:

.PHONY: fmt
fmt:
gofumpt -l -w .
gofumpt -l -w .

mocks:
go install github.com/vektra/mockery/[email protected]
mockery --srcpkg github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/organization_service --name=ClientService
mockery --srcpkg github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/project_service --name=ClientService
48 changes: 48 additions & 0 deletions configure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package hcpvaultengine

import (
"github.com/hashicorp/vault/api"
"golang.org/x/oauth2"
)

// TODO: reevaluate this cache strategy
// Disk? Memory? Memdb?
var cache *HCPVClusterCache

type HCPVClusterCache struct {
// Memory cache of the token source
Source oauth2.TokenSource

// Memory cache of the cluster address
Address string

// Memory cache of the cluster ID
ID string
}

// ConfigureHCPProxy adds a client-side middleware, an implementation of http.RoundTripper on top of the base transport,
// that will add a cookie to every request made from the CLI client. Additionally, it overrides the configuration's address
// The address will be that of the proxy by default and the cookie will have the HCP access token data necessary to make requests to
// the cluster through HCP.
//
// TODO: is there a better way to change the configuration without parametizing the Vault Config?
func ConfigureHCPProxy(client *api.Client) error {
if cache != nil {
// TODO: reevaluate this. Which scheme? https?
addr := "https://" + cache.Address
err := client.SetAddress(addr)
if err != nil {
return err
}

// TODO: understand and reevaluate exactly what it means to get the token from the source or to get the TokenSource.
token, err := cache.Source.Token()
if err != nil {
return err
}

client.SetHCPToken(token)
}

return nil
}
217 changes: 209 additions & 8 deletions connect.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package hcpvaultengine

import (
"errors"
"flag"
"fmt"
"strconv"
"strings"

hcprmo "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/organization_service"
hcprmp "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/project_service"
hcprmm "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/models"
hcpvs "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/stable/2020-11-25/client/vault_service"
hcpvsm "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/stable/2020-11-25/models"
"github.com/mitchellh/cli"
)

Expand All @@ -15,8 +23,6 @@ type HCPConnectCommand struct {
Ui cli.Ui

flagNonInteractiveMethod bool

connectHandler ConnectHandler
}

func (c *HCPConnectCommand) Help() string {
Expand All @@ -32,18 +38,49 @@ func (c *HCPConnectCommand) Run(args []string) int {
return 1
}

if c.flagNonInteractiveMethod {
c.connectHandler = &nonInteractiveConnectHandler{}
} else {
c.connectHandler = &interactiveConnectHandler{}
}
connectHandler := c.connectHandlerFactory()

authStatus, err := c.connectHandler.Connect(args)
hcpHttpClient, err := connectHandler.Connect(args)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to connect to HCP: %s", err))
return 1
}

// List orgs
// If list is greater than 1, ask for user input c.Ui.Ask()
//
// should we add pagination?
organizationID, err := c.getOrganization(hcprmo.New(hcpHttpClient, nil))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP organization information: %s", err))
return 1
}

// List projects for chosen org
// If list is greater than 1, ask for user input c.Ui.Ask()
//
// should we add pagination?
projectID, err := c.getProject(hcprmp.New(hcpHttpClient, nil))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP project information: %s", err))
return 1
}

// List clusters for org+project
// If list is greater than 1, ask for user input c.Ui.Ask()
//
// should we add pagination?
err = c.getCluster(organizationID, projectID, hcpvs.New(hcpHttpClient, nil))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP Vault Cluster information: %s", err))
return 1
}

cache = &HCPVClusterCache{}

// Cache details -- in memory? in disk?
// In memory for POC

return 0
}

Expand All @@ -56,3 +93,167 @@ func (c *HCPConnectCommand) Flags() *flag.FlagSet {
mainSet.Var(&boolValue{target: &c.flagNonInteractiveMethod}, "non-interactive", "")
return mainSet
}

func (c *HCPConnectCommand) getOrganization(rmOrgClient hcprmo.ClientService) (organizationID string, err error) {
organizationsResp, err := rmOrgClient.OrganizationServiceList(&hcprmo.OrganizationServiceListParams{}, nil)
switch {
case err != nil:
return "", err
case organizationsResp.GetPayload() == nil:
return "", errors.New("payload is nil")
case len(organizationsResp.GetPayload().Organizations) < 1:
return "", errors.New("no organizations available")
case len(organizationsResp.GetPayload().Organizations) > 1:
var orgs []string
for i, org := range organizationsResp.GetPayload().Organizations {
if org.State == hcprmm.HashicorpCloudResourcemanagerOrganizationOrganizationStateACTIVE.Pointer() {
orgs = append(orgs, fmt.Sprintf("%d: Organization name: %s Organization ID: %s", i, org.Name, org.ID))
}
}
userInput, err := c.Ui.Ask(fmt.Sprintf("Choose one of the following organizations: %s", strings.Join(orgs, "\n")))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP organization information: %s", err))
return "", err
}
// convert userInput to int
var index int
index, err = strconv.Atoi(userInput)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP organization information: %s", err))
return "", err
}
// if conversion fails, return an error
// else validate that the index is within boundaries of the organization slice
if index > len(orgs) || index < len(orgs) {
return "", errors.New("invalid organization chosen")
}

// set the org ID
organizationID = organizationsResp.GetPayload().Organizations[index].ID
organizationName := organizationsResp.GetPayload().Organizations[index].Name
c.Ui.Info(fmt.Sprintf("HCP Organization: %s", organizationName))

break
case len(organizationsResp.GetPayload().Organizations) == 1:
organization := organizationsResp.GetPayload().Organizations[0]
if *organization.State != hcprmm.HashicorpCloudResourcemanagerOrganizationOrganizationStateACTIVE {
return "", errors.New("organization is not active")
}
organizationID = organization.ID
c.Ui.Info(fmt.Sprintf("HCP Organization: %s", organization.Name))
}
return organizationID, nil
}

func (c *HCPConnectCommand) getProject(rmProjClient hcprmp.ClientService) (projectID string, err error) {
projectResp, err := rmProjClient.ProjectServiceList(&hcprmp.ProjectServiceListParams{}, nil)
switch {
case err != nil:
return "", err
case projectResp.GetPayload() == nil:
return "", errors.New("payload is nil")
case len(projectResp.GetPayload().Projects) < 1:
return "", errors.New("no projects available")
case len(projectResp.GetPayload().Projects) > 1:
var projs []string
for i, proj := range projectResp.GetPayload().Projects {
if *proj.State == hcprmm.HashicorpCloudResourcemanagerProjectProjectStateACTIVE {
projs = append(projs, fmt.Sprintf("%d: Project name: %s Project ID: %s", i, proj.Name, proj.ID))
}
}
userInput, err := c.Ui.Ask(fmt.Sprintf("Choose one of the following projects: %s", strings.Join(projs, "\n")))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP project information: %s", err))
return "", err
}
// convert userInput to int
var index int
index, err = strconv.Atoi(userInput)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP project information: %s", err))
return "", err
}
// else validate that the index is within boundaries of the organization slice
if index > len(projs) || index < len(projs) {
return "", errors.New("invalid project chosen")
}

// set the org ID
projectID = projectResp.GetPayload().Projects[index].ID
projectName := projectResp.GetPayload().Projects[index].Name
c.Ui.Info(fmt.Sprintf("HCP Project: %s", projectName))

break
case len(projectResp.GetPayload().Projects) == 1:
project := projectResp.GetPayload().Projects[0]
if *project.State != hcprmm.HashicorpCloudResourcemanagerProjectProjectStateACTIVE {
return "", errors.New("organization is not active")
}
projectID = project.ID
c.Ui.Info(fmt.Sprintf("HCP Project: %s", project.Name))
}
return projectID, nil
}

func (c *HCPConnectCommand) getCluster(organizationID string, projectID string, vsClient hcpvs.ClientService) error {
clustersResp, err := vsClient.List(&hcpvs.ListParams{LocationOrganizationID: organizationID, LocationProjectID: projectID}, nil)
switch {
case err != nil:
return err
case clustersResp.GetPayload() == nil:
return errors.New("payload is nil")
case len(clustersResp.GetPayload().Clusters) < 1:
return errors.New("no projects available")
case len(clustersResp.GetPayload().Clusters) > 1:
var clusters []string
for i, cluster := range clustersResp.GetPayload().Clusters {
if *cluster.State == hcpvsm.HashicorpCloudVault20201125ClusterStateRUNNING {
clusters = append(clusters, fmt.Sprintf("%d: HCP Vault Cluster name: %s HCP Vault Cluster ID: %s", i, cluster.ID, cluster.ResourceID))
}
}
userInput, err := c.Ui.Ask(fmt.Sprintf("Choose one of the following HCP Vault Clusters: %s", strings.Join(clusters, "\n")))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP Vault Cluster information: %s", err))
return err
}
// convert userInput to int
var index int
index, err = strconv.Atoi(userInput)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to get HCP Vault Cluster information: %s", err))
return err
}
// else validate that the index is within boundaries of the organization slice
if index > len(clusters) || index < len(clusters) {
return errors.New("invalid cluster chosen")
}

// set the org ID
clusterName := clustersResp.GetPayload().Clusters[index].ID
c.Ui.Info(fmt.Sprintf("HCP Vault Cluster: %s", clusterName))

cache.Address = clustersResp.GetPayload().Clusters[index].DNSNames.Proxy

break
case len(clustersResp.GetPayload().Clusters) == 1:
cluster := clustersResp.GetPayload().Clusters[0]
if *cluster.State != hcpvsm.HashicorpCloudVault20201125ClusterStateRUNNING {
return errors.New("cluster is not running")
}
projectID = cluster.ResourceID
c.Ui.Info(fmt.Sprintf("HCP Vault Cluster: %s", cluster.ID))

cache.Address = clustersResp.GetPayload().Clusters[0].DNSNames.Proxy

break
}

return nil
}

func (c *HCPConnectCommand) connectHandlerFactory() ConnectHandler {
if c.flagNonInteractiveMethod {
return &nonInteractiveConnectHandler{}
}
return &interactiveConnectHandler{}
}
65 changes: 57 additions & 8 deletions connect_handler.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package hcpvaultengine

import (
"errors"
"flag"
httptransport "github.com/go-openapi/runtime/client"
"github.com/hashicorp/hcp-sdk-go/config"
"github.com/hashicorp/hcp-sdk-go/httpclient"
"golang.org/x/oauth2"
)

type AuthStatus struct {
Token string //should this be encrypted? how?
Token oauth2.TokenSource
}

var (
Expand All @@ -10,18 +19,58 @@ var (
)

type ConnectHandler interface {
// should we parse this before [during the Run function common flag parsin] into a struct that's specific to the different handlers?
Connect(args []string) (AuthStatus, error)
Connect(args []string) (*httptransport.Runtime, error)
}

type interactiveConnectHandler struct{}

func (h *interactiveConnectHandler) Connect(args []string) (AuthStatus, error) {
return AuthStatus{}, nil
func (h *interactiveConnectHandler) Connect(_ []string) (*httptransport.Runtime, error) {
// Start a callback listener
// Define timeout: 2 minutes?
return nil, nil
}

type nonInteractiveConnectHandler struct {
flagClientID string
flagSecretID string
}

func (h *nonInteractiveConnectHandler) Connect(args []string) (*httptransport.Runtime, error) {
f := h.Flags()

if err := f.Parse(args); err != nil {
return nil, err
}

if h.flagClientID == "" || h.flagSecretID == "" {
return nil, errors.New("client ID and Secret ID need to be set in non-interactive mode")
}

opts := []config.HCPConfigOption{config.FromEnv()}
opts = append(opts, config.WithClientCredentials(h.flagClientID, h.flagSecretID))
opts = append(opts, config.WithoutBrowserLogin())

cfg, err := config.NewHCPConfig(opts...)
if err != nil {
return nil, err
}

// Cache token source
cache.Source = cfg

hcpClient, err := httpclient.New(httpclient.Config{HCPConfig: cfg})
if err != nil {
return nil, err
}

return hcpClient, nil
}

type nonInteractiveConnectHandler struct{}
func (h *nonInteractiveConnectHandler) Flags() *flag.FlagSet {
mainSet := flag.NewFlagSet("", flag.ContinueOnError)

mainSet.Var(&stringValue{target: &h.flagClientID}, "client-id", "")
mainSet.Var(&stringValue{target: &h.flagSecretID}, "secret-id", "")

func (h *nonInteractiveConnectHandler) Connect(args []string) (AuthStatus, error) {
return AuthStatus{}, nil
return mainSet
}
Loading

0 comments on commit 00ddf16

Please sign in to comment.