From e268bdf51a5444d51f2ed82160e1100bbffcec48 Mon Sep 17 00:00:00 2001 From: LUC DUZAN Date: Thu, 11 Jul 2024 14:14:14 +0200 Subject: [PATCH] improve cli api for quentin (#49) --- client/client.go | 80 +++++++++++++++------- client/client_test.go | 45 ++++++++++--- cmd/login.go | 18 +++-- cmd/root.go | 4 +- cmd/token.go | 6 +- docker/initializer.json | 142 ++++++++++++++++++++++++++++++++++++++-- test_final_exec.sh | 6 ++ 7 files changed, 254 insertions(+), 47 deletions(-) diff --git a/client/client.go b/client/client.go index 25a3b69..80be763 100644 --- a/client/client.go +++ b/client/client.go @@ -22,43 +22,74 @@ type Client struct { kinds schema.KindCatalog } -func Make(apiKey string, baseUrl string, debug bool, key, cert, cacert string, insecure bool) (*Client, error) { +type ApiParameter struct { + ApiKey string + BaseUrl string + Debug bool + Key string + Cert string + Cacert string + CdkUser string + CdkPassword string + Insecure bool +} + +func Make(apiParameter ApiParameter) (*Client, error) { //apiKey is set later because it's not mandatory for getting the openapi and parsing different kind //or to get jwt token - restyClient := resty.New().SetDebug(debug).SetHeader("X-CDK-CLIENT", "CLI/"+utils.GetConduktorVersion()) + restyClient := resty.New().SetDebug(apiParameter.Debug).SetHeader("X-CDK-CLIENT", "CLI/"+utils.GetConduktorVersion()) + + if apiParameter.BaseUrl == "" { + return nil, fmt.Errorf("Please set CDK_BASE_URL") + } - if (key == "" && cert != "") || (key != "" && cert == "") { - return nil, fmt.Errorf("key and cert must be provided together") - } else if key != "" && cert != "" { - certificate, err := tls.LoadX509KeyPair(cert, key) + if (apiParameter.Key == "" && apiParameter.Cert != "") || (apiParameter.Key != "" && apiParameter.Cert == "") { + return nil, fmt.Errorf("CDK_KEY and CDK_CERT must be provided together") + } else if apiParameter.Key != "" && apiParameter.Cert != "" { + certificate, err := tls.LoadX509KeyPair(apiParameter.Cert, apiParameter.Key) restyClient.SetCertificates(certificate) if err != nil { return nil, err } } - if cacert != "" { - restyClient.SetRootCertificate(cacert) + if (apiParameter.CdkUser != "" && apiParameter.CdkPassword == "") || (apiParameter.CdkUser == "" && apiParameter.CdkPassword != "") { + return nil, fmt.Errorf("CDK_USER and CDK_PASSWORD must be provided together") + } + if apiParameter.CdkUser != "" && apiParameter.ApiKey != "" { + return nil, fmt.Errorf("Can't set both CDK_USER and CDK_API_KEY") + } + + if apiParameter.Cacert != "" { + restyClient.SetRootCertificate(apiParameter.Cacert) } result := &Client{ - apiKey: apiKey, - baseUrl: baseUrl, + apiKey: apiParameter.ApiKey, + baseUrl: apiParameter.BaseUrl, client: restyClient, kinds: nil, } - if apiKey != "" { + if apiParameter.Insecure { + result.IgnoreUntrustedCertificate() + } + + if apiParameter.CdkUser != "" { + jwtToken, err := result.Login(apiParameter.CdkUser, apiParameter.CdkPassword) + if err != nil { + return nil, fmt.Errorf("Could not login: %s", err) + } + result.apiKey = jwtToken.AccessToken + } + + if apiParameter.ApiKey != "" { result.setApiKeyInRestClient() } else { //it will be set later only when really needed //so aim is not fail when CDK_API_KEY is not set before printing the cmd help } - if insecure { - result.IgnoreUntrustedCertificate() - } - err := result.initKindFromApi() if err != nil { fmt.Fprintf(os.Stderr, "Cannot access the Conduktor API: %s\nUsing offline defaults.\n", err) @@ -69,17 +100,18 @@ func Make(apiKey string, baseUrl string, debug bool, key, cert, cacert string, i } func MakeFromEnv() (*Client, error) { - baseUrl := os.Getenv("CDK_BASE_URL") - if baseUrl == "" { - return nil, fmt.Errorf("Please set CDK_BASE_URL") + apiParameter := ApiParameter{ + BaseUrl: os.Getenv("CDK_BASE_URL"), + Debug: strings.ToLower(os.Getenv("CDK_DEBUG")) == "true", + Cert: os.Getenv("CDK_CERT"), + Cacert: os.Getenv("CDK_CACERT"), + ApiKey: os.Getenv("CDK_API_KEY"), + CdkUser: os.Getenv("CDK_USER"), + CdkPassword: os.Getenv("CDK_PASSWORD"), + Insecure: strings.ToLower(os.Getenv("CDK_INSECURE")) == "true", } - debug := strings.ToLower(os.Getenv("CDK_DEBUG")) == "true" - key := os.Getenv("CDK_KEY") - cert := os.Getenv("CDK_CERT") - cacert := os.Getenv("CDK_CACERT") - insecure := strings.ToLower(os.Getenv("CDK_INSECURE")) == "true" - client, err := Make("", baseUrl, debug, key, cert, cacert, insecure) + client, err := Make(apiParameter) if err != nil { return nil, fmt.Errorf("Cannot create client: %s", err) } diff --git a/client/client_test.go b/client/client_test.go index aee4388..c57e7a5 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -11,7 +11,10 @@ func TestApplyShouldWork(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } @@ -54,7 +57,10 @@ func TestApplyWithDryModeShouldWork(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } @@ -92,7 +98,10 @@ func TestApplyShouldFailIfNo2xx(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } @@ -130,7 +139,10 @@ func TestGetShouldWork(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } @@ -162,7 +174,10 @@ func TestGetShouldFailIfN2xx(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } @@ -193,7 +208,10 @@ func TestDescribeShouldWork(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } @@ -225,7 +243,10 @@ func TestDescribeShouldFailIfNo2xx(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl/api" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } @@ -256,7 +277,10 @@ func TestDeleteShouldWork(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } @@ -287,7 +311,10 @@ func TestDeleteShouldFailOnNot2XX(t *testing.T) { defer httpmock.Reset() baseUrl := "http://baseUrl" apiKey := "aToken" - client, err := Make(apiKey, baseUrl, false, "", "", "", false) + client, err := Make(ApiParameter{ + ApiKey: apiKey, + BaseUrl: baseUrl, + }) if err != nil { panic(err) } diff --git a/cmd/login.go b/cmd/login.go index 6a69fbc..3deca6c 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -3,7 +3,9 @@ package cmd import ( "fmt" "os" + "strings" + "github.com/conduktor/ctl/client" "github.com/spf13/cobra" ) @@ -14,20 +16,28 @@ var loginCmd = &cobra.Command{ Long: `Use must use CDK_USER CDK_PASSWORD environment variables to login`, Args: cobra.RangeArgs(0, 0), Run: func(cmd *cobra.Command, args []string) { + specificApiClient, err := client.Make(client.ApiParameter{BaseUrl: os.Getenv("CDK_BASE_URL"), Debug: strings.ToLower(os.Getenv("CDK_DEBUG")) == "true"}) + if *debug { + specificApiClient.ActivateDebug() + } + if err != nil { + fmt.Fprintf(os.Stderr, "Could not login: %s\n", err) + os.Exit(1) + } username := os.Getenv("CDK_USER") if username == "" { fmt.Fprintln(os.Stderr, "Please set CDK_USER") - os.Exit(1) + os.Exit(2) } password := os.Getenv("CDK_PASSWORD") if password == "" { fmt.Fprintln(os.Stderr, "Please set CDK_PASSWORD") - os.Exit(2) + os.Exit(3) } - token, err := apiClient().Login(username, password) + token, err := specificApiClient.Login(username, password) if err != nil { fmt.Fprintf(os.Stderr, "Could not login: %s\n", err) - os.Exit(3) + os.Exit(4) } fmt.Println(token.AccessToken) }, diff --git a/cmd/root.go b/cmd/root.go index 0f40241..fbdf0cf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,7 +25,7 @@ func apiClient() *client.Client { var rootCmd = &cobra.Command{ Use: "conduktor", Short: "Command line tools for conduktor", - Long: `Make sure you've set the environment variables CDK_API_KEY (generated from Console) and CDK_BASE_URL. + Long: `Make sure you've set the environment variables CDK_USER/CDK_PASSWORD or CDK_API_KEY (generated from Console) and CDK_BASE_URL. Additionally, you can configure client TLS authentication by providing your certificate paths in CDK_KEY and CDK_CERT. For server TLS authentication, you can ignore the certificate by setting CDK_INSECURE=true, or provide a certificate authority using CDK_CACERT.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { @@ -50,7 +50,7 @@ func Execute() { func init() { apiClient_, apiClientError = client.MakeFromEnv() - kinds := schema.KindCatalog{} + var kinds schema.KindCatalog if apiClientError == nil { kinds = apiClient_.GetKinds() } else { diff --git a/cmd/token.go b/cmd/token.go index 342c1c2..7e0edb3 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -125,7 +125,7 @@ var deleteTokenCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { err := apiClient().DeleteToken(args[0]) if err != nil { - fmt.Fprintf(os.Stderr, "Could not create admin token: %s\n", err) + fmt.Fprintf(os.Stderr, "Could not delete token: %s\n", err) os.Exit(1) } fmt.Println("Token deleted") @@ -134,9 +134,9 @@ var deleteTokenCmd = &cobra.Command{ func init() { applicationInstanceNameForList = listApplicationInstanceTokenCmd.PersistentFlags().StringP("application-instance", "i", "", "Application instance name") - rootCmd.MarkPersistentFlagRequired("application-instance") applicationInstanceNameForCreate = createApplicationInstanceTokenCmd.PersistentFlags().StringP("application-instance", "i", "", "Application instance name") - rootCmd.MarkPersistentFlagRequired("application-instance") + listApplicationInstanceTokenCmd.MarkPersistentFlagRequired("application-instance") + createApplicationInstanceTokenCmd.MarkPersistentFlagRequired("application-instance") listTokenCmd.AddCommand(listApplicationInstanceTokenCmd) listTokenCmd.AddCommand(listAdminCmd) tokenCmd.AddCommand(listTokenCmd) diff --git a/docker/initializer.json b/docker/initializer.json index 77d036a..2720419 100644 --- a/docker/initializer.json +++ b/docker/initializer.json @@ -11,7 +11,9 @@ "statusCode": 200, "body": "\"Created\"", "headers": { - "Content-Type": ["application/json"] + "Content-Type": [ + "application/json" + ] } } }, @@ -27,7 +29,9 @@ "statusCode": 200, "body": "[{}]", "headers": { - "Content-Type": ["application/json"] + "Content-Type": [ + "application/json" + ] } } }, @@ -43,7 +47,9 @@ "statusCode": 200, "body": "{}", "headers": { - "Content-Type": ["application/json"] + "Content-Type": [ + "application/json" + ] } } }, @@ -59,8 +65,134 @@ "statusCode": 200, "body": "{}", "headers": { - "Content-Type": ["application/json"] + "Content-Type": [ + "application/json" + ] } } + }, + { + "httpRequest": { + "method": "Post", + "path": "/api/login", + "body": { + "username": "admin", + "password": "secret" + } + }, + "httpResponse": { + "statusCode": 200, + "body": { + "access_token": "yo", + "refresh_token": "yo", + "token_type": "yo", + "expires_in": 0 + }, + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Get", + "path": "/token/v1/admin_tokens", + "headers": { + "Authorization": "Bearer yo" + } + }, + "httpResponse": { + "statusCode": 200, + "body": [], + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "GET", + "path": "/token/v1/application_instance_tokens/my_app_instance", + "headers": { + "Authorization": "Bearer yo" + } + }, + "httpResponse": { + "statusCode": 200, + "body": [], + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Post", + "path": "/token/v1/admin_tokens", + "headers": { + "Authorization": "Bearer yo" + }, + "body": { + "name": "a_admin_token" + } + }, + "httpResponse": { + "statusCode": 200, + "body": { + "id": "000", + "name": "a_admin_token", + "created_at": "", + "token": "a_token" + }, + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "Post", + "path": "/token/v1/application_instance_tokens/my_app_instance", + "headers": { + "Authorization": "Bearer yo" + }, + "body": { + "name": "a_admin_token" + } + }, + "httpResponse": { + "statusCode": 200, + "body": { + "id": "000", + "name": "a_admin_token", + "created_at": "", + "token": "a_token" + }, + "headers": { + "Content-Type": [ + "application/json" + ] + } + } + }, + { + "httpRequest": { + "method": "DELETE", + "path": "/token/v1/0-0-0-0-0", + "headers": { + "Authorization": "Bearer yo" + } + }, + "httpResponse": { + "statusCode": 200 + } } -] +] \ No newline at end of file diff --git a/test_final_exec.sh b/test_final_exec.sh index 791253c..f335e55 100755 --- a/test_final_exec.sh +++ b/test_final_exec.sh @@ -18,6 +18,12 @@ main() { docker compose -f docker/docker-compose.yml run conduktor apply -f /test_resource.yml docker compose -f docker/docker-compose.yml run conduktor get Topic yolo --cluster=my-cluster docker compose -f docker/docker-compose.yml run conduktor delete Topic yolo -v --cluster=my-cluster + docker compose -f docker/docker-compose.yml run -e CDK_USER=admin -e CDK_PASSWORD=secret conduktor login + docker compose -f docker/docker-compose.yml run conduktor token list admin + docker compose -f docker/docker-compose.yml run conduktor token list application-instance -i=my_app_instance + docker compose -f docker/docker-compose.yml run conduktor token create admin a_admin_token + docker compose -f docker/docker-compose.yml run conduktor token create application-instance -i=my_app_instance a_admin_token + docker compose -f docker/docker-compose.yml run conduktor token delete 0-0-0-0-0 } main "$@"