Skip to content
This repository has been archived by the owner on May 7, 2024. It is now read-only.

Commit

Permalink
Add credential information to 'show cluster' output (#308)
Browse files Browse the repository at this point in the history
* Update dependencies (for gsclientgen)

* Boyscouting: variable names in camelCase

* Add GetCredential client wrapper function

* Add test case for BYOC cluster

* Add logic to fetch credential details

* Handle specific errors

* Improve test with real-world data
  • Loading branch information
marians committed Oct 26, 2018
1 parent ab85fe7 commit 10e07f4
Show file tree
Hide file tree
Showing 64 changed files with 1,990 additions and 3,392 deletions.
147 changes: 129 additions & 18 deletions Gopkg.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions client/client.go
Expand Up @@ -451,6 +451,22 @@ func (w *WrapperV2) GetOrganizations(p *AuxiliaryParams) (*organizations.GetOrga
return response, nil
}

// GetCredential calls the API's getCredential operation using the V2 client.
func (w *WrapperV2) GetCredential(organizationID string, credentialID string, p *AuxiliaryParams) (*organizations.GetCredentialOK, error) {
params := organizations.NewGetCredentialParams().WithOrganizationID(organizationID).WithCredentialID(credentialID)
err := setParamsWithAuthorization(p, w, params)
if err != nil {
return nil, microerror.Mask(err)
}

response, err := w.gsclient.Organizations.GetCredential(params, nil)
if err != nil {
return nil, clienterror.New(err)
}

return response, nil
}

// SetCredentials calls the API's addCredentials operation of an organization.
func (w *WrapperV2) SetCredentials(organizationID string, addCredentialsRequest *models.V4AddCredentialsRequest, p *AuxiliaryParams) (*organizations.AddCredentialsCreated, error) {
params := organizations.NewAddCredentialsParams().WithOrganizationID(organizationID).WithBody(addCredentialsRequest)
Expand Down
9 changes: 9 additions & 0 deletions client/clienterror/clienterror.go
Expand Up @@ -272,6 +272,15 @@ func New(err error) *APIError {
}
}

// get credential
if myerr, ok := err.(*organizations.GetCredentialDefault); ok {
return &APIError{
ErrorMessage: myerr.Error(),
HTTPStatusCode: myerr.Code(),
OriginalError: myerr,
}
}

// HTTP level error cases
if runtimeAPIError, ok := err.(*runtime.APIError); ok {
ae := &APIError{
Expand Down
10 changes: 10 additions & 0 deletions commands/error.go
Expand Up @@ -242,6 +242,16 @@ func IsOrganizationNotSpecifiedError(err error) bool {
return microerror.Cause(err) == organizationNotSpecifiedError
}

// credentialNotFoundError means that the specified credential could not be found
var credentialNotFoundError = &microerror.Error{
Kind: "credentialNotFoundError",
}

// IsCredentialNotFoundError asserts credentialNotFoundError
func IsCredentialNotFoundError(err error) bool {
return microerror.Cause(err) == credentialNotFoundError
}

// yamlFileNotReadableError means a YAML file was not readable
var yamlFileNotReadableError = &microerror.Error{
Kind: "yamlFileNotReadableError",
Expand Down
32 changes: 16 additions & 16 deletions commands/list_releases.go
Expand Up @@ -133,10 +133,10 @@ func listReleasesRunOutput(cmd *cobra.Command, extraArgs []string) {

for i, release := range releases {
created := util.ShortDate(util.ParseDate(*release.Timestamp))
kubernetes_version := "n/a"
container_linux_version := "n/a"
coredns_version := "n/a"
calico_version := "n/a"
kubernetesVersion := "n/a"
containerLinuxVersion := "n/a"
coreDNSVersion := "n/a"
calicoVersion := "n/a"

// As long as the status information is not specific in the API
// we start with deprecated, find the active one and then switch
Expand Down Expand Up @@ -171,16 +171,16 @@ func listReleasesRunOutput(cmd *cobra.Command, extraArgs []string) {

for _, component := range release.Components {
if *component.Name == "kubernetes" {
kubernetes_version = *component.Version
kubernetesVersion = *component.Version
}
if *component.Name == "containerlinux" {
container_linux_version = *component.Version
containerLinuxVersion = *component.Version
}
if *component.Name == "coredns" {
coredns_version = *component.Version
coreDNSVersion = *component.Version
}
if *component.Name == "calico" {
calico_version = *component.Version
calicoVersion = *component.Version
}
}

Expand All @@ -189,20 +189,20 @@ func listReleasesRunOutput(cmd *cobra.Command, extraArgs []string) {
color.YellowString(*release.Version),
color.YellowString(status),
color.YellowString(created),
color.YellowString(kubernetes_version),
color.YellowString(container_linux_version),
color.YellowString(coredns_version),
color.YellowString(calico_version),
color.YellowString(kubernetesVersion),
color.YellowString(containerLinuxVersion),
color.YellowString(coreDNSVersion),
color.YellowString(calicoVersion),
}, "|"))
} else {
output = append(output, strings.Join([]string{
*release.Version,
status,
created,
kubernetes_version,
container_linux_version,
coredns_version,
calico_version,
kubernetesVersion,
containerLinuxVersion,
coreDNSVersion,
calicoVersion,
}, "|"))
}
}
Expand Down
65 changes: 63 additions & 2 deletions commands/show_cluster.go
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"os"
"strings"

"github.com/fatih/color"
"github.com/giantswarm/columnize"
Expand Down Expand Up @@ -94,7 +95,6 @@ func verifyShowClusterPreconditions(args showClusterArguments, cmdLineArgs []str

// getClusterDetails returns details for one cluster.
func getClusterDetails(clusterID, activityName string) (*models.V4ClusterDetailsResponse, error) {

// perform API call
auxParams := ClientV2.DefaultAuxiliaryParams()
auxParams.ActivityName = activityName
Expand All @@ -120,6 +120,31 @@ func getClusterDetails(clusterID, activityName string) (*models.V4ClusterDetails
return response.Payload, nil
}

func getOrgCredentials(orgName, credentialID, activityName string) (*models.V4GetCredentialResponse, error) {
auxParams := ClientV2.DefaultAuxiliaryParams()
auxParams.ActivityName = activityName

response, err := ClientV2.GetCredential(orgName, credentialID, auxParams)
if err != nil {
if clientErr, ok := err.(*clienterror.APIError); ok {
switch clientErr.HTTPStatusCode {
case http.StatusForbidden:
return nil, microerror.Mask(accessForbiddenError)
case http.StatusUnauthorized:
return nil, microerror.Mask(notAuthorizedError)
case http.StatusNotFound:
return nil, microerror.Mask(credentialNotFoundError)
case http.StatusInternalServerError:
return nil, microerror.Mask(internalServerError)
}
}

return nil, microerror.Mask(err)
}

return response.Payload, nil
}

// sumWorkerCPUs adds up the worker's CPU cores
func sumWorkerCPUs(workerDetails []*models.V4ClusterDetailsResponseWorkersItems) uint {
sum := uint(0)
Expand Down Expand Up @@ -151,14 +176,35 @@ func showClusterRunOutput(cmd *cobra.Command, cmdLineArgs []string) {
args := defaultShowClusterArguments()
args.clusterID = cmdLineArgs[0]

if args.verbose {
fmt.Println(color.WhiteString("Fetching details for cluster %s", args.clusterID))
}

clusterDetails, err := getClusterDetails(args.clusterID, showClusterActivityName)

var credentialDetails *models.V4GetCredentialResponse

if err == nil && clusterDetails.CredentialID != "" {
if args.verbose {
fmt.Println(color.WhiteString("Fetching details for credential %s", clusterDetails.CredentialID))
}

credentialDetails, err = getOrgCredentials(clusterDetails.Owner, clusterDetails.CredentialID, showClusterActivityName)
}

if err != nil {
handleCommonErrors(err)

var headline = ""
var subtext = ""

// TODO: handle common and specific errors
switch {
case IsClusterNotFoundError(err):
headline = "Cluster not found"
subtext = "The cluster with this ID could not be found. Please use 'gsctl list clusters' to list all available clusters."
case IsCredentialNotFoundError(err):
headline = "Credential not found"
subtext = "Credentials with the given ID could not be found."
case err.Error() == "":
return
default:
Expand Down Expand Up @@ -187,6 +233,21 @@ func showClusterRunOutput(cmd *cobra.Command, cmdLineArgs []string) {
}
output = append(output, color.YellowString("Created:")+"|"+util.ShortDate(created))
output = append(output, color.YellowString("Organization:")+"|"+clusterDetails.Owner)

if credentialDetails != nil && credentialDetails.ID != "" {
if credentialDetails.Aws != nil {
parts := strings.Split(credentialDetails.Aws.Roles.Awsoperator, ":")
if len(parts) > 3 {
output = append(output, color.YellowString("AWS account:")+"|"+parts[4])
} else {
output = append(output, color.YellowString("AWS account:")+"|n/a")
}
} else if credentialDetails.Azure != nil {
output = append(output, color.YellowString("Azure subscription:")+"|"+credentialDetails.Azure.Credential.SubscriptionID)
output = append(output, color.YellowString("Azure tenant:")+"|"+credentialDetails.Azure.Credential.TenantID)
}
}

output = append(output, color.YellowString("Kubernetes API endpoint:")+"|"+clusterDetails.APIEndpoint)

if clusterDetails.ReleaseVersion != "" {
Expand Down
111 changes: 111 additions & 0 deletions commands/show_cluster_test.go
Expand Up @@ -4,6 +4,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/giantswarm/gsctl/config"
Expand All @@ -22,6 +23,7 @@ func TestShowAWSCluster(t *testing.T) {
"create_date": "2017-11-20T12:00:00.000000Z",
"owner": "acmeorg",
"release_version": "0.3.0",
"credential_id": "",
"workers": [
{"aws": {"instance_type": "m3.large"}, "memory": {"size_gb": 5}, "storage": {"size_gb": 50}, "cpu": {"cores": 2}, "labels": {"foo": "bar"}},
{"aws": {"instance_type": "m3.large"}, "memory": {"size_gb": 5}, "storage": {"size_gb": 50}, "cpu": {"cores": 2}, "labels": {"foo": "bar"}},
Expand Down Expand Up @@ -231,3 +233,112 @@ func TestShowClusterMissingID(t *testing.T) {
}

}

// TestShowAWSBYOCCluster tests fetching cluster details for a BYOC cluster on AWS,
// which means the credential_id in cluster details is not empty
func TestShowAWSBYOCCluster(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

if r.Method == "GET" && r.URL.Path == "/v4/clusters/cluster-id/" {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"id": "cluster-id",
"create_date": "2018-10-25T18:29:34Z",
"api_endpoint": "https://api.nh9t2.g8s.fra-1.giantswarm.io",
"owner": "acmeorg",
"name": "test-cluster",
"release_version": "4.2.0",
"credential_id": "credential-id",
"workers": [
{
"cpu": {
"cores": 2
},
"memory": {
"size_gb": 7.5
},
"storage": {
"size_gb": 32
},
"aws": {
"instance_type": "m3.large"
}
},
{
"cpu": {
"cores": 2
},
"memory": {
"size_gb": 7.5
},
"storage": {
"size_gb": 32
},
"aws": {
"instance_type": "m3.large"
}
}
]
}`))
} else if r.Method == "GET" && r.URL.Path == "/v4/organizations/acmeorg/credentials/credential-id/" {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"id": "credential-id",
"provider": "aws",
"aws": {
"roles": {
"admin": "arn:aws:iam::123456789012:role/GiantSwarmAdmin",
"awsoperator": "arn:aws:iam::123456789012:role/GiantSwarmAWSOperator"
}
}
}`))
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer mockServer.Close()

// temp config
configDir, _ := ioutil.TempDir("", config.ProgramName)
config.Initialize(configDir)

testArgs := showClusterArguments{
apiEndpoint: mockServer.URL,
clusterID: "cluster-id",
scheme: "giantswarm",
authToken: "my-token",
}

cmdAPIEndpoint = mockServer.URL
initClient()

err := verifyShowClusterPreconditions(testArgs, []string{testArgs.clusterID})
if err != nil {
t.Error(err)
}

details, showErr := getClusterDetails(testArgs.clusterID, showClusterActivityName)
if showErr != nil {
t.Error(showErr)
}

if details.ID != testArgs.clusterID {
t.Errorf("Expected cluster ID '%s', got '%s'", testArgs.clusterID, details.ID)
}

credentialDetails, err := getOrgCredentials(details.Owner, details.CredentialID, showClusterActivityName)
if err != nil {
t.Error(err)
}

if credentialDetails != nil && credentialDetails.Aws != nil && credentialDetails.Aws.Roles != nil && credentialDetails.Aws.Roles.Awsoperator == "" {
t.Error("AWS operator role ARN is empty, should not be.")
}

parts := strings.Split(credentialDetails.Aws.Roles.Awsoperator, ":")
if parts[4] != "123456789012" {
t.Errorf("Did not get the expected AWS account ID, instead got %s from %s", parts[4], credentialDetails.Aws.Roles.Awsoperator)
}

}
4 changes: 3 additions & 1 deletion vendor/github.com/Jeffail/gabs/gabs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 10e07f4

Please sign in to comment.