feat: support configuring multiple hosts when creating a cluster (#90)

* feat: support configuring multiple hosts when creating a cluster

* chore: update release notes

* feat: support multiple hosts configuration for system cluster initialization
This commit is contained in:
silenceqi 2025-01-23 09:04:14 +08:00 committed by GitHub
parent d914028e68
commit f3417ee6de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 229 additions and 124 deletions

View File

@ -67,8 +67,7 @@ elasticsearch:
basic_auth:
username: ingest
password: password
endpoints:
- $[[SETUP_ENDPOINT]]
endpoints: $[[SETUP_ENDPOINTS]]
pipeline:
- name: bulk_request_ingest

View File

@ -61,6 +61,6 @@ pipeline:
# key_file: /xxx/client.key
# skip_insecure_verify: false
schema: "$[[SETUP_SCHEME]]"
hosts: # receiver endpoint, fallback in order
- "$[[SETUP_ENDPOINT]]"
# receiver endpoint, fallback in order
hosts: $[[SETUP_HOSTS]]
valid_status_code: [200,201] #panic on other status code

View File

@ -11,6 +11,7 @@ Information about release notes of INFINI Console is provided here.
### Breaking changes
### Features
- Support configuring multiple hosts when creating a cluster
### Bug fix
### Improvements

View File

@ -65,9 +65,18 @@ func (h *APIHandler) HandleCreateClusterAction(w http.ResponseWriter, req *http.
h.WriteError(w, err.Error(), http.StatusInternalServerError)
return
}
// TODO validate data format
conf.Enabled = true
if len(conf.Hosts) > 0 && conf.Host == "" {
conf.Host = conf.Hosts[0]
}
conf.Host = strings.TrimSpace(conf.Host)
if conf.Host == "" {
h.WriteError(w, "host is required", http.StatusBadRequest)
return
}
if conf.Schema == "" {
conf.Schema = "http"
}
conf.Endpoint = fmt.Sprintf("%s://%s", conf.Schema, conf.Host)
conf.ID = util.GetUUID()
ctx := &orm.Context{

View File

@ -80,23 +80,29 @@ func (h TestAPI) HandleTestConnectionAction(w http.ResponseWriter, req *http.Req
} else if config.Host != "" && config.Schema != "" {
url = fmt.Sprintf("%s://%s", config.Schema, config.Host)
config.Endpoint = url
} else {
resBody["error"] = fmt.Sprintf("invalid config: %v", util.MustToJSON(config))
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
if url == "" {
panic(errors.Error("invalid url: " + util.MustToJSON(config)))
if url != "" && !util.StringInArray(config.Endpoints, url) {
config.Endpoints = append(config.Endpoints, url)
}
if !util.SuffixStr(url, "/") {
url = fmt.Sprintf("%s/", url)
if config.Schema != "" && len(config.Hosts) > 0 {
for _, host := range config.Hosts {
host = strings.TrimSpace(host)
if host == "" {
continue
}
url = fmt.Sprintf("%s://%s", config.Schema, host)
if !util.StringInArray(config.Endpoints, url) {
config.Endpoints = append(config.Endpoints, url)
}
}
}
if len(config.Endpoints) == 0 {
panic(errors.Error(fmt.Sprintf("invalid config: %v", util.MustToJSON(config))))
}
// limit the number of endpoints to a maximum of 10 to prevent excessive processing
if len(config.Endpoints) > 10 {
config.Endpoints = config.Endpoints[0:10]
}
freq.SetRequestURI(url)
freq.Header.SetMethod("GET")
if (config.BasicAuth == nil || (config.BasicAuth != nil && config.BasicAuth.Username == "")) &&
config.CredentialID != "" && config.CredentialID != "manual" {
credential, err := common.GetCredential(config.CredentialID)
@ -112,59 +118,86 @@ func (h TestAPI) HandleTestConnectionAction(w http.ResponseWriter, req *http.Req
config.BasicAuth = &auth
}
}
var (
i int
clusterUUID string
)
for i, url = range config.Endpoints {
if !util.SuffixStr(url, "/") {
url = fmt.Sprintf("%s/", url)
}
if config.BasicAuth != nil && strings.TrimSpace(config.BasicAuth.Username) != "" {
freq.SetBasicAuth(config.BasicAuth.Username, config.BasicAuth.Password.Get())
freq.SetRequestURI(url)
freq.Header.SetMethod("GET")
if config.BasicAuth != nil && strings.TrimSpace(config.BasicAuth.Username) != "" {
freq.SetBasicAuth(config.BasicAuth.Username, config.BasicAuth.Password.Get())
}
const testClientName = "elasticsearch_test_connection"
err = api.GetFastHttpClient(testClientName).DoTimeout(freq, fres, 10*time.Second)
if err != nil {
panic(err)
}
var statusCode = fres.StatusCode()
if statusCode > 300 || statusCode == 0 {
resBody["error"] = fmt.Sprintf("invalid status code: %d", statusCode)
h.WriteJSON(w, resBody, 500)
return
}
b := fres.Body()
clusterInfo := &elastic.ClusterInformation{}
err = json.Unmarshal(b, clusterInfo)
if err != nil {
panic(err)
}
resBody["version"] = clusterInfo.Version.Number
resBody["cluster_uuid"] = clusterInfo.ClusterUUID
resBody["cluster_name"] = clusterInfo.ClusterName
resBody["distribution"] = clusterInfo.Version.Distribution
if i == 0 {
clusterUUID = clusterInfo.ClusterUUID
} else {
//validate whether two endpoints point to the same cluster
if clusterUUID != clusterInfo.ClusterUUID {
resBody["error"] = fmt.Sprintf("invalid multiple cluster endpoints: %v", config.Endpoints)
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
//skip fetch cluster health info if it's not the first endpoint
break
}
//fetch cluster health info
freq.SetRequestURI(fmt.Sprintf("%s/_cluster/health", url))
fres.Reset()
err = api.GetFastHttpClient(testClientName).Do(freq, fres)
if err != nil {
resBody["error"] = fmt.Sprintf("error on get cluster health: %v", err)
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
healthInfo := &elastic.ClusterHealth{}
err = json.Unmarshal(fres.Body(), &healthInfo)
if err != nil {
resBody["error"] = fmt.Sprintf("error on decode cluster health info : %v", err)
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
resBody["status"] = healthInfo.Status
resBody["number_of_nodes"] = healthInfo.NumberOfNodes
resBody["number_of_data_nodes"] = healthInfo.NumberOf_data_nodes
resBody["active_shards"] = healthInfo.ActiveShards
freq.Reset()
fres.Reset()
}
const testClientName = "elasticsearch_test_connection"
err = api.GetFastHttpClient(testClientName).DoTimeout(freq, fres, 10*time.Second)
if err != nil {
panic(err)
}
var statusCode = fres.StatusCode()
if statusCode > 300 || statusCode == 0 {
resBody["error"] = fmt.Sprintf("invalid status code: %d", statusCode)
h.WriteJSON(w, resBody, 500)
return
}
b := fres.Body()
clusterInfo := &elastic.ClusterInformation{}
err = json.Unmarshal(b, clusterInfo)
if err != nil {
panic(err)
}
resBody["version"] = clusterInfo.Version.Number
resBody["cluster_uuid"] = clusterInfo.ClusterUUID
resBody["cluster_name"] = clusterInfo.ClusterName
resBody["distribution"] = clusterInfo.Version.Distribution
//fetch cluster health info
freq.SetRequestURI(fmt.Sprintf("%s/_cluster/health", config.Endpoint))
fres.Reset()
err = api.GetFastHttpClient(testClientName).Do(freq, fres)
if err != nil {
resBody["error"] = fmt.Sprintf("error on get cluster health: %v", err)
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
healthInfo := &elastic.ClusterHealth{}
err = json.Unmarshal(fres.Body(), &healthInfo)
if err != nil {
resBody["error"] = fmt.Sprintf("error on decode cluster health info : %v", err)
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
resBody["status"] = healthInfo.Status
resBody["number_of_nodes"] = healthInfo.NumberOfNodes
resBody["number_of_data_nodes"] = healthInfo.NumberOf_data_nodes
resBody["active_shards"] = healthInfo.ActiveShards
h.WriteJSON(w, resBody, http.StatusOK)
}

View File

@ -136,11 +136,12 @@ func (module *Module) Stop() error {
type SetupRequest struct {
Cluster struct {
Host string `json:"host"`
Schema string `json:"schema"`
Endpoint string `json:"endpoint"`
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
Schema string `json:"schema"`
Endpoint string `json:"endpoint"`
Username string `json:"username"`
Password string `json:"password"`
Hosts []string `json:"hosts"`
} `json:"cluster"`
Skip bool `json:"skip"`
@ -796,8 +797,19 @@ func (module *Module) initializeTemplate(w http.ResponseWriter, r *http.Request,
return w.Write([]byte(request.Cluster.Password))
case "SETUP_SCHEME":
return w.Write([]byte(strings.Split(request.Cluster.Endpoint, "://")[0]))
case "SETUP_ENDPOINT":
return w.Write([]byte(strings.Split(request.Cluster.Endpoint, "://")[1]))
case "SETUP_ENDPOINTS":
endpoints := []string{request.Cluster.Endpoint}
for _, host := range request.Cluster.Hosts {
endpoint := fmt.Sprintf("%s://%s", request.Cluster.Schema, host)
if !util.StringInArray(endpoints, endpoint) {
endpoints = append(endpoints, endpoint)
}
}
endpointsStr := fmt.Sprintf("[%s]", strings.Join(endpoints, ", "))
return w.Write([]byte(endpointsStr))
case "SETUP_HOSTS":
hostsStr := fmt.Sprintf("[%s]", strings.Join(request.Cluster.Hosts, ", "))
return w.Write([]byte(hostsStr))
case "SETUP_TEMPLATE_NAME":
return w.Write([]byte(cfg1.TemplateName))
case "SETUP_INDEX_PREFIX":

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Alert, Button, Form, Icon, Input, Switch } from 'antd';
import { Alert, Button, Form, Icon, Input, Switch, Select } from 'antd';
import request from '@/utils/request';
import { formatMessage } from "umi/locale";
import TrimSpaceInput from '@/components/TrimSpaceInput';
@ -49,9 +49,9 @@ export default ({ onNext, form, formData, onFormDataChange }) => {
setTestLoading(true);
setTestStatus();
setTestError();
const { host, isTLS, isAuth, username, password } = values;
const { hosts, isTLS, isAuth, username, password } = values;
const body = {
host: host.trim(),
hosts: (hosts || []).map(host=>host.trim()),
schema: isTLS === true ? "https" : "http",
}
if (isAuth) {
@ -104,32 +104,41 @@ export default ({ onNext, form, formData, onFormDataChange }) => {
const onFormDataSave = () => {
const values = form.getFieldsValue();
const { host, isAuth, username, password } = form.getFieldsValue();
const { hosts, isAuth, username, password } = values;
onFormDataChange({
host: host.trim(), isAuth, username, password
hosts: (hosts || []).map(host=>host.trim()),
isAuth, username, password
})
onNext();
}
const validateHostsRule = (rule, value, callback) => {
let vals = value || [];
for(let i = 0; i < vals.length; i++) {
if (!/^[\w\.\-_~%]+(\:\d+)?$/.test(vals[i])) {
return callback(formatMessage({ id: 'guide.cluster.host.validate'}));
}
}
// validation passed
callback();
};
const { getFieldDecorator } = form;
return (
<Form {...formItemLayout} onSubmit={onSubmit} colon={false}>
<Form.Item label={formatMessage({ id: 'guide.cluster.host'})}>
{getFieldDecorator("host", {
initialValue: formData.host,
{getFieldDecorator("hosts", {
initialValue: formData.hosts,
rules: [
{
required: true,
message: formatMessage({ id: 'guide.cluster.host.required'}),
},
{
type: "string",
pattern: /^[\w\.\-_~%]+(\:\d+)?$/,
message: formatMessage({ id: 'guide.cluster.host.validate'}),
},
validator: validateHostsRule,
}
],
})(<TrimSpaceInput placeholder="127.0.0.1:9200" onChange={resetTestStatus}/>)}
})(<Select placeholder="127.0.0.1:9200" mode="tags" allowClear={true} onChange={resetTestStatus}/>)}
</Form.Item>
<Form.Item label="TLS">
{getFieldDecorator("isTLS", {

View File

@ -54,9 +54,12 @@ export default ({ onPrev, onNext, form, formData, onFormDataChange }) => {
const onCheck = async () => {
try {
setCheckLoading(true);
const { host, isTLS, isAuth, username, password } = formData;
const { hosts, isTLS, isAuth, username, password } = formData;
const host = hosts[0];
const cluster = {
endpoint: isTLS ? `https://${host}` : `http://${host}`
endpoint: isTLS ? `https://${host}` : `http://${host}`,
hosts: hosts,
schema: isTLS ? "https": "http",
}
if (isAuth) {
cluster.username = username
@ -110,14 +113,17 @@ export default ({ onPrev, onNext, form, formData, onFormDataChange }) => {
}
})
const {
host,
hosts,
isTLS,
isAuth,
username,
password,
} = formData;
const host = hosts[0];
const cluster = {
endpoint: isTLS ? `https://${host}` : `http://${host}`,
hosts: hosts,
schema: isTLS ? "https": "http"
};
if (isAuth) {
cluster.username = username;

View File

@ -83,7 +83,7 @@ export default ({ onPrev, onNext, form, formData, onFormDataChange }) => {
try {
setLoading(true);
const {
host,
hosts,
isTLS,
isAuth,
username,
@ -94,8 +94,11 @@ export default ({ onPrev, onNext, form, formData, onFormDataChange }) => {
credential_secret,
} = formData;
const body = {};
const host = hosts[0];
const cluster = {
endpoint: isTLS ? `https://${host}` : `http://${host}`,
hosts: hosts,
schema: isTLS ? "https" : "http",
};
if (isAuth) {
cluster.username = username;
@ -191,14 +194,16 @@ export default ({ onPrev, onNext, form, formData, onFormDataChange }) => {
setLoading(true);
const {
host,
hosts,
isTLS,
isAuth,
username,
password,
} = formData;
const host = hosts[0];
const cluster = {
endpoint: isTLS ? `https://${host}` : `http://${host}`,
hosts: hosts,
};
if (isAuth) {
cluster.username = username;

View File

@ -8,7 +8,7 @@ import {
Button,
Switch,
message,
Spin,
Spin, Select,
} from "antd";
import router from "umi/router";
@ -158,6 +158,7 @@ class ClusterForm extends React.Component {
let newVals = {
name: values.name,
host: values.host,
hosts: values.hosts,
credential_id:
values.credential_id !== MANUAL_VALUE
? values.credential_id
@ -237,7 +238,7 @@ class ClusterForm extends React.Component {
tryConnect = async (type) => {
const { dispatch, form } = this.props;
if (this.state.needAuth) {
if (type == "agent") {
if (type === "agent") {
this.setState({
...this.state,
credentialRequired: false,
@ -252,7 +253,7 @@ class ClusterForm extends React.Component {
}
}
let fieldNames = this.validateFieldNames;
if (type == "agent") {
if (type === "agent") {
fieldNames = this.agentValidateFieldNames;
}
setTimeout(() => {
@ -272,7 +273,7 @@ class ClusterForm extends React.Component {
schema: values.isTLS === true ? "https" : "http",
};
if (type == "agent") {
if (type === "agent") {
newVals = {
...newVals,
...{
@ -319,7 +320,7 @@ class ClusterForm extends React.Component {
});
this.clusterUUID = res.cluster_uuid;
}
if (type == "agent") {
if (type === "agent") {
this.setState({ btnLoadingAgent: false });
} else {
this.setState({ btnLoading: false });
@ -329,6 +330,17 @@ class ClusterForm extends React.Component {
}, 200);
};
validateHostsRule = (rule, value, callback) => {
let vals = value || [];
for(let i = 0; i < vals.length; i++) {
if (!/^[\w\.\-_~%]+(\:\d+)?$/.test(vals[i])) {
return callback(formatMessage({ id: "cluster.regist.form.verify.valid.endpoint" }));
}
}
// validation passed
callback();
};
render() {
const { getFieldDecorator } = this.props.form;
const formItemLayout = {
@ -354,6 +366,16 @@ class ClusterForm extends React.Component {
},
};
const { editValue, editMode } = this.props.clusterConfig;
//add host value to hosts field if it's empty
if(editValue.host){
if(!editValue.hosts){
editValue.hosts = [editValue.host];
}else{
if (!editValue.hosts.includes(editValue.host)) {
editValue.hosts.push(editValue.host);
}
}
}
return (
<PageHeaderWrapper>
<Card
@ -427,15 +449,11 @@ class ClusterForm extends React.Component {
id: "cluster.manage.label.cluster_host",
})}
>
{getFieldDecorator("host", {
initialValue: editValue.host,
{getFieldDecorator("hosts", {
initialValue: editValue.hosts,
rules: [
{
type: "string",
pattern: /^[\w\.\-_~%]+(\:\d+)?$/,
message: formatMessage({
id: "cluster.regist.form.verify.valid.endpoint",
}),
validator: this.validateHostsRule,
},
{
required: true,
@ -444,7 +462,7 @@ class ClusterForm extends React.Component {
}),
},
],
})(<TrimSpaceInput placeholder="127.0.0.1:9200" />)}
})(<Select placeholder="127.0.0.1:9200" mode="tags" />)}
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
{getFieldDecorator("version", {

View File

@ -103,7 +103,7 @@ const ClusterStep = ({ dispatch, history, query }) => {
username: values.username,
password: values.password,
},
host: values.host,
hosts: values.hosts,
credential_id:
values.credential_id !== MANUAL_VALUE
? values.credential_id
@ -142,6 +142,7 @@ const ClusterStep = ({ dispatch, history, query }) => {
version: clusterConfig.version,
distribution: clusterConfig.distribution,
host: clusterConfig.host,
hosts: clusterConfig.hosts,
location: clusterConfig.location,
credential_id:
clusterConfig.credential_id !== MANUAL_VALUE

View File

@ -64,7 +64,7 @@ export class ExtraStep extends React.Component {
return;
}
let newVals = {
host: initialValue.host,
hosts: initialValue?.hosts || [],
schema: initialValue.isTLS === true ? "https" : "http",
};
newVals = {

View File

@ -23,17 +23,33 @@ export class InitialStep extends React.Component {
needAuth: val,
});
};
handleEndpointChange = (event) => {
const val = event.target.value;
this.setState({
isPageTLS: isTLS(val)
})
handleEndpointChange = (value) => {
if(!value.length) {
return
}
const val = value[value.length - 1];
if(val.startsWith("http://") || val.startsWith("https://")){
this.props.form.setFieldsValue({ isTLS: isTLS(val)})
this.setState({
isPageTLS: isTLS(val)
})
}
};
isPageTLSChange = (val) => {
this.setState({
isPageTLS: val,
});
};
validateHostsRule = (rule, value, callback) => {
let vals = value || [];
for(let i = 0; i < vals.length; i++) {
if (!/^[\w\.\-_~%]+(\:\d+)?$/.test(vals[i])) {
return callback(formatMessage({ id: "cluster.regist.form.verify.valid.endpoint" }));
}
}
// validation passed
callback();
};
render() {
const {
form: { getFieldDecorator },
@ -88,10 +104,10 @@ export class InitialStep extends React.Component {
id: "cluster.manage.table.column.endpoint",
})}
>
{getFieldDecorator("host", {
initialValue: initialValue?.host || "",
{getFieldDecorator("hosts", {
initialValue: initialValue?.hosts || [],
normalize: (value) => {
return removeHttpSchema(value || "").trim()
return (value || []).map((v) => removeHttpSchema(v || "").trim());
},
validateTrigger: ["onChange", "onBlur"],
rules: [
@ -102,14 +118,10 @@ export class InitialStep extends React.Component {
}),
},
{
type: "string",
pattern: /^[\w\.\-_~%]+(\:\d+)?$/, //(https?:\/\/)?
message: formatMessage({
id: "cluster.regist.form.verify.valid.endpoint",
}),
},
validator: this.validateHostsRule,
}
],
})(<Input placeholder="127.0.0.1:9200" onChange={this.handleEndpointChange}/>)}
})(<Select placeholder="127.0.0.1:9200" mode="tags" allowClear={true} onChange={this.handleEndpointChange}/>)}
</Form.Item>
<Form.Item label="TLS">
{getFieldDecorator("isTLS", {

View File

@ -39,7 +39,7 @@ export const ResultStep = (props) => {
</Col>
<Col xs={24} sm={16}>
{clusterConfig?.host}
{clusterConfig?.hosts.map((host) => <div>{host}</div>)}
</Col>
</Row>
<Row>