Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions cluster-sample-topology.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# For more configuration options, see:
# https://github.com/naver/arcus-memcached/blob/master/docs/administration/commandline_args.md

servicecode: my-cluster # required
path: /home/arcus/arcus-memcached # required
zookeeper: zk1:2181,zk2:2181,zk3:2181 # required (comma-separated ensemble)

servers:
- address: cache1:11211 # required (host:port)
- address: cache2:11211
- address: cache3:11211
config: # optional - per-node override
options: "-l 192.168.1.3" # optional - override global options

global_config:
options: "-t 4 -c 1024 -b 1024 -B auto -m 64" # memcached command-line arguments

# Enterprise edition: every server must have a `group` block.
# Each group must have exactly one master and at most one slave.
# Community and enterprise servers cannot be mixed in the same cluster.
#
# servers:
# - address: cache1:11211
# group: { name: g1, role: master, port: 33533 }
# - address: cache2:11211
# group: { name: g1, role: slave, port: 33533 }
12 changes: 10 additions & 2 deletions cmd/cluster/deploy.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package cluster

import "github.com/spf13/cobra"
import (
"github.com/jam2in/arcusctl/internal/cluster"
"github.com/spf13/cobra"
)

var deployCmd = &cobra.Command{
Use: "deploy <version> <topology.yml>",
Short: "Deploy a new Arcus cluster",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
// TODO: deploy ๊ตฌํ˜„
version := args[0]
topologyPath := args[1]

if err := cluster.Deploy(version, topologyPath); err != nil {
panic(err)
}
},
}
150 changes: 150 additions & 0 deletions internal/cluster/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package cluster

import (
"fmt"
"os"
"text/tabwriter"

"github.com/jam2in/arcusctl/internal"
"github.com/jam2in/arcusctl/internal/store"
"github.com/jam2in/arcusctl/internal/topology"
)

func Deploy(version string, topoPath string) error {
topo, topologyBytes, edition, err := prepareTopology(topoPath)
if err != nil {
return err
}

printPlan(topo, version, edition)
if !internal.Confirm("Proceed with deployment? (y/N): ") {
fmt.Println("Aborted.")
return nil
}

localTarPath, err := ensureDownloaded(version, edition)
if err != nil {
return err
}

installed, err := installServers(topo, version, localTarPath, edition)
if err != nil {
printRecoveryGuide(topo, installed, version, edition)
return err
}

if err := registerZNodes(topo, edition); err != nil {
printRecoveryGuide(topo, installed, version, edition)
return fmt.Errorf("register ZNodes in ZooKeeper: %w", err)
}

if err := store.SaveCluster(topo.ServiceCode, version, topologyBytes); err != nil {
printRecoveryGuide(topo, installed, version, edition)
return fmt.Errorf("save metadata: %w", err)
}

fmt.Printf(
"Arcus cluster %q deployed. Run 'cluster start %s' to launch.\n",
topo.ServiceCode, topo.ServiceCode,
)
return nil
}

func prepareTopology(topoPath string) (*topology.ClusterTopology, []byte, topology.ClusterEdition, error) {
topo, rawData, err := topology.LoadCluster(topoPath)
if err != nil {
return nil, nil, "", err
}

if err := topo.Validate(); err != nil {
return nil, nil, "", err
}

if store.ClusterExists(topo.ServiceCode) {
return nil, nil, "", fmt.Errorf(
"arcus cluster %q already exists",
topo.ServiceCode,
)
}

edition, err := topo.Edition()
if err != nil {
return nil, nil, "", err
}

exists, err := clusterExistsInZK(topo.ZooKeeper, topo.ServiceCode, edition)
if err != nil {
return nil, nil, "", err
}

if exists {
return nil, nil, "", fmt.Errorf(
"arcus cluster %q already exists in ZooKeeper",
topo.ServiceCode,
)
}

return topo, rawData, edition, nil
}

func printPlan(
topo *topology.ClusterTopology,
version string,
edition topology.ClusterEdition,
) {
fmt.Printf(
"Arcus cluster %q will be deployed (edition: %s, version: %s)\n\n",
topo.ServiceCode, edition, version,
)

installPath := memcachedInstallPath(topo.Path, version)

writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)

if edition == topology.EnterpriseEdition {
fmt.Fprintln(writer, "GROUP\tROLE\tADDRESS\tDIRECTORY")
fmt.Fprintln(writer, "\t\t\t")
for _, server := range topo.Servers {
fmt.Fprintf(writer, "%s\t%s\t%s\t%s\n",
server.Group.Name, server.Group.Role, server.Address, installPath)
}
} else {
fmt.Fprintln(writer, "ADDRESS\tDIRECTORY")
fmt.Fprintln(writer, "\t")
for _, server := range topo.Servers {
fmt.Fprintf(writer, "%s\t%s\n",
server.Address, installPath)
}
}

writer.Flush()

fmt.Println()
fmt.Println("Attention:")
fmt.Println(" 1. If the topology is not what you expected, check your yaml file.")
fmt.Println(" 2. Please confirm there is no port/directory conflicts in same host.")
}

func printRecoveryGuide(
topo *topology.ClusterTopology,
installed []topology.CacheServer,
version string,
edition topology.ClusterEdition,
) {
fmt.Println("\nDeployment failed. Manual recovery required.")

if len(installed) > 0 {
fmt.Println("The following servers have been partially installed.")
for _, s := range installed {
fmt.Printf(" - %s\n", s.Host())
}
cleanupPath := memcachedInstallPath(topo.Path, version)
fmt.Println("\nTo clean up, manually run on each server:")
fmt.Printf(" $ rm -rf %s\n", cleanupPath)
}

root := rootForEdition(edition)
fmt.Println("\nZooKeeper nodes may have been created under:")
fmt.Printf(" %s/{cache_list, client_list, cache_server_mapping}/%s ...\n", root, topo.ServiceCode)
fmt.Println("They are left in place. To retry from a clean state, remove them.")
}
55 changes: 55 additions & 0 deletions internal/cluster/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cluster

import (
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/jam2in/arcusctl/internal"
"github.com/jam2in/arcusctl/internal/topology"
)

const arcusDownloadURLTemplate = "https://github.com/naver/arcus-memcached/releases/download/%s/arcus-memcached-%s.tar.gz"

func ensureDownloaded(version string, edition topology.ClusterEdition) (string, error) {
dir := imageDir(edition)

if err := os.MkdirAll(dir, 0755); err != nil {
return "", fmt.Errorf("create image dir: %w", err)
}

filename := fmt.Sprintf("arcus-memcached-%s.tar.gz", version)
localTarPath := filepath.Join(dir, filename)

// check if the file already exists
// enterprise edition may not be downloadable, so we check for existence first
if _, err := os.Stat(localTarPath); err == nil {
fmt.Printf("Using existing file: %s\n", localTarPath)
return localTarPath, nil
}

if edition == topology.EnterpriseEdition {
return "", fmt.Errorf("enterprise archive not found: place %s in %s", filename, dir)
}

url := fmt.Sprintf(arcusDownloadURLTemplate, version, version)
fmt.Printf("Downloading %s...\n", url)

cmd := exec.Command("wget", "-q", "-O", localTarPath, url)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
_ = os.Remove(localTarPath)
return "", fmt.Errorf("download %q failed: %w", version, err)
}

return localTarPath, nil
}

func imageDir(edition topology.ClusterEdition) string {
if edition == topology.EnterpriseEdition {
return filepath.Join(internal.Config.Home, "images", "arcus-enterprise")
}
return filepath.Join(internal.Config.Home, "images", "arcus-community")
}
Loading
Loading