\$\begingroup\$
\$\endgroup\$
2
This is my first Go project. I've written a CLI app to provision a database cluster on docker containers. Since it's a distributed database, you can configure per-node services. For example: to configure a 3-node cluster (version 7.1.4) with kv on node1, query/index on node2, and analytics on node3, run the following:
./cluster -v 7.1.4 -s n0:kv,n1:n1ql+index,n2:cbas
// main.go
package main
import (
"bytes"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"regexp"
"strings"
flag "github.com/spf13/pflag"
)
func main() {
log.SetFlags(0)
_, err: = exec.LookPath("docker")
if err != nil {
log.Fatal("Please install docker. Go to https://docs.docker.com/engine/install/")
}
makeDockerNetwork()
// TODO: support other linux OS besides ubuntu 20 for internal build
// TODO: support arm arch
version: = flag.StringP("version", "v", "", "CBS version")
noinit: = flag.Bool("noinit", false, "Do not initialize node")
services: = flag.StringP("services", "s", "", "Per node services")
indexStorage: = flag.StringP("index", "i", "plasma", "Index storage mode - plasma, memory_optimized")
username: = flag.StringP("username", "u", "admin", "Administrator username")
password: = flag.StringP("password", "p", "password", "Administrator password")
sampleBucket: = flag.StringP("bucket", "b", "", "Load sample bucket")
flag.Parse()
allNodeServices: = [] string {}
if *version == "" {
log.Fatal("Please provide CBS version")
} else {
re: = regexp.MustCompile(`^(\d\.\d\.\d)-(\d{4,5})$`)
if re.MatchString(*version) {
matches: = re.FindStringSubmatch(*version)
buildImage(matches[1], matches[2])
}
}
if !*noinit {
if *services == "" {
log.Fatal("Please specify per node services")
} else {
nodeServiceTuples: = strings.Split(*services, ",")
for _, t: = range nodeServiceTuples {
services: = strings.Replace(strings.Split(t, ":")[1], "+", ",", -1)
allNodeServices = append(allNodeServices, services)
}
}
}
port: = getNextPort()
name: = randomName()
makeNode(port, name + "0", *version)
waitTillNodeIsUp(port)
if *noinit {
os.Exit(0)
}
initFirstNode(port, allNodeServices[0], *username, *password, *indexStorage)
// Provision other nodes
if len(allNodeServices) > 1 {
IP: = getIP(name + "0")
allNodeStrings: = fmt.Sprintf("ns_1@%s,", IP)
for nodePort, i := port + 1, 1; i < len(allNodeServices); i, nodePort = i + 1, nodePort + 1 {
nodeServices: = allNodeServices[i]
nodeString: = addNode(port, nodePort, fmt.Sprintf("%s%d", name, i), *version, nodeServices, *username, *password)
allNodeStrings += nodeString
}
rebalance(port, allNodeStrings, *username, *password)
}
if *sampleBucket != "" {
httpDo(http.MethodPost,
fmt.Sprintf("http://localhost:%d/sampleBuckets/install", port),
bytes.NewBufferString(fmt.Sprintf("[\"%s\"]", *sampleBucket)),
Options {
user: *username,
pass: *password,
errorMessage: "install sample bucket failed",
status: http.StatusAccepted,
},
)
}
log.Printf("First node of cluster - %s - is ready on port %d\n", name + "0", port)
}
// cluster.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os/exec"
"strings"
"time"
)
const (
MAX_TRIES = 120
)
type Options struct {
user, pass, errorMessage string
status int
}
func httpDo(method, uri string, data *bytes.Buffer, opt Options)[] byte {
// cleanup before panic
defer func() {
if p: = recover();
p != nil {
cleanupContainers()
panic(p)
}
}()
client: = & http.Client {}
req, err: = http.NewRequest(method, uri, data)
if err != nil {
cleanupContainers()
log.Fatalf("%s\nNewRequest failed for %+v: %v", opt.errorMessage, req, err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if opt.user != "" && opt.pass != "" {
req.SetBasicAuth(opt.user, opt.pass)
}
req.Close = true
resp, err: = client.Do(req)
if err != nil {
cleanupContainers()
log.Fatalf("%s\nsend request failed %+v: %v", opt.errorMessage, req, err)
}
defer resp.Body.Close()
respBody, err: = io.ReadAll(resp.Body)
if err != nil {
cleanupContainers()
log.Fatalf("%s\ncould not read resp body for request %+v: %v", opt.errorMessage, req, err)
}
if resp.StatusCode != opt.status {
rb, err: = req.GetBody()
if err != nil {
cleanupContainers()
log.Fatalf("%s\ncould not get req body for request %+v: %v", opt.errorMessage, req, err)
}
reqBody, err: = io.ReadAll(rb)
if err != nil {
cleanupContainers()
log.Fatalf("%s\ncould not read req body for request %+v: %v", opt.errorMessage, req, err)
}
cleanupContainers()
log.Fatalf("%s\n%+v\nreqBody: %s\n%+v\nrespBody: %s", opt.errorMessage, req, string(reqBody), resp, string(respBody))
}
return respBody
}
func makeNode(port int, containerName string, version string) {
cmd: = exec.Command(
"docker", "run", "--rm", "-d",
"-p", fmt.Sprintf("%d:8091", port),
"-p", "8092-8096",
"-p", "11207",
"-p", "11210",
"-p", "18091-18096",
"--name", containerName,
"--privileged",
"--network=cb-net",
fmt.Sprintf("couchbase/server:%s", version),
)
if err: = cmd.Run();err != nil {
log.Fatalf("could not create node: %v", err)
}
runningContainers = append(runningContainers, containerName)
}
func waitTillNodeIsUp(port int) {
url: = fmt.Sprintf("http://localhost:%d", port)
for tries: = 1;tries <= MAX_TRIES;tries++{
resp, err: = http.Get(url)
if resp != nil && err != nil {
log.Printf("HTTP GET for waitTillNodeIsUp failed: %v", err)
time.Sleep(1 * time.Second)
continue
}
if resp != nil && resp.StatusCode == http.StatusOK {
return
}
time.Sleep(1 * time.Second)
}
cleanupContainers()
log.Fatal("node is not up in 2 min")
}
func initFirstNode(port int, services, username, password, storageMode string) {
httpDo(http.MethodPost,
fmt.Sprintf("http://localhost:%d/settings/indexes", port),
bytes.NewBufferString(url.Values {
"storageMode": {
storageMode
}
}.Encode()),
Options {
errorMessage: "Index configuration failed",
status: http.StatusOK,
},
)
httpDo(http.MethodPost,
fmt.Sprintf("http://localhost:%d/node/controller/setupServices", port),
bytes.NewBufferString(url.Values {
"services": {
services
}
}.Encode()),
Options {
errorMessage: "Setting up services failed",
status: http.StatusOK,
},
)
httpDo(http.MethodPost,
fmt.Sprintf("http://localhost:%d/settings/web", port),
bytes.NewBufferString(url.Values {
"username": {
username
},
"password": {
password
},
"port": {
"8091"
}
}.Encode()),
Options {
errorMessage: "Setting up username and password failed",
status: http.StatusOK,
},
)
// Set memory quotas
// n1ql and backup don't have quotas
form: = url.Values {}
form.Add("memoryQuota", "256")
form.Add("indexMemoryQuota", "256")
form.Add("cbasMemoryQuota", "1024")
form.Add("eventingMemoryQuota", "256")
form.Add("ftsMemoryQuota", "256")
httpDo(http.MethodPost,
fmt.Sprintf("http://localhost:%d/pools/default", port),
bytes.NewBufferString(form.Encode()),
Options {
user: username,
pass: password,
errorMessage: "Setting up memory quotas failed",
status: http.StatusOK,
},
)
}
func addNode(port, nodePort int, hostname, version, services, username, password string) string {
makeNode(nodePort, hostname, version)
waitTillNodeIsUp(nodePort)
IP: = getIP(hostname)
form: = url.Values {}
form.Add("hostname", IP)
form.Add("user", username)
form.Add("password", password)
form.Add("services", services)
httpDo(http.MethodPost,
fmt.Sprintf("http://localhost:%d/controller/addNode", port),
bytes.NewBufferString(form.Encode()),
Options {
user: username,
pass: password,
errorMessage: "Add node failed",
status: http.StatusOK,
},
)
return fmt.Sprintf("ns_1@%s,", IP)
}
func rebalance(port int, knownNodes, username, password string) {
nodes: = url.QueryEscape(strings.TrimSuffix(knownNodes, ","))
form: = url.Values {}
form.Add("knownNodes", nodes)
httpDo(http.MethodPost,
fmt.Sprintf("http://localhost:%d/controller/rebalance", port),
bytes.NewBufferString(strings.ReplaceAll(form.Encode(), "%25", "%")),
Options {
user: username,
pass: password,
errorMessage: "Rebalance failed",
status: http.StatusOK,
},
)
for tries: = 1;tries <= MAX_TRIES;tries++{
body: = httpDo(http.MethodGet,
fmt.Sprintf("http://localhost:%d/pools/default/rebalanceProgress", port),
bytes.NewBufferString(""),
Options {
user: username,
pass: password,
errorMessage: "Could not get rebalance progress",
status: http.StatusOK,
},
)
progress: = struct {
Status string `json:"status"`
} {}
if err: = json.NewDecoder(strings.NewReader(string(body))).Decode( & progress);err != nil {
cleanupContainers()
log.Fatalf("Could not decode rebalance progress: %v", err)
}
if progress.Status == "none" {
return
}
time.Sleep(1 * time.Second)
}
cleanupContainers()
log.Fatal("Could not rebalance in 2 min")
}
// misc.go
package main
import (
"fmt"
"log"
"math/rand"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
)
var (
runningContainers = [] string {}
)
func randomName() string {
rand.Seed(time.Now().UnixNano())
return words[rand.Intn(len(words))]
}
func getNextPort() int {
out, err: = exec.Command("docker",
"container",
"ls",
"--format",
"{{.Ports}}",
"--filter",
"status=running").Output()
if err != nil {
cleanupContainers()
log.Fatalf("could not list running containers: %v", err)
}
lastPort: = 9000
re: = regexp.MustCompile(`:9\d{3}`)
for _, line: = range strings.Split(string(out), "\n") {
matches: = re.FindStringSubmatch(line)
if len(matches) > 0 {
port, err: = strconv.Atoi(matches[0][1: ])
if err != nil {
continue // silently ignore error
}
if port > lastPort {
lastPort = port
}
}
}
return lastPort + 1
}
func getIP(containerID string) string {
out, err: = exec.Command("docker",
"inspect",
"-f",
"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}", containerID).Output()
if err != nil {
cleanupContainers()
log.Fatalf("could not get IP of container %s: %v", containerID, err)
}
return fmt.Sprint(strings.TrimSpace(string(out)))
}
// if network cb-net does not exist, create it
func makeDockerNetwork() {
cmd: = exec.Command("docker", "network", "inspect", "cb-net")
if err: = cmd.Run();err != nil {
cmd: = exec.Command("docker", "network", "create", "cb-net")
if err: = cmd.Run();err != nil {
log.Fatalf("could not create docker network cb-net: %v", err)
}
log.Println("created docker network cb-net")
}
}
func cleanupContainers() {
containers: = strings.Join(runningContainers[: ], " ")
cmd: = exec.Command(
"docker",
"stop",
containers,
)
if err: = cmd.Run();err != nil {
log.Fatalf("could not remove containers %s:\n%v", containers, err)
}
}
// internal_build.go
package main
import (
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
"text/template"
)
func buildImage(version, build string) {
output, err: = exec.Command("docker", "image", "ls").Output()
if err != nil {
log.Fatalf("could not list docker images: %s", err)
}
tempFile, err: = os.CreateTemp("/tmp/", "docker.file.*")
if err != nil {
log.Fatalf("could not create temp file: %s", err)
}
defer os.Remove(tempFile.Name())
if !strings.Contains(string(output), "couchbase/server") || !strings.Contains(string(output), fmt.Sprintf("%s-%s", version, build)) {
writeDockerfile(version, build, tempFile)
cmd: = exec.Command("docker", "build", ".", "-f", tempFile.Name(), "-t", fmt.Sprintf("couchbase/server:%s-%s", version, build))
stderr, err: = cmd.StderrPipe()
if err != nil {
log.Fatalf("could not build docker image: %s", err)
}
if err: = cmd.Start();
err != nil {
log.Fatalf("could not build docker image: %s", err)
}
errMessage, err: = io.ReadAll(stderr)
if err != nil {
log.Fatalf("could not build docker image: %s", err)
}
log.Println(string(errMessage))
if err: = cmd.Wait();
err != nil {
log.Fatalf("could not build docker image: %s", err)
}
}
}
func writeDockerfile(version, buildNum string, file * os.File) {
vars: = struct {
Package string
URL string
} {
Package: fmt.Sprintf("couchbase-server-enterprise_%s-%s-ubuntu20.04_amd64.deb", version, buildNum),
URL: fmt.Sprintf("http://builds.com/%s/%s", version, buildNum),
}
const dockerFile = `
FROM ubuntu:20.04
ENV PATH=$PATH:/opt/couchbase/bin:/opt/couchbase/bin/tools:/opt/couchbase/bin/install
RUN set -x && \
apt-get update && \
apt-get install -yq runit wget chrpath tzdata \
lsof lshw sysstat net-tools numactl bzip2 libtinfo5 && \
apt-get autoremove && apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN if [ ! -x /usr/sbin/runsvdir-start ]; then \
cp -a /etc/runit/2 /usr/sbin/runsvdir-start; \
fi
RUN groupadd -g 1000 couchbase && useradd couchbase -u 1000 -g couchbase -M
RUN set -x && \
export INSTALL_DONT_START_SERVER=1 && \
wget {{.URL}}/{{.Package}} && \
apt install -y ./{{.Package}} && \
rm -f ./{{.Package}} && \
apt-get autoremove && apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN mkdir -p /etc/service/couchbase-server/
RUN printf "#!/bin/sh \n\
exec 2>&1 \n\
cd /opt/couchbase \n\
mkdir -p var/lib/couchbase \n\
var/lib/couchbase/config \n\
var/lib/couchbase/data \n\
var/lib/couchbase/stats \n\
var/lib/couchbase/logs \n\
var/lib/moxi \n\
\n\
chown -R couchbase:couchbase var \n\
if [ \"\$(whoami)\" = \"couchbase\" ]; then \n\
exec /opt/couchbase/bin/couchbase-server -- -kernel global_enable_tracing false -noinput \n\
else \n\
exec chpst -ucouchbase /opt/couchbase/bin/couchbase-server -- -kernel global_enable_tracing false -noinput \n\
fi" > /etc/service/couchbase-server/run
RUN chmod a+x /etc/service/couchbase-server/run
RUN chown -R couchbase:couchbase /etc/service
RUN mkdir -p /etc/runit/runsvdir/default/couchbase-server/supervise \
&& chown -R couchbase:couchbase \
/etc/service \
/etc/runit/runsvdir/default/couchbase-server/supervise
RUN printf "#!/bin/sh \
echo \"Running in Docker container - 0ドル not available\"" > /usr/local/bin/dummy.sh
RUN chmod a+x /usr/local/bin/dummy.sh
RUN ln -s dummy.sh /usr/local/bin/iptables-save && \
ln -s dummy.sh /usr/local/bin/lvdisplay && \
ln -s dummy.sh /usr/local/bin/vgdisplay && \
ln -s dummy.sh /usr/local/bin/pvdisplay
RUN printf "#!/bin/bash \n\
set -e \n\
\
staticConfigFile=/opt/couchbase/etc/couchbase/static_config \n\
restPortValue=8091 \n\
function overridePort() { \n\
portName=\1ドル \n\
portNameUpper=\$(echo \$portName | awk '{print toupper(\0ドル)}') \n\
portValue=\${!portNameUpper} \n\
if [ \"\$portValue\" != \"\" ]; then \n\
if grep -Fq \"{\${portName},\" \${staticConfigFile} \n\
then \n\
echo \"Don't override port \${portName} because already available in \$staticConfigFile\" \n\
else \
echo \"Override port '\$portName' with value '\$portValue'\" \n\
echo \"{\$portName, \$portValue}.\" >> \${staticConfigFile} \n\
\
if [ \${portName} == \"rest_port\" ]; then \n\
restPortValue=\${portValue} \n\
fi \n\
fi \n\
fi \n\
} \n\
\n\
overridePort \"rest_port\" \n\
overridePort \"mccouch_port\" \n\
overridePort \"memcached_port\" \n\
overridePort \"query_port\" \n\
overridePort \"ssl_query_port\" \n\
overridePort \"fts_http_port\" \n\
overridePort \"moxi_port\" \n\
overridePort \"ssl_rest_port\" \n\
overridePort \"ssl_capi_port\" \n\
overridePort \"ssl_proxy_downstream_port\" \n\
overridePort \"ssl_proxy_upstream_port\" \n\
\n\
[[ \"\$1\" == \"couchbase-server\" ]] && { \n\
\n\
if [ \"\$(whoami)\" = \"couchbase\" ]; then \n\
\n\
if [ ! -w /opt/couchbase/var -o \n\
\$(find /opt/couchbase/var -maxdepth 0 -printf '%%u') != \"couchbase\" ]; then \n\
echo \"/opt/couchbase/var is not owned and writable by UID 1000\" \n\
echo \"Aborting as Couchbase Server will likely not run\" \n\
exit 1 \n\
fi \n\
fi \n\
echo \"Starting Couchbase Server -- Web UI available at http://ip:\$restPortValue\" \n\
echo \"and logs available in /opt/couchbase/var/lib/couchbase/logs\" \n\
exec /usr/sbin/runsvdir-start \n\
} \n\
\n\
exec \"\$@\" "> /entrypoint.sh
RUN chmod a+x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["couchbase-server"]
`
tmpl,
err: = template.New("internal").Parse(dockerFile)
if err != nil {
log.Fatalf("could not create template: %s", err)
}
err = tmpl.Execute(file, vars)
if err != nil {
log.Fatalf("could not execute template: %s", err)
}
}
// words.go
package main
var words = [...] string {
"amber",
"apron",
"arise",
"beach",
"beard",
"beast",
"blaze",
"blend",
"bless",
"bloom",
"bonus",
"braid",
"brick",
"brush",
"bumpy",
"bunny",
"chaos",
"chart",
"chess",
"chill",
"choir",
"clerk",
"coast",
"comet",
"coral",
"crate",
"crisp",
"ditch",
"drown",
"equip",
"erase",
"evoke",
"feast",
"fever",
"flame",
"flute",
"forge",
"glide",
"globe",
"grape",
"jeans",
"juice",
"lemon",
"lodge",
"logic",
"magic",
"march",
"opera",
"panda",
"panic",
"plump",
"pride",
"prize",
"proof",
"proud",
"quota",
"radar",
"ridge",
"rifle",
"route",
"sable",
"salad",
"scoop",
"seize",
"shame",
"shine",
"sloth",
"smile",
"spade",
"spark",
"spike",
"spoil",
"sport",
"spray",
"stall",
"straw",
"swamp",
"swarm",
"sweep",
"swing",
"thick",
"thigh",
"thorn",
"thump",
"tiger",
"toast",
"torch",
"towel",
"trust",
"virus",
"vocal",
"watch",
"weave",
"whale",
"whisk",
"wrist",
"yacht",
}
```
Toby Speight
87.1k14 gold badges104 silver badges322 bronze badges
-
1\$\begingroup\$ Please post the entire code in the original post. Links are not permissible; they eventually go stale. \$\endgroup\$Rish– Rish2023年06月01日 00:04:55 +00:00Commented Jun 1, 2023 at 0:04
-
\$\begingroup\$ Removed github link and posted the entire code. \$\endgroup\$user219820– user2198202023年06月01日 03:10:22 +00:00Commented Jun 1, 2023 at 3:10
lang-golang