From 20b415bc87f239965ad55099307b4bfdb40ee7c9 Mon Sep 17 00:00:00 2001 From: Vasiliy Ostanin Date: Fri, 31 May 2019 17:02:30 +0500 Subject: [PATCH] init --- .gitignore | 4 + .goreleaser.yml | 26 +++++ cmd/backup.go | 246 +++++++++++++++++++++++++++++++++++++++++++++ cmd/restore.go | 85 ++++++++++++++++ go.mod | 18 ++++ go.sum | 65 ++++++++++++ readme.md | 27 +++++ swarm-evacuator.go | 27 +++++ 8 files changed, 498 insertions(+) create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 cmd/backup.go create mode 100644 cmd/restore.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 readme.md create mode 100644 swarm-evacuator.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa4f920 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +*.iml +.idea +*.json diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..deb0f93 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,26 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: + # you may remove this if you don't use vgo + - go mod download +builds: +- env: + - CGO_ENABLED=0 +archives: +- replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/cmd/backup.go b/cmd/backup.go new file mode 100644 index 0000000..27a5c83 --- /dev/null +++ b/cmd/backup.go @@ -0,0 +1,246 @@ +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "time" + + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/pkg/errors" + + "github.com/spf13/cobra" + + dockertypes "github.com/docker/docker/api/types" + dockerclient "github.com/docker/docker/client" +) + +type BackupStruct struct { + Networks map[string]dockertypes.NetworkCreate + Services map[string]swarm.ServiceSpec + Secrets map[string]swarm.SecretSpec + Configs map[string]swarm.ConfigSpec +} + +type Evacuation struct { + cli *dockerclient.Client + Backup BackupStruct +} + +func Backup(cmd *cobra.Command, args []string) { + dest := args[0] + + err := performBackup(dest) + if err != nil { + panic(err) + } +} + +func performBackup(dest string) error { + e := Evacuation{ + Backup: BackupStruct{}, + } + + cli, err := dockerclient.NewEnvClient() + if err != nil { + return err + } + e.cli = cli + + bg := context.Background() + + networks, err := cli.NetworkList(bg, dockertypes.NetworkListOptions{}) + if err != nil { + return err + } + + e.Backup.Networks = map[string]dockertypes.NetworkCreate{} + networkIdNames := map[string]string{} + skipNetworks := map[string]bool{"bridge": true, "docker_gwbridge": true, "ingress": true, "host": true, "none": true} + + for _, network := range networks { + spec := dockertypes.NetworkCreate{ + Driver: network.Driver, + EnableIPv6: network.EnableIPv6, + IPAM: &network.IPAM, + Internal: network.Internal, + Attachable: network.Attachable, + Options: network.Options, + Labels: network.Labels, + } + + if skipNetworks[network.Name] { + continue + } + + networkIdNames[network.ID] = network.Name + e.Backup.Networks[network.Name] = spec + } + + // services + services, err := cli.ServiceList(bg, dockertypes.ServiceListOptions{}) + if err != nil { + return err + } + e.Backup.Services = map[string]swarm.ServiceSpec{} + + services, err = cli.ServiceList(bg, dockertypes.ServiceListOptions{}) + if err != nil { + return err + } + e.Backup.Services = map[string]swarm.ServiceSpec{} + + for _, service := range services { + if service.Spec.Name == "evacuation" { + continue + } + + serviceSpec := service.Spec + for i, n := range serviceSpec.Networks { + serviceSpec.Networks[i].Target = networkIdNames[n.Target] + } + + for i, n := range serviceSpec.TaskTemplate.Networks { + serviceSpec.TaskTemplate.Networks[i].Target = networkIdNames[n.Target] + } + + for i, _ := range serviceSpec.TaskTemplate.ContainerSpec.Secrets { + serviceSpec.TaskTemplate.ContainerSpec.Secrets[i].SecretID = "" + } + + for i, _ := range serviceSpec.TaskTemplate.ContainerSpec.Configs { + serviceSpec.TaskTemplate.ContainerSpec.Configs[i].ConfigID = "" + } + + e.Backup.Services[serviceSpec.Name] = serviceSpec + } + + err = e.LoadSecretsData() + if err != nil { + return err + } + + bjson, err := json.MarshalIndent(e.Backup, "", " ") + if err != nil { + return err + } + + return ioutil.WriteFile(dest, bjson, os.FileMode(0600)) +} + +func (e *Evacuation) LoadSecretsData() error { + bg := context.Background() + secretReferences := []*swarm.SecretReference{} + + info, err := e.cli.Info(bg) + if err != nil { + return err + } + + // secrets + e.Backup.Secrets = map[string]swarm.SecretSpec{} + secrets, err := e.cli.SecretList(bg, dockertypes.SecretListOptions{}) + for _, secret := range secrets { + e.Backup.Secrets[secret.Spec.Name] = secret.Spec + secretReferences = append(secretReferences, &swarm.SecretReference{ + SecretName: secret.Spec.Name, + SecretID: secret.ID, + File: &swarm.SecretReferenceFileTarget{ + Name: secret.Spec.Name, + UID: "0", + GID: "0", + Mode: os.FileMode(020), + }, + }) + } + + // configs + e.Backup.Configs = map[string]swarm.ConfigSpec{} + configs, err := e.cli.ConfigList(bg, dockertypes.ConfigListOptions{}) + for _, config := range configs { + e.Backup.Configs[config.Spec.Name] = config.Spec + } + + loaderSpec := swarm.ServiceSpec{ + TaskTemplate: swarm.TaskSpec{ + Placement: &swarm.Placement{ + Constraints: []string{fmt.Sprintf("node.id==%s", info.Swarm.NodeID)}, + }, + ContainerSpec: &swarm.ContainerSpec{ + Image: "busybox", + Command: []string{"sleep", "100000"}, + Secrets: secretReferences, + }, + }, + } + loaderSpec.Name = "evacuation" + + _ = e.cli.ServiceRemove(bg, "evacuation") + + service, err := e.cli.ServiceCreate(bg, loaderSpec, dockertypes.ServiceCreateOptions{}) + if err != nil { + return err + } + + // "com.docker.swarm.service.name" + containerFilter := filters.NewArgs() + containerFilter.Add("label", fmt.Sprintf("com.docker.swarm.service.id=%s", service.ID)) + + var container *dockertypes.Container + tries := 0 + + for { + if tries > 10 { + return errors.New("failed to create export container") + } + containers, _ := e.cli.ContainerList(bg, dockertypes.ContainerListOptions{Filters: containerFilter}) + if len(containers) > 0 { + container = &containers[0] + break + } + + tries += 1 + time.Sleep(time.Second * 5) + } + + fmt.Printf("evac container id: %s\n", container.ID) + for i, s := range e.Backup.Secrets { + fmt.Printf("loading %v\n", fmt.Sprintf("/run/secrets/%s", s.Name)) + + id, err := e.cli.ContainerExecCreate(bg, container.ID, dockertypes.ExecConfig{ + AttachStdout: true, + Detach: false, + Tty: true, + Cmd: []string{"cat", fmt.Sprintf("/run/secrets/%s", s.Name)}, + }) + + if err != nil { + return err + } + + containerConn, err := e.cli.ContainerExecAttach(bg, id.ID, dockertypes.ExecStartCheck{Detach: false, Tty: true}) + if err != nil { + return err + } + + defer containerConn.Close() + + buf := new(bytes.Buffer) + + _, err = containerConn.Reader.WriteTo(buf) + if err != nil { + return err + } + + containerConn.Close() + + s.Data = buf.Bytes() + e.Backup.Secrets[i] = s + } + + return nil +} diff --git a/cmd/restore.go b/cmd/restore.go new file mode 100644 index 0000000..c22fd5a --- /dev/null +++ b/cmd/restore.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "context" + "encoding/json" + "io/ioutil" + + "github.com/docker/docker/api/types" + "github.com/spf13/cobra" + + dockerclient "github.com/docker/docker/client" +) + +func Restore(cmd *cobra.Command, args []string) { + src := args[0] + + err := performRestore(src) + if err != nil { + panic(err) + } +} + +func performRestore(src string) error { + b, err := ioutil.ReadFile(src) + if err != nil { + return err + } + + e := Evacuation{} + cli, err := dockerclient.NewEnvClient() + if err != nil { + return err + } + e.cli = cli + + bg := context.Background() + + err = json.Unmarshal(b, &e.Backup) + if err != nil { + return err + } + + for name, net := range e.Backup.Networks { + _, err := e.cli.NetworkCreate(bg, name, net) + if err != nil { + return err + } + } + + secretNameId := map[string]string{} + for n, s := range e.Backup.Secrets { + r, err := e.cli.SecretCreate(bg, s) + if err != nil { + return err + } + + secretNameId[n] = r.ID + } + + configNameId := map[string]string{} + for n, c := range e.Backup.Configs { + r, err := e.cli.ConfigCreate(bg, c) + if err != nil { + return err + } + + configNameId[n] = r.ID + } + + for _, service := range e.Backup.Services { + for is, secret := range service.TaskTemplate.ContainerSpec.Secrets { + service.TaskTemplate.ContainerSpec.Secrets[is].SecretID = secretNameId[secret.SecretName] + } + for ic, config := range service.TaskTemplate.ContainerSpec.Configs { + service.TaskTemplate.ContainerSpec.Configs[ic].ConfigID = configNameId[config.ConfigName] + } + + _, err = e.cli.ServiceCreate(bg, service, types.ServiceCreateOptions{}) + if err != nil { + return err + } + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f5e70fa --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module swarm-backup + +require ( + github.com/Microsoft/go-winio v0.4.12 // indirect + github.com/davecgh/go-spew v1.1.1 + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v1.13.1 + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/gogo/protobuf v1.2.1 // indirect + github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/pkg/errors v0.8.1 + github.com/spf13/cobra v0.0.4 + golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect +) + +replace github.com/docker/docker v1.13.1 => github.com/docker/engine v0.0.0-20180718150940-a3ef7e9a9bda diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e357c16 --- /dev/null +++ b/go.sum @@ -0,0 +1,65 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc= +github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.0.0-20170601211448-f5ec1e2936dc/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= +github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/engine v0.0.0-20180718150940-a3ef7e9a9bda h1:v1VUX0+ILrFSsGTp2FUvfgHSiQ6wmI1NnCho1MQ9CYU= +github.com/docker/engine v0.0.0-20180718150940-a3ef7e9a9bda/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.4 h1:S0tLZ3VOKl2Te0hpq8+ke0eSJPfCnNTPiDlsfwi1/NE= +github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0360a4f --- /dev/null +++ b/readme.md @@ -0,0 +1,27 @@ +swarm backup +==== + +Creates backup of services, secrets, configs, networks into json file with possibility to restore into different docker swarm cluster. + +By default uses `DOCKER_HOST` and other envs (just like docker/cli) + +Usage: + +``` +swarm-backup backup b.json +swarm-backup restore b.json +``` + +Notes: +--- + +Internal networks, such as: +- bridge +- docker_gwbridge +- ingress + +are not backing up. + +Should be run on active manager. + +Backup process creates new service `evacuation` to get secrets data on the current swarm manager. diff --git a/swarm-evacuator.go b/swarm-evacuator.go new file mode 100644 index 0000000..e903782 --- /dev/null +++ b/swarm-evacuator.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/spf13/cobra" + "swarm-backup/cmd" +) + +func main() { + var cmdBackup = &cobra.Command{ + Use: "backup [backup.json]", + Short: "Backup current state of swarm", + Args: cobra.MinimumNArgs(1), + Run: cmd.Backup, + } + + var cmdRestore = &cobra.Command{ + Use: "restore [backup.json]", + Short: "Restore swarm services from backup", + Args: cobra.MinimumNArgs(1), + Run: cmd.Restore, + } + + var rootCmd = &cobra.Command{Use: ""} + rootCmd.AddCommand(cmdBackup, cmdRestore) + + rootCmd.Execute() +}