From f21434a97177121bdf8c036ee493376e30104d07 Mon Sep 17 00:00:00 2001 From: Prakash Senthil Vel <23444145+prakashsvmx@users.noreply.github.com> Date: Wed, 22 Feb 2023 01:20:04 +0530 Subject: [PATCH] Export import server config from settings page (#2664) --- integration/admin_api_integration_test.go | 115 ++++++++++++--- integration/sample-import-config.txt | 28 ++++ models/config_export_response.go | 70 +++++++++ models/env_override.go | 2 +- .../screens/Console/Common/Hooks/useApi.tsx | 4 +- .../ConfigurationOptions.tsx | 14 ++ .../ExportConfigButton.tsx | 43 ++++++ .../ImportConfigButton.tsx | 91 ++++++++++++ restapi/admin_config.go | 55 +++++++ restapi/embedded_spec.go | 130 +++++++++++++++++ .../operations/configuration/export_config.go | 88 ++++++++++++ .../configuration/export_config_parameters.go | 63 ++++++++ .../configuration/export_config_responses.go | 135 ++++++++++++++++++ .../configuration/export_config_urlbuilder.go | 104 ++++++++++++++ .../configuration/post_configs_import.go | 88 ++++++++++++ .../post_configs_import_parameters.go | 103 +++++++++++++ .../post_configs_import_responses.go | 115 +++++++++++++++ .../post_configs_import_urlbuilder.go | 104 ++++++++++++++ restapi/operations/console_api.go | 24 ++++ swagger-console.yml | 43 ++++++ 20 files changed, 1397 insertions(+), 22 deletions(-) create mode 100644 integration/sample-import-config.txt create mode 100644 models/config_export_response.go create mode 100644 portal-ui/src/screens/Console/Configurations/ConfigurationPanels/ExportConfigButton.tsx create mode 100644 portal-ui/src/screens/Console/Configurations/ConfigurationPanels/ImportConfigButton.tsx create mode 100644 restapi/operations/configuration/export_config.go create mode 100644 restapi/operations/configuration/export_config_parameters.go create mode 100644 restapi/operations/configuration/export_config_responses.go create mode 100644 restapi/operations/configuration/export_config_urlbuilder.go create mode 100644 restapi/operations/configuration/post_configs_import.go create mode 100644 restapi/operations/configuration/post_configs_import_parameters.go create mode 100644 restapi/operations/configuration/post_configs_import_responses.go create mode 100644 restapi/operations/configuration/post_configs_import_urlbuilder.go diff --git a/integration/admin_api_integration_test.go b/integration/admin_api_integration_test.go index 749bedc72..ee0a39e05 100644 --- a/integration/admin_api_integration_test.go +++ b/integration/admin_api_integration_test.go @@ -22,8 +22,12 @@ import ( "bytes" "encoding/json" "fmt" + "io" "log" + "mime/multipart" "net/http" + "os" + "path" "testing" "time" @@ -155,33 +159,33 @@ func NotifyPostgres() (*http.Response, error) { func TestNotifyPostgres(t *testing.T) { // Variables - assert := assert.New(t) + asserter := assert.New(t) // Test response, err := NotifyPostgres() finalResponse := inspectHTTPResponse(response) - assert.Nil(err) + asserter.Nil(err) if err != nil { log.Println(err) - assert.Fail(finalResponse) + asserter.Fail(finalResponse) return } if response != nil { - assert.Equal(200, response.StatusCode, finalResponse) + asserter.Equal(200, response.StatusCode, finalResponse) } } func TestRestartService(t *testing.T) { - assert := assert.New(t) + asserter := assert.New(t) restartResponse, restartError := RestartService() - assert.Nil(restartError) + asserter.Nil(restartError) if restartError != nil { log.Println(restartError) return } addObjRsp := inspectHTTPResponse(restartResponse) if restartResponse != nil { - assert.Equal( + asserter.Equal( 204, restartResponse.StatusCode, addObjRsp, @@ -212,18 +216,18 @@ func ListPoliciesWithBucket(bucketName string) (*http.Response, error) { func TestListPoliciesWithBucket(t *testing.T) { // Test Variables bucketName := "testlistpolicieswithbucket" - assert := assert.New(t) + asserter := assert.New(t) // Test response, err := ListPoliciesWithBucket(bucketName) - assert.Nil(err) + asserter.Nil(err) if err != nil { log.Println(err) return } parsedResponse := inspectHTTPResponse(response) if response != nil { - assert.Equal( + asserter.Equal( 200, response.StatusCode, parsedResponse, @@ -254,18 +258,18 @@ func ListUsersWithAccessToBucket(bucketName string) (*http.Response, error) { func TestListUsersWithAccessToBucket(t *testing.T) { // Test Variables bucketName := "testlistuserswithaccesstobucket1" - assert := assert.New(t) + asserter := assert.New(t) // Test response, err := ListUsersWithAccessToBucket(bucketName) - assert.Nil(err) + asserter.Nil(err) if err != nil { log.Println(err) return } parsedResponse := inspectHTTPResponse(response) if response != nil { - assert.Equal( + asserter.Equal( 200, response.StatusCode, parsedResponse, @@ -274,16 +278,16 @@ func TestListUsersWithAccessToBucket(t *testing.T) { } func TestGetNodes(t *testing.T) { - assert := assert.New(t) + asserter := assert.New(t) getNodesResponse, getNodesError := GetNodes() - assert.Nil(getNodesError) + asserter.Nil(getNodesError) if getNodesError != nil { log.Println(getNodesError) return } addObjRsp := inspectHTTPResponse(getNodesResponse) if getNodesResponse != nil { - assert.Equal( + asserter.Equal( 200, getNodesResponse.StatusCode, addObjRsp, @@ -312,16 +316,89 @@ func ArnList() (*http.Response, error) { } func TestArnList(t *testing.T) { - assert := assert.New(t) + asserter := assert.New(t) resp, err := ArnList() - assert.Nil(err) + asserter.Nil(err) if err != nil { log.Println(err) return } objRsp := inspectHTTPResponse(resp) if resp != nil { - assert.Equal( + asserter.Equal( + 200, + resp.StatusCode, + objRsp, + ) + } +} + +func ExportConfig() (*http.Response, error) { + request, err := http.NewRequest( + "GET", "http://localhost:9090/api/v1/configs/export", nil) + if err != nil { + log.Println(err) + } + request.Header.Add("Cookie", fmt.Sprintf("token=%s", token)) + client := &http.Client{ + Timeout: 2 * time.Second, + } + response, err := client.Do(request) + return response, err +} + +func ImportConfig() (*http.Response, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + formFile, _ := writer.CreateFormFile("file", "sample-import-config.txt") + fileDir, _ := os.Getwd() + fileName := "sample-import-config.txt" + filePath := path.Join(fileDir, fileName) + file, _ := os.Open(filePath) + io.Copy(formFile, file) + writer.Close() + request, err := http.NewRequest( + "POST", "http://localhost:9090/api/v1/configs/import", + bytes.NewReader(body.Bytes()), + ) + if err != nil { + log.Println(err) + } + request.Header.Add("Cookie", fmt.Sprintf("token=%s", token)) + request.Header.Set("Content-Type", writer.FormDataContentType()) + + client := &http.Client{ + Timeout: 2 * time.Second, + } + + rsp, _ := client.Do(request) + if rsp.StatusCode != http.StatusOK { + log.Printf("Request failed with response code: %d", rsp.StatusCode) + } + return rsp, err +} + +func TestExportConfig(t *testing.T) { + asserter := assert.New(t) + resp, err := ExportConfig() + asserter.Nil(err) + objRsp := inspectHTTPResponse(resp) + if resp != nil { + asserter.Equal( + 200, + resp.StatusCode, + objRsp, + ) + } +} + +func TestImportConfig(t *testing.T) { + asserter := assert.New(t) + resp, err := ImportConfig() + asserter.Nil(err) + objRsp := inspectHTTPResponse(resp) + if resp != nil { + asserter.Equal( 200, resp.StatusCode, objRsp, diff --git a/integration/sample-import-config.txt b/integration/sample-import-config.txt new file mode 100644 index 000000000..14ebd8632 --- /dev/null +++ b/integration/sample-import-config.txt @@ -0,0 +1,28 @@ +subnet license= api_key= proxy= +# callhome enable=off frequency=24h +# site name= region= +# api requests_max=0 requests_deadline=10s cluster_deadline=10s cors_allow_origin=* remote_transport_deadline=2h list_quorum=strict replication_priority=auto transition_workers=100 stale_uploads_cleanup_interval=6h stale_uploads_expiry=24h delete_cleanup_interval=5m disable_odirect=off gzip_objects=off +# scanner speed=default +# compression enable=off allow_encryption=off extensions=.txt,.log,.csv,.json,.tar,.xml,.bin mime_types=text/*,application/json,application/xml,binary/octet-stream +# identity_openid enable= display_name= config_url= client_id= client_secret= claim_name=policy claim_userinfo= role_policy= claim_prefix= redirect_uri= redirect_uri_dynamic=off scopes= vendor= keycloak_realm= keycloak_admin_url= +# identity_ldap server_addr= srv_record_name= user_dn_search_base_dn= user_dn_search_filter= group_search_filter= group_search_base_dn= tls_skip_verify=off server_insecure=off server_starttls=off lookup_bind_dn= lookup_bind_password= +# identity_tls skip_verify=off +# identity_plugin url= auth_token= role_policy= role_id= +# policy_plugin url= auth_token= enable_http2=off +# logger_webhook enable=off endpoint= auth_token= client_cert= client_key= queue_size=100000 +# audit_webhook enable=off endpoint= auth_token= client_cert= client_key= queue_size=100000 +# audit_kafka enable=off topic= brokers= sasl_username= sasl_password= sasl_mechanism=plain client_tls_cert= client_tls_key= tls_client_auth=0 sasl=off tls=off tls_skip_verify=off version= +# notify_webhook enable=off endpoint= auth_token= queue_limit=0 queue_dir= client_cert= client_key= +# notify_amqp enable=off url= exchange= exchange_type= routing_key= mandatory=off durable=off no_wait=off internal=off auto_deleted=off delivery_mode=0 publisher_confirms=off queue_limit=0 queue_dir= +# notify_kafka enable=off topic= brokers= sasl_username= sasl_password= sasl_mechanism=plain client_tls_cert= client_tls_key= tls_client_auth=0 sasl=off tls=off tls_skip_verify=off queue_limit=0 queue_dir= version= +# notify_mqtt enable=off broker= topic= password= username= qos=0 keep_alive_interval=0s reconnect_interval=0s queue_dir= queue_limit=0 +# notify_nats enable=off address= subject= username= password= token= tls=off tls_skip_verify=off cert_authority= client_cert= client_key= ping_interval=0 jetstream=off streaming=off streaming_async=off streaming_max_pub_acks_in_flight=0 streaming_cluster_id= queue_dir= queue_limit=0 +# notify_nsq enable=off nsqd_address= topic= tls=off tls_skip_verify=off queue_dir= queue_limit=0 +# notify_mysql enable=off format=namespace dsn_string= table= queue_dir= queue_limit=0 max_open_connections=2 +# notify_postgres enable=off format=namespace connection_string= table= queue_dir= queue_limit=0 max_open_connections=2 +# notify_elasticsearch enable=off url= format=namespace index= queue_dir= queue_limit=0 username= password= +# notify_redis enable=off format=namespace address= key= password= queue_dir= queue_limit=0 +# etcd endpoints= path_prefix= coredns_path=/skydns client_cert= client_cert_key= +# cache drives= exclude= expiry=90 quota=80 after=0 watermark_low=70 watermark_high=80 range=on commit= +# storage_class standard= rrs=EC:1 +# heal bitrotscan=off max_sleep=1s max_io=100 \ No newline at end of file diff --git a/models/config_export_response.go b/models/config_export_response.go new file mode 100644 index 000000000..41efdbeda --- /dev/null +++ b/models/config_export_response.go @@ -0,0 +1,70 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// ConfigExportResponse config export response +// +// swagger:model configExportResponse +type ConfigExportResponse struct { + + // status + Status string `json:"status,omitempty"` + + // Returns base64 encoded value + Value string `json:"value,omitempty"` +} + +// Validate validates this config export response +func (m *ConfigExportResponse) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this config export response based on context it is used +func (m *ConfigExportResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *ConfigExportResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ConfigExportResponse) UnmarshalBinary(b []byte) error { + var res ConfigExportResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/env_override.go b/models/env_override.go index 79cb2903c..6052361f9 100644 --- a/models/env_override.go +++ b/models/env_override.go @@ -1,7 +1,7 @@ // Code generated by go-swagger; DO NOT EDIT. // This file is part of MinIO Console Server -// Copyright (c) 2022 MinIO, Inc. +// Copyright (c) 2023 MinIO, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by diff --git a/portal-ui/src/screens/Console/Common/Hooks/useApi.tsx b/portal-ui/src/screens/Console/Common/Hooks/useApi.tsx index 1f7155c47..7d4fbcb34 100644 --- a/portal-ui/src/screens/Console/Common/Hooks/useApi.tsx +++ b/portal-ui/src/screens/Console/Common/Hooks/useApi.tsx @@ -12,10 +12,10 @@ const useApi = ( ): [IsApiInProgress, ApiMethodToInvoke] => { const [isLoading, setIsLoading] = useState(false); - const callApi = (method: string, url: string, data?: any) => { + const callApi = (method: string, url: string, data?: any, headers?: any) => { setIsLoading(true); api - .invoke(method, url, data) + .invoke(method, url, data, headers) .then((res: any) => { setIsLoading(false); onSuccess(res); diff --git a/portal-ui/src/screens/Console/Configurations/ConfigurationPanels/ConfigurationOptions.tsx b/portal-ui/src/screens/Console/Configurations/ConfigurationPanels/ConfigurationOptions.tsx index 5e7005f8f..b7d841f20 100644 --- a/portal-ui/src/screens/Console/Configurations/ConfigurationPanels/ConfigurationOptions.tsx +++ b/portal-ui/src/screens/Console/Configurations/ConfigurationPanels/ConfigurationOptions.tsx @@ -34,6 +34,9 @@ import ScreenTitle from "../../Common/ScreenTitle/ScreenTitle"; import ConfigurationForm from "./ConfigurationForm"; import { IAM_PAGES } from "../../../../common/SecureComponent/permissions"; import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper"; +import ExportConfigButton from "./ExportConfigButton"; +import ImportConfigButton from "./ImportConfigButton"; +import { Box } from "@mui/material"; interface IConfigurationOptions { classes: any; @@ -78,6 +81,17 @@ const ConfigurationOptions = ({ classes }: IConfigurationOptions) => { } title={"MinIO Configuration:"} + actions={ + + + + + } /> { + const dispatch = useDispatch(); + const [isReqLoading, invokeApi] = useApi( + (res: any) => { + //base64 encoded information so decode before downloading. + performDownload( + new Blob([window.atob(res.value)]), + `minio-server-config-${DateTime.now().toFormat( + "LL-dd-yyyy-HH-mm-ss" + )}.conf` + ); + }, + (err) => { + dispatch(setErrorSnackMessage(err)); + } + ); + + return ( + +