From d228f81cd46d18b9688c280a719d1cfe44cfff1a Mon Sep 17 00:00:00 2001 From: Matt Spilchen Date: Fri, 8 Dec 2023 08:37:05 -0400 Subject: [PATCH] Sync from server repo (14ee0894010) --- CONTRIBUTING.md | 4 +- README.md | 60 ++++++++++++++++++++---- go.mod | 10 ++-- go.sum | 20 ++++---- vclusterops/cluster_op_engine_context.go | 19 ++++---- vclusterops/helpers.go | 23 +++++++++ vclusterops/helpers_test.go | 16 +++++++ vclusterops/http_adapter.go | 52 ++++++++++++-------- vclusterops/http_adapter_test.go | 31 +++++++++++- vclusterops/nma_download_config.go | 21 ++++++--- vclusterops/nma_spread_security_op.go | 16 +++++-- vclusterops/nma_upload_config.go | 23 ++++++--- vclusterops/scrutinize.go | 4 +- 13 files changed, 228 insertions(+), 71 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b05895..12d35a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,8 +10,8 @@ If you find a bug, [submit an issue](https://github.com/vertica/vcluster/issues) with a complete and reproducible bug report. For issues that are **not suitable** to be reported publicly on the GitHub -issue system (e.g. security related issues), report your issues to [Vertica -open source team](mailto:vertica-opensrc@microfocus.com) directly or file a +issue system (e.g., security related issues), report your issues to [Vertica +open source team](mailto:vertica-opensrc@opentext.com) directly or file a case with Vertica support, if you have a support account. # Feature Requests diff --git a/README.md b/README.md index ca20f09..3405433 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/vertica/vcluster.svg)](https://pkg.go.dev/github.com/vertica/vcluster) -This repository contains the Go library and command-line interface (CLI) to -administer a Vertica cluster with HTTP RESTful interfaces. These interfaces are -exposed by the following services: +This repository contains the vcluster-ops Go library and command-line interface to administer a Vertica cluster with a REST API. The REST API endpoints are exposed by the following services: - Node Management Agent (NMA) - Embedded HTTPS service -This CLI tool combines REST calls to provide a coherent Go interface to perform -administrator-level operations, including: creating a database, scaling -up/down, restarting the cluster, and stopping the cluster. +This CLI tool combines REST calls to provide a coherent Go interface so that you can perform the following administrator operations: +- Create a database +- Scale a cluster up and down +- Restart a cluster +- Stop a cluster +- Revive an Eon database -Traditionally, these operations were completed with -[admintools](https://docs.vertica.com/latest/en/admin/using-admin-tools/admin-tools-reference/writing-admin-tools-scripts/). +Historically, these operations were performed with [admintools](https://docs.vertica.com/latest/en/admin/using-admin-tools/admin-tools-reference/writing-admin-tools-scripts/). However, admintools is not suitable for containerized environments because it relies on SSH for communications and maintains a state file (admintools.conf) on each Vertica host. @@ -36,7 +36,8 @@ vcluster/ └── vclusterops ├── test_data ├── util - └── vlog + ├── vlog + └── vstruct ``` - `/cmd/vcluster`: The `/cmd` directory contains executable code. The @@ -56,3 +57,44 @@ directories in this project. project and does not fit logically into an existing package. - `/vclusterops/vlog`: Sets up a logging utility that writes to `/opt/vertica/log/vcluster.log`. +- `/vclusterops/vstruct`: Contains helper structs used by vcluster-ops. + + +## Usage +Each source file in `vclusterops/` contains a `VOptions` struct with option fields that you can set for that operation, and a `VOptionsFactory` factory function that returns a struct with sensible option defaults. General database and authentication options are available in `DatabaseOptions` in `vclusterops/vcluster_database_options.go`. + +The following example imports the `vclusterops` library, and then calls functions from `vclusterops/create_db.go` to create a database: + + +``` +import "github.com/vertica/vcluster/vclusterops" + +// get default create_db options +opts := vclusterops.VCreateDatabaseOptionsFactory() + +// set database options +opts.RawHosts = []string{"host1_ip", "host2_ip", "host3_ip"} +opts.DBName = "my_database" +*opts.ForceRemovalAtCreation = true +opts.CatalogPrefix = "/data" +opts.DataPrefix = "/data" + +// set authentication options +opts.Key = "your_tls_key" +opts.Cert = "your_tls_cert" +opts.CaCert = "your_ca_cert" +*opts.UserName = "database_username" +opts.Password = "database_password" + +// pass opts to VCreateDatabase function +vdb, err := vclusterops.VCreateDatabase(&opts) +if err != nil { + // handle the error here +} +``` + +We can use similar way to set up and call other vcluster-ops commands. + + +## Licensing +vcluster is open source code and is under the Apache 2.0 license. Please see `LICENSE` for details. \ No newline at end of file diff --git a/go.mod b/go.mod index 0f104ad..664003f 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/tonglil/buflogr v1.0.1 go.uber.org/zap v1.25.0 golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea - golang.org/x/sys v0.8.0 + golang.org/x/sys v0.15.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.26.2 k8s.io/client-go v0.26.2 @@ -18,7 +18,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -35,10 +35,10 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/term v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/go.sum b/go.sum index 8cdc22a..61efebd 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -13,6 +15,8 @@ github.com/deckarep/golang-set/v2 v2.3.1/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpO github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -139,8 +143,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= @@ -157,16 +161,16 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/vclusterops/cluster_op_engine_context.go b/vclusterops/cluster_op_engine_context.go index 5a79cf4..a9c674d 100644 --- a/vclusterops/cluster_op_engine_context.go +++ b/vclusterops/cluster_op_engine_context.go @@ -18,15 +18,16 @@ package vclusterops import "github.com/vertica/vcluster/vclusterops/vlog" type opEngineExecContext struct { - dispatcher requestDispatcher - networkProfiles map[string]networkProfile - nmaVDatabase nmaVDatabase - upHosts []string // a sorted host list that contains all up nodes - nodesInfo []NodeInfo - defaultSCName string // store the default subcluster name of the database - hostsWithLatestCatalog []string - startupCommandMap map[string][]string // store start up command map to restart nodes - dbInfo string // store the db info that retrieved from communal storage + dispatcher requestDispatcher + networkProfiles map[string]networkProfile + nmaVDatabase nmaVDatabase + upHosts []string // a sorted host list that contains all up nodes + nodesInfo []NodeInfo + defaultSCName string // store the default subcluster name of the database + hostsWithLatestCatalog []string + primaryHostsWithLatestCatalog []string + startupCommandMap map[string][]string // store start up command map to restart nodes + dbInfo string // store the db info that retrieved from communal storage } func makeOpEngineExecContext(logger vlog.Printer) opEngineExecContext { diff --git a/vclusterops/helpers.go b/vclusterops/helpers.go index ff12df4..a33eb50 100644 --- a/vclusterops/helpers.go +++ b/vclusterops/helpers.go @@ -21,6 +21,7 @@ import ( "path" "strings" + mapset "github.com/deckarep/golang-set/v2" "github.com/vertica/vcluster/vclusterops/util" "github.com/vertica/vcluster/vclusterops/vlog" ) @@ -73,6 +74,28 @@ func updateCatalogPathMapFromCatalogEditor(hosts []string, nmaVDB *nmaVDatabase, return nil } +// Get primary nodes with latest catalog from catalog editor if the primaryHostsWithLatestCatalog info doesn't exist in execContext +func getPrimaryHostsWithLatestCatalog(nmaVDB *nmaVDatabase, hostsWithLatestCatalog []string, execContext *opEngineExecContext) []string { + if len(execContext.primaryHostsWithLatestCatalog) > 0 { + return execContext.primaryHostsWithLatestCatalog + } + emptyPrimaryHostsString := []string{} + primaryHostsSet := mapset.NewSet[string]() + for host, vnode := range nmaVDB.HostNodeMap { + if vnode.IsPrimary { + primaryHostsSet.Add(host) + } + } + hostsWithLatestCatalogSet := mapset.NewSet(hostsWithLatestCatalog...) + primaryHostsWithLatestCatalog := hostsWithLatestCatalogSet.Intersect(primaryHostsSet) + primaryHostsWithLatestCatalogList := primaryHostsWithLatestCatalog.ToSlice() + if len(primaryHostsWithLatestCatalogList) == 0 { + return emptyPrimaryHostsString + } + execContext.primaryHostsWithLatestCatalog = primaryHostsWithLatestCatalogList // save the primaryHostsWithLatestCatalog to execContext + return primaryHostsWithLatestCatalogList +} + // The following structs will store hosts' necessary information for https_get_up_nodes_op, // https_get_nodes_information_from_running_db, and incoming operations. type nodeStateInfo struct { diff --git a/vclusterops/helpers_test.go b/vclusterops/helpers_test.go index c44787f..407ee70 100644 --- a/vclusterops/helpers_test.go +++ b/vclusterops/helpers_test.go @@ -55,6 +55,22 @@ func TestForupdateCatalogPathMapFromCatalogEditorNegative(t *testing.T) { assert.ErrorContains(t, err, "fail to get host with highest catalog version") } +func TestForGetPrimaryHostsWithLatestCatalog(t *testing.T) { + // prepare data for nmaVDB + mockNmaVNode1 := &nmaVNode{CatalogPath: "/data/test_db/v_test_db_node0001_catalog/Catalog", Address: "192.168.1.101", IsPrimary: false} + mockNmaVNode2 := &nmaVNode{CatalogPath: "/data/test_db/v_test_db_node0002_catalog/Catalog", Address: "192.168.1.102", IsPrimary: true} + mockHostNodeMap := map[string]*nmaVNode{"192.168.1.101": mockNmaVNode1, "192.168.1.102": mockNmaVNode2} + mockNmaVDB := &nmaVDatabase{HostNodeMap: mockHostNodeMap} + hostsWithLatestCatalog := []string{"192.168.1.101", "192.168.1.102", "192.168.1.104"} + // successfully get a primary host with latest catalog + primaryHostsWithLatestCatalog := getPrimaryHostsWithLatestCatalog(mockNmaVDB, hostsWithLatestCatalog, &opEngineExecContext{}) + assert.Equal(t, primaryHostsWithLatestCatalog, []string{"192.168.1.102"}) + // Unable to find any primary hosts with the latest catalog + hostsWithLatestCatalog = []string{} + primaryHostsWithLatestCatalog = getPrimaryHostsWithLatestCatalog(mockNmaVDB, hostsWithLatestCatalog, &opEngineExecContext{}) + assert.Equal(t, primaryHostsWithLatestCatalog, []string{}) +} + func TestForgetInitiatorHost(t *testing.T) { nodesList1 := []string{"10.0.0.0", "10.0.0.1", "10.0.0.2"} hostsToSkip1 := []string{"10.0.0.10", "10.0.0.11"} diff --git a/vclusterops/http_adapter.go b/vclusterops/http_adapter.go index 7bdfa37..29dbd44 100644 --- a/vclusterops/http_adapter.go +++ b/vclusterops/http_adapter.go @@ -60,7 +60,7 @@ func makeHTTPDownloadAdapter(logger vlog.Printer, } type responseBodyHandler interface { - readResponseBody(resp *http.Response) (string, error) + processResponseBody(resp *http.Response) (string, error) } // empty struct for default behavior of reading response body into memory @@ -156,37 +156,35 @@ func (adapter *httpAdapter) sendRequest(request *hostHTTPRequest, resultChannel } func (adapter *httpAdapter) generateResult(resp *http.Response) hostHTTPResult { - bodyString, err := adapter.respBodyHandler.readResponseBody(resp) + bodyString, err := adapter.respBodyHandler.processResponseBody(resp) if err != nil { return adapter.makeExceptionResult(err) } - if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if isSuccess(resp) { return adapter.makeSuccessResult(bodyString, resp.StatusCode) } return adapter.makeFailResult(resp.Header, bodyString, resp.StatusCode) } -func (reader *responseBodyReader) readResponseBody(resp *http.Response) (bodyString string, err error) { - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - err = fmt.Errorf("fail to read the response body: %w", err) - return "", err - } - bodyString = string(bodyBytes) - - return bodyString, nil +func (*responseBodyReader) processResponseBody(resp *http.Response) (bodyString string, err error) { + return readResponseBody(resp) } -func (downloader *responseBodyDownloader) readResponseBody(resp *http.Response) (bodyString string, err error) { - bytesWritten, err := downloader.downloadFile(resp) - if err != nil { - err = fmt.Errorf("fail to stream the response body to file %s: %w", downloader.destFilePath, err) - } else { - downloader.logger.Info("File downloaded", "File", downloader.destFilePath, "Bytes", bytesWritten) +func (downloader *responseBodyDownloader) processResponseBody(resp *http.Response) (bodyString string, err error) { + if isSuccess(resp) { + bytesWritten, err := downloader.downloadFile(resp) + if err != nil { + err = fmt.Errorf("fail to stream the response body to file %s: %w", downloader.destFilePath, err) + } else { + downloader.logger.Info("File downloaded", "File", downloader.destFilePath, "Bytes", bytesWritten) + } + return "", err } - return "", err + // in case of error, we get an RFC7807 error, not a file + return readResponseBody(resp) } +// downloadFile uses buffered read/writes to download the http response body to a file func (downloader *responseBodyDownloader) downloadFile(resp *http.Response) (bytesWritten int64, err error) { file, err := os.Create(downloader.destFilePath) if err != nil { @@ -196,6 +194,22 @@ func (downloader *responseBodyDownloader) downloadFile(resp *http.Response) (byt return io.Copy(file, resp.Body) } +// readResponseBody attempts to read the entire contents of the http response into bodyString +func readResponseBody(resp *http.Response) (bodyString string, err error) { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + err = fmt.Errorf("fail to read the response body: %w", err) + return "", err + } + bodyString = string(bodyBytes) + + return bodyString, nil +} + +func isSuccess(resp *http.Response) bool { + return resp.StatusCode >= 200 && resp.StatusCode < 300 +} + // makeSuccessResult is a factory method for hostHTTPResult when a success // response comes back from a REST endpoints. func (adapter *httpAdapter) makeSuccessResult(content string, statusCode int) hostHTTPResult { diff --git a/vclusterops/http_adapter_test.go b/vclusterops/http_adapter_test.go index 179ce87..33e085c 100644 --- a/vclusterops/http_adapter_test.go +++ b/vclusterops/http_adapter_test.go @@ -145,8 +145,9 @@ func TestHandleSuccessResponseCodes(t *testing.T) { func TestHandleRFC7807Response(t *testing.T) { adapter := httpAdapter{respBodyHandler: &responseBodyReader{}} + detail := "Cannot access communal storage" rfcErr := rfc7807.New(rfc7807.CommunalAccessError). - WithDetail("Cannot access communal storage") + WithDetail(detail) b, err := json.Marshal(rfcErr) assert.Equal(t, err, nil) mockBodyReader := MockReadCloser{ @@ -165,7 +166,33 @@ func TestHandleRFC7807Response(t *testing.T) { ok := errors.As(result.err, &problem) assert.True(t, ok) assert.Equal(t, 500, problem.Status) - assert.Equal(t, "Cannot access communal storage", problem.Detail) + assert.Equal(t, detail, problem.Detail) +} + +func TestHandleFileDownloadErrorResponse(t *testing.T) { + adapter := httpAdapter{respBodyHandler: &responseBodyDownloader{destFilePath: "/never/use/me"}} + detail := "Something went horribly wrong and this is not a file" + rfcErr := rfc7807.New(rfc7807.GenericHTTPInternalServerError). + WithDetail(detail) + b, err := json.Marshal(rfcErr) + assert.Equal(t, err, nil) + mockBodyReader := MockReadCloser{ + body: b, + } + mockResp := &http.Response{ + StatusCode: rfcErr.Status, + Header: http.Header{}, + Body: &mockBodyReader, + } + mockResp.Header.Add("Content-Type", rfc7807.ContentType) + result := adapter.generateResult(mockResp) + assert.Equal(t, result.status, FAILURE) + assert.NotEqual(t, result.err, nil) + problem := &rfc7807.VProblem{} + ok := errors.As(result.err, &problem) + assert.True(t, ok) + assert.Equal(t, 500, problem.Status) + assert.Equal(t, detail, problem.Detail) } func TestHandleGenericErrorResponse(t *testing.T) { diff --git a/vclusterops/nma_download_config.go b/vclusterops/nma_download_config.go index 59f323c..85f5839 100644 --- a/vclusterops/nma_download_config.go +++ b/vclusterops/nma_download_config.go @@ -73,12 +73,22 @@ func (op *nmaDownloadConfigOp) prepare(execContext *opEngineExecContext) error { // vdb is built by calling /cluster and /nodes endpoints of a running db. // If nodes' info is not available in vdb, we will get the host from execContext.nmaVDatabase which is build by reading the catalog editor if op.vdb == nil || len(op.vdb.HostNodeMap) == 0 { + nmaVDB := execContext.nmaVDatabase if op.hosts == nil { - // If the host input is a nil value, we find the host with the latest catalog version to update the host input. - // Otherwise, we use the host input. - hostsWithLatestCatalog := execContext.hostsWithLatestCatalog - if len(hostsWithLatestCatalog) == 0 { - return fmt.Errorf("could not find at least one host with the latest catalog") + var hostsWithLatestCatalog []string + // If SpreadEncryption is enabled, the primary node with the latest catalog will be the sourceConfigHost. + if nmaVDB.SpreadEncryption != "" { + hostsWithLatestCatalog = getPrimaryHostsWithLatestCatalog(&nmaVDB, execContext.hostsWithLatestCatalog, execContext) + if len(hostsWithLatestCatalog) == 0 { + return fmt.Errorf("could not find at least one primary host with the latest catalog") + } + } else { + // If the host input is a nil value, we find the host with the latest catalog version to update the host input. + // Otherwise, we use the host input. + hostsWithLatestCatalog = execContext.hostsWithLatestCatalog + if len(hostsWithLatestCatalog) == 0 { + return fmt.Errorf("could not find at least one host with the latest catalog") + } } hostWithLatestCatalog := hostsWithLatestCatalog[:1] // update the host with the latest catalog @@ -86,7 +96,6 @@ func (op *nmaDownloadConfigOp) prepare(execContext *opEngineExecContext) error { } // For createDb and AddNodes, sourceConfigHost input is the bootstrap host. // we update the catalogPathMap for next download operation's steps from information of catalog editor - nmaVDB := execContext.nmaVDatabase err := updateCatalogPathMapFromCatalogEditor(op.hosts, &nmaVDB, op.catalogPathMap) if err != nil { return fmt.Errorf("failed to get catalog paths from catalog editor: %w", err) diff --git a/vclusterops/nma_spread_security_op.go b/vclusterops/nma_spread_security_op.go index e464ba4..0571c23 100644 --- a/vclusterops/nma_spread_security_op.go +++ b/vclusterops/nma_spread_security_op.go @@ -142,11 +142,21 @@ func (op *nmaSpreadSecurityOp) processResult(_ *opEngineExecContext) error { // setRuntimeParms will set options based on runtime context. func (op *nmaSpreadSecurityOp) setRuntimeParms(execContext *opEngineExecContext) error { - // Always pull the hosts at runtime using the node with the latest catalog. + // A core dump can happen if we send the /v1/catalog/spread-security settings to a secondary node. + // Need to use the primary node because fetching global settings to perform a catalog lookup isn't available on a secondary node. + // Always pull the hosts at runtime using the primary node with the latest catalog. // Need to use the ones with the latest catalog because those are the hosts // that we copy the spread.conf from during start db. - op.hosts = execContext.hostsWithLatestCatalog - + hostsWithLatestCatalog := execContext.hostsWithLatestCatalog + if len(hostsWithLatestCatalog) == 0 { + return fmt.Errorf("could not find at least one host with the latest catalog") + } + // Use only a primary host with the latest catalog as the sourceConfigHost + primaryHostsWithLatestCatalog := getPrimaryHostsWithLatestCatalog(&execContext.nmaVDatabase, hostsWithLatestCatalog, execContext) + if len(primaryHostsWithLatestCatalog) == 0 { + return fmt.Errorf("could not find at least one primary host with the latest catalog") + } + op.hosts = []string{primaryHostsWithLatestCatalog[0]} op.catalogPathMap = make(map[string]string, len(op.hosts)) err := updateCatalogPathMapFromCatalogEditor(op.hosts, &execContext.nmaVDatabase, op.catalogPathMap) if err != nil { diff --git a/vclusterops/nma_upload_config.go b/vclusterops/nma_upload_config.go index 201ea40..89a773c 100644 --- a/vclusterops/nma_upload_config.go +++ b/vclusterops/nma_upload_config.go @@ -108,12 +108,24 @@ func (op *nmaUploadConfigOp) prepare(execContext *opEngineExecContext) error { // This case is used for restarting nodes operation. // Otherwise, we set catalogPathMap from the catalog editor (start_db, create_db). if op.vdb == nil || len(op.vdb.HostNodeMap) == 0 { + nmaVDB := execContext.nmaVDatabase if op.sourceConfigHost == nil { - // if the host with the highest catalog version for starting a database or starting nodes is nil value - // we identify the hosts that need to be synchronized. - hostsWithLatestCatalog := execContext.hostsWithLatestCatalog - if len(hostsWithLatestCatalog) == 0 { - return fmt.Errorf("could not find at least one host with the latest catalog") + var hostsWithLatestCatalog []string + // If SpreadEncryption is enabled, synchronize the catalog of the primary node with + // the latest catalog to the rest of the nodes (both primary and secondary nodes). + if nmaVDB.SpreadEncryption != "" { + hostsWithLatestCatalog = getPrimaryHostsWithLatestCatalog(&nmaVDB, execContext.hostsWithLatestCatalog, execContext) + if len(hostsWithLatestCatalog) == 0 { + return fmt.Errorf("could not find at least one primary host with the latest catalog") + } + hostsWithLatestCatalog = hostsWithLatestCatalog[:1] + } else { + // if the host with the highest catalog version for starting a database or starting nodes is nil value + // we identify the hosts that need to be synchronized. + hostsWithLatestCatalog = execContext.hostsWithLatestCatalog + if len(hostsWithLatestCatalog) == 0 { + return fmt.Errorf("could not find at least one host with the latest catalog") + } } hostsNeedCatalogSync := util.SliceDiff(op.destHosts, hostsWithLatestCatalog) // Update the hosts that need to synchronize the catalog @@ -129,7 +141,6 @@ func (op *nmaUploadConfigOp) prepare(execContext *opEngineExecContext) error { op.hosts = util.SliceDiff(op.destHosts, op.sourceConfigHost) } // Update the catalogPathMap for next upload operation's steps from information of catalog editor - nmaVDB := execContext.nmaVDatabase err := updateCatalogPathMapFromCatalogEditor(op.hosts, &nmaVDB, op.catalogPathMap) if err != nil { return fmt.Errorf("failed to get catalog paths from catalog editor: %w", err) diff --git a/vclusterops/scrutinize.go b/vclusterops/scrutinize.go index b5ba973..7866d25 100644 --- a/vclusterops/scrutinize.go +++ b/vclusterops/scrutinize.go @@ -178,8 +178,8 @@ func (vcc *VClusterCommands) VScrutinize(options *VScrutinizeOptions) error { } func tarAndRemoveDirectory(id string, log vlog.Printer) (err error) { - tarballPath := "/tmp/scrutinize/" + id + ".tgz" - cmd := exec.Command("tar", "czf", tarballPath, "-C", "/tmp/scrutinize/remote", id) + tarballPath := "/tmp/scrutinize/" + id + ".tar" + cmd := exec.Command("tar", "cf", tarballPath, "-C", "/tmp/scrutinize/remote", id) log.Info("running command %s with args %v", cmd.Path, cmd.Args) if err = cmd.Run(); err != nil { return