diff --git a/.secrets.baseline b/.secrets.baseline index 2ecab4df..42b60f56 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -221,7 +221,7 @@ "hashed_secret": "4d55af37dbbb6a42088d917caa1ca25428ec42c9", "is_secret": false, "is_verified": false, - "line_number": 50, + "line_number": 51, "type": "Secret Keyword", "verified_result": null } diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index e167e158..20f9047e 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -198,6 +198,21 @@ The `galasactl` tool can generate the following errors: - GAL1200E: An attempt to delete a user numbered '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' - GAL1201E: An attempt to delete a user numbered '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: '{}' - GAL1202E: An attempt to delete a user numbered '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. +- GAL1203E: Failed to get roles. Sending the get request to the Galasa service failed. Cause is {} +- GAL1204E: Failed to get roles. Unexpected http status code {} received from the server. +- GAL1205E: Failed to get roles. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} +- GAL1206E: Failed to get roles. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' +- GAL1207E: Failed to get roles. Unexpected http status code {} received from the server. Error details from the server are: '{}' +- GAL1208E: Failed to get roles. Unexpected http status code {} received from the server. Error details from the server are not in the json format. +- GAL1209E: An attempt to get a role named '{}' failed. Unexpected http status code {} received from the server. +- GAL1210E: An attempt to get a role named '{}' failed. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} +- GAL1211E: An attempt to get a role named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' +- GAL1212E: An attempt to get a role named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: '{}' +- GAL1213E: An attempt to get a role named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. +- GAL1214E: An attempt to get a role named '{}' failed. Sending the get request to the Galasa service failed. Cause is {} +- GAL1215E: Invalid role name provided. The name provided with the --name flag cannot be empty, contain spaces or dots (.), and must only contain characters in the Latin-1 character set. +- GAL1216E: Role name {} is not known on the Galasa service. +- GAL1217E: Role output format {} is not usable when a list of roles is requested. Use the --name flag if you wish to see a role in yaml format. - GAL1225E: Failed to open file '{}' cause: {}. Check that this file exists, and that you have read permissions. - GAL1226E: Internal failure. Contents of gzip could be read, but not decoded. New gzip reader failed: file: {} error: {} - GAL1227E: Internal failure. Contents of gzip could not be decoded. {} error: {} diff --git a/docs/generated/galasactl.md b/docs/generated/galasactl.md index 9729626d..c3b73e23 100644 --- a/docs/generated/galasactl.md +++ b/docs/generated/galasactl.md @@ -21,6 +21,7 @@ A tool for controlling Galasa resources using the command-line. * [galasactl project](galasactl_project.md) - Manipulate local project source code * [galasactl properties](galasactl_properties.md) - Manages properties in an ecosystem * [galasactl resources](galasactl_resources.md) - Manages resources in an ecosystem +* [galasactl roles](galasactl_roles.md) - Manage roles stored in the Galasa service * [galasactl runs](galasactl_runs.md) - Manage test runs in the ecosystem * [galasactl secrets](galasactl_secrets.md) - Manage secrets stored in the Galasa service's credentials store * [galasactl users](galasactl_users.md) - Manages users in an ecosystem diff --git a/docs/generated/galasactl_Roles.md b/docs/generated/galasactl_Roles.md new file mode 100644 index 00000000..6d327243 --- /dev/null +++ b/docs/generated/galasactl_Roles.md @@ -0,0 +1,29 @@ +## galasactl roles + +Manage roles stored in the Galasa service + +### Synopsis + +The parent command for operations to manipulate Roles in the Galasa service + +### Options + +``` + -b, --bootstrap string Bootstrap URL. Should start with 'http://' or 'file://'. If it starts with neither, it is assumed to be a fully-qualified path. If missing, it defaults to use the 'bootstrap.properties' file in your GALASA_HOME. Example: http://example.com/bootstrap, file:///user/myuserid/.galasa/bootstrap.properties , file://C:/Users/myuserid/.galasa/bootstrap.properties + -h, --help Displays the options for the 'roles' command. + --rate-limit-retries int The maximum number of retries that should be made when requests to the Galasa Service fail due to rate limits being exceeded. Must be a whole number. Defaults to 3 retries (default 3) + --rate-limit-retry-backoff-secs float The amount of time in seconds to wait before retrying a command if it failed due to rate limits being exceeded. Defaults to 1 second. (default 1) +``` + +### Options inherited from parent commands + +``` + --galasahome string Path to a folder where Galasa will read and write files and configuration settings. The default is '${HOME}/.galasa'. This overrides the GALASA_HOME environment variable which may be set instead. + -l, --log string File to which log information will be sent. Any folder referred to must exist. An existing file will be overwritten. Specify "-" to log to stderr. Defaults to not logging. +``` + +### SEE ALSO + +* [galasactl](galasactl.md) - CLI for Galasa +* [galasactl roles get](galasactl_roles_get.md) - Get Roles used in a Galasa service + diff --git a/docs/generated/galasactl_Roles_get.md b/docs/generated/galasactl_Roles_get.md new file mode 100644 index 00000000..1ae3fb9e --- /dev/null +++ b/docs/generated/galasactl_Roles_get.md @@ -0,0 +1,34 @@ +## galasactl roles get + +Get Roles used in a Galasa service + +### Synopsis + +Get a list of Roles from a Galasa service + +``` +galasactl roles get [flags] +``` + +### Options + +``` + --format string the output format of the returned Roles. Supported formats are: 'summary', 'yaml'. (default "summary") + -h, --help Displays the options for the 'roles get' command. + --name string An optional flag that identifies the secret to be retrieved. +``` + +### Options inherited from parent commands + +``` + -b, --bootstrap string Bootstrap URL. Should start with 'http://' or 'file://'. If it starts with neither, it is assumed to be a fully-qualified path. If missing, it defaults to use the 'bootstrap.properties' file in your GALASA_HOME. Example: http://example.com/bootstrap, file:///user/myuserid/.galasa/bootstrap.properties , file://C:/Users/myuserid/.galasa/bootstrap.properties + --galasahome string Path to a folder where Galasa will read and write files and configuration settings. The default is '${HOME}/.galasa'. This overrides the GALASA_HOME environment variable which may be set instead. + -l, --log string File to which log information will be sent. Any folder referred to must exist. An existing file will be overwritten. Specify "-" to log to stderr. Defaults to not logging. + --rate-limit-retries int The maximum number of retries that should be made when requests to the Galasa Service fail due to rate limits being exceeded. Must be a whole number. Defaults to 3 retries (default 3) + --rate-limit-retry-backoff-secs float The amount of time in seconds to wait before retrying a command if it failed due to rate limits being exceeded. Defaults to 1 second. (default 1) +``` + +### SEE ALSO + +* [galasactl roles](galasactl_roles.md) - Manage roles stored in the Galasa service + diff --git a/go.mod b/go.mod index 67c0c1d6..be851525 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,8 @@ require ( github.com/kr/pretty v0.1.0 // indirect github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.4 - golang.org/x/image v0.15.0 - golang.org/x/text v0.15.0 // indirect + github.com/stretchr/testify v1.10.0 + golang.org/x/image v0.23.0 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index fc3ea515..a0945878 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -25,44 +26,74 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= -golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/pkg/cmd/commandCollection.go b/pkg/cmd/commandCollection.go index f060ef48..3f9517db 100644 --- a/pkg/cmd/commandCollection.go +++ b/pkg/cmd/commandCollection.go @@ -66,6 +66,8 @@ const ( COMMAND_NAME_USERS = "users" COMMAND_NAME_USERS_GET = "users get" COMMAND_NAME_USERS_DELETE = "users delete" + COMMAND_NAME_ROLES = "roles" + COMMAND_NAME_ROLES_GET = "roles get" ) // ----------------------------------------------------------------- @@ -158,6 +160,10 @@ func (commands *commandCollectionImpl) init(factory spi.Factory) error { err = commands.addUsersCommands(factory, rootCommand, commsFlagSet) } + if err == nil { + err = commands.addRolesCommands(factory, rootCommand, commsFlagSet) + } + if err == nil { commands.setHelpFlags() } @@ -447,6 +453,25 @@ func (commands *commandCollectionImpl) addUsersCommands(factory spi.Factory, roo return err } +func (commands *commandCollectionImpl) addRolesCommands(factory spi.Factory, rootCommand spi.GalasaCommand, commsFlagSet GalasaFlagSet) error { + + var err error + var rolesCommand spi.GalasaCommand + var rolesGetCommand spi.GalasaCommand + + rolesCommand, err = NewRolesCmd(rootCommand, commsFlagSet) + + if err == nil { + rolesGetCommand, err = NewRolesGetCommand(factory, rolesCommand, commsFlagSet) + if err == nil { + commands.commandMap[rolesCommand.Name()] = rolesCommand + commands.commandMap[rolesGetCommand.Name()] = rolesGetCommand + } + } + + return err +} + func (commands *commandCollectionImpl) setHelpFlags() { for _, command := range commands.commandMap { command.CobraCommand().Flags().BoolP("help", "h", false, "Displays the options for the '"+command.Name()+"' command.") diff --git a/pkg/cmd/roles.go b/pkg/cmd/roles.go new file mode 100644 index 00000000..232a37e1 --- /dev/null +++ b/pkg/cmd/roles.go @@ -0,0 +1,93 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "github.com/galasa-dev/cli/pkg/spi" + "github.com/spf13/cobra" +) + +type RolesCmdValues struct { + name string +} + +type RolesCommand struct { + cobraCommand *cobra.Command + values *RolesCmdValues +} + +// ------------------------------------------------------------------------------------------------ +// Constructors +// ------------------------------------------------------------------------------------------------ + +func NewRolesCmd(rootCommand spi.GalasaCommand, commsFlagSet GalasaFlagSet) (spi.GalasaCommand, error) { + cmd := new(RolesCommand) + err := cmd.init(rootCommand, commsFlagSet) + return cmd, err +} + +// ------------------------------------------------------------------------------------------------ +// Public functions +// ------------------------------------------------------------------------------------------------ + +func (cmd *RolesCommand) Name() string { + return COMMAND_NAME_ROLES +} + +func (cmd *RolesCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *RolesCommand) Values() interface{} { + return cmd.values +} + +// ------------------------------------------------------------------------------------------------ +// Private functions +// ------------------------------------------------------------------------------------------------ + +func (cmd *RolesCommand) init(rootCommand spi.GalasaCommand, commsFlagSet GalasaFlagSet) error { + + var err error + + cmd.values = &RolesCmdValues{} + cmd.cobraCommand, err = cmd.createCobraCommand(rootCommand, commsFlagSet) + + return err +} + +func (cmd *RolesCommand) createCobraCommand(rootCommand spi.GalasaCommand, commsFlagSet GalasaFlagSet) (*cobra.Command, error) { + + var err error + + RolesCobraCmd := &cobra.Command{ + Use: "roles", + Short: "Manage roles stored in the Galasa service", + Long: "The parent command for operations to manipulate Roles in the Galasa service", + } + + RolesCobraCmd.PersistentFlags().AddFlagSet(commsFlagSet.Flags()) + rootCommand.CobraCommand().AddCommand(RolesCobraCmd) + + return RolesCobraCmd, err +} + +func addRolesNameFlag(cmd *cobra.Command, isMandatory bool, RolesCmdValues *RolesCmdValues) { + + flagName := "name" + var description string + if isMandatory { + description = "A mandatory flag that identifies the secret to be created or manipulated." + } else { + description = "An optional flag that identifies the secret to be retrieved." + } + + cmd.Flags().StringVar(&RolesCmdValues.name, flagName, "", description) + + if isMandatory { + cmd.MarkFlagRequired(flagName) + } +} diff --git a/pkg/cmd/rolesGet.go b/pkg/cmd/rolesGet.go new file mode 100644 index 00000000..fc51653d --- /dev/null +++ b/pkg/cmd/rolesGet.go @@ -0,0 +1,149 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "log" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/roles" + "github.com/galasa-dev/cli/pkg/spi" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/spf13/cobra" +) + +type RolesGetCmdValues struct { + outputFormat string +} + +type RolesGetCommand struct { + values *RolesGetCmdValues + cobraCommand *cobra.Command +} + +// ------------------------------------------------------------------------------------------------ +// Constructors methods +// ------------------------------------------------------------------------------------------------ +func NewRolesGetCommand( + factory spi.Factory, + rolesGetCommand spi.GalasaCommand, + commsFlagSet GalasaFlagSet, +) (spi.GalasaCommand, error) { + + cmd := new(RolesGetCommand) + + err := cmd.init(factory, rolesGetCommand, commsFlagSet) + return cmd, err +} + +// ------------------------------------------------------------------------------------------------ +// Public methods +// ------------------------------------------------------------------------------------------------ +func (cmd *RolesGetCommand) Name() string { + return COMMAND_NAME_ROLES_GET +} + +func (cmd *RolesGetCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *RolesGetCommand) Values() interface{} { + return cmd.values +} + +// ------------------------------------------------------------------------------------------------ +// Private methods +// ------------------------------------------------------------------------------------------------ +func (cmd *RolesGetCommand) init(factory spi.Factory, RolesCommand spi.GalasaCommand, commsFlagSet GalasaFlagSet) error { + var err error + + cmd.values = &RolesGetCmdValues{} + cmd.cobraCommand, err = cmd.createCobraCmd(factory, RolesCommand, commsFlagSet.Values().(*CommsFlagSetValues)) + + return err +} + +func (cmd *RolesGetCommand) createCobraCmd( + factory spi.Factory, + RolesCommand spi.GalasaCommand, + commsFlagSetValues *CommsFlagSetValues, +) (*cobra.Command, error) { + + var err error + + RolesCommandValues := RolesCommand.Values().(*RolesCmdValues) + RolesGetCobraCmd := &cobra.Command{ + Use: "get", + Short: "Get Roles used in a Galasa service", + Long: "Get a list of Roles from a Galasa service", + Aliases: []string{COMMAND_NAME_ROLES_GET}, + RunE: func(cobraCommand *cobra.Command, args []string) error { + executionFunc := func() error { + return cmd.executeRolesGet(factory, RolesCommand.Values().(*RolesCmdValues), commsFlagSetValues) + } + return executeCommandWithRetries(factory, commsFlagSetValues, executionFunc) + }, + } + + addRolesNameFlag(RolesGetCobraCmd, false, RolesCommandValues) + + formatters := roles.GetFormatterNamesAsString() + RolesGetCobraCmd.Flags().StringVar(&cmd.values.outputFormat, "format", "summary", "the output format of the returned Roles. Supported formats are: "+formatters+".") + + RolesCommand.CobraCommand().AddCommand(RolesGetCobraCmd) + + return RolesGetCobraCmd, err +} + +func (cmd *RolesGetCommand) executeRolesGet( + factory spi.Factory, + RolesCmdValues *RolesCmdValues, + commsFlagSetValues *CommsFlagSetValues, +) error { + + var err error + // Operations on the file system will all be relative to the current folder. + fileSystem := factory.GetFileSystem() + + commsFlagSetValues.isCapturingLogs = true + + log.Println("Galasa CLI - Get Roles from the ecosystem") + + env := factory.GetEnvironment() + + var galasaHome spi.GalasaHome + galasaHome, err = utils.NewGalasaHome(fileSystem, env, commsFlagSetValues.CmdParamGalasaHomePath) + if err == nil { + + var urlService *api.RealUrlResolutionService = new(api.RealUrlResolutionService) + var bootstrapData *api.BootstrapData + bootstrapData, err = api.LoadBootstrap(galasaHome, fileSystem, env, commsFlagSetValues.bootstrap, urlService) + if err == nil { + + var console = factory.GetStdOutConsole() + + apiServerUrl := bootstrapData.ApiServerURL + log.Printf("The API server is at '%s'\n", apiServerUrl) + + authenticator := factory.GetAuthenticator( + apiServerUrl, + galasaHome, + ) + + var apiClient *galasaapi.APIClient + apiClient, err = authenticator.GetAuthenticatedAPIClient() + + byteReader := factory.GetByteReader() + + if err == nil { + err = roles.GetRoles(RolesCmdValues.name, cmd.values.outputFormat, console, apiClient, byteReader) + } + } + } + + return err +} diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index c07021cc..2e7957a3 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -16,7 +16,7 @@ import ( var ( RATE_LIMIT_STATUS_CODES_MAP = map[int]struct{}{ http.StatusServiceUnavailable: {}, - http.StatusTooManyRequests: {}, + http.StatusTooManyRequests: {}, } ) @@ -60,10 +60,10 @@ func NewMessageTypeFromError(err error) *MessageType { // It can be treated as a normal error, but also holds a message-type // So we can programmatically tell the difference between various errors as required. type GalasaError struct { - msgType *MessageType - message string + msgType *MessageType + message string httpStatus int - cause error + cause error } type GalasaCommsError interface { @@ -141,7 +141,7 @@ func (err *GalasaError) IsRetryRequired() bool { for isGalasaError && currentError.GetCause() != nil { currentError, isGalasaError = currentError.GetCause().(*GalasaError) } - + // Pull the HTTP status code out of the original error, which could be 0 if the error is client-side if isGalasaError && currentError != nil { statusCode = currentError.GetHttpStatusCode() @@ -149,7 +149,7 @@ func (err *GalasaError) IsRetryRequired() bool { _, isRetryRequired := RATE_LIMIT_STATUS_CODES_MAP[statusCode] - return isRetryRequired + return isRetryRequired } const ( @@ -381,6 +381,25 @@ var ( GALASA_ERROR_DELETE_USER_SERVER_REPORTED_ERROR = NewMessageType("GAL1201E: An attempt to delete a user numbered '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1201, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_USER_EXPLANATION_NOT_JSON = NewMessageType("GAL1202E: An attempt to delete a user numbered '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1202, STACK_TRACE_NOT_WANTED) + // When getting multiple roles... + GALASA_ERROR_GET_ROLES_REQUEST_FAILED = NewMessageType("GAL1203E: Failed to get roles. Sending the get request to the Galasa service failed. Cause is %v", 1203, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLES_NO_RESPONSE_CONTENT = NewMessageType("GAL1204E: Failed to get roles. Unexpected http status code %v received from the server.", 1204, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLES_RESPONSE_BODY_UNREADABLE = NewMessageType("GAL1205E: Failed to get roles. Unexpected http status code %v received from the server. Error details from the server could not be read. Cause: %s", 1205, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLES_UNPARSEABLE_CONTENT = NewMessageType("GAL1206E: Failed to get roles. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1206, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLES_SERVER_REPORTED_ERROR = NewMessageType("GAL1207E: Failed to get roles. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1207, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLES_EXPLANATION_NOT_JSON = NewMessageType("GAL1208E: Failed to get roles. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1208, STACK_TRACE_NOT_WANTED) + + // When getting a single named role... + GALASA_ERROR_GET_ROLE_NO_RESPONSE_CONTENT = NewMessageType("GAL1209E: An attempt to get a role named '%s' failed. Unexpected http status code %v received from the server.", 1209, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLE_RESPONSE_BODY_UNREADABLE = NewMessageType("GAL1210E: An attempt to get a role named '%s' failed. Unexpected http status code %v received from the server. Error details from the server could not be read. Cause: %s", 1210, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLE_UNPARSEABLE_CONTENT = NewMessageType("GAL1211E: An attempt to get a role named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1211, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLE_SERVER_REPORTED_ERROR = NewMessageType("GAL1212E: An attempt to get a role named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1212, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLE_EXPLANATION_NOT_JSON = NewMessageType("GAL1213E: An attempt to get a role named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1213, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_GET_ROLE_REQUEST_FAILED = NewMessageType("GAL1214E: An attempt to get a role named '%s' failed. Sending the get request to the Galasa service failed. Cause is %v", 1214, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_INVALID_ROLE_NAME = NewMessageType("GAL1215E: Invalid role name provided. The name provided with the --name flag cannot be empty, contain spaces or dots (.), and must only contain characters in the Latin-1 character set.", 1215, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_ROLE_NAME_NOT_FOUND = NewMessageType("GAL1216E: Role name %v is not known on the Galasa service.", 1216, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_ROLES_LIST_NOT_COMPATIBLE_WITH_YAML_FORMAT = NewMessageType("GAL1217E: Role output format %v is not usable when a list of roles is requested. Use the --name flag if you wish to see a role in yaml format.", 1217, STACK_TRACE_NOT_WANTED) + // Warnings... GALASA_WARNING_MAVEN_NO_GALASA_OBR_REPO = NewMessageType("GAL2000W: Warning: Maven configuration file settings.xml should contain a reference to a Galasa repository so that the galasa OBR can be resolved. The official release repository is '%s', and 'pre-release' repository is '%s'", 2000, STACK_TRACE_WANTED) diff --git a/pkg/propertiesformatter/propertyFormatter.go b/pkg/propertiesformatter/propertyFormatter.go index 61608c7a..995fdff1 100644 --- a/pkg/propertiesformatter/propertyFormatter.go +++ b/pkg/propertiesformatter/propertyFormatter.go @@ -7,7 +7,6 @@ package propertiesformatter import ( - "fmt" "strings" "github.com/galasa-dev/cli/pkg/galasaapi" @@ -47,20 +46,6 @@ type PropertyFormatter interface { GetName() string } -// ----------------------------------------------------- -// Functions for tables -func calculateMaxLengthOfEachColumn(table [][]string) []int { - columnLengths := make([]int, len(table[0])) - for _, row := range table { - for i, val := range row { - if len(val) > columnLengths[i] { - columnLengths[i] = len(val) - } - } - } - return columnLengths -} - func substituteNewLines(originalPropValue string) string { newLinesReplacedValue := strings.Replace(originalPropValue, "\n", "\\n", -1) return newLinesReplacedValue @@ -73,20 +58,3 @@ func cropExtraLongValue(originalPropValue string) string { } return croppedValue } - -func writeFormattedTableToStringBuilder(table [][]string, buff *strings.Builder, columnLengths []int) { - for _, row := range table { - for column, val := range row { - - // For every column except the last one, add spacing. - if column < len(row)-1 { - // %-*s : variable space-padding length, padding is on the right. - buff.WriteString(fmt.Sprintf("%-*s", columnLengths[column], val)) - buff.WriteString(" ") - } else { - buff.WriteString(val) - } - } - buff.WriteString("\n") - } -} diff --git a/pkg/propertiesformatter/summaryFormatter.go b/pkg/propertiesformatter/summaryFormatter.go index f33a4564..87f10684 100644 --- a/pkg/propertiesformatter/summaryFormatter.go +++ b/pkg/propertiesformatter/summaryFormatter.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" ) // ----------------------------------------------------- @@ -30,7 +31,7 @@ func (*PropertySummaryFormatter) GetName() string { } func (*PropertySummaryFormatter) FormatProperties(cpsProperties []galasaapi.GalasaProperty) (string, error) { - var result string = "" + var result string var err error buff := strings.Builder{} totalProperties := len(cpsProperties) @@ -52,8 +53,8 @@ func (*PropertySummaryFormatter) FormatProperties(cpsProperties []galasaapi.Gala table = append(table, line) } - columnLengths := calculateMaxLengthOfEachColumn(table) - writeFormattedTableToStringBuilder(table, &buff, columnLengths) + columnLengths := utils.CalculateMaxLengthOfEachColumn(table) + utils.WriteFormattedTableToStringBuilder(table, &buff, columnLengths) buff.WriteString("\n") @@ -65,7 +66,7 @@ func (*PropertySummaryFormatter) FormatProperties(cpsProperties []galasaapi.Gala } func (*PropertySummaryFormatter) FormatNamespaces(namespaces []galasaapi.Namespace) (string, error) { - var result string = "" + var result string var err error buff := strings.Builder{} totalNamespaces := len(namespaces) @@ -83,8 +84,8 @@ func (*PropertySummaryFormatter) FormatNamespaces(namespaces []galasaapi.Namespa table = append(table, line) } - columnLengths := calculateMaxLengthOfEachColumn(table) - writeFormattedTableToStringBuilder(table, &buff, columnLengths) + columnLengths := utils.CalculateMaxLengthOfEachColumn(table) + utils.WriteFormattedTableToStringBuilder(table, &buff, columnLengths) buff.WriteString("\n") diff --git a/pkg/roles/roles.go b/pkg/roles/roles.go new file mode 100644 index 00000000..e85276ac --- /dev/null +++ b/pkg/roles/roles.go @@ -0,0 +1,35 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package roles + +import ( + "strings" + + galasaErrors "github.com/galasa-dev/cli/pkg/errors" +) + +func validateRoleName(name string) (string, error) { + var err error + name = strings.TrimSpace(name) + + if name == "" || strings.ContainsAny(name, " .\n\t") || !isLatin1(name) { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_ROLE_NAME) + } + return name, err +} + +// Checks if a given string contains only characters in the Latin-1 character set (codepoints 0-255), +// returning true if so, and false otherwise +func isLatin1(str string) bool { + isValidLatin1 := true + for _, character := range str { + if character > 255 { + isValidLatin1 = false + break + } + } + return isValidLatin1 +} diff --git a/pkg/roles/rolesGet.go b/pkg/roles/rolesGet.go new file mode 100644 index 00000000..0f2d8aed --- /dev/null +++ b/pkg/roles/rolesGet.go @@ -0,0 +1,276 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package roles + +import ( + "context" + "log" + "net/http" + "sort" + "strings" + + "github.com/galasa-dev/cli/pkg/embedded" + galasaErrors "github.com/galasa-dev/cli/pkg/errors" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/rolesformatter" + "github.com/galasa-dev/cli/pkg/spi" +) + +var ( + formatters = createFormatters() +) + +func GetRoles( + roleName string, + format string, + console spi.Console, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) error { + var err error + var chosenFormatter rolesformatter.RolesFormatter + roles := make([]galasaapi.RBACRole, 0) + + chosenFormatter, err = validateFormatFlag(format, roleName) + if err == nil { + if roleName != "" { + // The user has provided a Role name, so try to get that Role + var role *galasaapi.RBACRole + role, err = getRoleByName(roleName, apiClient, byteReader) + if err == nil { + roles = append(roles, *role) + } + } else { + // Get all Roles + roles, err = getRolesFromRestApi(apiClient, byteReader) + } + + // If we were able to get the Roles, format them as requested by the user + if err == nil { + var formattedOutput string + formattedOutput, err = chosenFormatter.FormatRoles(roles) + if err == nil { + console.WriteString(formattedOutput) + } + } + } + log.Printf("GetRoles exiting. err is %v\n", err) + return err +} + +func getRoleByName( + roleName string, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) (*galasaapi.RBACRole, error) { + var err error + var role *galasaapi.RBACRole + roleName, err = validateRoleName(roleName) + if err == nil { + role, err = getRoleFromRestApiGivenName(roleName, apiClient, byteReader) + } + + return role, err +} + +func getRoleFromRestApiGivenName( + roleName string, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) (*galasaapi.RBACRole, error) { + var err error + var role *galasaapi.RBACRole + + roleId, err := roleNameToRoleId(roleName, apiClient, byteReader) + if err == nil { + role, err = getRoleFromRestApiGivenId(roleId, apiClient, byteReader) + } + return role, err +} + +func getRoleFromRestApiGivenId( + roleId string, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) (*galasaapi.RBACRole, error) { + var err error + var httpResponse *http.Response + var context context.Context = context.Background() + var restApiVersion string + var role *galasaapi.RBACRole + + restApiVersion, err = embedded.GetGalasactlRestApiVersion() + if err == nil { + role, httpResponse, err = apiClient.RoleBasedAccessControlAPIApi.GetRBACRole(context, roleId). + ClientApiVersion(restApiVersion). + Execute() + + if httpResponse != nil { + defer httpResponse.Body.Close() + } + + if err != nil { + if httpResponse == nil { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_GET_ROLE_REQUEST_FAILED, err.Error()) + } else { + err = galasaErrors.HttpResponseToGalasaError( + httpResponse, + roleId, + byteReader, + galasaErrors.GALASA_ERROR_GET_ROLE_NO_RESPONSE_CONTENT, + galasaErrors.GALASA_ERROR_GET_ROLE_RESPONSE_BODY_UNREADABLE, + galasaErrors.GALASA_ERROR_GET_ROLE_UNPARSEABLE_CONTENT, + galasaErrors.GALASA_ERROR_GET_ROLE_SERVER_REPORTED_ERROR, + galasaErrors.GALASA_ERROR_GET_ROLE_EXPLANATION_NOT_JSON, + ) + } + } + } + return role, err +} + +func roleNameToRoleId(roleName string, apiClient *galasaapi.APIClient, byteReader spi.ByteReader) (string, error) { + + var roleId string + + roles, err := getRolesFromRestApi(apiClient, byteReader) + if err == nil { + role, err := findRoleWithName(roles, roleName) + if err == nil { + roleId = *(role.Metadata).Id + } + } + return roleId, err +} + +func findRoleWithName(roles []galasaapi.RBACRole, roleNameToFind string) (*galasaapi.RBACRole, error) { + var roleFound *galasaapi.RBACRole + var err error + + for index, role := range roles { + if *role.Metadata.Name == roleNameToFind { + roleFound = &roles[index] + } + } + + if roleFound == nil { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_ROLE_NAME_NOT_FOUND, roleNameToFind) + } + + return roleFound, err +} + +func getRolesFromRestApi( + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) ([]galasaapi.RBACRole, error) { + var err error + var httpResponse *http.Response + var context context.Context = context.Background() + var restApiVersion string + var roles []galasaapi.RBACRole + var rolesMetadata []galasaapi.RBACRoleMetadata + + restApiVersion, err = embedded.GetGalasactlRestApiVersion() + + if err == nil { + rolesMetadata, httpResponse, err = apiClient.RoleBasedAccessControlAPIApi.GetRBACRoles(context). + ClientApiVersion(restApiVersion). + Execute() + + if httpResponse != nil { + defer httpResponse.Body.Close() + } + + if err != nil { + if httpResponse == nil { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_GET_ROLES_REQUEST_FAILED, err.Error()) + } else { + err = galasaErrors.HttpResponseToGalasaError( + httpResponse, + "", + byteReader, + galasaErrors.GALASA_ERROR_GET_ROLES_NO_RESPONSE_CONTENT, + galasaErrors.GALASA_ERROR_GET_ROLES_RESPONSE_BODY_UNREADABLE, + galasaErrors.GALASA_ERROR_GET_ROLES_UNPARSEABLE_CONTENT, + galasaErrors.GALASA_ERROR_GET_ROLES_SERVER_REPORTED_ERROR, + galasaErrors.GALASA_ERROR_GET_ROLES_EXPLANATION_NOT_JSON, + ) + } + } else { + // The role metadata for all the roles was obtained. + // Turn these into simulated full role records so they can all be formatted with the same code. + // But we must beware, because lots of the data is missing. + // (So we can't output in yaml for example) + roles = createSimulatedRoleRecordsFromRolesMetadata(rolesMetadata) + } + } + return roles, err +} + +func createSimulatedRoleRecordsFromRolesMetadata(rolesMetadata []galasaapi.RBACRoleMetadata) []galasaapi.RBACRole { + var roles []galasaapi.RBACRole = make([]galasaapi.RBACRole, 0) + // Note: We use the index here and de-reference it ourselves on purpose. + // This is subtle: + // If we used the value, then taking the address of that value will result in filling the roles array returned + // with records which all point to the same variable defined in this scope. The variable is defined only once, + // so it essentially creates an array of values where each one is referencing the same metadata instance. + // If we didn't do that we would have to create our own metadata structure, and copy fields from one to the other. + for index := range rolesMetadata { + role := galasaapi.RBACRole{} + role.Metadata = &(rolesMetadata[index]) + roles = append(roles, role) + + log.Printf("roles: %v\n", roles) + } + return roles +} + +func createFormatters() map[string]rolesformatter.RolesFormatter { + formatters := make(map[string]rolesformatter.RolesFormatter, 0) + summaryFormatter := rolesformatter.NewRolesSummaryFormatter() + yamlFormatter := rolesformatter.NewRolesYamlFormatter() + + formatters[summaryFormatter.GetName()] = summaryFormatter + formatters[yamlFormatter.GetName()] = yamlFormatter + + return formatters +} + +func GetFormatterNamesAsString() string { + names := make([]string, 0, len(formatters)) + for name := range formatters { + names = append(names, name) + } + sort.Strings(names) + formatterNames := strings.Builder{} + + for index, formatterName := range names { + + if index != 0 { + formatterNames.WriteString(", ") + } + formatterNames.WriteString("'" + formatterName + "'") + } + + return formatterNames.String() +} + +func validateFormatFlag(outputFormatString string, roleName string) (rolesformatter.RolesFormatter, error) { + var err error + + chosenFormatter, isPresent := formatters[outputFormatString] + + if !isPresent { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_OUTPUT_FORMAT, outputFormatString, GetFormatterNamesAsString()) + } + + if roleName == "" && chosenFormatter.GetName() == rolesformatter.YAML_FORMATTER_NAME { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_ROLES_LIST_NOT_COMPATIBLE_WITH_YAML_FORMAT, outputFormatString) + } + + return chosenFormatter, err +} diff --git a/pkg/roles/rolesGet_test.go b/pkg/roles/rolesGet_test.go new file mode 100644 index 00000000..418edab9 --- /dev/null +++ b/pkg/roles/rolesGet_test.go @@ -0,0 +1,638 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package roles + +const ( + API_VERSION = "galasa-dev/v1alpha1" + DUMMY_ENCODING = "myencoding" + DUMMY_USERNAME = "dummy-username" + DUMMY_PASSWORD = "dummy-password" +) + +/* +func createMockGalasaSecret(secretName string, description string) galasaapi.GalasaSecret { + secret := *galasaapi.NewGalasaSecret() + + secret.SetApiVersion(API_VERSION) + secret.SetKind("GalasaSecret") + + secretMetadata := *galasaapi.NewGalasaSecretMetadata() + secretMetadata.SetName(secretName) + secretMetadata.SetEncoding(DUMMY_ENCODING) + secretMetadata.SetType("UsernamePassword") + secretMetadata.SetLastUpdatedBy(DUMMY_USERNAME) + secretMetadata.SetLastUpdatedTime(time.Date(2024, 01, 01, 10, 0, 0, 0, time.UTC)) + + if description != "" { + secretMetadata.SetDescription(description) + } + + secretData := *galasaapi.NewGalasaSecretData() + secretData.SetUsername(DUMMY_USERNAME) + secretData.SetPassword(DUMMY_PASSWORD) + + secret.SetMetadata(secretMetadata) + secret.SetData(secretData) + return secret +} + +func generateExpectedSecretYaml(secretName string, description string) string { + return fmt.Sprintf(`apiVersion: %s +kind: GalasaSecret +metadata: + name: %s + description: %s + lastUpdatedTime: 2024-01-01T10:00:00Z + lastUpdatedBy: %s + encoding: %s + type: UsernamePassword +data: + username: %s + password: %s`, API_VERSION, secretName, description, DUMMY_USERNAME, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) +} + +func TestCanGetASecretByName(t *testing.T) { + // Given... + secretName := "SYSTEM1" + description := "my SYSTEM1 secret" + outputFormat := "summary" + + // Create the mock secret to return + secret := createMockGalasaSecret(secretName, description) + secretBytes, _ := json.Marshal(secret) + secretJson := string(secretBytes) + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets/"+secretName, http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + writer.Write([]byte(secretJson)) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + expectedOutput := + `name description +SYSTEM1 UsernamePassword 2024-01-01 10:00:00 dummy-username my SYSTEM1 secret + +Total:1 +` + assert.Nil(t, err, "GetSecrets returned an unexpected error") + assert.Equal(t, expectedOutput, console.ReadText()) +} + +func TestCanGetASecretByNameInYamlFormat(t *testing.T) { + // Given... + secretName := "SYSTEM1" + description := "my SYSTEM1 secret" + outputFormat := "yaml" + + // Create the mock secret to return + secret := createMockGalasaSecret(secretName, description) + secretBytes, _ := json.Marshal(secret) + secretJson := string(secretBytes) + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets/"+secretName, http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + writer.Write([]byte(secretJson)) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + expectedOutput := generateExpectedSecretYaml(secretName, description) + "\n" + assert.Nil(t, err, "GetSecrets returned an unexpected error") + assert.Equal(t, expectedOutput, console.ReadText()) +} + +func TestCanGetAllSecretsOk(t *testing.T) { + // Given... + // Don't provide a secret name so that we can get all secrets + secretName := "" + outputFormat := "summary" + + // Create the mock secret to return + secrets := make([]galasaapi.GalasaSecret, 0) + secret1Name := "BOB" + secret2Name := "BLAH" + description1 := "my BOB secret" + description2 := "my BLAH secret" + secret1 := createMockGalasaSecret(secret1Name, description1) + secret2 := createMockGalasaSecret(secret2Name, description2) + + secrets = append(secrets, secret1, secret2) + secretsBytes, _ := json.Marshal(secrets) + secretsJson := string(secretsBytes) + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets", http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + writer.Write([]byte(secretsJson)) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + expectedOutput := + `name type last-updated(UTC) last-updated-by description +BOB UsernamePassword 2024-01-01 10:00:00 dummy-username my BOB secret +BLAH UsernamePassword 2024-01-01 10:00:00 dummy-username my BLAH secret + +Total:2 +` + assert.Nil(t, err, "GetSecrets returned an unexpected error") + assert.Equal(t, expectedOutput, console.ReadText()) +} + +func TestGetASecretWithUnknownFormatDisplaysError(t *testing.T) { + // Given... + secretName := "MYSECRET" + outputFormat := "UNKNOWN FORMAT!" + + // The client-side validation should fail, so no HTTP interactions will be performed + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "GetSecrets did not return an error as expected") + consoleOutputText := err.Error() + assert.Contains(t, consoleOutputText, "GAL1067E") + assert.Contains(t, consoleOutputText, "Unsupported value 'UNKNOWN FORMAT!'") + assert.Contains(t, consoleOutputText, "'summary', 'yaml'") +} + +func TestGetASecretWithBlankNameDisplaysError(t *testing.T) { + // Given... + secretName := " " + outputFormat := "summary" + + // The client-side validation should fail, so no HTTP interactions will be performed + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "GetSecrets did not return an error as expected") + consoleOutputText := err.Error() + assert.Contains(t, consoleOutputText, "GAL1172E") + assert.Contains(t, consoleOutputText, " Invalid secret name provided") +} + +func TestGetNonExistantSecretDisplaysError(t *testing.T) { + // Given... + nonExistantSecret := "secretDoesNotExist123" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets/"+nonExistantSecret, http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusNotFound) + writer.Write([]byte(`{ "error_message": "No such secret exists" }`)) + } + + interactions := []utils.HttpInteraction{getSecretInteraction} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + nonExistantSecret, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet did not return an error but it should have") + consoleOutputText := err.Error() + assert.Contains(t, consoleOutputText, nonExistantSecret) + assert.Contains(t, consoleOutputText, "GAL1177E") + assert.Contains(t, consoleOutputText, "Error details from the server are: 'No such secret exists'") +} + +func TestSecretsGetFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets/"+secretName, http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet did not return an error but it should have") + errorMsg := err.Error() + assert.Contains(t, errorMsg, secretName) + assert.Contains(t, errorMsg, "GAL1174E") +} + +func TestSecretsGetFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets/"+secretName, http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + writer.Header().Set("Content-Type", "application/notJsonOnPurpose") + writer.Write([]byte("something not json but non-zero-length.")) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet did not return an error but it should have") + errorMsg := err.Error() + assert.Contains(t, errorMsg, secretName) + assert.Contains(t, errorMsg, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, errorMsg, "GAL1178E") + assert.Contains(t, errorMsg, "Error details from the server are not in the json format") +} + +func TestSecretsGetFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets/"+secretName, http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{ "this": "isBadJson because it doesnt end in a close braces" `)) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet did not return an error but it should have") + errorMsg := err.Error() + assert.Contains(t, errorMsg, secretName) + assert.Contains(t, errorMsg, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, errorMsg, "GAL1176E") + assert.Contains(t, errorMsg, "Error details from the server are not in a valid json format") + assert.Contains(t, errorMsg, "Cause: 'unexpected end of JSON input'") +} + +func TestSecretsGetFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets/"+secretName, http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{}`)) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReaderAsMock(true) + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet returned an unexpected error") + errorMsg := err.Error() + assert.Contains(t, errorMsg, secretName) + assert.Contains(t, errorMsg, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, errorMsg, "GAL1175E") + assert.Contains(t, errorMsg, "Error details from the server could not be read") +} + +func TestGetAllSecretsFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets", http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet did not return an error but it should have") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1180E") +} + +func TestGetAllSecretsFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets", http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + writer.Header().Set("Content-Type", "application/notJsonOnPurpose") + writer.Write([]byte("something not json but non-zero-length.")) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet did not return an error but it should have") + errorMsg := err.Error() + assert.Contains(t, errorMsg, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, errorMsg, "GAL1184E") + assert.Contains(t, errorMsg, "Error details from the server are not in the json format") +} + +func TestGetAllSecretsFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets", http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{ "this": "isBadJson because it doesnt end in a close braces" `)) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet did not return an error but it should have") + errorMsg := err.Error() + assert.Contains(t, errorMsg, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, errorMsg, "GAL1182E") + assert.Contains(t, errorMsg, "Error details from the server are not in a valid json format") + assert.Contains(t, errorMsg, "Cause: 'unexpected end of JSON input'") +} + +func TestGetAllSecretsFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "" + outputFormat := "summary" + + // Create the expected HTTP interactions with the API server + getSecretInteraction := utils.NewHttpInteraction("/secrets", http.MethodGet) + getSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{}`)) + } + + interactions := []utils.HttpInteraction{ + getSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReaderAsMock(true) + + // When... + err := GetRoles( + secretName, + outputFormat, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SecretsGet returned an unexpected error") + errorMsg := err.Error() + assert.Contains(t, errorMsg, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, errorMsg, "GAL1181E") + assert.Contains(t, errorMsg, "Error details from the server could not be read") +} +*/ diff --git a/pkg/rolesformatter/rolesFormatter.go b/pkg/rolesformatter/rolesFormatter.go new file mode 100644 index 00000000..5f47bb85 --- /dev/null +++ b/pkg/rolesformatter/rolesFormatter.go @@ -0,0 +1,30 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package rolesformatter + +import ( + "github.com/galasa-dev/cli/pkg/galasaapi" +) + +// Displays roles in the following format: +// name description +// admin Someone with super-user access +// tester Able to write and launch tests +// Total:2 + +// ----------------------------------------------------- +// RoleFormatter - implementations can take a collection of roles +// and turn them into a string for display to the user. +const ( + HEADER_ROLE_NAME = "name" + HEADER_ROLE_DESCRIPTION = "description" +) + +type RolesFormatter interface { + FormatRoles(roles []galasaapi.RBACRole) (string, error) + GetName() string +} diff --git a/pkg/rolesformatter/summaryFormatter.go b/pkg/rolesformatter/summaryFormatter.go new file mode 100644 index 00000000..869df113 --- /dev/null +++ b/pkg/rolesformatter/summaryFormatter.go @@ -0,0 +1,68 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package rolesformatter + +import ( + "log" + "strconv" + "strings" + + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" +) + +// ----------------------------------------------------- +// Summary format. +const ( + SUMMARY_FORMATTER_NAME = "summary" +) + +type RolesSummaryFormatter struct { +} + +func NewRolesSummaryFormatter() RolesFormatter { + return new(RolesSummaryFormatter) +} + +func (*RolesSummaryFormatter) GetName() string { + return SUMMARY_FORMATTER_NAME +} + +func (*RolesSummaryFormatter) FormatRoles(roles []galasaapi.RBACRole) (string, error) { + var result string + var err error = nil + buff := strings.Builder{} + total := len(roles) + + if total > 0 { + var table [][]string + + var headers = []string{ + HEADER_ROLE_NAME, + HEADER_ROLE_DESCRIPTION, + } + + table = append(table, headers) + for _, role := range roles { + var line []string + name := role.Metadata.GetName() + description := role.Metadata.GetDescription() + log.Printf("FormatRoles: adding role name %s\n", name) + line = append(line, name, description) + table = append(table, line) + } + + columnLengths := utils.CalculateMaxLengthOfEachColumn(table) + utils.WriteFormattedTableToStringBuilder(table, &buff, columnLengths) + + buff.WriteString("\n") + + } + buff.WriteString("Total:" + strconv.Itoa(total) + "\n") + + result = buff.String() + return result, err +} diff --git a/pkg/rolesformatter/summaryFormatter_test.go b/pkg/rolesformatter/summaryFormatter_test.go new file mode 100644 index 00000000..cf1fb443 --- /dev/null +++ b/pkg/rolesformatter/summaryFormatter_test.go @@ -0,0 +1,122 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package rolesformatter + +/* +import ( + "testing" + "time" + + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/stretchr/testify/assert" +) + +const ( + API_VERSION = "galasa-dev/v1alpha1" + DUMMY_ENCODING = "myencoding" + DUMMY_USERNAME = "dummy-username" + DUMMY_PASSWORD = "dummy-password" +) + +func createMockGalasaSecretWithDescription( + secretName string, + description string, +) galasaapi.GalasaSecret { + secret := *galasaapi.NewGalasaSecret() + + secret.SetApiVersion(API_VERSION) + secret.SetKind("GalasaSecret") + + secretMetadata := *galasaapi.NewGalasaSecretMetadata() + secretMetadata.SetName(secretName) + secretMetadata.SetEncoding(DUMMY_ENCODING) + secretMetadata.SetType("UsernamePassword") + secretMetadata.SetLastUpdatedBy(DUMMY_USERNAME) + secretMetadata.SetLastUpdatedTime(time.Date(2024, 01, 01, 10, 0, 0, 0, time.UTC)) + + if description != "" { + secretMetadata.SetDescription(description) + } + + secretData := *galasaapi.NewGalasaSecretData() + secretData.SetUsername(DUMMY_USERNAME) + secretData.SetPassword(DUMMY_PASSWORD) + + secret.SetMetadata(secretMetadata) + secret.SetData(secretData) + return secret +} + +func TestSecretSummaryFormatterNoDataReturnsTotalCountAllZeros(t *testing.T) { + // Given... + formatter := NewRolesSummaryFormatter() + roles := make([]galasaapi.RBACRole, 0) + + // When... + actualFormattedOutput, err := formatter.FormatRoles(roles) + + // Then... + assert.Nil(t, err) + expectedFormattedOutput := "Total:0\n" + assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) +} + +func TestSecretSummaryFormatterSingleDataReturnsCorrectly(t *testing.T) { + // Given... + formatter := NewSecretSummaryFormatter() + description := "secret for system1" + secretName := "MYSECRET" + secret1 := createMockGalasaSecretWithDescription(secretName, description) + secrets := []galasaapi.GalasaSecret{secret1} + + // When... + actualFormattedOutput, err := formatter.FormatSecrets(secrets) + + // Then... + assert.Nil(t, err) + expectedFormattedOutput := + `name type last-updated(UTC) last-updated-by description +MYSECRET UsernamePassword 2024-01-01 10:00:00 dummy-username secret for system1 + +Total:1 +` + assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) +} + +func TestSecretSummaryFormatterMultipleDataSeperatesWithNewLine(t *testing.T) { + // Given.. + formatter := NewSecretSummaryFormatter() + secrets := make([]galasaapi.GalasaSecret, 0) + + secret1Name := "SECRET1" + secret1Description := "my first secret" + secret2Name := "SECRET2" + secret2Description := "my second secret" + secret3Name := "SECRET3" + secret3Description := "my third secret" + + secret1 := createMockGalasaSecretWithDescription(secret1Name, secret1Description) + secret2 := createMockGalasaSecretWithDescription(secret2Name, secret2Description) + secret3 := createMockGalasaSecretWithDescription(secret3Name, secret3Description) + secrets = append(secrets, secret1, secret2, secret3) + + // When... + actualFormattedOutput, err := formatter.FormatSecrets(secrets) + + // Then... + assert.Nil(t, err) + expectedFormattedOutput := + `name type last-updated(UTC) last-updated-by description +SECRET1 UsernamePassword 2024-01-01 10:00:00 dummy-username my first secret +SECRET2 UsernamePassword 2024-01-01 10:00:00 dummy-username my second secret +SECRET3 UsernamePassword 2024-01-01 10:00:00 dummy-username my third secret + +Total:3 +` + assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) +} + +*/ diff --git a/pkg/rolesformatter/yamlFormatter.go b/pkg/rolesformatter/yamlFormatter.go new file mode 100644 index 00000000..fbc2edd2 --- /dev/null +++ b/pkg/rolesformatter/yamlFormatter.go @@ -0,0 +1,53 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package rolesformatter + +import ( + "strings" + + "github.com/galasa-dev/cli/pkg/galasaapi" + "gopkg.in/yaml.v3" +) + +const ( + YAML_FORMATTER_NAME = "yaml" +) + +type RolesYamlFormatter struct { +} + +func NewRolesYamlFormatter() RolesFormatter { + return new(RolesYamlFormatter) +} + +func (*RolesYamlFormatter) GetName() string { + return YAML_FORMATTER_NAME +} + +func (*RolesYamlFormatter) FormatRoles(roles []galasaapi.RBACRole) (string, error) { + var err error + buff := strings.Builder{} + + for index, role := range roles { + content := "" + + if index > 0 { + content += "---\n" + } + + var yamlRepresentationBytes []byte + yamlRepresentationBytes, err = yaml.Marshal(role) + if err == nil { + yamlStr := string(yamlRepresentationBytes) + content += yamlStr + } + + buff.WriteString(content) + } + + result := buff.String() + return result, err +} diff --git a/pkg/rolesformatter/yamlFormatter_test.go b/pkg/rolesformatter/yamlFormatter_test.go new file mode 100644 index 00000000..ae070af1 --- /dev/null +++ b/pkg/rolesformatter/yamlFormatter_test.go @@ -0,0 +1,92 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package rolesformatter + +/* +import ( + "fmt" + "testing" + + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/stretchr/testify/assert" +) + +func createMockGalasaSecret(secretName string) galasaapi.GalasaSecret { + return createMockGalasaSecretWithDescription(secretName, "") +} + +func generateExpectedSecretYaml(secretName string) string { + return fmt.Sprintf( + `apiVersion: %s +kind: GalasaSecret +metadata: + name: %s + lastUpdatedTime: 2024-01-01T10:00:00Z + lastUpdatedBy: %s + encoding: %s + type: UsernamePassword +data: + username: %s + password: %s`, API_VERSION, secretName, DUMMY_USERNAME, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) +} + +func TestSecretsYamlFormatterNoDataReturnsBlankString(t *testing.T) { + // Given... + formatter := NewSecretYamlFormatter() + formattableSecret := make([]galasaapi.GalasaSecret, 0) + + // When... + actualFormattedOutput, err := formatter.FormatSecrets(formattableSecret) + + // Then... + assert.Nil(t, err) + expectedFormattedOutput := "" + assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) +} + +func TestSecretsYamlFormatterSingleDataReturnsCorrectly(t *testing.T) { + // Given.. + formatter := NewSecretYamlFormatter() + formattableSecrets := make([]galasaapi.GalasaSecret, 0) + secretName := "SECRET1" + secret1 := createMockGalasaSecret(secretName) + formattableSecrets = append(formattableSecrets, secret1) + + // When... + actualFormattedOutput, err := formatter.FormatSecrets(formattableSecrets) + + // Then... + assert.Nil(t, err) + expectedFormattedOutput := generateExpectedSecretYaml(secretName) + "\n" + assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) +} + +func TestSecretsYamlFormatterMultipleDataSeperatesWithNewLine(t *testing.T) { + // For.. + formatter := NewSecretYamlFormatter() + formattableSecrets := make([]galasaapi.GalasaSecret, 0) + + secret1Name := "MYSECRET" + secret2Name := "MY-NEXT-SECRET" + secret1 := createMockGalasaSecret(secret1Name) + secret2 := createMockGalasaSecret(secret2Name) + formattableSecrets = append(formattableSecrets, secret1, secret2) + + // When... + actualFormattedOutput, err := formatter.FormatSecrets(formattableSecrets) + + // Then... + assert.Nil(t, err) + expectedSecret1Output := generateExpectedSecretYaml(secret1Name) + expectedSecret2Output := generateExpectedSecretYaml(secret2Name) + expectedFormattedOutput := fmt.Sprintf(`%s +--- +%s +`, expectedSecret1Output, expectedSecret2Output) + assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) +} + +*/ diff --git a/pkg/runsformatter/detailsFormatter.go b/pkg/runsformatter/detailsFormatter.go index d4187e54..b1ad940c 100644 --- a/pkg/runsformatter/detailsFormatter.go +++ b/pkg/runsformatter/detailsFormatter.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" ) // ----------------------------------------------------- @@ -38,7 +39,7 @@ func (*DetailsFormatter) IsNeedingMethodDetails() bool { } func (*DetailsFormatter) FormatRuns(runs []FormattableTest) (string, error) { - var result string = "" + var result string var err error totalResults := len(runs) @@ -54,15 +55,15 @@ func (*DetailsFormatter) FormatRuns(runs []FormattableTest) (string, error) { } else { accumulateResults(resultCountsMap, run) coreDetailsTable := tabulateCoreRunDetails(run) - coreDetailsColumnLengths := calculateMaxLengthOfEachColumn(coreDetailsTable) - writeFormattedTableToStringBuilder(coreDetailsTable, &buff, coreDetailsColumnLengths) + coreDetailsColumnLengths := utils.CalculateMaxLengthOfEachColumn(coreDetailsTable) + utils.WriteFormattedTableToStringBuilder(coreDetailsTable, &buff, coreDetailsColumnLengths) buff.WriteString("\n") methodTable := initialiseMethodTable() methodTable = tabulateRunMethodsToTable(run.Methods, methodTable) - methodColumnLengths := calculateMaxLengthOfEachColumn(methodTable) - writeFormattedTableToStringBuilder(methodTable, &buff, methodColumnLengths) + methodColumnLengths := utils.CalculateMaxLengthOfEachColumn(methodTable) + utils.WriteFormattedTableToStringBuilder(methodTable, &buff, methodColumnLengths) if i < len(runs)-1 { buff.WriteString("\n---\n\n") diff --git a/pkg/runsformatter/runsFormatter.go b/pkg/runsformatter/runsFormatter.go index 4e6cbf9f..77a8fe1e 100644 --- a/pkg/runsformatter/runsFormatter.go +++ b/pkg/runsformatter/runsFormatter.go @@ -84,37 +84,6 @@ type RunsFormatter interface { IsNeedingMethodDetails() bool } -// ----------------------------------------------------- -// Functions for tables -func calculateMaxLengthOfEachColumn(table [][]string) []int { - columnLengths := make([]int, len(table[0])) - for _, row := range table { - for i, val := range row { - if len(val) > columnLengths[i] { - columnLengths[i] = len(val) - } - } - } - return columnLengths -} - -func writeFormattedTableToStringBuilder(table [][]string, buff *strings.Builder, columnLengths []int) { - for _, row := range table { - for column, val := range row { - - // For every column except the last one, add spacing. - if column < len(row)-1 { - // %-*s : variable space-padding length, padding is on the right. - buff.WriteString(fmt.Sprintf("%-*s", columnLengths[column], val)) - buff.WriteString(" ") - } else { - buff.WriteString(val) - } - } - buff.WriteString("\n") - } -} - // ----------------------------------------------------- // Functions for time formats and duration func formatTimeReadable(rawTime string) string { diff --git a/pkg/runsformatter/summaryFormatter.go b/pkg/runsformatter/summaryFormatter.go index adaeb572..86d3c706 100644 --- a/pkg/runsformatter/summaryFormatter.go +++ b/pkg/runsformatter/summaryFormatter.go @@ -8,6 +8,8 @@ package runsformatter import ( "log" "strings" + + "github.com/galasa-dev/cli/pkg/utils" ) // ----------------------------------------------------- @@ -61,8 +63,8 @@ func (*SummaryFormatter) FormatRuns(testResultsData []FormattableTest) (string, } } - columnLengths := calculateMaxLengthOfEachColumn(table) - writeFormattedTableToStringBuilder(table, &buff, columnLengths) + columnLengths := utils.CalculateMaxLengthOfEachColumn(table) + utils.WriteFormattedTableToStringBuilder(table, &buff, columnLengths) buff.WriteString("\n") } diff --git a/pkg/secretsformatter/secretsFormatter.go b/pkg/secretsformatter/secretsFormatter.go index 878600bd..27cf993b 100644 --- a/pkg/secretsformatter/secretsFormatter.go +++ b/pkg/secretsformatter/secretsFormatter.go @@ -7,10 +7,7 @@ package secretsformatter import ( - "fmt" - "strings" - - "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/galasaapi" ) // Displays secrets in the following format: @@ -24,45 +21,14 @@ import ( // SecretsFormatter - implementations can take a collection of secrets // and turn them into a string for display to the user. const ( - HEADER_SECRET_NAME = "name" - HEADER_SECRET_TYPE = "type" - HEADER_SECRET_DESCRIPTION = "description" - HEADER_LAST_UPDATED_TIME = "last-updated(UTC)" - HEADER_LAST_UPDATED_BY = "last-updated-by" + HEADER_SECRET_NAME = "name" + HEADER_SECRET_TYPE = "type" + HEADER_SECRET_DESCRIPTION = "description" + HEADER_LAST_UPDATED_TIME = "last-updated(UTC)" + HEADER_LAST_UPDATED_BY = "last-updated-by" ) type SecretsFormatter interface { - FormatSecrets(secrets []galasaapi.GalasaSecret) (string, error) - GetName() string -} - -// ----------------------------------------------------- -// Functions for tables -func calculateMaxLengthOfEachColumn(table [][]string) []int { - columnLengths := make([]int, len(table[0])) - for _, row := range table { - for i, val := range row { - if len(val) > columnLengths[i] { - columnLengths[i] = len(val) - } - } - } - return columnLengths -} - -func writeFormattedTableToStringBuilder(table [][]string, buff *strings.Builder, columnLengths []int) { - for _, row := range table { - for column, val := range row { - - // For every column except the last one, add spacing. - if column < len(row)-1 { - // %-*s : variable space-padding length, padding is on the right. - buff.WriteString(fmt.Sprintf("%-*s", columnLengths[column], val)) - buff.WriteString(" ") - } else { - buff.WriteString(val) - } - } - buff.WriteString("\n") - } + FormatSecrets(secrets []galasaapi.GalasaSecret) (string, error) + GetName() string } diff --git a/pkg/secretsformatter/summaryFormatter.go b/pkg/secretsformatter/summaryFormatter.go index 23437731..f415b1aa 100644 --- a/pkg/secretsformatter/summaryFormatter.go +++ b/pkg/secretsformatter/summaryFormatter.go @@ -6,72 +6,73 @@ package secretsformatter import ( - "strconv" - "strings" + "strconv" + "strings" - "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" ) // ----------------------------------------------------- // Summary format. const ( - SUMMARY_FORMATTER_NAME = "summary" + SUMMARY_FORMATTER_NAME = "summary" ) type SecretSummaryFormatter struct { } func NewSecretSummaryFormatter() SecretsFormatter { - return new(SecretSummaryFormatter) + return new(SecretSummaryFormatter) } func (*SecretSummaryFormatter) GetName() string { - return SUMMARY_FORMATTER_NAME + return SUMMARY_FORMATTER_NAME } func (*SecretSummaryFormatter) FormatSecrets(secrets []galasaapi.GalasaSecret) (string, error) { - var result string = "" - var err error = nil - buff := strings.Builder{} - totalSecrets := len(secrets) - - if totalSecrets > 0 { - var table [][]string - - var headers = []string{ - HEADER_SECRET_NAME, - HEADER_SECRET_TYPE, - HEADER_LAST_UPDATED_TIME, - HEADER_LAST_UPDATED_BY, - HEADER_SECRET_DESCRIPTION, - } - - table = append(table, headers) - for _, secret := range secrets { - var line []string - name := secret.Metadata.GetName() - secretType := secret.Metadata.GetType() - secretDescription := secret.Metadata.GetDescription() - lastUpdatedTime := secret.Metadata.GetLastUpdatedTime() - - lastUpdatedTimeReadable := "" - if !lastUpdatedTime.IsZero() { - lastUpdatedTimeReadable = lastUpdatedTime.Format("2006-01-02 15:04:05") - } - lastUpdatedBy := secret.Metadata.GetLastUpdatedBy() - - line = append(line, name, string(secretType), lastUpdatedTimeReadable, lastUpdatedBy, secretDescription) - table = append(table, line) - } - - columnLengths := calculateMaxLengthOfEachColumn(table) - writeFormattedTableToStringBuilder(table, &buff, columnLengths) - - buff.WriteString("\n") - - } - buff.WriteString("Total:" + strconv.Itoa(totalSecrets) + "\n") - - result = buff.String() - return result, err + var result string = "" + var err error = nil + buff := strings.Builder{} + totalSecrets := len(secrets) + + if totalSecrets > 0 { + var table [][]string + + var headers = []string{ + HEADER_SECRET_NAME, + HEADER_SECRET_TYPE, + HEADER_LAST_UPDATED_TIME, + HEADER_LAST_UPDATED_BY, + HEADER_SECRET_DESCRIPTION, + } + + table = append(table, headers) + for _, secret := range secrets { + var line []string + name := secret.Metadata.GetName() + secretType := secret.Metadata.GetType() + secretDescription := secret.Metadata.GetDescription() + lastUpdatedTime := secret.Metadata.GetLastUpdatedTime() + + lastUpdatedTimeReadable := "" + if !lastUpdatedTime.IsZero() { + lastUpdatedTimeReadable = lastUpdatedTime.Format("2006-01-02 15:04:05") + } + lastUpdatedBy := secret.Metadata.GetLastUpdatedBy() + + line = append(line, name, string(secretType), lastUpdatedTimeReadable, lastUpdatedBy, secretDescription) + table = append(table, line) + } + + columnLengths := utils.CalculateMaxLengthOfEachColumn(table) + utils.WriteFormattedTableToStringBuilder(table, &buff, columnLengths) + + buff.WriteString("\n") + + } + buff.WriteString("Total:" + strconv.Itoa(totalSecrets) + "\n") + + result = buff.String() + return result, err } diff --git a/pkg/tokensformatter/summaryFormatter.go b/pkg/tokensformatter/summaryFormatter.go index ee5a995d..852b393c 100644 --- a/pkg/tokensformatter/summaryFormatter.go +++ b/pkg/tokensformatter/summaryFormatter.go @@ -54,8 +54,8 @@ func (*TokenSummaryFormatter) FormatTokens(authTokens []galasaapi.AuthToken) (st table = append(table, line) } - columnLengths := calculateMaxLengthOfEachColumn(table) - writeFormattedTableToStringBuilder(table, &buff, columnLengths) + columnLengths := utils.CalculateMaxLengthOfEachColumn(table) + utils.WriteFormattedTableToStringBuilder(table, &buff, columnLengths) buff.WriteString("\n") diff --git a/pkg/tokensformatter/tokensFormatter.go b/pkg/tokensformatter/tokensFormatter.go index ccc1f714..9a88a162 100644 --- a/pkg/tokensformatter/tokensFormatter.go +++ b/pkg/tokensformatter/tokensFormatter.go @@ -7,9 +7,6 @@ package tokensformatter import ( - "fmt" - "strings" - "github.com/galasa-dev/cli/pkg/galasaapi" ) @@ -34,34 +31,3 @@ type TokenFormatter interface { FormatTokens(tokenResults []galasaapi.AuthToken) (string, error) GetName() string } - -// ----------------------------------------------------- -// Functions for tables -func calculateMaxLengthOfEachColumn(table [][]string) []int { - columnLengths := make([]int, len(table[0])) - for _, row := range table { - for i, val := range row { - if len(val) > columnLengths[i] { - columnLengths[i] = len(val) - } - } - } - return columnLengths -} - -func writeFormattedTableToStringBuilder(table [][]string, buff *strings.Builder, columnLengths []int) { - for _, row := range table { - for column, val := range row { - - // For every column except the last one, add spacing. - if column < len(row)-1 { - // %-*s : variable space-padding length, padding is on the right. - buff.WriteString(fmt.Sprintf("%-*s", columnLengths[column], val)) - buff.WriteString(" ") - } else { - buff.WriteString(val) - } - } - buff.WriteString("\n") - } -} diff --git a/pkg/usersformatter/summaryFormatter.go b/pkg/usersformatter/summaryFormatter.go index 034c7c22..dede1e45 100644 --- a/pkg/usersformatter/summaryFormatter.go +++ b/pkg/usersformatter/summaryFormatter.go @@ -34,7 +34,7 @@ func (*UserSummaryFormatter) GetName() string { } func (*UserSummaryFormatter) FormatUsers(users []galasaapi.UserData) (string, error) { - var result string = "" + var result string var err error = nil buff := strings.Builder{} totalUsers := len(users) @@ -67,8 +67,8 @@ func (*UserSummaryFormatter) FormatUsers(users []galasaapi.UserData) (string, er table = append(table, line) } - columnLengths := calculateMaxLengthOfEachColumn(table) - writeFormattedTableToStringBuilder(table, &buff, columnLengths) + columnLengths := utils.CalculateMaxLengthOfEachColumn(table) + utils.WriteFormattedTableToStringBuilder(table, &buff, columnLengths) buff.WriteString("\n") diff --git a/pkg/usersformatter/usersformatter.go b/pkg/usersformatter/usersformatter.go index 2b39e746..f5bb68eb 100644 --- a/pkg/usersformatter/usersformatter.go +++ b/pkg/usersformatter/usersformatter.go @@ -7,9 +7,6 @@ package usersformatter import ( - "fmt" - "strings" - "github.com/galasa-dev/cli/pkg/galasaapi" ) @@ -33,34 +30,3 @@ type UserFormatter interface { FormatUsers(userResults []galasaapi.UserData) (string, error) GetName() string } - -// ----------------------------------------------------- -// Functions for tables -func calculateMaxLengthOfEachColumn(table [][]string) []int { - columnLengths := make([]int, len(table[0])) - for _, row := range table { - for i, val := range row { - if len(val) > columnLengths[i] { - columnLengths[i] = len(val) - } - } - } - return columnLengths -} - -func writeFormattedTableToStringBuilder(table [][]string, buff *strings.Builder, columnLengths []int) { - for _, row := range table { - for column, val := range row { - - // For every column except the last one, add spacing. - if column < len(row)-1 { - // %-*s : variable space-padding length, padding is on the right. - buff.WriteString(fmt.Sprintf("%-*s", columnLengths[column], val)) - buff.WriteString(" ") - } else { - buff.WriteString(val) - } - } - buff.WriteString("\n") - } -} diff --git a/pkg/utils/formatterUtils.go b/pkg/utils/formatterUtils.go new file mode 100644 index 00000000..5b367a76 --- /dev/null +++ b/pkg/utils/formatterUtils.go @@ -0,0 +1,42 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package utils + +import ( + "fmt" + "strings" +) + +// ----------------------------------------------------- +// Functions for tables +func CalculateMaxLengthOfEachColumn(table [][]string) []int { + columnLengths := make([]int, len(table[0])) + for _, row := range table { + for i, val := range row { + if len(val) > columnLengths[i] { + columnLengths[i] = len(val) + } + } + } + return columnLengths +} + +func WriteFormattedTableToStringBuilder(table [][]string, buff *strings.Builder, columnLengths []int) { + for _, row := range table { + for column, val := range row { + + // For every column except the last one, add spacing. + if column < len(row)-1 { + // %-*s : variable space-padding length, padding is on the right. + buff.WriteString(fmt.Sprintf("%-*s", columnLengths[column], val)) + buff.WriteString(" ") + } else { + buff.WriteString(val) + } + } + buff.WriteString("\n") + } +}