├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── c.conf ├── config ├── config.go └── pgconfig.go ├── main.go ├── monitor ├── action.go ├── action_test.go ├── decision.go ├── decision_test.go └── mock │ └── mock.go ├── site ├── .gitignore ├── .nanofile ├── Boxfile ├── Gemfile ├── Gemfile.lock ├── README.md ├── config.rb ├── data │ ├── docs_index.yml │ └── project_info.yml ├── helpers │ └── nav_helpers.rb └── source │ ├── docs │ ├── index.html.md │ ├── test-1.html.md │ ├── test-2 │ │ ├── index.html.md │ │ ├── sub1.html.md │ │ ├── sub2 │ │ │ ├── index.html.md │ │ │ ├── sub2-sub1.html.md │ │ │ └── sub2-sub2.html.md │ │ └── sub3.html.md │ └── test-3.html.md │ ├── images │ ├── background.png │ ├── favicon.png │ └── middleman.png │ ├── index.html.haml │ ├── javascripts │ ├── all.js │ ├── highlight.min.js │ └── jquery-2.1.4.min.js │ ├── layouts │ ├── landing.haml │ └── layout.haml │ ├── partials │ ├── _README.md │ ├── _gh-btns.haml │ ├── _nb-foot.haml │ ├── _nb-head.haml │ └── svgs │ │ ├── _nb-logo.haml │ │ ├── _project-logo.svg │ │ └── _yoke.haml │ └── stylesheets │ ├── _media-queries.scss │ ├── _mixins.scss │ ├── _normalize.css │ ├── _syntax.scss │ └── all.scss ├── state ├── bounce.go ├── mock │ └── mock.go ├── rpc.go ├── state.go └── state_test.go ├── update_mocks.sh └── yokeadm ├── commands ├── clusterList.go ├── commands.go └── memberDemote.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # 27 | .DS_Store 28 | TODO 29 | status 30 | yoke 31 | yokeamd/yokeamd 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: true 3 | 4 | services: 5 | - postgresql 6 | 7 | env: 8 | - PATH=$PATH:/usr/lib/postgresql/9.4/bin 9 | - PATH=$PATH:/usr/lib/postgresql/9.3/bin 10 | - PATH=$PATH:/usr/lib/postgresql/9.2/bin 11 | - PATH=$PATH:/usr/lib/postgresql/9.1/bin 12 | 13 | go: 14 | - 1.5 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![yoke logo](http://nano-assets.gopagoda.io/readme-headers/yoke.png)](http://nanobox.io/open-source#yoke) 2 | [![Build Status](https://travis-ci.org/nanopack/yoke.svg)](https://travis-ci.org/nanopack/yoke) 3 | 4 | Yoke is a Postgres redundancy/auto-failover solution that provides a high-availability PostgreSQL cluster that's simple to manage. 5 | 6 | 7 | ### Requirements 8 | 9 | Yoke has the following requirements/dependencies to run: 10 | 11 | - A 3-server cluster consisting of a 'primary', 'secondary', and 'monitor' node 12 | - 'primary' & 'secondary' nodes need ssh connections between each other (w/o passwords) 13 | - 'primary' & 'secondary' nodes need rsync (or some alternative sync_command) installed 14 | - 'primary' & 'secondary' nodes should have postgres installed under a postgres user, and in the `path`. Yoke tries calling 'postgres' and 'pg_ctl' 15 | - 'primary' & 'secondary' nodes run postgres as a child process so it should not be started independently 16 | 17 | Each node in the cluster requires its own config.ini file with the following options (provided values are defaults): 18 | 19 | ```ini 20 | [config] 21 | # the IP which this node will broadcast to other nodes 22 | advertise_ip= 23 | # the port which this node will broadcast to other nodes 24 | advertise_port=4400 25 | # the directory where postgresql was installed 26 | data_dir=/data 27 | # delay before node decides what to do with postgresql instance 28 | decision_timeout=30 29 | # log verbosity (trace, debug, info, warn error, fatal) 30 | log_level=warn 31 | # REQUIRED - the IP:port combination of all nodes that are to be in the cluster (e.g. 'role=m.y.i.p:4400') 32 | primary= 33 | secondary= 34 | monitor= 35 | # SmartOS REQUIRED - either 'primary', 'secondary', or 'monitor' (the cluster needs exactly one of each) 36 | role= 37 | # the postgresql port 38 | pg_port=5432 39 | # the directory where node status information is stored 40 | status_dir=./status 41 | # the command you would like to use to sync the data from this node to the other when this node is master 42 | sync_command=rsync -ae "ssh -o StrictHostKeyChecking=no" --delete {{local_dir}} {{slave_ip}}:{{slave_dir}} 43 | 44 | [vip] 45 | # Virtual Ip you would like to use 46 | ip= 47 | # Command to use when adding the vip. This will be called as {{add_command}} {{vip}} 48 | add_command= 49 | # Command to use when removing the vip. This will be called as {{remove_command}} {{vip}} 50 | remove_command= 51 | 52 | [role_change] 53 | # When this nodes role changes we will call the command with the new role as its arguement '{{command}} {{(master|slave|single}))' 54 | command= 55 | ``` 56 | 57 | 58 | ### Startup 59 | Once all configurations are in place, start yoke by running: 60 | 61 | ``` 62 | ./yoke ./primary.ini 63 | ``` 64 | 65 | **Note:** The ini file can be named anything and reside anywhere. All Yoke needs is the /path/to/config.ini on startup. 66 | 67 | 68 | ### Yoke CLI - yokeadm 69 | 70 | Yoke comes with its own CLI, yokeadm, that allows for limited introspection into the cluster. 71 | 72 | #### Building the CLI: 73 | 74 | ``` 75 | cd ./yokeadm 76 | go build 77 | ./yokeadm 78 | ``` 79 | 80 | ##### Usage: 81 | 82 | ``` 83 | yokeadm (: OR ) [GLOBAL FLAG] [SUB FLAGS] 84 | ``` 85 | 86 | ##### Available Commands: 87 | 88 | - list : Returns status information for all nodes in the cluster 89 | - demote : Advises a node to demote 90 | 91 | ### Documentation 92 | 93 | Complete documentation is available on [godoc](http://godoc.org/github.com/nanopack/yoke). 94 | 95 | 96 | ### Licence 97 | 98 | Mozilla Public License Version 2.0 99 | 100 | [![open source](http://nano-assets.gopagoda.io/open-src/nanobox-open-src.png)](http://nanobox.io/open-source) 101 | -------------------------------------------------------------------------------- /c.conf: -------------------------------------------------------------------------------- 1 | [config] 2 | log_level=debug 3 | monitor=127.0.0.1:4000 4 | primary=127.0.0.1:4001 5 | secondary=127.0.0.1:4002 6 | 7 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // 2 | package config 3 | 4 | import ( 5 | "net" 6 | "os" 7 | "os/exec" 8 | "os/user" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/jcelliott/lumber" 13 | "github.com/vaughan0/go-ini" 14 | ) 15 | 16 | // Config is the struct of all global configuration data 17 | // it is set by a config file that is the first arguement 18 | // given to the exec 19 | type Config struct { 20 | Role string 21 | AdvertiseIp string 22 | AdvertisePort int 23 | PGPort int 24 | Monitor string 25 | Primary string 26 | Secondary string 27 | DataDir string 28 | StatusDir string 29 | SyncCommand string 30 | DecisionTimeout int 31 | Vip string 32 | VipAddCommand string 33 | VipRemoveCommand string 34 | RoleChangeCommand string 35 | SystemUser string 36 | } 37 | 38 | // establish constants 39 | // these are singleton values that are used throughout 40 | // the package. 41 | var ( 42 | 43 | // 44 | Conf = Config{ 45 | AdvertisePort: 4400, 46 | PGPort: 5432, 47 | DataDir: "/data/", 48 | StatusDir: "./status/", 49 | SyncCommand: "rsync -a --delete {{local_dir}} {{slave_ip}}:{{slave_dir}}", 50 | DecisionTimeout: 10, 51 | SystemUser: SystemUser(), 52 | } 53 | 54 | // 55 | Log = lumber.NewConsoleLogger(lumber.INFO) 56 | ) 57 | 58 | // init Initializeds the config file and the other constants 59 | func Init(path string) { 60 | 61 | // 62 | file, err := ini.LoadFile(path) 63 | if err != nil { 64 | Log.Error("[config.init] Failed to load config file - ", err) 65 | os.Exit(1) 66 | } 67 | 68 | // no conversion required for strings. 69 | if role, ok := file.Get("config", "role"); ok { 70 | Conf.Role = role 71 | } 72 | 73 | if dDir, ok := file.Get("config", "data_dir"); ok { 74 | Conf.DataDir = dDir 75 | } 76 | 77 | // make sure the datadir ends with a slash this should make it easier to handle 78 | if !strings.HasSuffix(Conf.DataDir, "/") { 79 | Conf.DataDir = Conf.DataDir + "/" 80 | } 81 | 82 | if sDir, ok := file.Get("config", "status_dir"); ok { 83 | Conf.StatusDir = sDir 84 | } 85 | 86 | if sMonitor, ok := file.Get("config", "monitor"); ok { 87 | Conf.Monitor = sMonitor 88 | } 89 | if sPrimary, ok := file.Get("config", "primary"); ok { 90 | Conf.Primary = sPrimary 91 | } 92 | if sSecondary, ok := file.Get("config", "secondary"); ok { 93 | Conf.Secondary = sSecondary 94 | } 95 | 96 | if !strings.HasSuffix(Conf.StatusDir, "/") { 97 | Conf.StatusDir = Conf.StatusDir + "/" 98 | } 99 | 100 | if sync, ok := file.Get("config", "sync_command"); ok { 101 | Conf.SyncCommand = sync 102 | } 103 | 104 | if ip, ok := file.Get("config", "advertise_ip"); ok { 105 | Conf.AdvertiseIp = ip 106 | } 107 | 108 | if vip, ok := file.Get("vip", "ip"); ok { 109 | Conf.Vip = vip 110 | } 111 | if vipAddCommand, ok := file.Get("vip", "add_command"); ok { 112 | Conf.VipAddCommand = vipAddCommand 113 | } 114 | if vipRemoveCommand, ok := file.Get("vip", "remove_command"); ok { 115 | Conf.VipRemoveCommand = vipRemoveCommand 116 | } 117 | 118 | if rcCommand, ok := file.Get("role_change", "command"); ok { 119 | Conf.RoleChangeCommand = rcCommand 120 | } 121 | 122 | parseInt(&Conf.AdvertisePort, file, "config", "advertise_port") 123 | parseInt(&Conf.PGPort, file, "config", "pg_port") 124 | parseInt(&Conf.DecisionTimeout, file, "config", "decision_timeout") 125 | 126 | if logLevel, ok := file.Get("config", "Log_level"); ok { 127 | switch logLevel { 128 | case "TRACE", "trace": 129 | Log.Level(lumber.TRACE) 130 | case "DEBUG", "debug": 131 | Log.Level(lumber.DEBUG) 132 | case "INFO", "info": 133 | Log.Level(lumber.INFO) 134 | case "WARN", "warn": 135 | Log.Level(lumber.WARN) 136 | case "ERROR", "error": 137 | Log.Level(lumber.ERROR) 138 | case "FATAL", "fatal": 139 | Log.Level(lumber.FATAL) 140 | } 141 | } 142 | confirmPeers() 143 | confirmRole() 144 | confirmAdvertiseIp() 145 | confirmAdvertisePort() 146 | 147 | } 148 | 149 | func confirmPeers() { 150 | if Conf.Monitor == "" || Conf.Primary == "" || Conf.Secondary == "" { 151 | Log.Fatal("I need connection Credentials for monitor, primary and secondary") 152 | Log.Close() 153 | os.Exit(1) 154 | } 155 | } 156 | 157 | func confirmRole() { 158 | if Conf.Role == "" { 159 | Conf.Role = getRole() 160 | } 161 | if Conf.Role != "monitor" && Conf.Role != "primary" && Conf.Role != "secondary" { 162 | Log.Fatal("I could not find the appropriate role: ", Conf.Role) 163 | Log.Close() 164 | os.Exit(1) 165 | } 166 | } 167 | 168 | func confirmAdvertiseIp() { 169 | if Conf.AdvertiseIp == "" || Conf.AdvertiseIp == "0.0.0.0" { 170 | getAdvertiseData() 171 | } 172 | if Conf.AdvertiseIp == "" || Conf.AdvertiseIp == "0.0.0.0" { 173 | Log.Fatal("I could not find the appropriate AdvertiseIP: ", Conf.AdvertiseIp) 174 | Log.Close() 175 | os.Exit(1) 176 | } 177 | } 178 | 179 | func confirmAdvertisePort() { 180 | if Conf.AdvertisePort == 0 { 181 | Log.Fatal("I could not find the appropriate Port to listen on (port:'0').") 182 | Log.Close() 183 | os.Exit(1) 184 | } 185 | } 186 | 187 | func getRole() string { 188 | ifaces, err := net.Interfaces() 189 | if err != nil { 190 | return "" 191 | } 192 | // handle err 193 | for _, i := range ifaces { 194 | addrs, _ := i.Addrs() 195 | // handle err 196 | for _, addr := range addrs { 197 | str := strings.Split(addr.String(), "/")[0] 198 | switch { 199 | case strings.HasPrefix(Conf.Monitor, str): 200 | return "monitor" 201 | case strings.HasPrefix(Conf.Primary, str): 202 | return "primary" 203 | case strings.HasPrefix(Conf.Monitor, str): 204 | return "secondary" 205 | } 206 | } 207 | } 208 | return "" 209 | } 210 | 211 | func getAdvertiseData() { 212 | if Conf.AdvertiseIp == "" || Conf.AdvertiseIp == "0.0.0.0" || Conf.AdvertisePort == 0 { 213 | Log.Info(Conf.AdvertiseIp) 214 | var self string 215 | switch Conf.Role { 216 | case "monitor": 217 | self = Conf.Monitor 218 | case "primary": 219 | self = Conf.Primary 220 | case "secondary": 221 | self = Conf.Secondary 222 | } 223 | Log.Info(self) 224 | connArr := strings.Split(self, ":") 225 | if len(connArr) == 2 { 226 | Conf.AdvertiseIp = connArr[0] 227 | 228 | i, err := strconv.ParseInt(connArr[1], 10, 64) 229 | if err == nil { 230 | Conf.AdvertisePort = int(i) 231 | } 232 | } 233 | 234 | } 235 | } 236 | 237 | // 238 | func parseInt(val *int, file ini.File, section, name string) { 239 | if port, ok := file.Get(section, name); ok { 240 | i, err := strconv.ParseInt(port, 10, 64) 241 | if err != nil { 242 | Log.Fatal(name + " is not an int") 243 | Log.Close() 244 | os.Exit(1) 245 | } 246 | *val = int(i) 247 | } 248 | } 249 | 250 | // 251 | func parseArr(val *[]string, file ini.File, section, name string) { 252 | if peers, ok := file.Get(section, name); ok { 253 | *val = strings.Split(peers, ",") 254 | } 255 | } 256 | 257 | func SystemUser() string { 258 | username := "postgres" 259 | usr, err := user.Current() 260 | if err != nil { 261 | cmd := exec.Command("bash", "-c", "whoami") 262 | bytes, err := cmd.Output() 263 | if err == nil { 264 | str := string(bytes) 265 | return strings.TrimSpace(str) 266 | } 267 | } 268 | 269 | username = usr.Username 270 | return username 271 | } 272 | -------------------------------------------------------------------------------- /config/pgconfig.go: -------------------------------------------------------------------------------- 1 | // pgconfig.go provides methods for configuring a node corresponding to if it is 2 | // running a 'primary' or 'backup' instance of postgres. 3 | 4 | package config 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "fmt" 10 | "os" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | var ( 16 | replicationRegex = regexp.MustCompile(`^\s*#?\s*(local|host)\s*(replication)`) 17 | overwriteRegex = regexp.MustCompile(`^\s*#?\s*(listen_addresses|port|wal_level|archive_mode|archive_command|max_wal_senders|wal_keep_segments|hot_standby|synchronous_standby_names)\s*=\s*`) 18 | ) 19 | 20 | // configureHBAConf attempts to open the 'pg_hba.conf' file. Once open it will scan 21 | // the file line by line looking for replication settings, and overwrite only those 22 | // settings with the settings required for redundancy on Yoke 23 | func ConfigureHBAConf(ip string) error { 24 | 25 | // open the pg_hba.conf 26 | file := Conf.DataDir + "pg_hba.conf" 27 | f, err := os.OpenFile(file, os.O_RDWR, 0644) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | buffer := &bytes.Buffer{} 33 | defer f.Close() 34 | 35 | scanner := bufio.NewScanner(f) 36 | 37 | // scan the file line by line to build an 'entry' to be re-written back to the 38 | // file, skipping ('removing') any line that deals with redundancy. 39 | for scanner.Scan() { 40 | 41 | line := scanner.Text() 42 | 43 | // stop scanning if a special prefix is encountered. 44 | if strings.HasPrefix(line, "#~") { 45 | break 46 | } 47 | 48 | // only save lines that don't match custom configuration that we do 49 | if !replicationRegex.MatchString(line) { 50 | _, err := fmt.Fprintln(buffer, line) 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | } 56 | 57 | if err := f.Truncate(0); err != nil { 58 | return err 59 | } 60 | 61 | if _, err := f.Seek(0, 0); err != nil { 62 | return err 63 | } 64 | 65 | // add a replication connection into the hba.conf file so that data can be replicated 66 | // to other nodes 67 | _, err = fmt.Fprintf(f, `%v 68 | #~----------------------------------------------------------------------------- 69 | # YOKE CONFIG 70 | #------------------------------------------------------------------------------ 71 | 72 | # these configuration options have been removed from their standard location and 73 | # placed here so that Yoke could override them with the neccessary values 74 | # to configure redundancy. 75 | 76 | # IMPORTANT: these settings will always be overriden when the server boots. They 77 | # are set dynamically and so should never change. 78 | 79 | host replication %s %s/32 trust 80 | `, string(buffer.Bytes()), Conf.SystemUser, ip) 81 | 82 | return err 83 | } 84 | 85 | // configurePGConf attempts to open the 'postgresql.conf' file. Once open it will 86 | // scan the file line by line looking for replication settings, and overwrite only 87 | // those settings with the settings required for redundancy 88 | func ConfigurePGConf(ip string, port int) error { 89 | 90 | // open the postgresql.conf 91 | file := Conf.DataDir + "postgresql.conf" 92 | f, err := os.OpenFile(file, os.O_RDWR, 0644) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | buffer := &bytes.Buffer{} 98 | 99 | defer f.Close() 100 | 101 | scanner := bufio.NewScanner(f) 102 | 103 | // scan the file line by line to build an 'entry' to be re-written back to the 104 | // file, skipping ('removing') any lines that need to be manually configured 105 | for scanner.Scan() { 106 | 107 | line := scanner.Text() 108 | 109 | // stop scanning if a special prefix is encountered. This ensures there are 110 | // no duplicate Nanobox comment blocks 111 | if strings.HasPrefix(line, "#~") { 112 | break 113 | } 114 | 115 | // build the 'entry' from all lines that don't match the custom configurations 116 | if !overwriteRegex.MatchString(line) { 117 | _, err := fmt.Fprintln(buffer, line) 118 | 119 | if err != nil { 120 | return err 121 | } 122 | } 123 | } 124 | 125 | if err := f.Truncate(0); err != nil { 126 | return err 127 | } 128 | 129 | if _, err := f.Seek(0, 0); err != nil { 130 | return err 131 | } 132 | 133 | // write manual configurations into an 'entry' 134 | _, err = fmt.Fprintf(f, `%v 135 | #~----------------------------------------------------------------------------- 136 | # YOKE CONFIG 137 | #------------------------------------------------------------------------------ 138 | 139 | # these configuration options have been removed from their standard location and 140 | # placed here so that Nanobox could override them with the neccessary values 141 | # to configure redundancy. 142 | 143 | # IMPORTANT: these settings will always be overriden when the server boots. They 144 | # are set dynamically and so should never change. 145 | 146 | listen_addresses = '%s' # what IP address(es) to listen on; 147 | # comma-separated list of addresses; 148 | # defaults to 'localhost'; use '*' for all 149 | # (change requires restart) 150 | port = %d # (change requires restart) 151 | wal_level = hot_standby # minimal, archive, or hot_standby 152 | # (change requires restart) 153 | archive_mode = on # allows archiving to be done 154 | # (change requires restart) 155 | archive_command = 'exit 0' # command to use to archive a logfile segment 156 | # placeholders: %%p = path of file to archive 157 | # %%f = file name only 158 | # e.g. 'test ! -f /mnt/server/archivedir/%%f && cp %%p /mnt/server/archivedir/%%f' 159 | max_wal_senders = 10 # max number of walsender processes 160 | # (change requires restart) 161 | wal_keep_segments = 16 # in logfile segments, 16MB each; 0 disables 162 | hot_standby = on # "on" allows queries during recovery 163 | # (change requires restart) 164 | synchronous_standby_names = '*' # standby servers that provide sync rep 165 | # comma-separated list of application_name 166 | # from standby(s); '*' = any 167 | `, string(buffer.Bytes()), ip, port) 168 | 169 | return err 170 | } 171 | 172 | // createRecovery creates a 'recovery.conf' file with the necessary settings 173 | // required for redundancy on Yoke. This method is called on the node that 174 | // is being configured to run the 'backup' instance of postgres 175 | func createRecovery(ip string, port int) error { 176 | 177 | file := Conf.DataDir + "recovery.conf" 178 | 179 | // open/truncate the recover.conf 180 | f, err := os.OpenFile(file, os.O_RDWR, 0644) 181 | if err != nil { 182 | return err 183 | } 184 | if err := f.Truncate(0); err != nil { 185 | return nil 186 | } 187 | defer f.Close() 188 | 189 | // write manual configuration an 'entry' 190 | _, err = fmt.Fprintf(f, `#~----------------------------------------------------------------------------- 191 | # YOKE CONFIG 192 | #------------------------------------------------------------------------------ 193 | 194 | # IMPORTANT: this config file is dynamically generated by Yoke for redundancy 195 | # any changes made here will be overriden. 196 | 197 | # When standby_mode is enabled, the PostgreSQL server will work as a standby. It 198 | # tries to connect to the primary according to the connection settings 199 | # primary_conninfo, and receives XLOG records continuously. 200 | standby_mode = on 201 | primary_conninfo = 'host=%s port=%d application_name=backup' 202 | 203 | # restore_command specifies the shell command that is executed to copy log files 204 | # back from archival storage. This parameter is *required* for an archive 205 | # recovery, but optional for streaming replication. The given command satisfies 206 | # the requirement without doing anything. 207 | restore_command = 'exit 0' 208 | 209 | # the presence of this file will stop this this node from recovering from the 210 | # remote node. 211 | trigger_file = '/data/var/db/postgresql/i-am-primary' 212 | `, ip, port) 213 | 214 | return err 215 | } 216 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "runtime" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/nanobox-io/golang-scribble" 14 | "github.com/nanopack/yoke/config" 15 | "github.com/nanopack/yoke/monitor" 16 | "github.com/nanopack/yoke/state" 17 | ) 18 | 19 | // 20 | func main() { 21 | if len(os.Args) != 2 { 22 | fmt.Println("Missing required config file!") 23 | fmt.Println("Please, run yoke with configuration file as argument (e.g. $ yoke /etc/yoke/yoke.ini)") 24 | os.Exit(1) 25 | } 26 | config.Init(os.Args[1]) 27 | 28 | config.ConfigurePGConf("0.0.0.0", config.Conf.PGPort) 29 | 30 | store, err := scribble.New(config.Conf.StatusDir, &scribble.Options{Logger: config.Log}) 31 | if err != nil { 32 | config.Log.Fatal("Scribble did not setup correctly - ", err.Error()) 33 | os.Exit(1) 34 | } 35 | 36 | location := fmt.Sprintf("%v:%d", config.Conf.AdvertiseIp, config.Conf.AdvertisePort) 37 | me, err := state.NewLocalState(config.Conf.Role, location, config.Conf.DataDir, store) 38 | if err != nil { 39 | config.Log.Fatal("Failed to set local state - ", err.Error()) 40 | os.Exit(1) 41 | } 42 | 43 | me.ExposeRPCEndpoint("tcp", location) 44 | 45 | var other state.State 46 | var host string 47 | switch config.Conf.Role { 48 | case "primary": 49 | location := config.Conf.Secondary 50 | other = state.NewRemoteState("tcp", location, time.Second) 51 | host, _, err = net.SplitHostPort(location) 52 | if err != nil { 53 | config.Log.Fatal("Failed to split host:port for primary node - ", err.Error()) 54 | os.Exit(1) 55 | } 56 | case "secondary": 57 | location := config.Conf.Primary 58 | other = state.NewRemoteState("tcp", location, time.Second) 59 | host, _, err = net.SplitHostPort(location) 60 | if err != nil { 61 | config.Log.Fatal("Failed to split host:port for secondary node - ", err.Error()) 62 | os.Exit(1) 63 | } 64 | default: 65 | // nothing as the monitor does not need to monitor anything 66 | // the monitor just acts as a secondary mode of communication in network 67 | // splits 68 | } 69 | 70 | mon := state.NewRemoteState("tcp", config.Conf.Monitor, time.Second) 71 | 72 | var perform monitor.Performer 73 | finished := make(chan error) 74 | if other != nil { 75 | 76 | perform = monitor.NewPerformer(me, other, config.Conf) 77 | 78 | if err := perform.Initialize(); err != nil { 79 | config.Log.Fatal("Failed to initialize database - ", err.Error()) 80 | os.Exit(1) 81 | } 82 | 83 | if err := config.ConfigureHBAConf(host); err != nil { 84 | config.Log.Fatal("Failed to configure pg_hba.conf file - ", err.Error()) 85 | os.Exit(1) 86 | } 87 | 88 | if err := config.ConfigurePGConf("0.0.0.0", config.Conf.PGPort); err != nil { 89 | config.Log.Fatal("Failed to configure postgresql.conf file - ", err.Error()) 90 | os.Exit(1) 91 | } 92 | 93 | if err := perform.Start(); err != nil { 94 | config.Log.Fatal("Failed to start postgres - ", err.Error()) 95 | os.Exit(1) 96 | } 97 | 98 | go func() { 99 | decide := monitor.NewDecider(me, other, mon, perform) 100 | decide.Loop(time.Second * 2) 101 | }() 102 | 103 | go func() { 104 | err := perform.Loop() 105 | if err != nil { 106 | finished <- err 107 | } 108 | // how do I stop the decide loop? 109 | close(finished) 110 | }() 111 | } 112 | 113 | // signal Handle 114 | signals := make(chan os.Signal, 1) 115 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, os.Kill, syscall.SIGQUIT, syscall.SIGALRM) 116 | 117 | // Block until a signal is received. 118 | for { 119 | select { 120 | case err := <-finished: 121 | if err != nil { 122 | config.Log.Fatal("The performer is finished, something triggered a stop - ", err.Error()) 123 | os.Exit(1) 124 | } 125 | config.Log.Info("the database was shut down") 126 | return 127 | case signal := <-signals: 128 | switch signal { 129 | case syscall.SIGINT, os.Kill, syscall.SIGQUIT, syscall.SIGTERM: 130 | config.Log.Info("shutting down") 131 | if perform != nil { 132 | // stop the database, then wait for it to be stopped 133 | config.Log.Info("shutting down the database") 134 | perform.Stop() 135 | perform = nil 136 | config.Log.Info("waiting for the database") 137 | } else { 138 | return 139 | } 140 | case syscall.SIGALRM: 141 | config.Log.Info("Printing Stack Trace") 142 | stacktrace := make([]byte, 8192) 143 | length := runtime.Stack(stacktrace, true) 144 | fmt.Println(string(stacktrace[:length])) 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /monitor/action.go: -------------------------------------------------------------------------------- 1 | // 2 | package monitor 3 | 4 | import ( 5 | "bufio" 6 | "database/sql" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "os" 12 | "os/exec" 13 | "sync" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/hoisie/mustache" 18 | _ "github.com/lib/pq" 19 | "github.com/nanopack/yoke/config" 20 | "github.com/nanopack/yoke/state" 21 | ) 22 | 23 | var ( 24 | Done = errors.New("done") 25 | ) 26 | 27 | type ( 28 | Performer interface { 29 | TransitionToActive() 30 | TransitionToBackup() 31 | TransitionToSingle() 32 | Stop() 33 | Initialize() error 34 | Start() error 35 | Loop() error 36 | } 37 | 38 | performer struct { 39 | sync.Mutex 40 | step map[string]bool 41 | me state.State 42 | other state.State 43 | err chan error 44 | done chan interface{} 45 | cmd *exec.Cmd 46 | config config.Config 47 | } 48 | ) 49 | 50 | func NewPrefix(prefix string) io.Writer { 51 | read, write := io.Pipe() 52 | scan := bufio.NewScanner(read) 53 | go func() { 54 | for scan.Scan() { 55 | fmt.Printf("%v %v\n", prefix, scan.Text()) 56 | } 57 | }() 58 | return write 59 | } 60 | 61 | func NewPerformer(me state.State, other state.State, config config.Config) *performer { 62 | perform := performer{ 63 | config: config, 64 | step: map[string]bool{ 65 | "trigger": true, // this should only be there if the trigger file exists 66 | }, 67 | me: me, 68 | other: other, 69 | err: make(chan error), 70 | done: make(chan interface{}), 71 | } 72 | 73 | return &perform 74 | } 75 | 76 | func (performer *performer) Loop() error { 77 | config.Log.Info("Waiting for error") 78 | return <-performer.err 79 | } 80 | 81 | func (performer *performer) Stop() { 82 | config.Log.Info("going to stop") 83 | performer.Lock() 84 | defer performer.Unlock() 85 | config.Log.Info("stopping") 86 | performer.stop() 87 | config.Log.Info("stopped") 88 | } 89 | 90 | func (performer *performer) TransitionToSingle() { 91 | performer.Lock() 92 | defer performer.Unlock() 93 | 94 | // backups or actives can transition to single 95 | // it just means that the other node went down 96 | role, err := performer.me.GetDBRole() 97 | if err != nil { 98 | performer.err <- err 99 | return 100 | } 101 | if role == "single" { 102 | return 103 | } 104 | 105 | err = performer.Single() 106 | if err != nil { 107 | performer.err <- err 108 | } 109 | } 110 | 111 | func (performer *performer) TransitionToActive() { 112 | performer.Lock() 113 | defer performer.Unlock() 114 | 115 | role, err := performer.me.GetDBRole() 116 | if err != nil { 117 | performer.err <- err 118 | return 119 | } 120 | switch role { 121 | case "active": 122 | return 123 | case "backup": 124 | // backups must transition to single before they can become active. 125 | config.Log.Fatal("Something went seriously wrong, backups cannot transition to active: %v", err) 126 | os.Exit(1) 127 | } 128 | 129 | err = performer.Active() 130 | if err != nil { 131 | performer.err <- err 132 | } 133 | } 134 | 135 | func (performer *performer) TransitionToBackup() { 136 | performer.Lock() 137 | defer performer.Unlock() 138 | 139 | role, err := performer.me.GetDBRole() 140 | if err != nil { 141 | performer.err <- err 142 | return 143 | } 144 | if role == "backup" { 145 | return 146 | } 147 | 148 | err = performer.Backup() 149 | if err != nil { 150 | performer.err <- err 151 | } 152 | } 153 | 154 | func (performer *performer) stop() error { 155 | if performer.step["started"] { 156 | fmt.Println("sending signal") 157 | err := performer.cmd.Process.Signal(syscall.SIGINT) 158 | if err != nil { 159 | return err 160 | } 161 | fmt.Println("waiting for stop") 162 | <-performer.done 163 | fmt.Println("really stopped") 164 | performer.step["started"] = false 165 | } 166 | return nil 167 | } 168 | 169 | func (performer *performer) Initialize() error { 170 | _, err := os.Stat(performer.config.DataDir) 171 | switch { 172 | case os.IsNotExist(err): 173 | config.Log.Info("creating database") 174 | init := exec.Command("initdb", performer.config.DataDir) 175 | init.Stdout = NewPrefix("[initdb.stdout]") 176 | init.Stderr = NewPrefix("[initdb.stderr]") 177 | if err = init.Run(); err != nil { 178 | return err 179 | } 180 | default: 181 | config.Log.Info("database has already been created... skipping.") 182 | } 183 | return err 184 | } 185 | 186 | func (performer *performer) Start() error { 187 | config.Log.Info("going to start") 188 | performer.Lock() 189 | defer performer.Unlock() 190 | config.Log.Info("starting") 191 | return performer.startDB() 192 | } 193 | 194 | // The Single state. 195 | func (performer *performer) Single() error { 196 | config.Log.Info("transitioning to Single") 197 | 198 | // disable syncronus transaction commits. 199 | if err := performer.setSync(false, nil); err != nil { 200 | return err 201 | } 202 | 203 | if err := performer.replicate(false); err != nil { 204 | return err 205 | } 206 | 207 | config.Log.Info("[action] running DB as single") 208 | 209 | performer.addVip() 210 | performer.roleChangeCommand("single") 211 | performer.me.SetDBRole("single") 212 | 213 | return nil 214 | } 215 | 216 | func (performer *performer) sync(command string) error { 217 | sc := exec.Command("bash", "-c", command) 218 | sc.Stdout = NewPrefix("[pre-sync.stdout]") 219 | sc.Stderr = NewPrefix("[pre-sync.stderr]") 220 | config.Log.Info("[action] running pre-sync") 221 | config.Log.Debug("[action] pre-sync command(%s)", command) 222 | 223 | return sc.Run() 224 | } 225 | 226 | func (performer *performer) replicate(enabled bool) error { 227 | if performer.step["trigger"] == enabled { 228 | return nil 229 | } 230 | performer.step["trigger"] = enabled 231 | 232 | trigger := performer.config.StatusDir + "/i-am-primary" 233 | switch enabled { 234 | case true: 235 | return os.Remove(trigger) 236 | default: 237 | // this is a trigger file, it should stop this node from replicating from a 238 | // remote node, 239 | f, err := os.Create(trigger) 240 | if err != nil { 241 | return err 242 | } 243 | f.Close() 244 | return nil 245 | } 246 | } 247 | 248 | func (performer *performer) pgConnect() (*sql.DB, error) { 249 | fmt.Println("opening new connection to db") 250 | return sql.Open("postgres", fmt.Sprintf("user=%s database=postgres sslmode=disable host=localhost port=%d", performer.config.SystemUser, performer.config.PGPort)) 251 | } 252 | 253 | func (performer *performer) setSync(enabled bool, db *sql.DB) error { 254 | if db == nil { 255 | var err error 256 | db, err = performer.pgConnect() 257 | if err != nil { 258 | return err 259 | } 260 | defer db.Close() 261 | } 262 | var sync string 263 | switch enabled { 264 | case true: 265 | sync = "on" 266 | default: 267 | sync = "off" 268 | } 269 | _, err := db.Exec(fmt.Sprintf( 270 | `BEGIN; 271 | SET LOCAL synchronous_commit=off; 272 | ALTER USER %v SET synchronous_commit=%v; 273 | COMMIT;`, performer.config.SystemUser, sync)) 274 | return err 275 | 276 | } 277 | 278 | // The Active state. 279 | func (performer *performer) Active() error { 280 | config.Log.Info("transitioning to Active") 281 | if err := performer.replicate(false); err != nil { 282 | return err 283 | } 284 | 285 | // do an initial copy of files which might be corrupt because they are not consistant 286 | // this will be fixed later. we do this now so that a majority of the data will make it across without 287 | // having to pause the Durablility (ACID compliance) of postgres 288 | config.Log.Debug("[action] pre-backup started") 289 | dataDir, err := performer.other.GetDataDir() 290 | if err != nil { 291 | return err 292 | } 293 | location := performer.other.Location() 294 | ip, _, err := net.SplitHostPort(location) 295 | if err != nil { 296 | return err 297 | } 298 | sync := mustache.Render(performer.config.SyncCommand, map[string]string{"local_dir": performer.config.DataDir, "slave_ip": ip, "slave_dir": dataDir}) 299 | 300 | if err := performer.sync(sync); err != nil { 301 | return err 302 | } 303 | 304 | db, err := performer.pgConnect() 305 | if err != nil { 306 | return err 307 | } 308 | defer db.Close() 309 | 310 | // this informs postgres to make the files on disk consistant for copying, 311 | // all changes are kept in memory from this point on 312 | _, err = db.Exec("select pg_start_backup('replication')") 313 | if err != nil { 314 | return err 315 | } 316 | 317 | config.Log.Debug("[action] backup started") 318 | 319 | if err := performer.sync(sync); err != nil { 320 | // stop the backup, if it fails, there is nothing we can do so we return the original error 321 | db.Exec("select pg_stop_backup()") 322 | 323 | // something went wrong, we are the master still, so lets wait for the slave to reconnect 324 | return nil 325 | } 326 | 327 | // connect to DB and tell it to stop backup 328 | if _, err = db.Exec("select pg_stop_backup()"); err != nil { 329 | return err 330 | } 331 | 332 | config.Log.Debug("[action] backup complete") 333 | 334 | // if we were unsucessfull at setting the sync flag on the other node 335 | // then we need to start all over 336 | if performer.other.SetSynced(true) != nil { 337 | return nil 338 | } 339 | 340 | // enable syncronus transaction commits. 341 | if err := performer.setSync(true, db); err != nil { 342 | return err 343 | } 344 | 345 | performer.addVip() 346 | performer.roleChangeCommand("master") 347 | 348 | performer.me.SetDBRole("active") 349 | return nil 350 | } 351 | 352 | // The Backup state. 353 | func (performer *performer) Backup() error { 354 | config.Log.Info("transitioning to Backup") 355 | performer.removeVip() 356 | 357 | // TODO figure out if the recover.conf file needs to be regenerated. 358 | 359 | // wait for master server to be running 360 | for { 361 | ready, err := performer.me.HasSynced() 362 | if err != nil { 363 | return err 364 | } 365 | if ready { 366 | break 367 | } 368 | time.Sleep(time.Second) 369 | } 370 | 371 | config.Log.Debug("[action] starting database") 372 | performer.startDB() 373 | performer.roleChangeCommand("backup") 374 | return performer.me.SetDBRole("backup") 375 | } 376 | 377 | // this will kill the database that is running. reguardless of its current state 378 | func (performer *performer) killDB() { 379 | config.Log.Debug("[action] KillingDB") 380 | 381 | if performer.cmd == nil { 382 | config.Log.Debug("[action] nothing to kill") 383 | return 384 | } 385 | 386 | err := performer.cmd.Process.Signal(syscall.SIGINT) 387 | if err != nil { 388 | config.Log.Error("[action] Kill Signal error: %s", err.Error()) 389 | } 390 | } 391 | 392 | func (performer *performer) startDB() error { 393 | if !performer.step["started"] { 394 | config.Log.Info("[action] starting db") 395 | cmd := exec.Command("postgres", "-D", performer.config.DataDir) 396 | cmd.Stdout = NewPrefix("[postgres.stdout]") 397 | cmd.Stderr = NewPrefix("[postgres.stderr]") 398 | err := cmd.Start() 399 | if err != nil { 400 | return err 401 | } 402 | performer.cmd = cmd 403 | go performer.reportExit() 404 | 405 | // wait for postgres to exit, or for it to start correctly 406 | for { 407 | if performer.cmd == nil { 408 | return <-performer.err 409 | } 410 | conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%v", performer.config.PGPort)) 411 | fmt.Println("checking if postgres is up", conn, err, performer.config.PGPort) 412 | if err == nil { 413 | conn.Close() 414 | break 415 | } 416 | <-time.After(time.Second) 417 | } 418 | config.Log.Info("[action] db started") 419 | performer.step["started"] = true 420 | } 421 | return nil 422 | } 423 | 424 | func (performer *performer) reportExit() { 425 | err := performer.cmd.Wait() 426 | performer.cmd = nil 427 | fmt.Println("it exited", err) 428 | 429 | if err != nil { 430 | performer.err <- err 431 | } 432 | close(performer.done) 433 | } 434 | 435 | func (performer *performer) roleChangeCommand(role string) { 436 | if performer.config.RoleChangeCommand != "" { 437 | rcc := exec.Command("bash", "-c", fmt.Sprintf("%s %s", performer.config.RoleChangeCommand, role)) 438 | rcc.Stdout = NewPrefix("[RoleChangeCommand.stdout]") 439 | rcc.Stderr = NewPrefix("[RoleChangeCommand.stderr]") 440 | if err := rcc.Run(); err != nil { 441 | config.Log.Error("[action] RoleChangeCommand failed.") 442 | config.Log.Debug("[RoleChangeCommand.error] message: %s", err.Error()) 443 | } 444 | } 445 | } 446 | 447 | func (performer *performer) addVip() { 448 | if performer.vipable() { 449 | config.Log.Info("[action] Adding VIP") 450 | vAddCmd := exec.Command("bash", "-c", fmt.Sprintf("%s %s", performer.config.VipAddCommand, performer.config.Vip)) 451 | vAddCmd.Stdout = NewPrefix("[VIPAddCommand.stdout]") 452 | vAddCmd.Stderr = NewPrefix("[VIPAddCommand.stderr]") 453 | if err := vAddCmd.Run(); err != nil { 454 | config.Log.Error("[action] VIPAddCommand failed.") 455 | config.Log.Debug("[VIPAddCommand.error] message: %s", err.Error()) 456 | } 457 | } 458 | } 459 | 460 | func (performer *performer) removeVip() { 461 | if performer.vipable() { 462 | config.Log.Info("[action] Removing VIP") 463 | vRemoveCmd := exec.Command("bash", "-c", fmt.Sprintf("%s %s", performer.config.VipRemoveCommand, performer.config.Vip)) 464 | vRemoveCmd.Stdout = NewPrefix("[VIPRemoveCommand.stdout]") 465 | vRemoveCmd.Stderr = NewPrefix("[VIPRemoveCommand.stderr]") 466 | if err := vRemoveCmd.Run(); err != nil { 467 | config.Log.Error("[action] VIPRemoveCommand failed.") 468 | config.Log.Debug("[VIPRemoveCommand.error] message: %s", err.Error()) 469 | } 470 | } 471 | } 472 | 473 | func (performer *performer) vipable() bool { 474 | return performer.config.Vip != "" && performer.config.VipAddCommand != "" && performer.config.VipRemoveCommand != "" 475 | } 476 | -------------------------------------------------------------------------------- /monitor/action_test.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/nanopack/yoke/config" 9 | "github.com/nanopack/yoke/state/mock" 10 | ) 11 | 12 | func TestSingle(test *testing.T) { 13 | ctrl := gomock.NewController(test) 14 | defer ctrl.Finish() 15 | 16 | me := mock_state.NewMockState(ctrl) 17 | other := mock_state.NewMockState(ctrl) 18 | 19 | perform := start(me, other, test) 20 | defer perform.Stop() 21 | 22 | me.EXPECT().SetDBRole("single").Return(nil) 23 | 24 | err := perform.Single() 25 | if err != nil { 26 | test.Log(err) 27 | test.FailNow() 28 | } 29 | 30 | } 31 | 32 | func TestActive(test *testing.T) { 33 | ctrl := gomock.NewController(test) 34 | defer ctrl.Finish() 35 | 36 | me := mock_state.NewMockState(ctrl) 37 | other := mock_state.NewMockState(ctrl) 38 | 39 | perform := start(me, other, test) 40 | defer perform.Stop() 41 | 42 | other.EXPECT().GetDataDir().Return("test", nil) 43 | other.EXPECT().Location().Return("127.0.0.1:1234") 44 | 45 | other.EXPECT().SetSynced(true).Return(nil) 46 | 47 | me.EXPECT().SetDBRole("active").Return(nil) 48 | 49 | err := perform.Active() 50 | if err != nil { 51 | test.Log(err) 52 | test.FailNow() 53 | } 54 | 55 | } 56 | 57 | func TestBackup(test *testing.T) { 58 | ctrl := gomock.NewController(test) 59 | defer ctrl.Finish() 60 | 61 | me := mock_state.NewMockState(ctrl) 62 | other := mock_state.NewMockState(ctrl) 63 | 64 | perform := start(me, other, test) 65 | defer perform.Stop() 66 | 67 | me.EXPECT().HasSynced().Return(false, nil) 68 | me.EXPECT().HasSynced().Return(true, nil) 69 | me.EXPECT().SetDBRole("backup").Return(nil) 70 | 71 | err := perform.Backup() 72 | if err != nil { 73 | test.Log(err) 74 | test.FailNow() 75 | } 76 | 77 | } 78 | 79 | func start(me, other *mock_state.MockState, test *testing.T) *performer { 80 | // i need to create a tmp folder 81 | // start a action 82 | // then have it transition to different states. 83 | 84 | // overwrite the builtin config options 85 | config.Conf = config.Config{ 86 | PGPort: 4567, 87 | DataDir: os.TempDir() + "/postgres/", 88 | StatusDir: os.TempDir() + "/postgres/", 89 | SyncCommand: "true", 90 | SystemUser: config.SystemUser(), 91 | } 92 | 93 | perform := NewPerformer(me, other, config.Conf) 94 | 95 | // ignore all errors that come across this way 96 | go func() { 97 | <-perform.err 98 | }() 99 | 100 | err := perform.Initialize() 101 | if err != nil { 102 | test.Log(err) 103 | test.FailNow() 104 | } 105 | 106 | if err := config.ConfigureHBAConf("127.0.0.1"); err != nil { 107 | test.Log(err) 108 | test.FailNow() 109 | } 110 | if err := config.ConfigurePGConf("127.0.0.1", config.Conf.PGPort); err != nil { 111 | test.Log(err) 112 | test.FailNow() 113 | } 114 | 115 | err = perform.Start() 116 | if err != nil { 117 | test.Log(err) 118 | test.FailNow() 119 | } 120 | 121 | return perform 122 | } 123 | -------------------------------------------------------------------------------- /monitor/decision.go: -------------------------------------------------------------------------------- 1 | // 2 | package monitor 3 | 4 | import ( 5 | "errors" 6 | "os" 7 | "sync" 8 | "time" 9 | 10 | "github.com/nanopack/yoke/config" 11 | "github.com/nanopack/yoke/state" 12 | ) 13 | 14 | var ( 15 | ClusterUnaviable = errors.New("none of the nodes in the cluster are available") 16 | ) 17 | 18 | type ( 19 | Looper interface { 20 | Loop(time.Duration) error 21 | } 22 | 23 | decider struct { 24 | sync.Mutex 25 | 26 | me state.State 27 | other state.State 28 | monitor state.State 29 | performer Performer 30 | } 31 | ) 32 | 33 | func NewDecider(me, other, monitor state.State, performer Performer) Looper { 34 | decider := decider{ 35 | me: me, 36 | other: other, 37 | monitor: monitor, 38 | performer: performer, 39 | } 40 | for { 41 | // Really we only have to wait for a quorum, 2 out of 3 will allow everything to be ok. 42 | // But in certain conditions, this node was a backup that was down, and the current active 43 | // if offline, we need to wait for all 3 nodes. 44 | // So really we are going to wait for all 3 nodes to make it simple 45 | // me is already Ready. no need to call it 46 | config.Log.Info("waiting for cluster to be ready") 47 | other.Ready() 48 | monitor.Ready() 49 | config.Log.Info("cluster is ready") 50 | 51 | err := decider.reCheck() 52 | switch err { 53 | case ClusterUnaviable: // we try again. 54 | case nil: // the cluster was successfully rechecked 55 | return decider 56 | default: 57 | config.Log.Fatal("Another kind of error occured: %v", err) 58 | os.Exit(1) 59 | } 60 | } 61 | } 62 | 63 | // this is the main loop for monitoring the cluster and making any changes needed to 64 | // reflect changes in remote nodes in the cluster 65 | func (decider decider) Loop(check time.Duration) error { 66 | timer := time.Tick(check) 67 | for range timer { 68 | err := decider.reCheck() 69 | switch { 70 | case err == ClusterUnaviable: 71 | case err != nil: 72 | return err 73 | default: 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | // this is used to move a active node to a backup node 80 | func (decider decider) Demote() { 81 | decider.Lock() 82 | defer decider.Unlock() 83 | 84 | decider.performer.TransitionToBackup() 85 | } 86 | 87 | // this is used to move a backup node to an active node 88 | func (decider decider) Promote() { 89 | decider.Lock() 90 | defer decider.Unlock() 91 | 92 | decider.performer.TransitionToActive() 93 | } 94 | 95 | // Checks the other node in the cluster, falling back to bouncing the check off of the monitor, 96 | // to see if the states between this node and the remote node match up 97 | func (decider decider) reCheck() error { 98 | decider.Lock() 99 | defer decider.Unlock() 100 | 101 | var otherDBRole string 102 | var err error 103 | config.Log.Info("checking other role") 104 | otherDBRole, err = decider.other.GetDBRole() 105 | if err != nil { 106 | config.Log.Info("checking other role (bounce)") 107 | address := decider.other.Location() 108 | otherDBRole, err = decider.monitor.Bounce(address).GetDBRole() 109 | if err != nil { 110 | // this node can't talk to the other member of the cluster or the monitor 111 | // if this node is not in single mode it needs to shut off 112 | if role, err := decider.me.GetDBRole(); role != "single" || err != nil { 113 | config.Log.Info("stopping, no one here") 114 | decider.performer.Stop() 115 | return ClusterUnaviable 116 | } 117 | return nil 118 | } 119 | } 120 | 121 | config.Log.Info("other node is '%v'", otherDBRole) 122 | 123 | // we need to handle multiple possible states that the remote node is in 124 | switch otherDBRole { 125 | case "single": 126 | fallthrough 127 | case "active": 128 | decider.performer.TransitionToBackup() 129 | case "dead": 130 | DBrole, err := decider.me.GetDBRole() 131 | if err != nil { 132 | return err 133 | } 134 | if DBrole == "backup" { 135 | // if this node is not synced up to the previous master, then we must wait for the other node to 136 | // come online 137 | hasSynced, err := decider.me.HasSynced() 138 | if err != nil { 139 | return err 140 | } 141 | if !hasSynced { 142 | decider.performer.Stop() 143 | return ClusterUnaviable 144 | } 145 | } 146 | 147 | decider.performer.TransitionToSingle() 148 | case "initialized": 149 | role, err := decider.me.GetRole() 150 | if err != nil { 151 | return err 152 | } 153 | switch role { 154 | case "primary": 155 | decider.performer.TransitionToActive() 156 | case "secondary": 157 | decider.performer.TransitionToBackup() 158 | } 159 | case "backup": 160 | decider.performer.TransitionToActive() 161 | } 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /monitor/decision_test.go: -------------------------------------------------------------------------------- 1 | package monitor_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/nanopack/yoke/monitor" 9 | "github.com/nanopack/yoke/monitor/mock" 10 | "github.com/nanopack/yoke/state/mock" 11 | ) 12 | 13 | func TestPrimary(test *testing.T) { 14 | ctrl := gomock.NewController(test) 15 | defer ctrl.Finish() 16 | 17 | me := mock_state.NewMockState(ctrl) 18 | other := mock_state.NewMockState(ctrl) 19 | arbiter := mock_state.NewMockState(ctrl) 20 | perform := mock_monitor.NewMockPerformer(ctrl) 21 | 22 | other.EXPECT().Ready() 23 | arbiter.EXPECT().Ready() 24 | 25 | other.EXPECT().GetDBRole().Return("initialized", nil) 26 | me.EXPECT().GetRole().Return("primary", nil) 27 | perform.EXPECT().TransitionToActive() 28 | 29 | monitor.NewDecider(me, other, arbiter, perform) 30 | } 31 | 32 | func TestSecondary(test *testing.T) { 33 | ctrl := gomock.NewController(test) 34 | defer ctrl.Finish() 35 | 36 | me := mock_state.NewMockState(ctrl) 37 | other := mock_state.NewMockState(ctrl) 38 | arbiter := mock_state.NewMockState(ctrl) 39 | perform := mock_monitor.NewMockPerformer(ctrl) 40 | 41 | other.EXPECT().Ready() 42 | arbiter.EXPECT().Ready() 43 | 44 | other.EXPECT().GetDBRole().Return("initialized", nil) 45 | me.EXPECT().GetRole().Return("secondary", nil) 46 | perform.EXPECT().TransitionToBackup() 47 | 48 | monitor.NewDecider(me, other, arbiter, perform) 49 | } 50 | 51 | func TestSingle(test *testing.T) { 52 | ctrl := gomock.NewController(test) 53 | defer ctrl.Finish() 54 | 55 | me := mock_state.NewMockState(ctrl) 56 | other := mock_state.NewMockState(ctrl) 57 | arbiter := mock_state.NewMockState(ctrl) 58 | perform := mock_monitor.NewMockPerformer(ctrl) 59 | 60 | other.EXPECT().Ready() 61 | arbiter.EXPECT().Ready() 62 | 63 | other.EXPECT().GetDBRole().Return("single", nil) 64 | perform.EXPECT().TransitionToBackup() 65 | 66 | monitor.NewDecider(me, other, arbiter, perform) 67 | } 68 | 69 | func TestActive(test *testing.T) { 70 | ctrl := gomock.NewController(test) 71 | defer ctrl.Finish() 72 | 73 | me := mock_state.NewMockState(ctrl) 74 | other := mock_state.NewMockState(ctrl) 75 | arbiter := mock_state.NewMockState(ctrl) 76 | perform := mock_monitor.NewMockPerformer(ctrl) 77 | 78 | other.EXPECT().Ready() 79 | arbiter.EXPECT().Ready() 80 | 81 | other.EXPECT().GetDBRole().Return("active", nil) 82 | perform.EXPECT().TransitionToBackup() 83 | 84 | monitor.NewDecider(me, other, arbiter, perform) 85 | } 86 | 87 | func TestBackup(test *testing.T) { 88 | ctrl := gomock.NewController(test) 89 | defer ctrl.Finish() 90 | 91 | me := mock_state.NewMockState(ctrl) 92 | other := mock_state.NewMockState(ctrl) 93 | arbiter := mock_state.NewMockState(ctrl) 94 | perform := mock_monitor.NewMockPerformer(ctrl) 95 | 96 | other.EXPECT().Ready() 97 | arbiter.EXPECT().Ready() 98 | 99 | other.EXPECT().GetDBRole().Return("backup", nil) 100 | perform.EXPECT().TransitionToActive() 101 | 102 | monitor.NewDecider(me, other, arbiter, perform) 103 | } 104 | 105 | func TestOtherDead(test *testing.T) { 106 | ctrl := gomock.NewController(test) 107 | defer ctrl.Finish() 108 | 109 | me := mock_state.NewMockState(ctrl) 110 | other := mock_state.NewMockState(ctrl) 111 | bounce := mock_state.NewMockState(ctrl) 112 | arbiter := mock_state.NewMockState(ctrl) 113 | perform := mock_monitor.NewMockPerformer(ctrl) 114 | 115 | other.EXPECT().Ready() 116 | arbiter.EXPECT().Ready() 117 | 118 | other.EXPECT().GetDBRole().Return("", errors.New("dead")) 119 | other.EXPECT().Location().Return("127.0.0.1:1234") 120 | arbiter.EXPECT().Bounce("127.0.0.1:1234").Return(bounce) 121 | bounce.EXPECT().GetDBRole().Return("dead", nil) 122 | 123 | me.EXPECT().GetDBRole().Return("active", nil) 124 | 125 | perform.EXPECT().TransitionToSingle() 126 | 127 | monitor.NewDecider(me, other, arbiter, perform) 128 | } 129 | 130 | func TestOtherDeadButSingle(test *testing.T) { 131 | ctrl := gomock.NewController(test) 132 | defer ctrl.Finish() 133 | 134 | me := mock_state.NewMockState(ctrl) 135 | other := mock_state.NewMockState(ctrl) 136 | bounce := mock_state.NewMockState(ctrl) 137 | arbiter := mock_state.NewMockState(ctrl) 138 | perform := mock_monitor.NewMockPerformer(ctrl) 139 | 140 | other.EXPECT().Ready() 141 | arbiter.EXPECT().Ready() 142 | 143 | other.EXPECT().GetDBRole().Return("", errors.New("dead")) 144 | other.EXPECT().Location().Return("127.0.0.1:1234") 145 | arbiter.EXPECT().Bounce("127.0.0.1:1234").Return(bounce) 146 | bounce.EXPECT().GetDBRole().Return("", errors.New("dead")) 147 | 148 | me.EXPECT().GetDBRole().Return("single", nil) 149 | 150 | monitor.NewDecider(me, other, arbiter, perform) 151 | } 152 | 153 | func TestOtherDeadBackup(test *testing.T) { 154 | ctrl := gomock.NewController(test) 155 | defer ctrl.Finish() 156 | 157 | me := mock_state.NewMockState(ctrl) 158 | other := mock_state.NewMockState(ctrl) 159 | bounce := mock_state.NewMockState(ctrl) 160 | arbiter := mock_state.NewMockState(ctrl) 161 | perform := mock_monitor.NewMockPerformer(ctrl) 162 | 163 | other.EXPECT().Ready() 164 | arbiter.EXPECT().Ready() 165 | 166 | other.EXPECT().GetDBRole().Return("", errors.New("dead")) 167 | other.EXPECT().Location().Return("127.0.0.1:1234") 168 | arbiter.EXPECT().Bounce("127.0.0.1:1234").Return(bounce) 169 | bounce.EXPECT().GetDBRole().Return("dead", nil) 170 | 171 | me.EXPECT().GetDBRole().Return("backup", nil) 172 | me.EXPECT().HasSynced().Return(true, nil) 173 | 174 | perform.EXPECT().TransitionToSingle() 175 | 176 | monitor.NewDecider(me, other, arbiter, perform) 177 | } 178 | 179 | func TestOtherDeadBackupNotSync(test *testing.T) { 180 | ctrl := gomock.NewController(test) 181 | defer ctrl.Finish() 182 | 183 | me := mock_state.NewMockState(ctrl) 184 | other := mock_state.NewMockState(ctrl) 185 | bounce := mock_state.NewMockState(ctrl) 186 | arbiter := mock_state.NewMockState(ctrl) 187 | perform := mock_monitor.NewMockPerformer(ctrl) 188 | 189 | other.EXPECT().Ready().Times(2) 190 | arbiter.EXPECT().Ready().Times(2) 191 | 192 | other.EXPECT().GetDBRole().Return("", errors.New("dead")).Times(2) 193 | other.EXPECT().Location().Return("127.0.0.1:1234").Times(2) 194 | arbiter.EXPECT().Bounce("127.0.0.1:1234").Return(bounce).Times(2) 195 | bounce.EXPECT().GetDBRole().Return("dead", nil).Times(2) 196 | 197 | me.EXPECT().GetDBRole().Return("backup", nil).Times(2) 198 | me.EXPECT().HasSynced().Return(false, nil) 199 | 200 | perform.EXPECT().Stop() 201 | 202 | me.EXPECT().HasSynced().Return(true, nil) 203 | 204 | perform.EXPECT().TransitionToSingle() 205 | 206 | monitor.NewDecider(me, other, arbiter, perform) 207 | } 208 | 209 | func TestOtherTemporaryDead(test *testing.T) { 210 | ctrl := gomock.NewController(test) 211 | defer ctrl.Finish() 212 | 213 | me := mock_state.NewMockState(ctrl) 214 | other := mock_state.NewMockState(ctrl) 215 | bounce := mock_state.NewMockState(ctrl) 216 | arbiter := mock_state.NewMockState(ctrl) 217 | perform := mock_monitor.NewMockPerformer(ctrl) 218 | 219 | other.EXPECT().Ready().Times(2) 220 | arbiter.EXPECT().Ready().Times(2) 221 | 222 | other.EXPECT().GetDBRole().Return("", errors.New("dead")).Times(2) 223 | other.EXPECT().Location().Return("127.0.0.1:1234").Times(2) 224 | arbiter.EXPECT().Bounce("127.0.0.1:1234").Return(bounce).Times(2) 225 | 226 | bounce.EXPECT().GetDBRole().Return("", errors.New("dead")) 227 | 228 | me.EXPECT().GetDBRole().Return("active", nil) 229 | perform.EXPECT().Stop() 230 | 231 | bounce.EXPECT().GetDBRole().Return("dead", nil) 232 | me.EXPECT().GetDBRole().Return("single", nil) 233 | 234 | perform.EXPECT().TransitionToSingle() 235 | 236 | monitor.NewDecider(me, other, arbiter, perform) 237 | } 238 | -------------------------------------------------------------------------------- /monitor/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Automatically generated by MockGen. DO NOT EDIT! 2 | // Source: github.com/nanopack/yoke/monitor (interfaces: Performer) 3 | 4 | package mock_monitor 5 | 6 | import ( 7 | gomock "github.com/golang/mock/gomock" 8 | ) 9 | 10 | // Mock of Performer interface 11 | type MockPerformer struct { 12 | ctrl *gomock.Controller 13 | recorder *_MockPerformerRecorder 14 | } 15 | 16 | // Recorder for MockPerformer (not exported) 17 | type _MockPerformerRecorder struct { 18 | mock *MockPerformer 19 | } 20 | 21 | func NewMockPerformer(ctrl *gomock.Controller) *MockPerformer { 22 | mock := &MockPerformer{ctrl: ctrl} 23 | mock.recorder = &_MockPerformerRecorder{mock} 24 | return mock 25 | } 26 | 27 | func (_m *MockPerformer) EXPECT() *_MockPerformerRecorder { 28 | return _m.recorder 29 | } 30 | 31 | func (_m *MockPerformer) Initialize() error { 32 | ret := _m.ctrl.Call(_m, "Initialize") 33 | ret0, _ := ret[0].(error) 34 | return ret0 35 | } 36 | 37 | func (_mr *_MockPerformerRecorder) Initialize() *gomock.Call { 38 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Initialize") 39 | } 40 | 41 | func (_m *MockPerformer) Loop() error { 42 | ret := _m.ctrl.Call(_m, "Loop") 43 | ret0, _ := ret[0].(error) 44 | return ret0 45 | } 46 | 47 | func (_mr *_MockPerformerRecorder) Loop() *gomock.Call { 48 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Loop") 49 | } 50 | 51 | func (_m *MockPerformer) Start() error { 52 | ret := _m.ctrl.Call(_m, "Start") 53 | ret0, _ := ret[0].(error) 54 | return ret0 55 | } 56 | 57 | func (_mr *_MockPerformerRecorder) Start() *gomock.Call { 58 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Start") 59 | } 60 | 61 | func (_m *MockPerformer) Stop() { 62 | _m.ctrl.Call(_m, "Stop") 63 | } 64 | 65 | func (_mr *_MockPerformerRecorder) Stop() *gomock.Call { 66 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Stop") 67 | } 68 | 69 | func (_m *MockPerformer) TransitionToActive() { 70 | _m.ctrl.Call(_m, "TransitionToActive") 71 | } 72 | 73 | func (_mr *_MockPerformerRecorder) TransitionToActive() *gomock.Call { 74 | return _mr.mock.ctrl.RecordCall(_mr.mock, "TransitionToActive") 75 | } 76 | 77 | func (_m *MockPerformer) TransitionToBackup() { 78 | _m.ctrl.Call(_m, "TransitionToBackup") 79 | } 80 | 81 | func (_mr *_MockPerformerRecorder) TransitionToBackup() *gomock.Call { 82 | return _mr.mock.ctrl.RecordCall(_mr.mock, "TransitionToBackup") 83 | } 84 | 85 | func (_m *MockPerformer) TransitionToSingle() { 86 | _m.ctrl.Call(_m, "TransitionToSingle") 87 | } 88 | 89 | func (_mr *_MockPerformerRecorder) TransitionToSingle() *gomock.Call { 90 | return _mr.mock.ctrl.RecordCall(_mr.mock, "TransitionToSingle") 91 | } 92 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /site/.nanofile: -------------------------------------------------------------------------------- 1 | name: yolk-site 2 | -------------------------------------------------------------------------------- /site/Boxfile: -------------------------------------------------------------------------------- 1 | build: 2 | engine: tylerflint/middleman 3 | exec: 4 | - 'mv build/yoke.html build/index.html' 5 | -------------------------------------------------------------------------------- /site/Gemfile: -------------------------------------------------------------------------------- 1 | # If you do not have OpenSSL installed, update 2 | # the following line to use "http://" instead 3 | source 'https://rubygems.org' 4 | 5 | gem "middleman", "~>3.3.12" 6 | 7 | # Live-reloading plugin 8 | gem "middleman-livereload", github: 'middleman/middleman-livereload' 9 | 10 | # For faster file watcher updates on Windows: 11 | gem "wdm", "~> 0.1.0", :platforms => [:mswin, :mingw] 12 | 13 | # Windows does not come with time zone data 14 | gem "tzinfo-data", platforms: [:mswin, :mingw, :jruby] 15 | 16 | # Github Flavored Markdown 17 | gem 'redcarpet' 18 | -------------------------------------------------------------------------------- /site/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/middleman/middleman-livereload.git 3 | revision: e998762889a6d52566f97720f94a2db090d45549 4 | specs: 5 | middleman-livereload (3.4.3) 6 | em-websocket (~> 0.5.1) 7 | middleman-core (>= 3.3) 8 | rack-livereload (~> 0.3.15) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | activesupport (4.1.14) 14 | i18n (~> 0.6, >= 0.6.9) 15 | json (~> 1.7, >= 1.7.7) 16 | minitest (~> 5.1) 17 | thread_safe (~> 0.1) 18 | tzinfo (~> 1.1) 19 | celluloid (0.16.0) 20 | timers (~> 4.0.0) 21 | chunky_png (1.3.5) 22 | coffee-script (2.4.1) 23 | coffee-script-source 24 | execjs 25 | coffee-script-source (1.10.0) 26 | compass (1.0.3) 27 | chunky_png (~> 1.2) 28 | compass-core (~> 1.0.2) 29 | compass-import-once (~> 1.0.5) 30 | rb-fsevent (>= 0.9.3) 31 | rb-inotify (>= 0.9) 32 | sass (>= 3.3.13, < 3.5) 33 | compass-core (1.0.3) 34 | multi_json (~> 1.0) 35 | sass (>= 3.3.0, < 3.5) 36 | compass-import-once (1.0.5) 37 | sass (>= 3.2, < 3.5) 38 | em-websocket (0.5.1) 39 | eventmachine (>= 0.12.9) 40 | http_parser.rb (~> 0.6.0) 41 | erubis (2.7.0) 42 | eventmachine (1.0.8) 43 | execjs (2.6.0) 44 | ffi (1.9.10) 45 | haml (4.0.7) 46 | tilt 47 | hike (1.2.3) 48 | hitimes (1.2.3) 49 | hooks (0.4.1) 50 | uber (~> 0.0.14) 51 | http_parser.rb (0.6.0) 52 | i18n (0.7.0) 53 | json (1.8.3) 54 | kramdown (1.9.0) 55 | listen (2.10.1) 56 | celluloid (~> 0.16.0) 57 | rb-fsevent (>= 0.9.3) 58 | rb-inotify (>= 0.9) 59 | middleman (3.3.12) 60 | coffee-script (~> 2.2) 61 | compass (>= 1.0.0, < 2.0.0) 62 | compass-import-once (= 1.0.5) 63 | execjs (~> 2.0) 64 | haml (>= 4.0.5) 65 | kramdown (~> 1.2) 66 | middleman-core (= 3.3.12) 67 | middleman-sprockets (>= 3.1.2) 68 | sass (>= 3.4.0, < 4.0) 69 | uglifier (~> 2.5) 70 | middleman-core (3.3.12) 71 | activesupport (~> 4.1.0) 72 | bundler (~> 1.1) 73 | erubis 74 | hooks (~> 0.3) 75 | i18n (~> 0.7.0) 76 | listen (>= 2.7.9, < 3.0) 77 | padrino-helpers (~> 0.12.3) 78 | rack (>= 1.4.5, < 2.0) 79 | rack-test (~> 0.6.2) 80 | thor (>= 0.15.2, < 2.0) 81 | tilt (~> 1.4.1, < 2.0) 82 | middleman-sprockets (3.4.2) 83 | middleman-core (>= 3.3) 84 | sprockets (~> 2.12.1) 85 | sprockets-helpers (~> 1.1.0) 86 | sprockets-sass (~> 1.3.0) 87 | minitest (5.8.3) 88 | multi_json (1.11.2) 89 | padrino-helpers (0.12.5) 90 | i18n (~> 0.6, >= 0.6.7) 91 | padrino-support (= 0.12.5) 92 | tilt (~> 1.4.1) 93 | padrino-support (0.12.5) 94 | activesupport (>= 3.1) 95 | rack (1.6.4) 96 | rack-livereload (0.3.16) 97 | rack 98 | rack-test (0.6.3) 99 | rack (>= 1.0) 100 | rb-fsevent (0.9.6) 101 | rb-inotify (0.9.5) 102 | ffi (>= 0.5.0) 103 | redcarpet (3.3.3) 104 | sass (3.4.19) 105 | sprockets (2.12.4) 106 | hike (~> 1.2) 107 | multi_json (~> 1.0) 108 | rack (~> 1.0) 109 | tilt (~> 1.1, != 1.3.0) 110 | sprockets-helpers (1.1.0) 111 | sprockets (~> 2.0) 112 | sprockets-sass (1.3.1) 113 | sprockets (~> 2.0) 114 | tilt (~> 1.1) 115 | thor (0.19.1) 116 | thread_safe (0.3.5) 117 | tilt (1.4.1) 118 | timers (4.0.4) 119 | hitimes 120 | tzinfo (1.2.2) 121 | thread_safe (~> 0.1) 122 | uber (0.0.15) 123 | uglifier (2.7.2) 124 | execjs (>= 0.3.0) 125 | json (>= 1.8.0) 126 | 127 | PLATFORMS 128 | ruby 129 | 130 | DEPENDENCIES 131 | middleman (~> 3.3.12) 132 | middleman-livereload! 133 | redcarpet 134 | tzinfo-data 135 | wdm (~> 0.1.0) 136 | 137 | BUNDLED WITH 138 | 1.10.3 139 | -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | what's up? 2 | -------------------------------------------------------------------------------- /site/config.rb: -------------------------------------------------------------------------------- 1 | ### 2 | # Compass 3 | ### 4 | 5 | # Change Compass configuration 6 | # compass_config do |config| 7 | # config.output_style = :compact 8 | # end 9 | 10 | ### 11 | # Page options, layouts, aliases and proxies 12 | ### 13 | 14 | # Per-page layout changes: 15 | # 16 | # With no layout 17 | # page "/path/to/file.html", :layout => false 18 | # 19 | # With alternative layout 20 | # page "/path/to/file.html", :layout => :otherlayout 21 | # 22 | # A path which all have the same layout 23 | # with_layout :admin do 24 | # page "/admin/*" 25 | # end 26 | 27 | # Proxy pages (https://middlemanapp.com/advanced/dynamic_pages/) 28 | # proxy "/this-page-has-no-template.html", "/template-file.html", :locals => { 29 | # :which_fake_page => "Rendering a fake page with a local variable" } 30 | 31 | # Using Github Flavored Markdown 32 | set :markdown_engine, :redcarpet 33 | set :markdown, :tables => true, :autolink => true, :gh_blockcode => true, :fenced_code_blocks => true, :strikethrough => true, :with_toc_data => true 34 | 35 | activate :directory_indexes 36 | 37 | ### 38 | # Helpers 39 | ### 40 | 41 | # Automatic image dimensions on image_tag helper 42 | # activate :automatic_image_sizes 43 | 44 | # Reload the browser automatically whenever files change 45 | configure :development do 46 | activate :livereload do |opts| 47 | opts.host = '0.0.0.0' 48 | opts.js_host = 'yolk-site.dev' 49 | end 50 | end 51 | 52 | 53 | # Methods defined in the helpers block are available in templates 54 | # Methods defined in the helpers block are available in templates 55 | helpers do 56 | def nav_article_active(path) 57 | current_page.url.gsub(/\A\//, '') === path ? {:class => "active"} : {} 58 | end 59 | end 60 | 61 | set :css_dir, 'stylesheets' 62 | 63 | set :js_dir, 'javascripts' 64 | 65 | set :images_dir, 'images' 66 | 67 | set :partials_dir, 'partials' 68 | 69 | # Used when building links in the left nav of documentation 70 | set :nav_root, '/' 71 | 72 | # Build-specific configuration 73 | configure :build do 74 | # For example, change the Compass output style for deployment 75 | # activate :minify_css 76 | 77 | # Minify Javascript on build 78 | # activate :minify_javascript 79 | 80 | # Enable cache buster 81 | # activate :asset_hash 82 | 83 | # Use relative URLs 84 | # activate :relative_assets 85 | 86 | # Or use a different image path 87 | # set :http_prefix, "/Content/images/" 88 | 89 | # Used when building links in the left nav of documentation 90 | set :nav_root, '/' + data.project_info.name.downcase.gsub(/\s+/, "") + '/' 91 | end 92 | -------------------------------------------------------------------------------- /site/data/docs_index.yml: -------------------------------------------------------------------------------- 1 | docs: 2 | - title: "Introduction" 3 | path: "docs/" 4 | - title: "Test 1" 5 | path: "docs/test-1/" 6 | - title: "Test 2" 7 | path: "docs/test-2/" 8 | sub_docs: 9 | - title: "sub1" 10 | path: "docs/test-2/sub1/" 11 | - title: "sub2" 12 | path: "docs/test-2/sub2/" 13 | sub_docs: 14 | - title: "sub2-sub1" 15 | path: "docs/test-2/sub2/sub2-sub1/" 16 | - title: "sub2-sub2" 17 | path: "docs/test-2/sub2/sub2-sub2/" 18 | - title: "sub3" 19 | path: "docs/test-2/sub3/" 20 | - title: "Test 3" 21 | path: "docs/test-3/" -------------------------------------------------------------------------------- /site/data/project_info.yml: -------------------------------------------------------------------------------- 1 | # Project Information 2 | name: 'Yoke' 3 | subhead: 'High-Availability Redis Cluster' 4 | summary: 'The coolest The Postgres failover solution designed to fit with the Nanobox workflow. Includes election-based auto-failover and consistent read/write access, even with a node offline.' 5 | 6 | # Full Docs - Are there full docs included in this site 7 | full_docs: true 8 | 9 | # Download Links - If none exist, just remove 10 | downloads: 11 | - text: 'Linux 64bit' 12 | link: '#' 13 | - text: 'Linux 32bit' 14 | link: '#' 15 | 16 | # Talking Points - Just features to Highlight 17 | points: 18 | - title: 'Data Integrity' 19 | content: "Yoke's highest priority is preserving data integrity. Excluding acts of God, Yoke should never lose data." 20 | - title: 'Fully Functional with Down Nodes' 21 | content: "While other Postgres cluster tools typically switch to readonly when a node goes offline, Yoke continues to allow writes." 22 | - title: 'Proven in Production' 23 | content: "Yoke has been used in production by Pagoda Box and is used for redundant Postgres Services on Nanobox." 24 | 25 | # Project License - Options are 'mpl2' or 'mit'. If a license is not included, defaults to mpl2. 26 | license: mpl2 27 | -------------------------------------------------------------------------------- /site/helpers/nav_helpers.rb: -------------------------------------------------------------------------------- 1 | module NavHelpers 2 | def docs_in_order 3 | docs_list = [] 4 | data.docs_index.docs.each do |doc| 5 | docs_list << { title: doc.title, path: doc.path } 6 | next if doc.sub_docs.nil? 7 | 8 | doc.sub_docs.each do |sub_doc| 9 | docs_list << { category: doc.title, title: sub_doc.title, path: sub_doc.path } 10 | next if sub_doc.sub_docs.nil? 11 | 12 | sub_doc.sub_docs.each do |sub_sub_doc| 13 | docs_list << { category: doc.title, sub_doc: sub_doc.title, title: sub_sub_doc.title, path: sub_sub_doc.path } 14 | end 15 | end 16 | end 17 | docs_list 18 | end 19 | 20 | def get_prev_doc(current_article_path) 21 | docs = docs_in_order 22 | index = docs_in_order.find_index { |d| d[:path] == current_article_path } 23 | 24 | if index == 0 25 | nil 26 | else 27 | docs[index - 1] 28 | end 29 | end 30 | 31 | def get_next_doc(current_article_path) 32 | docs = docs_in_order 33 | index = docs_in_order.find_index { |d| d[:path] == current_article_path } 34 | 35 | if index == docs.count - 1 36 | nil 37 | else 38 | docs[index + 1] 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /site/source/docs/index.html.md: -------------------------------------------------------------------------------- 1 | # Yoke Documentation 2 | 3 | Lorem ipsum dolor sit amet, [inline link](https://nanobox.io) consectetur adipiscing elit. Duis eget ante eros. Praesent `inline code` eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 4 | 5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 6 | 7 | ## This is an H2 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 11 | 12 | ### This is an H3 13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 14 | 15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 16 | 17 | #### This is an H4 18 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 19 | 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 21 | 22 | ##### This is an H5 23 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 24 | 25 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 26 | 27 | ###### This is an H6 28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 29 | 30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis eget ante eros. Praesent eleifend viverra felis ut laoreet. Sed at tellus at ipsum tempor feugiat vel eget leo. Ut sollicitudin rutrum metus. Inline Link a tellus blandit vulputate. Curabitur vitae hendrerit turpis. Ut in aliquet nisl, in tincidunt tellus. Donec vitae ipsum consectetur, dictum tellus vel, vulputate lorem. 31 | 32 | This is *italicized* 33 | 34 | This is **bold** 35 | 36 | This is ~~strikethrough~~ 37 | 38 | This is text with [an inline link](https://nanobox.io) 39 | 40 | 1. This is the 1st item in an ordered list 41 | 2. This is the 2nd item in an ordered list 42 | 3. This is the 3rd item in an ordered list 43 | 44 | 45 | * This is an unordered list 46 | * This is an unordered list 47 | * This is an unordered list 48 | 49 | Below is an image: 50 | 51 | ![This is an Image](https://trello-attachments.s3.amazonaws.com/554b82df5e12ddf95313a8fe/381x198/e61e0a3ff391c11f24f10dfc8f88e770/upload_2015-05-07_at_9.26.24_am.png "This is an image") 52 | 53 | ```txt 54 | This is a code block using a
 tag
55 | ```
56 | 
57 | ```yaml
58 | # This is a comment in YAML This is a comment in YAML This is a comment in YAML This is a comment in YAML This is a comment in YAML
59 | 
60 | web1:
61 |   type: ruby
62 |   version: 5.6
63 |   before_exec:
64 |     - "bundler install"
65 | 
66 | worker1:
67 |   engine: "sanderson/ruby-unicorn"
68 |   version: 1.2
69 | ```
70 | 
71 | ```bash
72 | $ nanobox dev
73 | ```
74 | 
75 | 
76 | This is text with `inline code`
77 | 
78 | | Tables        | Are           | Cool  |
79 | | ------------- |:-------------:| -----:|
80 | | col 3 is      | right-aligned | $1600 |
81 | | col 2 is      | centered      |   $12 |
82 | | zebra stripes | are neat      |    $1 |
83 | | col 3 is      | right-aligned | $1600 |
84 | | col 2 is      | centered      |   $12 |
85 | | zebra stripes | are neat      |    $1 |
86 | 
87 | > This is blockquote that is very long and should wrap, but I guess we'll see, just because I don't know how wide this thing is.
88 | 
89 | Below is a horizontal rule
90 | 
91 | ---
92 | 
93 | Above is a horiztonal rule


--------------------------------------------------------------------------------
/site/source/docs/test-1.html.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/docs/test-1.html.md


--------------------------------------------------------------------------------
/site/source/docs/test-2/index.html.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/docs/test-2/index.html.md


--------------------------------------------------------------------------------
/site/source/docs/test-2/sub1.html.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: This is Sub-Doc 1
3 | ---
4 | 
5 | This is sub-doc 1


--------------------------------------------------------------------------------
/site/source/docs/test-2/sub2/index.html.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/docs/test-2/sub2/index.html.md


--------------------------------------------------------------------------------
/site/source/docs/test-2/sub2/sub2-sub1.html.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/docs/test-2/sub2/sub2-sub1.html.md


--------------------------------------------------------------------------------
/site/source/docs/test-2/sub2/sub2-sub2.html.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/docs/test-2/sub2/sub2-sub2.html.md


--------------------------------------------------------------------------------
/site/source/docs/test-2/sub3.html.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/docs/test-2/sub3.html.md


--------------------------------------------------------------------------------
/site/source/docs/test-3.html.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/docs/test-3.html.md


--------------------------------------------------------------------------------
/site/source/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/images/background.png


--------------------------------------------------------------------------------
/site/source/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/images/favicon.png


--------------------------------------------------------------------------------
/site/source/images/middleman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nanopack/yoke/7d81075e517825b1e065f592dcbe58ec0b69e52f/site/source/images/middleman.png


--------------------------------------------------------------------------------
/site/source/index.html.haml:
--------------------------------------------------------------------------------
1 | ---
2 | layout: landing
3 | ---
4 | 


--------------------------------------------------------------------------------
/site/source/javascripts/all.js:
--------------------------------------------------------------------------------
  1 | //= require_tree .
  2 | //= require jquery-2.1.4.min.js
  3 | //= require highlight.min.js
  4 | 
  5 | ////////////////// SYNTAX HIGHLIGHTING //////////////////
  6 | 
  7 | $(function() {
  8 |   hljs.initHighlightingOnLoad();
  9 | });
 10 | 
 11 | ///////////////////// TOGGLE MODAL /////////////////////
 12 | 
 13 | function toggleModal() {
 14 |   if($('#dl-modal').is(':visible')) {
 15 |     $('#dl-modal').fadeOut(200);
 16 |   } else {
 17 |     $('#dl-modal').fadeIn(200);
 18 |   };
 19 | }
 20 | 
 21 | //////////////////// MODAL BEHAVIOR ////////////////////
 22 | 
 23 | $(function() {
 24 |   $('a#download, #dl-modal').click(function (e) {
 25 |     e.preventDefault();
 26 |     toggleModal();
 27 |   })
 28 |   $(".container").click(function(e) {
 29 |       e.stopPropagation();
 30 |    });
 31 | })
 32 | 
 33 | /////////// OPEN / CLOSE RESPONSIVE CONTENTS ///////////
 34 | 
 35 | $(function() {
 36 |   $('#contents-btn').on('click', function(e) {
 37 |     $('#contents').toggleClass('closed');
 38 |     $('#contents-btn').toggleClass('open');
 39 |   })
 40 | })
 41 | 
 42 | /////////// ADD/REMOVE CLASS ON CONTENTS BTN ///////////
 43 | 
 44 | $(window).on('resize', function() {
 45 |   if (document.documentElement.clientWidth > 640) {
 46 |     if(!$('#contents').hasClass('closed')) {
 47 |       $('#contents').addClass('closed');      
 48 |       $('#contents-btn').removeClass('open');
 49 |     };
 50 |   }
 51 |   if (document.documentElement.clientWidth < 640) {
 52 |     if($('#contents').hasClass('closed')) {
 53 |       $('#contents-btn').removeClass('open');
 54 |     };
 55 |   }
 56 | })
 57 | 
 58 | /////////////// HIDE UNUSED NAV SECTIONS ///////////////
 59 | 
 60 | $(function(){
 61 |   $('ul#contents li:has(ul li.active)').addClass('open');
 62 |   $('ul#contents li:has(ul.sub)').addClass('more');
 63 | });
 64 | 
 65 | 
 66 | 
 67 | 
 68 | $(document).ready(function() {
 69 | 
 70 |   /////////////////// CONTENT FADE-IN ///////////////////
 71 | 
 72 |   setTimeout(function() {
 73 |     $('.fade-in').addClass('show');
 74 |   }, 800)
 75 |   setTimeout(function() {
 76 |     $('.fade-in-fast').addClass('show');
 77 |   }, 200)
 78 | 
 79 |   ///////////// ADD LINKS TO CONTENT HEADINGS /////////////
 80 | 
 81 |   $(".content h2, .content h3, .content h4, .content h5, .content h6").each(function() {
 82 |     var link = ""
 83 |     $(this).wrapInner( link );
 84 |   })
 85 | 
 86 |   //////////////////// SMOOTH SCROLL ////////////////////
 87 | 
 88 |   $('a[href^="#"]').on('click',function (e) {
 89 |     e.preventDefault();
 90 | 
 91 |     var target = this.hash;
 92 |     var $target = $(target);
 93 | 
 94 |     $('html, body').stop().animate({
 95 |       'scrollTop': $target.offset().top
 96 |     }, 400, 'swing', function () {
 97 |       window.location.hash = target;
 98 |     });
 99 |   });
100 | 
101 | });
102 | 


--------------------------------------------------------------------------------
/site/source/layouts/landing.haml:
--------------------------------------------------------------------------------
 1 | !!!
 2 | %html
 3 |   %head
 4 |     %meta{:charset => "utf-8"}/
 5 |     %meta{:content => "width=device-width, initial-scale=1", :name => "viewport"}/
 6 |     / Always force latest IE rendering engine or request Chrome Frame
 7 |     %meta{:content => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/
 8 |     / Use title if it's in the page YAML frontmatter
 9 |     %title= data.project_info.name + " - " + data.project_info.subhead || "A Nanobox Open Source Project"
10 |     %link{:href => "https://fonts.googleapis.com/css?family=Lato:400,400italic,700,700italic,300italic,300", :rel => "stylesheet", :type => "text/css"}
11 |     %link{ :href => nav_root + "stylesheets/all.css", :rel => "stylesheet", :type => "text/css" }
12 |     %script{ :src => nav_root + "javascripts/all.js", :type => "text/javascript" }
13 |   %body{:class => page_classes}
14 |     .row.flood-1
15 |       = partial "nb-head"
16 |       .project-logo
17 |         = partial "svgs/project-logo.svg"
18 |       .headline
19 |         %h1= data.project_info.name
20 |         %h2= data.project_info.subhead
21 |         %p= data.project_info.summary
22 |       .project-btns
23 |         %a{ :href => "https://github.com/nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")}" } Source Code
24 |         - if data.project_info.full_docs
25 |           / %a{ :href => "/#{data.project_info.name.downcase.gsub(/\s+/, "")}/docs" } Documentation
26 |           %a{ :href => "docs" } Documentation
27 |         - if data.project_info.downloads
28 |           %a#download{ :href => '#' } Download
29 |       = partial 'gh-btns'
30 | 
31 |     .row.flood-2
32 |       .tlk-pnts
33 |         - data.project_info.points.each do |point|
34 |           .item
35 |             %h4= point.title
36 |             %p= point.content
37 | 
38 |     .row.flood-1
39 |       .content
40 |         = find_and_preserve do
41 |           = partial "README.md"
42 | 
43 |     .row.flood-2
44 |       = partial 'nb-foot'
45 | 
46 |     - if data.project_info.downloads
47 |       #dl-modal
48 |         .container
49 |           - data.project_info.downloads.each do |download|
50 |             = link_to download.text, download.link
51 |         .close
52 |           ✕  


--------------------------------------------------------------------------------
/site/source/layouts/layout.haml:
--------------------------------------------------------------------------------
 1 | !!!
 2 | %html
 3 |   %head
 4 |     %meta{:charset => "utf-8"}/
 5 |     %meta{:content => "width=device-width, initial-scale=1", :name => "viewport"}/
 6 |     / Always force latest IE rendering engine or request Chrome Frame
 7 |     %meta{:content => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/
 8 |     / Use title if it's in the page YAML frontmatter
 9 |     %title
10 |       - if current_page.data.title
11 |         = current_page.data.title + " - " + data.project_info.name + " Documentation" || "A Nanobox Open Source Project"
12 |       - else
13 |         = data.project_info.name + " Documentation"
14 |     %link{:href => "https://fonts.googleapis.com/css?family=Lato:400,400italic,700,700italic,300italic,300", :rel => "stylesheet", :type => "text/css"}
15 |     %link{ :href => nav_root + "stylesheets/all.css", :rel => "stylesheet", :type => "text/css" }
16 |     %script{ :src => nav_root + "javascripts/all.js", :type => "text/javascript" }
17 |   %body.docs
18 |     .row
19 |       .top-nav
20 |         %a{ :href => nav_root }  
21 |           = partial "svgs/project-logo.svg"
22 |           %span= data.project_info.name
23 |         %p Documentation
24 |     .row
25 |       .wrapper
26 |         %ul#contents.closed
27 |           #contents-btn
28 |             %p Contents
29 |             #icon
30 |           - data.docs_index.docs.each do |doc|
31 |             %li{ nav_article_active( doc.path ) }
32 |               = link_to doc.title, nav_root + doc.path
33 |               - if doc.sub_docs
34 |                 %ul.sub
35 |                   - doc.sub_docs.each do |doc|
36 |                     %li{ nav_article_active( doc.path ) }
37 |                       = link_to doc.title, nav_root + doc.path
38 |                       - if doc.sub_docs
39 |                         %ul.sub
40 |                           - doc.sub_docs.each do |doc|
41 |                             %li{ nav_article_active( doc.path ) }
42 |                               = link_to doc.title, nav_root + doc.path
43 | 
44 |         .content.fade-in-fast
45 |           -if current_page.data.title
46 |             %h1= current_page.data.title
47 |           = find_and_preserve do
48 |             = yield
49 | 
50 |           #pagination
51 |             - prev_doc = get_prev_doc(current_page.url.gsub(/^\//, ""))
52 |             - if prev_doc
53 |               = link_to prev_doc[:title], nav_root + prev_doc[:path], class: "prev"
54 | 
55 |             - next_doc = get_next_doc(current_page.url.gsub(/^\//, ""))
56 |             - if next_doc
57 |               = link_to next_doc[:title], nav_root + next_doc[:path], class: "next"
58 | 
59 |     .row.flood-2
60 |       = partial "nb-foot"


--------------------------------------------------------------------------------
/site/source/partials/_README.md:
--------------------------------------------------------------------------------
 1 | ### Requirements
 2 | 
 3 | Yoke has the following requirements/dependancies to run:
 4 | 
 5 | - A 3-server cluster consisting of a 'primary', 'secondary', and 'monitor' node
 6 | - 'primary' & 'secondary' nodes need ssh connections between each other (w/o passwords)
 7 | - 'primary' & 'secondary' nodes need rsync (or some alternative sync_command) installed
 8 | - 'primary' & 'secondary' nodes should have postgres installed under a postgres user, and in the `path`. Yoke tries calling 'postgres' and 'pg_ctl'
 9 | - 'primary' & 'secondary' nodes run postgres as a child process so it should not be started independently
10 | 
11 | Each node in the cluster requires it's own config.ini file with the following options (provided values are defaults):
12 | 
13 | ```ini
14 | [config]
15 |   advertise_ip=         # REQUIRED - the IP which this node will broadcast to other nodes
16 |   advertise_port=4400   # the port which this node will broadcast to other nodes
17 |   data_dir=/data/       # the directory where postgresql was installed
18 |   decision_timeout=10   # delay before node dicides what to do with postgresql instance
19 |   log_level=info        # log verbosity (trace, debug, info, warn error, fatal)
20 |   peers=                # REQUIRED - the (comma delimited) IP:port combination of all nodes that are to be in the cluster
21 |   pg_port=5432          # the postgresql port
22 |   role=monitor          # REQUIRED - either 'primary', 'secondary', or 'monitor' (the cluster needs exactly one of each)
23 |   status_dir=./status/  # the directory where node status information is stored
24 |   sync_command='rsync -a --delete {{local_dir}} {{slave_ip}}:{{slave_dir}}' # the command you would like to use to sync the data from this node to the other when this node is master. This uses Mustache style templating so Yoke can fill in the {{local_dir}}, {{slave_ip}}, {{slave_dir}} if you want to use them.
25 | 
26 | [vip]
27 |   ip="1.2.3.4"          # Virtual Ip you would like to use
28 |   add_command           # Command to use when adding the vip. This will be called as {{add_command}} {{vip}}
29 |   remove_command        # Command to use when removeing the vip. This will be called as {{remove_command}} {{vip}}
30 | 
31 | [role_change]
32 |   command               # When this nodes role changes we will call the command with the new role as its arguement '{{command}} {{(master|slave|single}))'
33 | ```
34 | 
35 | 
36 | ### Startup
37 | Once all configurations are in place, Start yoke by running:
38 | 
39 | ```
40 | ./yoke ./primary.ini
41 | ```
42 | 
43 | **Note:** The ini file can be named anything and reside anywhere. All Yoke needs is the /path/to/config.ini on startup.
44 | 
45 | 
46 | ### Yoke CLI - yokeadm
47 | 
48 | Yoke comes with its own CLI, yokeadm, that allows for limited introspection into the cluster.
49 | 
50 | #### Building the CLI:
51 | 
52 | ```
53 | cd ./yokeadm
54 | go build
55 | ./yokeadm
56 | ```
57 | 
58 | ##### Usage:
59 | 
60 | ```
61 | yokeadm (: OR ) [GLOBAL FLAG]  [SUB FLAGS]
62 | ```
63 | 
64 | ##### Available Commands:
65 | 
66 | - list   : Returns status information for all nodes in the cluster
67 | - demote : Advises a node to demote
68 | 
69 | ### Documentation
70 | 
71 | Complete documentation is available on [godoc](http://godoc.org/github.com/nanopack/yoke).
72 | 
73 | 
74 | ### Contributing
75 | 
76 | Contributions to the Yoke project are welcome and encouraged. Yoke is a [Nanobox](https://nanobox.io) project and contributions should follow the [Nanobox Contribution Process & Guidelines](https://docs.nanobox.io/contributing/).


--------------------------------------------------------------------------------
/site/source/partials/_gh-btns.haml:
--------------------------------------------------------------------------------
1 | .gh-btns.fade-in
2 |   %a.github-button{"aria-label" => "Star nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")} on GitHub", "data-count-api" => "/repos/nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")}#stargazers_count", "data-count-aria-label" => "# stargazers on GitHub", "data-count-href" => "/nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")}/stargazers", "data-icon" => "octicon-star", :href => "https://github.com/nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")}"} Star
3 |   %a.github-button{"aria-label" => "Watch nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")} on GitHub", "data-count-api" => "/repos/nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")}#subscribers_count", "data-count-aria-label" => "# watchers on GitHub", "data-count-href" => "/nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")}/watchers", "data-icon" => "octicon-eye", :href => "https://github.com/nanopack/#{data.project_info.name.downcase.gsub(/\s+/, "")}"} Watch
4 |   %script#github-bjs{:async => "async", :defer => "defer", :src => "https://buttons.github.io/buttons.js"}


--------------------------------------------------------------------------------
/site/source/partials/_nb-foot.haml:
--------------------------------------------------------------------------------
 1 | .nb-foot
 2 |   .nb-info
 3 |     %a.nb-circle{ :href => 'https://nanobox.io' }
 4 |       = partial 'svgs/nb-logo'
 5 |     %p
 6 |       Made with
 7 |       %span.heart ♥
 8 |       by
 9 |       %a{ :href => 'https://nanobox.io' } Nanobox
10 |       under the
11 |       - if data.project_info.license == 'mit'
12 |         %a{ :href => 'https://opensource.org/licenses/MIT', :target => '_blank' }  MIT License
13 |       - else
14 |         %a{ :href => 'https://www.mozilla.org/en-US/MPL/2.0/', :target => '_blank' }  MPL 2.0
15 |     %p.contribute 
16 |       Want to contribute? View our
17 |       %a{ :href => 'http://nanopack.io/contributing' } contributing guidelines.


--------------------------------------------------------------------------------
/site/source/partials/_nb-head.haml:
--------------------------------------------------------------------------------
1 | %a.nb-project.fade-in{ :href => 'https://nanobox.io', :target => '_blank'}
2 |   = partial 'svgs/nb-logo'
3 |   %span
4 |     A Nanobox Project


--------------------------------------------------------------------------------
/site/source/partials/svgs/_nb-logo.haml:
--------------------------------------------------------------------------------
  1 | %svg#nb-logo{"enable-background" => "new 0 0 182.7 200", :space => "preserve", :version => "1.1", :viewbox => "0 0 182.7 200", :x => "0px", :xmlns => "http://www.w3.org/2000/svg", "xmlns:xlink" => "http://www.w3.org/1999/xlink", :y => "0px"}
  2 |   %g
  3 |     %polygon{:fill => "#FFFFFF", :points => "0.3,138.3 90.6,184.8 183,137.4 92.7,90.9 \t\t\t"}
  4 |     %polygon{:fill => "#FFFFFF", :points => "77.9,148.7 109.7,165.5 125.6,157.3 93.8,140.5 \t\t\t"}
  5 |     %polygon{:fill => "#D8DFE3", :points => "125.6,157.2 93.8,140.4 93.8,151.5 115.1,162.7 \t\t\t"}
  6 |     %polygon{:fill => "#E2E9ED", :points => "93.8,140.4 93.8,151.5 88.4,154.3 77.9,148.7 \t\t\t"}
  7 |     %polygon{:fill => "#FFFFFF", :points => "98.1,138.3 129.9,155 145.9,146.8 114,130.1 \t\t\t"}
  8 |     %polygon{:fill => "#D8DFE3", :points => "145.9,146.7 114,130 114,141 135.4,152.2 \t\t\t"}
  9 |     %polygon{:fill => "#E2E9ED", :points => "114,130 114,141 108.6,143.8 98.1,138.3 \t\t\t"}
 10 |     %polygon{:fill => "#FFFFFF", :points => "118.3,127.7 150.1,144.4 166.1,136.2 134.2,119.5 \t\t\t"}
 11 |     %polygon{:fill => "#D8DFE3", :points => "166.1,136.1 134.2,119.4 134.2,130.4 155.6,141.6 \t\t\t"}
 12 |     %polygon{:fill => "#E2E9ED", :points => "134.2,119.4 134.2,130.4 128.8,133.2 118.3,127.7 \t\t\t"}
 13 |     %polygon{:fill => "#D8DFE3", :points => "90.6,184.9 0.3,138.4 0.3,153.2 90.6,199.7 \t\t\t"}
 14 |     %polygon{:fill => "#E2E9ED", :points => "90.6,184.9 183,137.4 183,152.2 90.6,199.7 \t\t\t"}
 15 |     %polygon{:fill => "#FFFFFF", :points => "16.4,137.7 33,146.5 49,138.3 32.3,129.4 \t\t\t"}
 16 |     %polygon{:fill => "#D8DFE3", :points => "49,138.2 32.3,129.3 32.3,140.4 38.5,143.7 \t\t\t"}
 17 |     %polygon{:fill => "#F9FCFC", :points => "36.6,127.1 68.5,143.8 84.4,135.6 52.5,118.9 \t\t\t"}
 18 |     %polygon{:fill => "#D8DFE3", :points => "84.4,135.5 52.5,118.8 52.5,129.8 73.9,141 \t\t\t"}
 19 |     %polygon{:fill => "#E2E9ED", :points => "52.5,118.8 52.5,129.8 47.1,132.6 36.6,127.1 \t\t\t"}
 20 |     %polygon{:fill => "#F9FCFC", :points => "56.8,116.6 88.7,133.3 104.6,125.1 72.7,108.4 \t\t\t"}
 21 |     %polygon{:fill => "#D8DFE3", :points => "104.6,125 72.7,108.3 72.7,119.3 94.1,130.5 \t\t\t"}
 22 |     %polygon{:fill => "#E2E9ED", :points => "72.7,108.3 72.7,119.3 67.4,122.1 56.8,116.6 \t\t\t"}
 23 |     %polygon{:fill => "#F9FCFC", :points => "77,106 108.9,122.7 124.8,114.5 92.9,97.8 \t\t\t"}
 24 |     %polygon{:fill => "#D8DFE3", :points => "124.8,114.4 92.9,97.7 92.9,108.7 114.3,119.9 \t\t\t"}
 25 |     %polygon{:fill => "#E2E9ED", :points => "92.9,97.7 92.9,108.7 87.6,111.6 77,106 \t\t\t"}
 26 |     %g
 27 |       %defs
 28 |         %polygon#SVGID_1_{:points => "16.4,137.7 33,146.5 49,138.3 32.3,129.4 \t\t\t\t\t"}
 29 |       %clippath#SVGID_2_
 30 |         %use{:overflow => "visible", "xlink:href" => "#SVGID_1_"}
 31 |     %polygon{:fill => "#FFFFFF", :points => "73.9,167.3 90.5,176.2 106.5,167.9 89.8,159.1 \t\t\t"}
 32 |     %polygon{:fill => "#D8DFE3", :points => "106.5,167.8 89.8,159 89.8,170.1 96,173.4 \t\t\t"}
 33 |     %polygon{:fill => "#E2E9ED", :points => "89.8,159 89.8,170.1 83.9,173.1 73.3,167.6 \t\t\t"}
 34 |     %polygon{:fill => "#FFFFFF", :points => "44.2,152.5 60.8,161.3 76.7,153.1 60.1,144.2 \t\t\t"}
 35 |     %polygon{:fill => "#D8DFE3", :points => "76.7,153 60.1,144.1 60.1,155.2 66.2,158.5 \t\t\t"}
 36 |     %polygon{:fill => "#E2E9ED", :points => "60.1,144.1 60.1,155.2 54.7,158 44.2,152.5 \t\t\t"}
 37 |     %polygon{:fill => "#E2E9ED", :points => "32.3,129.3 32.3,140.4 26.9,143.2 16.4,137.6 \t\t\t"}
 38 |   %g
 39 |     %polygon{:fill => "#0A5F68", :points => "101.2,101.4 101.2,89.1 70.1,105.5 70.1,117.4 \t\t\t\t\t"}
 40 |     %polygon{:fill => "#064C54", :points => "70.1,105.5 54.6,97.4 54.6,109.4 70.1,117.4 \t\t\t\t\t"}
 41 |     %polygon{:fill => "#4EBFC9", :points => "64.5,108.3 33.4,124.6 17.9,116.6 48.9,100.3 \t\t\t\t\t"}
 42 |     %polygon{:fill => "#0A5F68", :points => "120.9,111.7 120.9,99.5 89.9,115.8 89.9,127.7 \t\t\t\t\t"}
 43 |     %polygon{:fill => "#064C54", :points => "89.9,115.8 74.3,107.8 74.3,119.7 89.9,127.7 \t\t\t\t\t"}
 44 |     %polygon{:fill => "#0A5F68", :points => "140.6,122.1 140.6,109.8 109.6,126.1 109.6,138 \t\t\t\t\t"}
 45 |     %polygon{:fill => "#064C54", :points => "109.6,126.1 94,118.1 94,130 109.6,138 \t\t\t\t\t"}
 46 |     %polygon{:fill => "#12848F", :points => "64.5,120.6 64.5,108.3 33.4,124.6 33.4,136.6 \t\t\t\t\t"}
 47 |     %polygon{:fill => "#0C6870", :points => "33.4,124.6 17.9,116.6 17.9,128.6 33.4,136.6 \t\t\t\t\t"}
 48 |     %polygon{:fill => "#4EBFC9", :points => "84.2,118.6 53.1,135 37.6,127 68.7,110.6 \t\t\t\t\t"}
 49 |     %polygon{:fill => "#12848F", :points => "84.2,130.9 84.2,118.6 53.1,135 53.1,146.9 \t\t\t\t\t"}
 50 |     %polygon{:fill => "#0C6870", :points => "53.1,135 37.6,127 37.6,138.9 53.1,146.9 \t\t\t\t\t"}
 51 |     %polygon{:fill => "#4EBFC9", :points => "103.9,128.9 72.8,145.3 57.3,137.3 88.4,121 \t\t\t\t\t"}
 52 |     %polygon{:fill => "#12848F", :points => "103.9,141.3 103.9,128.9 72.8,145.3 72.8,157.2 \t\t\t\t\t"}
 53 |     %polygon{:fill => "#0C6870", :points => "72.8,145.3 57.3,137.3 57.3,149.2 72.8,157.2 \t\t\t\t\t"}
 54 |     %polygon{:fill => "#4EBFC9", :points => "159.5,118.1 108.1,145 95.4,138.5 146.8,111.5 \t\t\t\t\t\t"}
 55 |     %polygon{:fill => "#12848F", :points => "159.5,130 159.5,118.1 108.1,145 108.1,156.7 \t\t\t\t\t\t"}
 56 |     %polygon{:fill => "#0C6870", :points => "108.1,145 95.4,138.5 95.4,150.2 108.1,156.7 \t\t\t\t\t\t"}
 57 |     %polygon{:fill => "#26828A", :points => "26.9,111.9 49,100.4 64.5,108.3 42.5,119.9 \t\t\t\t\t"}
 58 |     %polygon{:fill => "#26828A", :points => "46.7,122.1 68.7,110.6 84.2,118.6 62.2,130.2 \t\t\t\t\t"}
 59 |     %polygon{:fill => "#26828A", :points => "66.4,132.4 88.4,121 103.9,128.9 81.9,140.5 \t\t\t\t\t"}
 60 |     %polygon{:fill => "#26828A", :points => "95.4,138.5 101.2,141.5 152.5,114.5 146.8,111.5 \t\t\t\t\t"}
 61 |     %polygon{:fill => "#0A5F68", :points => "42.5,124.4 42.5,119.9 64.5,108.3 64.5,112.9 \t\t\t\t"}
 62 |     %polygon{:fill => "#0A5F68", :points => "62.7,134.7 62.7,130.2 84.4,118.6 84.6,123.1 \t\t\t\t"}
 63 |     %polygon{:fill => "#0A5F68", :points => "81.9,152.6 81.9,140.5 103.9,129 103.9,134 95.4,138.4 95.4,145.7 \t\t\t\t"}
 64 |     %polygon{:fill => "#26828A", :points => "74.3,107.7 105.4,91.4 120.9,99.5 89.8,115.7 \t\t\t\t"}
 65 |     %polygon{:fill => "#26828A", :points => "94,118 125.1,101.8 140.6,109.8 109.5,126.1 \t\t\t\t"}
 66 |     %polygon{:fill => "#26828A", :points => "54.6,97.4 85.7,81.1 101.2,89.1 70.1,105.5 \t\t\t\t"}
 67 |     %polygon{:fill => "#064C54", :points => "95.4,138.4 101.2,141.5 101.2,153.2 95.4,150.2 \t\t\t\t"}
 68 |     %polygon{:fill => "#4AF1FF", :points => "103.3,147.5 90.5,154 77.8,147.5 90.5,140.9 \t\t\t\t"}
 69 |     %polygon{:fill => "#31CFDC", :points => "103.3,159.4 103.3,147.5 90.5,154 90.5,165.7 \t\t\t\t"}
 70 |     %polygon{:fill => "#1FC1D1", :points => "90.5,154 77.8,147.5 77.8,159.2 90.5,165.7 \t\t\t\t"}
 71 |   %g
 72 |     %polygon{:fill => "#EC5268", :points => "91.5,122.8 178.8,77.7 178.8,90.9 91.5,135.9 \t\t\t"}
 73 |     %polygon{:fill => "#C74F62", :points => "91.5,122.8 4.1,77.7 4.1,90.9 91.5,135.9 \t\t\t"}
 74 |     %polygon{:fill => "#F97792", :points => "178.8,77.7 91.5,122.8 4.1,77.7 91.5,32.7 \t\t\t"}
 75 |     %polygon{:fill => "#F25C74", :points => "171.2,74.2 91.5,115.3 11.8,74.2 91.5,33.2 \t\t\t"}
 76 |   %g
 77 |     %polygon{:fill => "#2F4757", :points => "177.9,45.6 89.9,91 2,45.6 89.9,0.3 \t\t\t"}
 78 |     %polygon{:fill => "#50728A", :points => "40.6,38.3 23.1,47.3 20,45.6 37.5,36.6 \t\t\t"}
 79 |     %polygon{:fill => "#47657A", :points => "59.4,34.3 34.7,47 31.6,45.4 56.2,32.7 \t\t\t"}
 80 |     %polygon{:fill => "#47657A", :points => "64.9,37.2 40.3,49.9 37.2,48.3 61.8,35.6 \t\t\t"}
 81 |     %polygon{:fill => "#47657A", :points => "70.5,40.1 45.9,52.8 42.7,51.1 67.4,38.4 \t\t\t"}
 82 |     %polygon{:fill => "#50728A", :points => "66,51.4 48.5,60.4 45.4,58.7 62.9,49.7 \t\t\t"}
 83 |     %polygon{:fill => "#47657A", :points => "84.8,47.4 60.1,60.1 57,58.5 81.6,45.8 \t\t\t"}
 84 |     %polygon{:fill => "#47657A", :points => "90.3,50.3 65.7,63 62.6,61.4 87.2,48.7 \t\t\t"}
 85 |     %polygon{:fill => "#47657A", :points => "95.9,53.2 71.3,65.9 68.1,64.2 92.8,51.5 \t\t\t"}
 86 |     %polygon{:fill => "#50728A", :points => "90.6,64 73.1,73 69.9,71.4 87.4,62.4 \t\t\t"}
 87 |     %polygon{:fill => "#47657A", :points => "109.3,60.1 84.7,72.8 81.5,71.1 106.2,58.4 \t\t\t"}
 88 |     %polygon{:fill => "#47657A", :points => "114.9,62.9 90.2,75.6 87.1,74 111.7,61.3 \t\t\t"}
 89 |     %polygon{:fill => "#47657A", :points => "120.4,65.8 95.8,78.5 92.7,76.9 117.3,64.2 \t\t\t"}
 90 |     %polygon{:fill => "#50728A", :points => "80,18.2 62.5,27.2 59.3,25.6 76.8,16.5 \t\t\t"}
 91 |     %polygon{:fill => "#47657A", :points => "98.7,14.3 74.1,27 70.9,25.3 95.6,12.6 \t\t\t"}
 92 |     %polygon{:fill => "#47657A", :points => "104.2,17.1 79.6,29.8 76.5,28.2 101.1,15.5 \t\t\t"}
 93 |     %polygon{:fill => "#47657A", :points => "109.8,20 85.2,32.7 82.1,31 106.7,18.4 \t\t\t"}
 94 |     %polygon{:fill => "#50728A", :points => "105.4,31.3 87.9,40.3 84.7,38.7 102.2,29.7 \t\t\t"}
 95 |     %polygon{:fill => "#47657A", :points => "124.1,27.4 99.5,40.1 96.3,38.4 121,25.7 \t\t\t"}
 96 |     %polygon{:fill => "#47657A", :points => "129.7,30.2 105,42.9 101.9,41.3 126.5,28.6 \t\t\t"}
 97 |     %polygon{:fill => "#47657A", :points => "135.2,33.1 110.6,45.8 107.5,44.2 132.1,31.5 \t\t\t"}
 98 |     %polygon{:fill => "#50728A", :points => "129.9,43.9 112.4,52.9 109.3,51.3 126.8,42.3 \t\t\t"}
 99 |     %polygon{:fill => "#47657A", :points => "148.6,40 124,52.7 120.9,51.1 145.5,38.4 \t\t\t"}
100 |     %polygon{:fill => "#47657A", :points => "154.2,42.9 129.5,55.6 126.4,53.9 151.1,41.2 \t\t\t"}
101 |     %polygon{:fill => "#47657A", :points => "159.7,45.7 135.1,58.4 132,56.8 156.6,44.1 \t\t\t"}
102 |     %polygon{:fill => "#1E2B33", :points => "89.9,91 177.9,45.6 177.9,58.9 89.9,104.2 \t\t\t"}
103 |     %polygon{:fill => "#19242B", :points => "89.9,91 2,45.6 2,58.9 89.9,104.2 \t\t\t"}


--------------------------------------------------------------------------------
/site/source/partials/svgs/_project-logo.svg:
--------------------------------------------------------------------------------
  1 | 


--------------------------------------------------------------------------------
/site/source/partials/svgs/_yoke.haml:
--------------------------------------------------------------------------------
 1 | %svg{"enable-background" => "new 0 0 350 350", :space => "preserve", :version => "1.1", :viewbox => "0 0 350 350", :x => "0px", :xmlns => "http://www.w3.org/2000/svg", "xmlns:xlink" => "http://www.w3.org/1999/xlink", :y => "0px"}
 2 |   %circle{:cx => "175", :cy => "175", :fill => "#FCE15B", :r => "175"}
 3 |   %g
 4 |     %path{:d => "M337.8,162.5c-4.7-11.1-12.6-25.7-23.5-32.5c-6.6-4.1-11.6-5.8-16.8-6.4c-11.5-10.4-26.3-16.2-41.9-16.2\r\n\t\tc-12.1,0-23.6,3.3-35.2,10c-4.9,2.9-9.7,5.3-14.4,7.4c-2.1,0.6-4.2,1.5-6.5,2.7c-9.1,3.4-17.4,5.2-24.6,5.2\r\n\t\tc-7.2,0-15.5-1.8-24.6-5.2c-2.3-1.2-4.4-2-6.5-2.7c-4.6-2.1-9.4-4.6-14.4-7.4c-11.6-6.8-23.1-10-35.2-10\r\n\t\tc-15.6,0-30.4,5.7-41.9,16.2c-5.2,0.6-10.2,2.3-16.8,6.4c-10.9,6.8-18.8,21.4-23.5,32.5c-0.4,0.9-0.4,2,0.1,2.9\r\n\t\tc2.2,4.5,9.1,18.4,38.4,26.1c7.2,1.9,11.7,4.4,13.8,5.8c-1.1,4-4.3,13.1-11.6,24.5c-2.6,4.1-1.9,7.7-0.1,9.5\r\n\t\tc0.9,0.9,2.1,1.3,3.3,1.3c1,0,2.1-0.3,3.1-1c7.4-5,12.4-11.2,15.6-16.5c0.2,2.1,0.3,4.1,0.4,6c1.4,20.5,2.3,34,19.2,34\r\n\t\tc0.3,0,0.6,0,0.8-0.1c0.3,0.1,0.5,0.1,0.8,0.1c16.9,0,17.9-13.5,19.2-34c0.1-1.9,0.3-3.9,0.4-6c3.3,5.2,8.3,11.4,15.6,16.5\r\n\t\tc1,0.7,2.1,1,3.1,1c1.2,0,2.4-0.4,3.3-1.3c1.9-1.8,2.5-5.5-0.1-9.5c-7.4-11.5-10.5-20.5-11.6-24.5c2-1.3,6-3.6,12.5-5.4\r\n\t\tc0.1,0.1,0.2,0.1,0.2,0.2c11.9,7.5,23.8,11.2,36.3,11.2c12.6,0,24.4-3.7,36.3-11.2c0.1-0.1,0.2-0.1,0.2-0.2\r\n\t\tc6.4,1.8,10.5,4.2,12.5,5.4c-1.1,4-4.3,13.1-11.6,24.5c-2.6,4.1-1.9,7.7-0.1,9.5c0.9,0.9,2.1,1.3,3.3,1.3c1,0,2.1-0.3,3.1-1\r\n\t\tc7.4-5,12.4-11.2,15.6-16.5c0.2,2.1,0.3,4.1,0.4,6c1.4,20.5,2.3,34,19.2,34c0.3,0,0.6,0,0.8-0.1c0.3,0.1,0.5,0.1,0.8,0.1\r\n\t\tc16.9,0,17.9-13.5,19.2-34c0.1-1.9,0.3-3.9,0.4-6c3.3,5.2,8.3,11.4,15.6,16.5c1,0.7,2.1,1,3.1,1c1.2,0,2.4-0.4,3.3-1.3\r\n\t\tc1.9-1.8,2.5-5.5-0.1-9.5c-7.4-11.5-10.5-20.5-11.6-24.5c2.1-1.4,6.6-3.9,13.8-5.8c29.3-7.7,36.1-21.5,38.4-26.1\r\n\t\tC338.2,164.5,338.2,163.4,337.8,162.5z", :fill => "#E8E8E8"}
 5 |   %path{:d => "M75.2,162.9l-8.3-3.8c-1.1,2.3,0,5,2.3,6.1C71.4,166.2,74.1,165.2,75.2,162.9z", :fill => "#206B9E"}
 6 |   %path{:d => "M108.5,184.2c-0.1-0.1-3.1-1.2-6.2-1.8c-1.6-0.3-3.1-0.5-4.4-0.6c-0.6-0.1-1.1-0.1-1.4-0.1\r\n\tc-0.3,0-0.5,0-0.5,0c-0.1,0-0.2,0-0.3,0c-0.2,0-0.4,0.1-0.6,0.1c-0.3-0.1-0.6-0.1-0.9-0.1c0,0-0.2,0-0.5,0c-0.3,0-0.8,0-1.4,0.1\r\n\tc-1.2,0.1-2.8,0.3-4.4,0.6c-3.1,0.6-6.1,1.7-6.2,1.8c-1.1,0.4-1.7,1.7-1.2,2.8c0.4,1.1,1.7,1.7,2.8,1.2c-0.1,0,2.6-0.8,5.4-1\r\n\tc1.4-0.2,2.8-0.2,3.8-0.2c0.5,0,1,0,1.3,0c0.3,0,0.5,0,0.5,0c0.1,0,0.2,0,0.3,0c0.2,0,0.3-0.1,0.5-0.1c0.3,0.1,0.5,0.1,0.8,0.1\r\n\tc0,0,0.2,0,0.5,0c0.3,0,0.8,0,1.3,0c1,0,2.4,0,3.8,0.2c2.8,0.3,5.5,1,5.4,1c1.1,0.4,2.4-0.1,2.8-1.2\r\n\tC110.2,185.9,109.7,184.6,108.5,184.2z", :fill => "#206B9E"}
 7 |   %path{:d => "M106.4,194.1c-0.1-0.1-2.6-1.1-5.2-1.5c-1.3-0.3-2.6-0.4-3.6-0.5c-0.5,0-0.9-0.1-1.2-0.1c-0.3,0-0.4,0-0.4,0\r\n\tc-0.1,0-0.1,0-0.2,0c-0.2,0-0.4,0.1-0.6,0.1c-0.3-0.1-0.6-0.1-0.9-0.1c0,0-0.2,0-0.4,0c-0.3,0-0.7,0-1.2,0.1c-1,0.1-2.3,0.2-3.6,0.5\r\n\tc-2.6,0.5-5,1.5-5.2,1.5c-1.1,0.5-1.6,1.7-1.2,2.8c0.5,1.1,1.7,1.6,2.8,1.2c-0.1,0,2-0.6,4.3-0.9c1.1-0.1,2.3-0.2,3.1-0.2\r\n\tc0.4,0,0.8,0,1,0c0.3,0,0.4,0,0.4,0c0.1,0,0.1,0,0.2,0c0.2,0,0.4-0.1,0.5-0.1c0.3,0.1,0.5,0.2,0.8,0.1c0,0,0.2,0,0.4,0\r\n\tc0.3,0,0.6,0,1,0c0.8,0,2,0,3.1,0.2c2.2,0.2,4.4,0.9,4.3,0.9c1.1,0.5,2.4-0.1,2.8-1.2C108,195.8,107.5,194.6,106.4,194.1z", :fill => "#206B9E"}
 8 |   %path{:d => "M121.3,165.1c2.3-1.1,3.3-3.8,2.3-6.1l-8.3,3.8C116.3,165.2,119,166.2,121.3,165.1z", :fill => "#206B9E"}
 9 |   %path{:d => "M268.1,184.2c-0.1-0.1-3.1-1.2-6.2-1.8c-1.6-0.3-3.1-0.5-4.4-0.6c-0.6-0.1-1.1-0.1-1.4-0.1\r\n\tc-0.3,0-0.5,0-0.5,0c-0.1,0-0.2,0-0.3,0c-0.2,0-0.4,0.1-0.6,0.1c-0.3-0.1-0.6-0.1-0.9-0.1c0,0-0.2,0-0.5,0c-0.3,0-0.8,0-1.4,0.1\r\n\tc-1.2,0.1-2.8,0.3-4.4,0.6c-3.1,0.6-6.1,1.7-6.2,1.8c-1.1,0.4-1.7,1.7-1.2,2.8c0.4,1.1,1.7,1.7,2.8,1.2c-0.1,0,2.6-0.8,5.4-1\r\n\tc1.4-0.2,2.8-0.2,3.8-0.2c0.5,0,1,0,1.3,0c0.3,0,0.5,0,0.5,0c0.1,0,0.2,0,0.3,0c0.2,0,0.3-0.1,0.5-0.1c0.3,0.1,0.5,0.1,0.8,0.1\r\n\tc0,0,0.2,0,0.5,0c0.3,0,0.8,0,1.3,0c1,0,2.4,0,3.8,0.2c2.8,0.3,5.5,1,5.4,1c1.1,0.4,2.4-0.1,2.8-1.2\r\n\tC269.8,185.9,269.3,184.6,268.1,184.2z", :fill => "#206B9E"}
10 |   %path{:d => "M266,194.1c-0.1-0.1-2.6-1.1-5.2-1.5c-1.3-0.3-2.6-0.4-3.6-0.5c-0.5,0-0.9-0.1-1.2-0.1c-0.3,0-0.4,0-0.4,0\r\n\tc-0.1,0-0.1,0-0.2,0c-0.2,0-0.4,0.1-0.6,0.1c-0.3-0.1-0.6-0.1-0.9-0.1c0,0-0.2,0-0.4,0c-0.3,0-0.7,0-1.2,0.1c-1,0.1-2.3,0.2-3.6,0.5\r\n\tc-2.6,0.5-5,1.5-5.2,1.5c-1.1,0.5-1.6,1.7-1.2,2.8c0.5,1.1,1.7,1.6,2.8,1.2c-0.1,0,2-0.6,4.3-0.9c1.1-0.1,2.3-0.2,3.1-0.2\r\n\tc0.4,0,0.8,0,1,0c0.3,0,0.4,0,0.4,0c0.1,0,0.1,0,0.2,0c0.2,0,0.4-0.1,0.5-0.1c0.3,0.1,0.5,0.2,0.8,0.1c0,0,0.2,0,0.4,0\r\n\tc0.3,0,0.6,0,1,0c0.8,0,2,0,3.1,0.2c2.2,0.2,4.4,0.9,4.3,0.9c1.1,0.5,2.4-0.1,2.8-1.2C267.6,195.8,267.1,194.6,266,194.1z", :fill => "#206B9E"}
11 |   %path{:d => "M337.8,162.5c-4.7-11.1-12.6-25.7-23.5-32.5c-6.6-4.1-11.6-5.8-16.8-6.4c-11.5-10.4-26.3-16.2-41.9-16.2\r\n\tc-12.1,0-23.6,3.3-35.2,10c-4.9,2.9-9.7,5.3-14.4,7.4c-2.1,0.6-4.2,1.5-6.5,2.7c-9.1,3.4-17.4,5.2-24.6,5.2\r\n\tc-7.2,0-15.5-1.8-24.6-5.2c-2.3-1.2-4.4-2-6.5-2.7c-4.6-2.1-9.4-4.6-14.4-7.4c-11.6-6.8-23.1-10-35.2-10c-15.6,0-30.4,5.7-41.9,16.2\r\n\tc-5.2,0.6-10.2,2.3-16.8,6.4c-10.9,6.8-18.8,21.4-23.5,32.5c-0.4,0.9-0.4,2,0.1,2.9c2.2,4.5,9.1,18.4,38.4,26.1\r\n\tc7.2,1.9,11.7,4.4,13.8,5.8c-1.1,4-4.3,13.1-11.6,24.5c-2.6,4.1-1.9,7.7-0.1,9.5c0.9,0.9,2.1,1.3,3.3,1.3c1,0,2.1-0.3,3.1-1\r\n\tc7.4-5,12.4-11.2,15.6-16.5c0.2,2.1,0.3,4.1,0.4,6c1.4,20.5,2.3,34,19.2,34c0.3,0,0.6,0,0.8-0.1c0.3,0.1,0.5,0.1,0.8,0.1\r\n\tc16.9,0,17.9-13.5,19.2-34c0.1-1.9,0.3-3.9,0.4-6c3.3,5.2,8.3,11.4,15.6,16.5c1,0.7,2.1,1,3.1,1c1.2,0,2.4-0.4,3.3-1.3\r\n\tc1.9-1.8,2.5-5.5-0.1-9.5c-7.4-11.5-10.5-20.5-11.6-24.5c2-1.3,6-3.6,12.5-5.4c0.1,0.1,0.2,0.1,0.2,0.2\r\n\tc11.9,7.5,23.8,11.2,36.3,11.2c12.6,0,24.4-3.7,36.3-11.2c0.1-0.1,0.2-0.1,0.2-0.2c6.4,1.8,10.5,4.2,12.5,5.4\r\n\tc-1.1,4-4.3,13.1-11.6,24.5c-2.6,4.1-1.9,7.7-0.1,9.5c0.9,0.9,2.1,1.3,3.3,1.3c1,0,2.1-0.3,3.1-1c7.4-5,12.4-11.2,15.6-16.5\r\n\tc0.2,2.1,0.3,4.1,0.4,6c1.4,20.5,2.3,34,19.2,34c0.3,0,0.6,0,0.8-0.1c0.3,0.1,0.5,0.1,0.8,0.1c16.9,0,17.9-13.5,19.2-34\r\n\tc0.1-1.9,0.3-3.9,0.4-6c3.3,5.2,8.3,11.4,15.6,16.5c1,0.7,2.1,1,3.1,1c1.2,0,2.4-0.4,3.3-1.3c1.9-1.8,2.5-5.5-0.1-9.5\r\n\tc-7.4-11.5-10.5-20.5-11.6-24.5c2.1-1.4,6.6-3.9,13.8-5.8c29.3-7.7,36.1-21.5,38.4-26.1C338.2,164.5,338.2,163.4,337.8,162.5z\r\n\t M255.6,113.7c11.4,0,22.3,3.4,31.6,9.7c-1.1,0-2.2,0.1-3.4,0.2c-3.9,0.2-8.3,0.4-13.8,0.4c-0.3,0-0.6,0.1-0.9,0.1\r\n\tc-3.7-1.4-8.1-2.2-13.4-2.2c-0.3,0-0.6,0-0.8,0.1c-0.3-0.1-0.5-0.1-0.8-0.1c-5.3,0-9.7,0.8-13.4,2.2c-0.3-0.1-0.6-0.1-0.9-0.1\r\n\tc-5.5,0-9.9-0.2-13.8-0.4c-1.1-0.1-2.1-0.1-3.2-0.1c0.3-0.2,0.6-0.4,1-0.5C234.2,116.7,244.7,113.7,255.6,113.7z M229.7,176.3\r\n\tc-2-1.8-7-6.7-7-11.7c0-3.6,1.1-5.9,2.6-9c0.2-0.5,0.5-1,0.7-1.5c2,1.5,4.3,2.5,6.2,3.3c2,0.9,4.8,2,5.1,3.1\r\n\tc0.3,1.8-3.2,8.1-4.9,11.1C231.4,173.2,230.4,174.9,229.7,176.3z M224.4,138.9c-0.4-0.1-0.8-0.2-1.2-0.3c-2.1-0.5-4.9-0.8-7.8-0.8\r\n\tc-1.4,0-2.8,0.1-4.2,0.2c-1.3,0.1-2.6,0.3-3.6,0.6c-1,0.2-1.9,0.5-2.5,0.7c-0.6,0.2-0.9,0.3-0.9,0.3c-1,0.3-1.7,1.4-1.4,2.4\r\n\tc0.2,1.1,1.3,1.9,2.5,1.7l0.2,0c0,0,0.3-0.1,0.8-0.2c0.5-0.1,1.3-0.3,2.2-0.3c0.9-0.1,2-0.2,3.1-0.2c1.1,0,2.4,0,3.6,0.2\r\n\tc2.4,0.3,4.8,0.8,6.6,1.4c0.1,0,0.2,0.1,0.4,0.1c0,0,0,0.1,0,0.1c-0.8,3.2-1.9,5.6-3,7.9c-1.6,3.4-3.3,6.9-3.3,12.1\r\n\tc0,9.8,10.2,17.7,10.6,18c0.3,0.2,0.5,0.3,0.8,0.4c-0.8,2.6-1.4,4.9-1.9,6.8c-3-1.6-7.3-3.6-13.3-5.2c-23.5-6.2-30.6-16.2-33.1-21\r\n\tc6-13.7,13-23.3,20.1-27.8c9.9-6.2,15.1-5.9,26.4-5.4c1.5,0.1,3.2,0.1,4.9,0.2C227.9,133.3,226,136.1,224.4,138.9z M175,196.2\r\n\tc-9.4,0-18.4-2.4-27.4-7.1c16-5.6,23.6-13,27.4-18.3c3.8,5.3,11.4,12.8,27.4,18.3C193.4,193.9,184.4,196.2,175,196.2z M124.7,155.6\r\n\tc1.5,3.1,2.6,5.4,2.6,9c0,4.7-4.5,9.6-7,11.7c-0.7-1.4-1.6-3.1-2.6-4.8c-1.7-3-5.2-9.3-4.9-11.1c0.2-1,3-2.2,5.1-3.1\r\n\tc1.9-0.8,4.2-1.8,6.2-3.3C124.2,154.6,124.5,155.1,124.7,155.6z M124.7,189.9c-0.4-1.9-1.1-4.3-1.9-6.8c0.3-0.1,0.6-0.2,0.8-0.4\r\n\tc0.4-0.3,10.6-8.2,10.6-18c0-5.2-1.7-8.7-3.3-12.1c-1.1-2.3-2.3-4.7-3-7.9c0,0,0-0.1,0-0.1c0.1,0,0.2-0.1,0.4-0.1\r\n\tc1.7-0.6,4.1-1.1,6.6-1.4c1.2-0.1,2.4-0.2,3.6-0.2c1.1,0,2.2,0.1,3.1,0.2c0.9,0.1,1.7,0.2,2.2,0.3c0.5,0.1,0.8,0.2,0.8,0.2l0.2,0\r\n\tc1,0.2,2.1-0.4,2.4-1.5c0.3-1.1-0.3-2.3-1.4-2.6c0,0-0.3-0.1-0.9-0.3c-0.6-0.2-1.4-0.4-2.5-0.7c-1-0.3-2.3-0.4-3.6-0.6\r\n\tc-1.3-0.1-2.8-0.2-4.2-0.2c-2.8,0-5.7,0.4-7.8,0.8c-0.4,0.1-0.8,0.2-1.2,0.3c-1.6-2.9-3.5-5.6-5.9-8.1c1.7-0.1,3.4-0.1,4.9-0.2\r\n\tc11.4-0.5,16.5-0.8,26.4,5.4c7.1,4.5,14.1,14,20.1,27.8c-2.6,4.8-9.6,14.8-33.1,21C132.1,186.3,127.7,188.3,124.7,189.9z M175,139.1\r\n\tc3.6,0,7.4-0.4,11.3-1.1c-4.5,5.2-8.3,11.5-11.3,17.5c-3-6-6.8-12.3-11.3-17.5C167.6,138.7,171.4,139.1,175,139.1z M126.4,122.9\r\n\tc0.3,0.2,0.6,0.4,1,0.5c-1,0-2.1,0.1-3.2,0.1c-3.9,0.2-8.3,0.4-13.8,0.4c-0.3,0-0.6,0.1-0.9,0.1c-3.7-1.4-8.1-2.2-13.5-2.2\r\n\tc-0.3,0-0.6,0-0.8,0.1c-0.3-0.1-0.5-0.1-0.8-0.1c-5.3,0-9.7,0.8-13.5,2.2c-0.3-0.1-0.6-0.1-0.9-0.1c-5.5,0-9.9-0.2-13.8-0.4\r\n\tc-1.2-0.1-2.3-0.1-3.4-0.2c9.2-6.3,20.2-9.7,31.6-9.7C105.3,113.7,115.8,116.7,126.4,122.9z M19.3,163.8c6-13.7,13-23.3,20.1-27.8\r\n\tc9.9-6.2,15.1-5.9,26.4-5.4c1.5,0.1,3.2,0.1,4.9,0.2c-2.4,2.5-4.3,5.2-5.9,8.1c-0.4-0.1-0.8-0.2-1.2-0.3c-2.1-0.5-4.9-0.8-7.8-0.8\r\n\tc-1.4,0-2.8,0.1-4.2,0.2c-1.3,0.1-2.6,0.3-3.6,0.6c-1,0.2-1.9,0.5-2.5,0.7c-0.6,0.2-0.9,0.3-0.9,0.3c-1,0.3-1.7,1.4-1.4,2.4\r\n\tc0.2,1.1,1.3,1.9,2.5,1.7l0.2,0c0,0,0.3-0.1,0.8-0.2c0.5-0.1,1.3-0.3,2.2-0.3c0.9-0.1,2-0.2,3.1-0.2c1.1,0,2.4,0,3.6,0.2\r\n\tc2.4,0.3,4.8,0.8,6.6,1.4c0.1,0,0.2,0.1,0.4,0.1c0,0,0,0.1,0,0.1c-0.8,3.2-1.9,5.6-3,7.9c-1.6,3.4-3.3,6.9-3.3,12.1\r\n\tc0,9.8,10.2,17.7,10.6,18c0.3,0.2,0.5,0.3,0.8,0.4c-0.8,2.6-1.4,4.9-1.9,6.8c-3-1.6-7.3-3.6-13.3-5.2\r\n\tC28.9,178.5,21.8,168.5,19.3,163.8z M70.1,176.3c-2-1.8-7-6.7-7-11.7c0-3.6,1.1-5.9,2.6-9c0.2-0.5,0.5-1,0.7-1.5\r\n\tc2,1.5,4.3,2.5,6.2,3.3c2,0.9,4.8,2,5.1,3.1c0.3,1.8-3.2,8.1-4.9,11.1C71.8,173.2,70.8,174.9,70.1,176.3z M58.9,224\r\n\tc6.5-10.4,9.7-18.7,11.2-23.5c1.2,0.8,2.4,2.1,3.2,3.8C71.7,208.1,67.5,216.9,58.9,224z M108.3,220.8c-1.5,22.6-2.7,27.5-12.2,27.5\r\n\tc-0.3,0-0.6,0-0.8,0.1c-0.3-0.1-0.5-0.1-0.8-0.1c-9.5,0-10.7-4.8-12.2-27.5c-0.3-4.2-0.6-8.9-1.1-14c-0.7-7.6-5.7-11.5-9-13.3\r\n\tc0.6-2.9,1.8-8,3.9-13.2c0.5-1.3,1.7-3.3,2.9-5.4c3.9-6.9,6.5-12,5.7-16c-1-4.5-5.3-6.4-9.2-8c-4.8-2-6.2-3-6-5\r\n\tc0.1-0.4,0.2-0.7,0.2-1.1c4.8-9.5,10.9-15.9,24.8-15.9c0.3,0,0.6,0,0.8-0.1c0.3,0.1,0.5,0.1,0.8,0.1c13.9,0,20,6.3,24.8,15.9\r\n\tc0.1,0.4,0.2,0.7,0.2,1.1c0.2,2-1.2,3-6,5c-3.8,1.6-8.2,3.4-9.2,8c-0.9,4,1.8,9.2,5.7,16c1.2,2.1,2.3,4.1,2.9,5.4\r\n\tc2.1,5.2,3.3,10.2,3.9,13.2c-3.3,1.8-8.4,5.7-9,13.3C108.9,212,108.5,216.7,108.3,220.8z M117.1,204.4c0.8-1.7,2.1-3,3.2-3.8\r\n\tc1.5,4.8,4.7,13.1,11.2,23.5C122.9,216.9,118.7,208.1,117.1,204.4z M218.5,224c6.5-10.4,9.7-18.7,11.2-23.5c1.2,0.8,2.4,2.1,3.2,3.8\r\n\tC231.3,208.1,227.1,216.9,218.5,224z M267.9,220.8c-1.5,22.6-2.7,27.4-12.2,27.4c-0.3,0-0.6,0-0.8,0.1c-0.3-0.1-0.5-0.1-0.8-0.1\r\n\tc-9.5,0-10.7-4.8-12.2-27.4c-0.3-4.2-0.6-8.9-1.1-14c-0.7-7.6-5.7-11.5-9-13.3c0.6-2.9,1.8-8,3.9-13.2c0.5-1.3,1.7-3.3,2.9-5.4\r\n\tc3.9-6.9,6.5-12,5.7-16c-1-4.5-5.3-6.4-9.2-8c-4.8-2-6.2-3-6-5c0.1-0.4,0.2-0.7,0.2-1.1c4.8-9.5,10.9-15.9,24.8-15.9\r\n\tc0.3,0,0.6,0,0.8-0.1c0.3,0.1,0.5,0.1,0.8,0.1c13.9,0,20,6.3,24.8,15.9c0.1,0.4,0.1,0.7,0.2,1.1c0.2,2-1.1,3-6,5\r\n\tc-3.8,1.6-8.2,3.4-9.2,8c-0.9,4,1.8,9.2,5.7,16c1.2,2.1,2.3,4.1,2.9,5.4c2.1,5.2,3.3,10.2,3.9,13.2c-3.3,1.8-8.4,5.7-9,13.3\r\n\tC268.5,212,268.1,216.7,267.9,220.8z M279.9,176.3c-0.7-1.4-1.6-3.1-2.6-4.8c-1.7-3-5.2-9.3-4.9-11.1c0.2-1,3-2.2,5.1-3.1\r\n\tc1.8-0.8,4.2-1.8,6.2-3.3c0.2,0.5,0.5,1,0.7,1.5c1.5,3.1,2.6,5.4,2.6,9C286.9,169.3,282.4,174.1,279.9,176.3z M276.7,204.4\r\n\tc0.8-1.7,2.1-3,3.2-3.8c1.5,4.8,4.7,13.1,11.2,23.5C282.5,216.9,278.3,208.1,276.7,204.4z M297.6,184.7c-5.9,1.6-10.3,3.5-13.3,5.2\r\n\tc-0.4-1.9-1.1-4.3-1.9-6.8c0.3-0.1,0.6-0.2,0.8-0.4c0.4-0.3,10.6-8.2,10.6-18c0-5.2-1.7-8.7-3.3-12.1c-1.1-2.3-2.3-4.7-3-7.9\r\n\tc0,0,0-0.1,0-0.1c0.1,0,0.2-0.1,0.4-0.1c1.7-0.6,4.1-1.1,6.6-1.4c1.2-0.1,2.4-0.2,3.6-0.2c1.1,0,2.2,0.1,3.1,0.2\r\n\tc0.9,0.1,1.7,0.2,2.2,0.3c0.5,0.1,0.8,0.2,0.8,0.2l0.2,0c1,0.2,2.1-0.4,2.4-1.5c0.3-1.1-0.3-2.3-1.4-2.6c0,0-0.3-0.1-0.9-0.3\r\n\tc-0.6-0.2-1.4-0.4-2.5-0.7c-1-0.3-2.3-0.4-3.6-0.6c-1.3-0.1-2.8-0.2-4.2-0.2c-2.8,0-5.7,0.4-7.8,0.8c-0.4,0.1-0.8,0.2-1.2,0.3\r\n\tc-1.6-2.9-3.5-5.6-5.9-8.1c1.7-0.1,3.4-0.1,4.9-0.2c11.4-0.5,16.5-0.8,26.4,5.4c7.1,4.5,14.1,14,20.1,27.8\r\n\tC328.2,168.5,321.1,178.5,297.6,184.7z", :fill => "#206B9E"}
12 |   %path{:d => "M274.8,162.9c1.1,2.3,3.8,3.3,6.1,2.3c2.3-1.1,3.3-3.8,2.3-6.1L274.8,162.9z", :fill => "#206B9E"}
13 |   %path{:d => "M234.8,162.9l-8.3-3.8c-1.1,2.3,0,5,2.3,6.1C231,166.2,233.7,165.2,234.8,162.9z", :fill => "#206B9E"}


--------------------------------------------------------------------------------
/site/source/stylesheets/_media-queries.scss:
--------------------------------------------------------------------------------
 1 | @import 'mixins';
 2 | 
 3 | // Upper and Lower Ends
 4 | $sm-upper: "(max-width: 40em)";      /* 640px */
 5 | $md-lower: "(min-width: 40.063em)";  /* 641px */
 6 | $md-upper: "(max-width: 54em)";      /* 864px */
 7 | $lg-lower: "(min-width: 54.063em)";  /* 865px */
 8 | $lg-upper: "(max-width: 90em)";      /* 1440px */
 9 | 
10 | // Specifications
11 | $screen: only screen !default;
12 | 
13 | $landscape: $screen and (orientation: landscape) !default;
14 | $portrait: $screen and (orientation: portrait) !default;
15 | 
16 | // Ranges
17 | $small-up: $screen !default;
18 | $small-only: $screen and $sm-upper !default;
19 | 
20 | $medium-up: $screen and $md-lower !default;
21 | $medium-only: $screen and $md-lower and $md-upper !default;
22 | 
23 | $large-up: $screen and $lg-lower !default;
24 | $large-only: $screen and $lg-lower and $lg-upper !default;
25 | 
26 | 
27 | // Styles -------------------------------------------------
28 | 
29 | 
30 | @media #{$small-up} { }
31 | @media #{$small-only} {
32 | 
33 |   body                { font-size: 15px; }
34 |   .nb-project         { display: block; text-align: center; }
35 |   .gh-btns            { position: initial; margin-top: 2em; text-align: center; }
36 |   .logo               { max-width: 15.5em; }
37 |   .project-btns a     { display: block; max-width: 100%; width: 90%; margin: .5em auto; @include box-sizing; font-size: 1.1em; }
38 |   .tlk-pnts .item     { max-width: 100% !important; }
39 |   #contents           { display: block; width: 100% !important; max-width: 100% !important; border-top: 1px solid $strk-light; border-bottom: 1px solid $strk-light; @include box-sizing;
40 |     &.closed          { height: 4em; }
41 |   }
42 |   .content            { width: 100% !important; max-width: 100% !important; }
43 | 
44 | }
45 | 
46 | @media #{$medium-up} { 
47 | 
48 |   #contents-btn       { display: none; }
49 | 
50 | }
51 | @media #{$medium-only} { 
52 | 
53 |   .tlk-pnts .item     { max-width: 45%; }
54 | 
55 | }
56 | 
57 | @media #{$large-up} { }
58 | @media #{$large-only} { }


--------------------------------------------------------------------------------
/site/source/stylesheets/_mixins.scss:
--------------------------------------------------------------------------------
 1 | @mixin transition($value) {
 2 |   -moz-transition: $value;
 3 |   -o-transition: $value;
 4 |   -ms-transition: $value;
 5 |   -webkit-transition: $value;
 6 |   transition: $value;
 7 | }
 8 | 
 9 | @mixin transition-delay($delay) {
10 |   -moz-transition-delay: $delay;
11 |   -o-transition-delay: $delay;
12 |   -webkit-transition-delay: $delay;
13 |   transition-delay: $delay;
14 | }
15 | 
16 | @mixin opacity($value){
17 |   $IEValue: $value*100;
18 |   opacity: $value;
19 |   -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity="+$IEValue+")";
20 |   filter: alpha(opacity=$IEValue);
21 | }
22 | 
23 | @mixin box-sizing($value:border-box) {
24 |   -moz-box-sizing: $value;
25 |   box-sizing: $value;
26 | }
27 | 
28 | @mixin border-radius ($radius:50%){
29 |   -webkit-border-radius: $radius $radius $radius $radius;
30 |   -moz-border-radius: $radius $radius $radius $radius;
31 |   border-radius: $radius $radius $radius $radius;
32 | }
33 | 
34 | @mixin transform ($transforms){
35 |   -webkit-transform: $transforms;
36 |   -moz-transform: $transforms;
37 |   -ms-transform: $transforms;
38 |   -o-transform: $transforms;
39 |   transform: $transforms;
40 | }
41 | 
42 | @mixin font-smoothing{
43 |   -webkit-font-smoothing: antialiased;
44 |   -moz-osx-font-smoothing: grayscale;
45 | }


--------------------------------------------------------------------------------
/site/source/stylesheets/_normalize.css:
--------------------------------------------------------------------------------
  1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */
  2 | 
  3 | /* ==========================================================================
  4 |    HTML5 display definitions
  5 |    ========================================================================== */
  6 | 
  7 | /*
  8 |  * Corrects `block` display not defined in IE 8/9.
  9 |  */
 10 | 
 11 | article,
 12 | aside,
 13 | details,
 14 | figcaption,
 15 | figure,
 16 | footer,
 17 | header,
 18 | hgroup,
 19 | nav,
 20 | section,
 21 | summary {
 22 |     display: block;
 23 | }
 24 | 
 25 | /*
 26 |  * Corrects `inline-block` display not defined in IE 8/9.
 27 |  */
 28 | 
 29 | audio,
 30 | canvas,
 31 | video {
 32 |     display: inline-block;
 33 | }
 34 | 
 35 | /*
 36 |  * Prevents modern browsers from displaying `audio` without controls.
 37 |  * Remove excess height in iOS 5 devices.
 38 |  */
 39 | 
 40 | audio:not([controls]) {
 41 |     display: none;
 42 |     height: 0;
 43 | }
 44 | 
 45 | /*
 46 |  * Addresses styling for `hidden` attribute not present in IE 8/9.
 47 |  */
 48 | 
 49 | [hidden] {
 50 |     display: none;
 51 | }
 52 | 
 53 | /* ==========================================================================
 54 |    Base
 55 |    ========================================================================== */
 56 | 
 57 | /*
 58 |  * 1. Sets default font family to sans-serif.
 59 |  * 2. Prevents iOS text size adjust after orientation change, without disabling
 60 |  *    user zoom.
 61 |  */
 62 | 
 63 | html {
 64 |     font-family: sans-serif; /* 1 */
 65 |     -webkit-text-size-adjust: 100%; /* 2 */
 66 |     -ms-text-size-adjust: 100%; /* 2 */
 67 | }
 68 | 
 69 | /*
 70 |  * Removes default margin.
 71 |  */
 72 | 
 73 | body {
 74 |     margin: 0;
 75 | }
 76 | 
 77 | /* ==========================================================================
 78 |    Links
 79 |    ========================================================================== */
 80 | 
 81 | /*
 82 |  * Addresses `outline` inconsistency between Chrome and other browsers.
 83 |  */
 84 | 
 85 | a:focus {
 86 |     outline: thin dotted;
 87 | }
 88 | 
 89 | /*
 90 |  * Improves readability when focused and also mouse hovered in all browsers.
 91 |  */
 92 | 
 93 | a:active,
 94 | a:hover {
 95 |     outline: 0;
 96 | }
 97 | 
 98 | /* ==========================================================================
 99 |    Typography
100 |    ========================================================================== */
101 | 
102 | /*
103 |  * Addresses `h1` font sizes within `section` and `article` in Firefox 4+,
104 |  * Safari 5, and Chrome.
105 |  */
106 | 
107 | h1 {
108 |     font-size: 2em;
109 | }
110 | 
111 | /*
112 |  * Addresses styling not present in IE 8/9, Safari 5, and Chrome.
113 |  */
114 | 
115 | abbr[title] {
116 |     border-bottom: 1px dotted;
117 | }
118 | 
119 | /*
120 |  * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
121 |  */
122 | 
123 | b,
124 | strong {
125 |     font-weight: bold;
126 | }
127 | 
128 | /*
129 |  * Addresses styling not present in Safari 5 and Chrome.
130 |  */
131 | 
132 | dfn {
133 |     font-style: italic;
134 | }
135 | 
136 | /*
137 |  * Addresses styling not present in IE 8/9.
138 |  */
139 | 
140 | mark {
141 |     background: #ff0;
142 |     color: #000;
143 | }
144 | 
145 | 
146 | /*
147 |  * Corrects font family set oddly in Safari 5 and Chrome.
148 |  */
149 | 
150 | code,
151 | kbd,
152 | pre,
153 | samp {
154 |     font-family: monospace, serif;
155 |     font-size: 1em;
156 | }
157 | 
158 | /*
159 |  * Improves readability of pre-formatted text in all browsers.
160 |  */
161 | 
162 | pre {
163 |     white-space: pre;
164 |     white-space: pre-wrap;
165 |     word-wrap: break-word;
166 | }
167 | 
168 | /*
169 |  * Sets consistent quote types.
170 |  */
171 | 
172 | q {
173 |     quotes: "\201C" "\201D" "\2018" "\2019";
174 | }
175 | 
176 | /*
177 |  * Addresses inconsistent and variable font size in all browsers.
178 |  */
179 | 
180 | small {
181 |     font-size: 80%;
182 | }
183 | 
184 | /*
185 |  * Prevents `sub` and `sup` affecting `line-height` in all browsers.
186 |  */
187 | 
188 | sub,
189 | sup {
190 |     font-size: 75%;
191 |     line-height: 0;
192 |     position: relative;
193 |     vertical-align: baseline;
194 | }
195 | 
196 | sup {
197 |     top: -0.5em;
198 | }
199 | 
200 | sub {
201 |     bottom: -0.25em;
202 | }
203 | 
204 | /* ==========================================================================
205 |    Embedded content
206 |    ========================================================================== */
207 | 
208 | /*
209 |  * Removes border when inside `a` element in IE 8/9.
210 |  */
211 | 
212 | img {
213 |     border: 0;
214 | }
215 | 
216 | /*
217 |  * Corrects overflow displayed oddly in IE 9.
218 |  */
219 | 
220 | svg:not(:root) {
221 |     overflow: hidden;
222 | }
223 | 
224 | /* ==========================================================================
225 |    Figures
226 |    ========================================================================== */
227 | 
228 | /*
229 |  * Addresses margin not present in IE 8/9 and Safari 5.
230 |  */
231 | 
232 | figure {
233 |     margin: 0;
234 | }
235 | 
236 | /* ==========================================================================
237 |    Forms
238 |    ========================================================================== */
239 | 
240 | /*
241 |  * Define consistent border, margin, and padding.
242 |  */
243 | 
244 | fieldset {
245 |     border: 1px solid #c0c0c0;
246 |     margin: 0 2px;
247 |     padding: 0.35em 0.625em 0.75em;
248 | }
249 | 
250 | /*
251 |  * 1. Corrects color not being inherited in IE 8/9.
252 |  * 2. Remove padding so people aren't caught out if they zero out fieldsets.
253 |  */
254 | 
255 | legend {
256 |     border: 0; /* 1 */
257 |     padding: 0; /* 2 */
258 | }
259 | 
260 | /*
261 |  * 1. Corrects font family not being inherited in all browsers.
262 |  * 2. Corrects font size not being inherited in all browsers.
263 |  * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome
264 |  */
265 | 
266 | button,
267 | input,
268 | select,
269 | textarea {
270 |     font-family: inherit; /* 1 */
271 |     font-size: 100%; /* 2 */
272 |     margin: 0; /* 3 */
273 | }
274 | 
275 | /*
276 |  * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in
277 |  * the UA stylesheet.
278 |  */
279 | 
280 | button,
281 | input {
282 |     line-height: normal;
283 | }
284 | 
285 | /*
286 |  * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
287 |  *    and `video` controls.
288 |  * 2. Corrects inability to style clickable `input` types in iOS.
289 |  * 3. Improves usability and consistency of cursor style between image-type
290 |  *    `input` and others.
291 |  */
292 | 
293 | button,
294 | html input[type="button"], /* 1 */
295 | input[type="reset"],
296 | input[type="submit"] {
297 |     -webkit-appearance: button; /* 2 */
298 |     cursor: pointer; /* 3 */
299 | }
300 | 
301 | /*
302 |  * Re-set default cursor for disabled elements.
303 |  */
304 | 
305 | button[disabled],
306 | input[disabled] {
307 |     cursor: default;
308 | }
309 | 
310 | /*
311 |  * 1. Addresses box sizing set to `content-box` in IE 8/9.
312 |  * 2. Removes excess padding in IE 8/9.
313 |  */
314 | 
315 | input[type="checkbox"],
316 | input[type="radio"] {
317 |     box-sizing: border-box; /* 1 */
318 |     padding: 0; /* 2 */
319 | }
320 | 
321 | /*
322 |  * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.
323 |  * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
324 |  *    (include `-moz` to future-proof).
325 |  */
326 | 
327 | input[type="search"] {
328 |     -webkit-appearance: textfield; /* 1 */
329 |     -moz-box-sizing: content-box;
330 |     -webkit-box-sizing: content-box; /* 2 */
331 |     box-sizing: content-box;
332 | }
333 | 
334 | /*
335 |  * Removes inner padding and search cancel button in Safari 5 and Chrome
336 |  * on OS X.
337 |  */
338 | 
339 | input[type="search"]::-webkit-search-cancel-button,
340 | input[type="search"]::-webkit-search-decoration {
341 |     -webkit-appearance: none;
342 | }
343 | 
344 | /*
345 |  * Removes inner padding and border in Firefox 4+.
346 |  */
347 | 
348 | button::-moz-focus-inner,
349 | input::-moz-focus-inner {
350 |     border: 0;
351 |     padding: 0;
352 | }
353 | 
354 | /*
355 |  * 1. Removes default vertical scrollbar in IE 8/9.
356 |  * 2. Improves readability and alignment in all browsers.
357 |  */
358 | 
359 | textarea {
360 |     overflow: auto; /* 1 */
361 |     vertical-align: top; /* 2 */
362 | }
363 | 
364 | /* ==========================================================================
365 |    Tables
366 |    ========================================================================== */
367 | 
368 | /*
369 |  * Remove most spacing between table cells.
370 |  */
371 | 
372 | table {
373 |     border-collapse: collapse;
374 |     border-spacing: 0;
375 | }


--------------------------------------------------------------------------------
/site/source/stylesheets/_syntax.scss:
--------------------------------------------------------------------------------
 1 | /*
 2 | 
 3 | github.com style (c) Vasily Polovnyov 
 4 | 
 5 | */
 6 | 
 7 | $code-bkg: #F8F8F8;
 8 | 
 9 | .hljs                     { display: block; overflow-x: auto; overflow: scroll; padding: 1.5em; color: #333; background: $code-bkg; -webkit-text-size-adjust: none; font-family: Consolas,Monaco,'Andale Mono',monospace; white-space: pre; word-wrap: normal; font-size: .85em; line-height: 1.8em; }
10 | 
11 | .hljs-comment,
12 | .diff .hljs-header        { color: #998; font-style: italic; }
13 | 
14 | .hljs-keyword,
15 | .css .rule .hljs-keyword,
16 | .hljs-winutils,
17 | .nginx .hljs-title,
18 | .hljs-subst,
19 | .hljs-request,
20 | .hljs-status              { color: #333; font-weight: bold; }
21 | 
22 | .hljs-number,
23 | .hljs-hexcolor,
24 | .ruby .hljs-constant      { color: #008080; }
25 | 
26 | .hljs-string,
27 | .hljs-tag .hljs-value,
28 | .hljs-doctag,
29 | .tex .hljs-formula        { color: #d14; }
30 | 
31 | .hljs-title,
32 | .hljs-id,
33 | .scss .hljs-preprocessor  { color: #900; font-weight: bold; }
34 | 
35 | .hljs-list .hljs-keyword,
36 | .hljs-subst               { font-weight: normal; }
37 | 
38 | .hljs-class .hljs-title,
39 | .hljs-type,
40 | .vhdl .hljs-literal,
41 | .tex .hljs-command        { color: #458; font-weight: bold; }
42 | 
43 | .hljs-tag,
44 | .hljs-tag .hljs-title,
45 | .hljs-rule .hljs-property,
46 | .django                   {
47 |   .hljs-tag .hljs-keyword { color: #000080; font-weight: normal; }
48 | }
49 | 
50 | .hljs-attribute,
51 | .hljs-variable,
52 | .lisp .hljs-body,
53 | .hljs-name                { color: #008080; }
54 | 
55 | .hljs-regexp              { color: #009926; }
56 | 
57 | .hljs-symbol,
58 | .ruby .hljs-symbol .hljs-string,
59 | .lisp .hljs-keyword,
60 | .clojure .hljs-keyword,
61 | .scheme .hljs-keyword,
62 | .tex .hljs-special,
63 | .hljs-prompt              { color: #990073; }
64 | 
65 | .hljs-built_in            { color: #0086b3; }
66 | 
67 | .hljs-preprocessor,
68 | .hljs-pragma,
69 | .hljs-pi,
70 | .hljs-doctype,
71 | .hljs-shebang,
72 | .hljs-cdata               { color: #999; font-weight: bold; }
73 | 
74 | .hljs-deletion            { background: #fdd; }
75 | 
76 | .hljs-addition            { background: #dfd; }
77 | 
78 | .diff .hljs-change        { background: #0086b3; }
79 | 
80 | .hljs-chunk               { color: #aaa; }


--------------------------------------------------------------------------------
/site/source/stylesheets/all.scss:
--------------------------------------------------------------------------------
  1 | @import 'normalize', 'mixins', 'syntax';
  2 | 
  3 | ///////////////// COLORS /////////////////
  4 | $flood-1: #fff;
  5 | $base-h: #206B9E;
  6 | $base-txt: #6D6D6D;
  7 | $base-link: #1791CE;
  8 | $base-link-hover: #E0C643;
  9 | 
 10 | $flood-2: #FCE15B;
 11 | $alt-h: #594906;
 12 | $alt-txt: #594906;
 13 | $alt-link: #0D6D9C;
 14 | $alt-link-hover: #9E8E04;
 15 | 
 16 | $base-hl: #EC5268;
 17 | $alt-hl: #12848F;
 18 | 
 19 | $table-bkg-0: #cbcbcb;
 20 | $table-bkg-1: #f8f8f8;
 21 | $strk-light: #E6E6E6;
 22 | 
 23 | 
 24 | body              { width: 100%; position: relative; font-size: 18px; background: $flood-1; color: $base-txt; @include box-sizing; @include font-smoothing; font-family: 'Lato', sans-serif; }
 25 | a                 { text-decoration: none; @include transition(all .2s); }
 26 | p, li             { line-height: 1.8em;
 27 |   code            { padding: .3em .5em; font-size: .95em; background-color: $code-bkg; }
 28 | }
 29 | 
 30 | ///////////////// FLOOD COLOR DEFINITIONS /////////////////
 31 | .row              {
 32 |   &.flood-1       { background: $flood-1; color: $base-txt;
 33 |     a             { color: $base-link;
 34 |       &:hover     { color: $base-link-hover; }
 35 |     }
 36 |   }
 37 |   &.flood-2       { background: $flood-2; color: $alt-txt;
 38 |     a             { color: $alt-link;
 39 |       &:hover     { color: $alt-link-hover; }
 40 |     }
 41 |   }
 42 | }
 43 | 
 44 | ///////////////// HEADER /////////////////
 45 | a.nb-project      { display: inline-block; margin: 0; color: $base-txt !important; font-size: .8em; font-style: italic;
 46 |   &:hover         { color: $base-link !important; }
 47 |   #nb-logo        { width: 2em; }
 48 |   span            { display: inline-block; margin: .5em 0 0 .5em; vertical-align: top;  }
 49 | }
 50 | .gh-btns          { position: absolute; top: 1.4em; right: 1.4em; }
 51 | 
 52 | ///////////////// MASTHEAD /////////////////
 53 | .project-logo     { margin: .5em auto; max-width: 15.5em; }
 54 | .headline         { max-width: 500px; margin: 0 auto; text-align: center;
 55 |   h1              { margin: .25em 0 .1em; color: $base-h; font-size: 3em; font-weight: 300; }
 56 |   h2              { margin: .25em 0 .1em; color: $base-h; font-size: 1.5em; font-weight: 300; }
 57 | }
 58 | 
 59 | ///////////////// PROJECT BUTTONS /////////////////
 60 | .project-btns     { max-width: 500px; margin: 2em auto 1em; text-align: center; font-size: .9em;
 61 |   a               { display: inline-block; width: 40%; max-width: 7em; padding: .75em 1em .9em; margin: 0 .25em; color: $base-txt !important; border-color: $base-txt !important; border-weight: .15em; border-style: solid; font-weight: 700;
 62 |     &:hover       { color: #fff !important; background: $base-h !important; border-color: $base-h !important; }
 63 |   }
 64 | }
 65 | 
 66 | ///////////////// TALKING POINTS /////////////////
 67 | .tlk-pnts         { position: relative; max-width: 1100px; margin: 0 auto; text-align: left;
 68 |   .item           { display: inline-block; max-width: 30%; margin: .75em; text-align: left; vertical-align: top;
 69 |     h4            { font-size: 1em; margin: 0; }
 70 |     p             { margin-top: .5em; font-size: .95em; line-height: 1.6em; }
 71 |   }
 72 | }
 73 | /////////////// README/DOC CONTENT ///////////////
 74 | .content          { position: relative; max-width: 630px; margin: 0 auto; padding: 1em; line-height: 1.6em; @include box-sizing;
 75 |   h1              { margin-top: .15em; font-size: 3.5em; font-weight: 300; line-height: 1em; }
 76 |   h2              { padding-top: 1em; margin: 0 0 .15em; font-size: 2.25em; font-weight: 500; }
 77 |   h3              { padding-top: 1.5em; margin: 0 0 .1em; font-size: 1.75em; font-weight: 500; }
 78 |   h4              { padding-top: 1.5em; margin: 0 0 .1em; font-size: 1.45em; font-weight: 500; font-style: italic;}
 79 |   h5              { padding-top: 1.5em; margin: 0; font-size: 1.05em; text-transform: uppercase; }
 80 |   h6              { padding-top: 1.5em; margin: 0; font-size: 1em; text-transform: uppercase; font-style: italic; }
 81 |   h3, h4          {
 82 |     & + p         { margin-top: .75em !important; }
 83 |   }
 84 |   h5,h6           {
 85 |     & + p         { margin-top: .25em !important;}
 86 |   }
 87 |   h2,h3,h4,h5,h6  { position: relative;
 88 |     a             { color: $base-txt !important;
 89 |       &:before    { content: '#'; position: absolute; display: block; left: -1em; font-size: 20px; font-weight: 500; @include opacity(.1); @include transition(all .2s); }
 90 |       &:hover:before { @include opacity(.6); }
 91 |     }
 92 |   }
 93 |   img             { max-width: 100%; }
 94 |   hr              { border-width: 1px 0 0; border-color: $strk-light; border-style: solid; }
 95 |   table           { width: 100%; @include box-sizing; }
 96 |   th              { padding: .35em .65em; background-color: $table-bkg-0; border-right: 1px solid $flood-1;
 97 |     &:last-child  { border-color: $table-bkg-0; }
 98 |   }
 99 |   tr:nth-child(2n+2){ background: $table-bkg-1; }
100 |   td              { padding: .35em .65em; border: 1px solid $table-bkg-0; }
101 | }
102 | 
103 | ///////////////// ROW STYLES /////////////////
104 | .row              { position: relative; width: 100%; padding: 1em 1em 9.5em 1em; @include box-sizing; @include transition(all .5s);
105 |   &:not(:first-child) { padding-top: .5em;
106 |     &:before      { content: ''; position: absolute; top: -5em; left: 0; display: block; width: 100%; height: 350px; background: inherit; @include transform(skewY(-3deg)); -webkit-backface-visibility: hidden; }
107 |   }
108 | }
109 | 
110 | ///////////////// FOOTER STYLES /////////////////
111 | .nb-foot          { position: relative; padding-top: 2em; text-align: center;
112 |   a.nb-circle     { display: block; background-color: #fff; width: 6em; height: 6em; margin: 0 auto 1.5em; padding: 1.4em; @include border-radius(50%); @include box-sizing; }
113 |   p               { margin-bottom: 0 !important; }
114 |   .heart          { font-size: 1.5em; vertical-align: sub; }
115 |   .contribute     { margin: 0; font-size: .8em; font-style: italic; }
116 | }
117 | 
118 | ///////////////// JS FADE-IN STYLES /////////////////
119 | .fade-in, .fade-in-fast{ @include opacity(0); @include transition(all .5s);
120 |   &.show          { @include opacity(1); }
121 | }
122 | 
123 | ///////////////// DOWNLOAD MODAL /////////////////
124 | #dl-modal         { display: none; position: fixed; top: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); padding: 2em; @include box-sizing;
125 |   .container      { position: relative; margin: 10em auto 0; max-width: 18em; padding: .8em; background: #fff; @include box-sizing;
126 |     a             { display: block; padding: .8em; margin: .5em; color: $base-h; border-color: $base-h; text-align: center; font-weight: bold; border-weight: 3px; border-style: solid;
127 |       &:hover     { color: #fff; background: $base-h; }
128 |     }
129 |     &:before      { content: "Download"; position: absolute; top: -2em; left: 0; width: 100%; color: #fff; font-size: 1.2em; text-align: center;  }
130 |   }
131 |   .close          { display: block; margin: 1em auto 0; padding: .8em; height: 3em; width: 3em; color: #fff; text-align: center; border: 1px solid #fff; cursor: pointer; @include border-radius; @include box-sizing; @include transition(all .2s);
132 |     &:hover       { background: #fff; color: #515151;}
133 |   }
134 | }
135 | 
136 | 
137 | ////////////////////////////////////////////////////
138 | /////////////// DOCUMENTATION STYLES ///////////////
139 | ////////////////////////////////////////////////////
140 | 
141 | body.docs         {
142 |   p               { margin: 1.5em 0; }
143 |   a               { color: $base-link;
144 |     &:hover       { color: $base-link-hover; }
145 |   }
146 |   .row            {
147 |     &:first-child { padding-bottom: 2em; }
148 |     &:nth-child(2):before{ content: none; }
149 |   }
150 |   .top-nav        { max-width: 1100px; margin: 0 auto; position: relative;
151 |     a             { color: $base-txt;
152 |       &:hover     { color: $base-link-hover; }
153 |     }
154 |     svg           { max-width: 3em; }
155 |     span          { display: inline-block; vertical-align: top; margin: .8em 0 0 .8em; font-size: 1.2em; font-weight: 300; }
156 |     p             { display: inline-block; vertical-align: top; margin: 1em 0 0 .8em; padding: 0 1em;  font-size: .95em; font-weight: 300; font-style: italic; border-left: 1px solid $strk-light; }
157 |   }
158 |   .wrapper        { position: relative; margin: 0 auto; max-width: 1100px; }
159 | 
160 |   //////////////// TABLE OF CONTENTS ////////////////
161 |   #contents-btn   { position: relative; padding: 1em 0; cursor: pointer;
162 |     p             { display: inline-block; margin: 0; }
163 |     #icon         { position: absolute; right: 0; top: 1.8em; height: 3px; width: 22px; background: $base-txt;
164 |       &:before    { content: ''; display: block; position: absolute; height: 3px; width: 22px; background: $base-txt; @include transform(translateY(-6px)); @include transition(all .15s); }
165 |       &:after     { content: ''; display: block; position: absolute; height: 3px; width: 22px; background: $base-txt; @include transform(translateY(6px)); @include transition(all .15s); }
166 |     }
167 |     &.open #icon {
168 |       &:before    { @include transform(translateY(0px)); }
169 |       &:after     { @include transform(translateY(0px)); }
170 |     }
171 |   }
172 |   ul#contents     { display: inline-block; width: 20%; list-style: none; padding: 0 1em; max-height: 1000em; overflow: hidden; padding-bottom: 1em;
173 |     ul            { padding-left: 1.35em; list-style: none; border-left: 1px dotted $strk-light;
174 |       &.sub       { background-color: #fff; max-height: 0; overflow: hidden; }
175 |     }
176 |     li            { position: relative; width: 100%;
177 |       &.active    {
178 |         & > a     { color: $base-hl; display: block; }
179 |         &:before  { content: '\2B22'; font-size: .5em; position: absolute; left: -1.75em; color: $base-hl; }
180 |         & > ul.sub{ max-height: 2000px; }
181 |         &.more > a:after{ display: none; }
182 |       }
183 |       &.more > a  {
184 |         &:after   { content: "+"; margin-left: .3em; color: $strk-light; font-size: 1em; }
185 |       }
186 |       &.open      {
187 |         & > ul.sub{ max-height: 2000px; }
188 |         &.more > a:after{ display: none; }
189 |       }
190 |       a           { color: $base-txt;
191 |         &:hover   { color: $base-hl; }
192 |       }
193 |     }
194 |   }
195 | 
196 |   //////// CONTENT STYLES SPECIFIC TO DOCS ////////
197 |   .content        { display: inline-block; width: 70%; margin: 0; vertical-align: top; }
198 |   #pagination     { position: relative; padding-top: 2em; border-top: 1px solid $strk-light; 
199 |     a             { max-width: 48%; color: $base-txt; font-style: italic;
200 |       &.prev      { position: absolute; left: 0;
201 |         &:before  { content: ''; width: .55em; height: .55em;margin-right: .5em; display: inline-block; border-top: 1px solid $base-txt; border-left: 1px solid $base-txt; @include transform(rotate(-45deg)); @include transition(border-color .2s); }
202 |       }
203 |       &.next      { position: absolute; right: 0;
204 |         &:after   { content: ''; width: .55em; height: .55em;margin-left: .5em; display: inline-block; border-top: 1px solid $base-txt; border-right: 1px solid $base-txt; @include transform(rotate(45deg)); @include transition(border-color .2s); }
205 |       }
206 |       &:hover     { color: $base-hl;
207 |         &:before,
208 |         &:after   { border-color: $base-hl !important; }
209 |       }
210 |     }
211 |   }
212 | 
213 | }
214 | 
215 | 
216 | 
217 | @import 'media-queries';
218 | 


--------------------------------------------------------------------------------
/state/bounce.go:
--------------------------------------------------------------------------------
  1 | package state
  2 | 
  3 | import "time"
  4 | 
  5 | type (
  6 | 	Bouncer struct {
  7 | 		bounce   remoteState
  8 | 		location string
  9 | 	}
 10 | 
 11 | 	BounceString struct {
 12 | 		Address string
 13 | 		Method  string
 14 | 		Timeout time.Duration
 15 | 		In      string
 16 | 	}
 17 | 
 18 | 	BounceBool struct {
 19 | 		Address string
 20 | 		Method  string
 21 | 		Timeout time.Duration
 22 | 		In      bool
 23 | 	}
 24 | 
 25 | 	BounceNil struct {
 26 | 		Address string
 27 | 		Method  string
 28 | 		Timeout time.Duration
 29 | 		In      Nil
 30 | 	}
 31 | )
 32 | 
 33 | func (c remoteState) Bounce(location string) State {
 34 | 	bounce := Bouncer{
 35 | 		bounce:   c,
 36 | 		location: location,
 37 | 	}
 38 | 	return bounce
 39 | }
 40 | 
 41 | func (wrap *StateRPC) BounceString(bounce BounceString, reply *string) error {
 42 | 	// we need an extra step just incase the next variable gets modified while being used
 43 | 	// to encode the reply in a Timeout condition
 44 | 	var next string
 45 | 	err := call("tcp", bounce.Address, bounce.Timeout, bounce.Method, bounce.In, &next)
 46 | 	if err == Timeout {
 47 | 		*reply = "dead"
 48 | 		return nil
 49 | 	}
 50 | 	*reply = next
 51 | 	return err
 52 | }
 53 | 
 54 | func (wrap *StateRPC) BounceBool(bounce BounceBool, reply *bool) error {
 55 | 	return call("tcp", bounce.Address, bounce.Timeout, bounce.Method, bounce.In, reply)
 56 | }
 57 | 
 58 | func (wrap *StateRPC) BounceNil(bounce BounceNil, reply *Nil) error {
 59 | 	return call("tcp", bounce.Address, bounce.Timeout, bounce.Method, bounce.In, reply)
 60 | }
 61 | 
 62 | func (bounce Bouncer) Bounce(location string) State {
 63 | 	// this should really return an error
 64 | 	return nil
 65 | }
 66 | 
 67 | func (bounce Bouncer) Ready() {
 68 | 	next := BounceNil{
 69 | 		In:      Nil{},
 70 | 		Address: bounce.location,
 71 | 		Timeout: bounce.bounce.timeout,
 72 | 		Method:  "StateRPC.Ready",
 73 | 	}
 74 | 
 75 | 	for bounce.bounce.call("StateRPC.BounceNil", next, &Nil{}) != nil {
 76 | 		<-time.After(time.Second)
 77 | 	}
 78 | }
 79 | 
 80 | func (bounce Bouncer) SetSynced(synced bool) error {
 81 | 	next := BounceBool{
 82 | 		In:      synced,
 83 | 		Address: bounce.location,
 84 | 		Timeout: bounce.bounce.timeout,
 85 | 		Method:  "StateRPC.SetSynced",
 86 | 	}
 87 | 	var out bool
 88 | 	return bounce.bounce.call("StateRPC.BounceBool", next, &out)
 89 | }
 90 | 
 91 | func (bounce Bouncer) HasSynced() (bool, error) {
 92 | 	var synced bool
 93 | 	next := BounceBool{
 94 | 		Address: bounce.location,
 95 | 		Timeout: bounce.bounce.timeout,
 96 | 		Method:  "StateRPC.HasSynced",
 97 | 	}
 98 | 	err := bounce.bounce.call("StateRPC.BounceBool", next, &synced)
 99 | 	return synced, err
100 | }
101 | 
102 | func (bounce Bouncer) Location() string {
103 | 	return bounce.location
104 | }
105 | 
106 | func (bounce Bouncer) GetDataDir() (string, error) {
107 | 	var dataDir string
108 | 	next := BounceString{
109 | 		Address: bounce.location,
110 | 		Timeout: bounce.bounce.timeout,
111 | 		Method:  "StateRPC.GetDataDir",
112 | 	}
113 | 	err := bounce.bounce.call("StateRPC.BounceString", next, &dataDir)
114 | 	return dataDir, err
115 | }
116 | 
117 | func (bounce Bouncer) GetRole() (string, error) {
118 | 	var role string
119 | 	next := BounceString{
120 | 		Address: bounce.location,
121 | 		Timeout: bounce.bounce.timeout,
122 | 		Method:  "StateRPC.GetRole",
123 | 	}
124 | 	err := bounce.bounce.call("StateRPC.BounceString", next, &role)
125 | 	return role, err
126 | }
127 | 
128 | func (bounce Bouncer) GetDBRole() (string, error) {
129 | 	var dbRole string
130 | 	next := BounceString{
131 | 		Address: bounce.location,
132 | 		Timeout: bounce.bounce.timeout,
133 | 		Method:  "StateRPC.GetDBRole",
134 | 	}
135 | 	err := bounce.bounce.call("StateRPC.BounceString", next, &dbRole)
136 | 	return dbRole, err
137 | }
138 | 
139 | func (bounce Bouncer) SetDBRole(role string) error {
140 | 	return NotSupported
141 | }
142 | 


--------------------------------------------------------------------------------
/state/mock/mock.go:
--------------------------------------------------------------------------------
  1 | // Automatically generated by MockGen. DO NOT EDIT!
  2 | // Source: github.com/nanopack/yoke/state (interfaces: State,Store)
  3 | 
  4 | package mock_state
  5 | 
  6 | import (
  7 | 	gomock "github.com/golang/mock/gomock"
  8 | 	state "github.com/nanopack/yoke/state"
  9 | )
 10 | 
 11 | // Mock of State interface
 12 | type MockState struct {
 13 | 	ctrl     *gomock.Controller
 14 | 	recorder *_MockStateRecorder
 15 | }
 16 | 
 17 | // Recorder for MockState (not exported)
 18 | type _MockStateRecorder struct {
 19 | 	mock *MockState
 20 | }
 21 | 
 22 | func NewMockState(ctrl *gomock.Controller) *MockState {
 23 | 	mock := &MockState{ctrl: ctrl}
 24 | 	mock.recorder = &_MockStateRecorder{mock}
 25 | 	return mock
 26 | }
 27 | 
 28 | func (_m *MockState) EXPECT() *_MockStateRecorder {
 29 | 	return _m.recorder
 30 | }
 31 | 
 32 | func (_m *MockState) Bounce(_param0 string) state.State {
 33 | 	ret := _m.ctrl.Call(_m, "Bounce", _param0)
 34 | 	ret0, _ := ret[0].(state.State)
 35 | 	return ret0
 36 | }
 37 | 
 38 | func (_mr *_MockStateRecorder) Bounce(arg0 interface{}) *gomock.Call {
 39 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "Bounce", arg0)
 40 | }
 41 | 
 42 | func (_m *MockState) GetDBRole() (string, error) {
 43 | 	ret := _m.ctrl.Call(_m, "GetDBRole")
 44 | 	ret0, _ := ret[0].(string)
 45 | 	ret1, _ := ret[1].(error)
 46 | 	return ret0, ret1
 47 | }
 48 | 
 49 | func (_mr *_MockStateRecorder) GetDBRole() *gomock.Call {
 50 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "GetDBRole")
 51 | }
 52 | 
 53 | func (_m *MockState) GetDataDir() (string, error) {
 54 | 	ret := _m.ctrl.Call(_m, "GetDataDir")
 55 | 	ret0, _ := ret[0].(string)
 56 | 	ret1, _ := ret[1].(error)
 57 | 	return ret0, ret1
 58 | }
 59 | 
 60 | func (_mr *_MockStateRecorder) GetDataDir() *gomock.Call {
 61 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "GetDataDir")
 62 | }
 63 | 
 64 | func (_m *MockState) GetRole() (string, error) {
 65 | 	ret := _m.ctrl.Call(_m, "GetRole")
 66 | 	ret0, _ := ret[0].(string)
 67 | 	ret1, _ := ret[1].(error)
 68 | 	return ret0, ret1
 69 | }
 70 | 
 71 | func (_mr *_MockStateRecorder) GetRole() *gomock.Call {
 72 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRole")
 73 | }
 74 | 
 75 | func (_m *MockState) HasSynced() (bool, error) {
 76 | 	ret := _m.ctrl.Call(_m, "HasSynced")
 77 | 	ret0, _ := ret[0].(bool)
 78 | 	ret1, _ := ret[1].(error)
 79 | 	return ret0, ret1
 80 | }
 81 | 
 82 | func (_mr *_MockStateRecorder) HasSynced() *gomock.Call {
 83 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "HasSynced")
 84 | }
 85 | 
 86 | func (_m *MockState) Location() string {
 87 | 	ret := _m.ctrl.Call(_m, "Location")
 88 | 	ret0, _ := ret[0].(string)
 89 | 	return ret0
 90 | }
 91 | 
 92 | func (_mr *_MockStateRecorder) Location() *gomock.Call {
 93 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "Location")
 94 | }
 95 | 
 96 | func (_m *MockState) Ready() {
 97 | 	_m.ctrl.Call(_m, "Ready")
 98 | }
 99 | 
100 | func (_mr *_MockStateRecorder) Ready() *gomock.Call {
101 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "Ready")
102 | }
103 | 
104 | func (_m *MockState) SetDBRole(_param0 string) error {
105 | 	ret := _m.ctrl.Call(_m, "SetDBRole", _param0)
106 | 	ret0, _ := ret[0].(error)
107 | 	return ret0
108 | }
109 | 
110 | func (_mr *_MockStateRecorder) SetDBRole(arg0 interface{}) *gomock.Call {
111 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "SetDBRole", arg0)
112 | }
113 | 
114 | func (_m *MockState) SetSynced(_param0 bool) error {
115 | 	ret := _m.ctrl.Call(_m, "SetSynced", _param0)
116 | 	ret0, _ := ret[0].(error)
117 | 	return ret0
118 | }
119 | 
120 | func (_mr *_MockStateRecorder) SetSynced(arg0 interface{}) *gomock.Call {
121 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "SetSynced", arg0)
122 | }
123 | 
124 | // Mock of Store interface
125 | type MockStore struct {
126 | 	ctrl     *gomock.Controller
127 | 	recorder *_MockStoreRecorder
128 | }
129 | 
130 | // Recorder for MockStore (not exported)
131 | type _MockStoreRecorder struct {
132 | 	mock *MockStore
133 | }
134 | 
135 | func NewMockStore(ctrl *gomock.Controller) *MockStore {
136 | 	mock := &MockStore{ctrl: ctrl}
137 | 	mock.recorder = &_MockStoreRecorder{mock}
138 | 	return mock
139 | }
140 | 
141 | func (_m *MockStore) EXPECT() *_MockStoreRecorder {
142 | 	return _m.recorder
143 | }
144 | 
145 | func (_m *MockStore) Read(_param0 string, _param1 string, _param2 interface{}) error {
146 | 	ret := _m.ctrl.Call(_m, "Read", _param0, _param1, _param2)
147 | 	ret0, _ := ret[0].(error)
148 | 	return ret0
149 | }
150 | 
151 | func (_mr *_MockStoreRecorder) Read(arg0, arg1, arg2 interface{}) *gomock.Call {
152 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "Read", arg0, arg1, arg2)
153 | }
154 | 
155 | func (_m *MockStore) Write(_param0 string, _param1 string, _param2 interface{}) error {
156 | 	ret := _m.ctrl.Call(_m, "Write", _param0, _param1, _param2)
157 | 	ret0, _ := ret[0].(error)
158 | 	return ret0
159 | }
160 | 
161 | func (_mr *_MockStoreRecorder) Write(arg0, arg1, arg2 interface{}) *gomock.Call {
162 | 	return _mr.mock.ctrl.RecordCall(_mr.mock, "Write", arg0, arg1, arg2)
163 | }
164 | 


--------------------------------------------------------------------------------
/state/rpc.go:
--------------------------------------------------------------------------------
  1 | package state
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 	"io"
  6 | 	"net"
  7 | 	"net/rpc"
  8 | 	"time"
  9 | )
 10 | 
 11 | var (
 12 | 	Timeout      = errors.New("Timeout")
 13 | 	NotSupported = errors.New("not supported")
 14 | )
 15 | 
 16 | type (
 17 | 	rpcDial struct {
 18 | 		client *rpc.Client
 19 | 		err    error
 20 | 	}
 21 | 
 22 | 	remoteState struct {
 23 | 		timeout  time.Duration
 24 | 		location string
 25 | 		network  string
 26 | 	}
 27 | 
 28 | 	StateRPC struct {
 29 | 		state *state
 30 | 	}
 31 | 
 32 | 	Nil struct{}
 33 | )
 34 | 
 35 | // Starts the RPC listening server, enables remote communication with local state objects
 36 | func (local *state) ExposeRPCEndpoint(network, location string) (io.Closer, error) {
 37 | 	wrap := StateRPC{
 38 | 		state: local,
 39 | 	}
 40 | 
 41 | 	server := rpc.NewServer()
 42 | 
 43 | 	if err := server.Register(&wrap); err != nil {
 44 | 		return nil, err
 45 | 	}
 46 | 
 47 | 	listener, err := net.Listen(network, location)
 48 | 	if err != nil {
 49 | 		return nil, err
 50 | 	}
 51 | 
 52 | 	go server.Accept(listener)
 53 | 	return listener, nil
 54 | }
 55 | 
 56 | // Creates and returns a State that represents a state reachable over an rpc connection
 57 | func NewRemoteState(network, location string, timeout time.Duration) State {
 58 | 	remote := remoteState{
 59 | 		timeout:  timeout,
 60 | 		network:  network,
 61 | 		location: location,
 62 | 	}
 63 | 	return remote
 64 | }
 65 | 
 66 | func call(network, location string, timeout time.Duration, method string, in interface{}, out interface{}) error {
 67 | 	res := make(chan error, 1)
 68 | 	go func() {
 69 | 		client, err := rpc.Dial(network, location)
 70 | 		if err != nil {
 71 | 			res <- err
 72 | 			return
 73 | 		}
 74 | 		defer client.Close()
 75 | 		res <- client.Call(method, in, out)
 76 | 	}()
 77 | 	select {
 78 | 	case err := <-res:
 79 | 		return err
 80 | 	case <-time.After(timeout):
 81 | 		return Timeout
 82 | 	}
 83 | }
 84 | 
 85 | func (c remoteState) call(method string, in interface{}, out interface{}) error {
 86 | 	return call(c.network, c.location, c.timeout, method, in, out)
 87 | }
 88 | 
 89 | func (c remoteState) Ready() {
 90 | 	for c.call("StateRPC.Ready", Nil{}, &Nil{}) != nil {
 91 | 		<-time.After(time.Second)
 92 | 	}
 93 | }
 94 | 
 95 | func (c remoteState) SetSynced(synced bool) error {
 96 | 	var out bool
 97 | 	return c.call("StateRPC.SetSynced", synced, &out)
 98 | }
 99 | 
100 | func (c remoteState) HasSynced() (bool, error) {
101 | 	var synced bool
102 | 	err := c.call("StateRPC.HasSynced", true, &synced)
103 | 	return synced, err
104 | }
105 | 
106 | func (c remoteState) Location() string {
107 | 	return c.location
108 | }
109 | 
110 | func (c remoteState) GetDataDir() (string, error) {
111 | 	var dataDir string
112 | 	err := c.call("StateRPC.GetDataDir", "", &dataDir)
113 | 	return dataDir, err
114 | }
115 | 
116 | func (c remoteState) GetRole() (string, error) {
117 | 	var role string
118 | 	err := c.call("StateRPC.GetRole", "", &role)
119 | 	return role, err
120 | }
121 | 
122 | func (c remoteState) GetDBRole() (string, error) {
123 | 	var role string
124 | 	err := c.call("StateRPC.GetDBRole", "", &role)
125 | 	return role, err
126 | }
127 | 
128 | func (c remoteState) SetDBRole(role string) error {
129 | 	return NotSupported
130 | }
131 | 
132 | func (wrap *StateRPC) Ready(a Nil, b *Nil) error {
133 | 	return nil
134 | }
135 | 
136 | func (wrap *StateRPC) GetDataDir(arg string, reply *string) error {
137 | 	*reply = wrap.state.DataDir
138 | 	return nil
139 | }
140 | 
141 | func (wrap *StateRPC) GetRole(arg string, reply *string) error {
142 | 	*reply = wrap.state.Role
143 | 	return nil
144 | }
145 | 
146 | func (wrap *StateRPC) GetDBRole(arg string, reply *string) error {
147 | 	*reply = wrap.state.DBRole
148 | 	return nil
149 | }
150 | 
151 | func (wrap *StateRPC) HasSynced(arg bool, reply *bool) error {
152 | 	*reply = wrap.state.synced
153 | 	return nil
154 | }
155 | 
156 | func (wrap *StateRPC) SetSynced(sync bool, out *bool) error {
157 | 	wrap.state.synced = sync
158 | 	return nil
159 | }
160 | 


--------------------------------------------------------------------------------
/state/state.go:
--------------------------------------------------------------------------------
  1 | //
  2 | package state
  3 | 
  4 | import (
  5 | 	"io"
  6 | )
  7 | 
  8 | type (
  9 | 	Store interface {
 10 | 		Read(string, string, interface{}) error
 11 | 		Write(string, string, interface{}) error
 12 | 	}
 13 | 
 14 | 	LocalState interface {
 15 | 		State
 16 | 		ExposeRPCEndpoint(string, string) (io.Closer, error)
 17 | 	}
 18 | 
 19 | 	State interface {
 20 | 		Ready()
 21 | 		GetDataDir() (string, error)
 22 | 		GetRole() (string, error)
 23 | 		GetDBRole() (string, error)
 24 | 		SetDBRole(string) error
 25 | 		HasSynced() (bool, error)
 26 | 		SetSynced(bool) error
 27 | 		Location() string
 28 | 		Bounce(location string) State
 29 | 	}
 30 | 
 31 | 	state struct {
 32 | 		store   Store
 33 | 		synced  bool
 34 | 		Role    string
 35 | 		DBRole  string
 36 | 		Address string
 37 | 		DataDir string
 38 | 	}
 39 | )
 40 | 
 41 | var states = "states"
 42 | 
 43 | // Creates and returns a state that represents a state on the local machine.
 44 | func NewLocalState(role, location, dataDir string, store Store) (LocalState, error) {
 45 | 	newState := state{}
 46 | 
 47 | 	// if we can't grab the previous state from the store, lets create a new one
 48 | 	if err := store.Read(states, role, &newState); err != nil {
 49 | 		newState = state{
 50 | 			DataDir: dataDir,
 51 | 			Role:    role,
 52 | 			DBRole:  "initialized",
 53 | 			synced:  false,
 54 | 			Address: location,
 55 | 		}
 56 | 		// something is wrong if we can't newState the new state, so return the error
 57 | 		if err = store.Write(states, role, &newState); err != nil {
 58 | 			return nil, err
 59 | 		}
 60 | 	}
 61 | 	newState.store = store
 62 | 	newState.synced = false
 63 | 	return &newState, nil
 64 | }
 65 | 
 66 | func (state *state) Ready() {}
 67 | 
 68 | func (state *state) Bounce(location string) State {
 69 | 	// this should really return an error of some sort...
 70 | 	return nil
 71 | }
 72 | 
 73 | func (state *state) HasSynced() (bool, error) {
 74 | 	return state.synced, nil
 75 | }
 76 | 
 77 | func (state *state) SetSynced(synced bool) error {
 78 | 	state.synced = synced
 79 | 	return nil
 80 | }
 81 | 
 82 | func (state *state) Location() string {
 83 | 	return state.Address
 84 | }
 85 | 
 86 | func (state *state) GetDataDir() (string, error) {
 87 | 	return state.DataDir, nil
 88 | }
 89 | 
 90 | func (state *state) GetRole() (string, error) {
 91 | 	return state.Role, nil
 92 | }
 93 | 
 94 | func (state *state) GetDBRole() (string, error) {
 95 | 	return state.DBRole, nil
 96 | }
 97 | 
 98 | func (state *state) SetDBRole(role string) error {
 99 | 	state.DBRole = role
100 | 	return state.store.Write(states, state.Role, state)
101 | }
102 | 


--------------------------------------------------------------------------------
/state/state_test.go:
--------------------------------------------------------------------------------
  1 | package state_test
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 	"github.com/golang/mock/gomock"
  6 | 	"testing"
  7 | 	"time"
  8 | 
  9 | 	"github.com/nanopack/yoke/state"
 10 | 	"github.com/nanopack/yoke/state/mock"
 11 | )
 12 | 
 13 | var fakeErr = errors.New("general")
 14 | 
 15 | func TestLocal(test *testing.T) {
 16 | 	ctrl := gomock.NewController(test)
 17 | 	defer ctrl.Finish()
 18 | 
 19 | 	store := mock_state.NewMockStore(ctrl)
 20 | 
 21 | 	store.EXPECT().Read("states", "something", gomock.Any()).Return(fakeErr).Times(2)
 22 | 	store.EXPECT().Write("states", "something", gomock.Any()).Return(fakeErr)
 23 | 
 24 | 	out, err := state.NewLocalState("something", "wherever", "//here", store)
 25 | 	if err == nil {
 26 | 		test.Log("should have gotten an error", out, err)
 27 | 		test.FailNow()
 28 | 	}
 29 | 
 30 | 	store.EXPECT().Write("states", "something", gomock.Any()).Return(nil)
 31 | 	local, err := state.NewLocalState("something", "wherever", "//here", store)
 32 | 	if err != nil {
 33 | 		test.Log(err)
 34 | 		test.FailNow()
 35 | 	}
 36 | 
 37 | 	testState(local, store, test)
 38 | 
 39 | 	// now for specific tests to local
 40 | 	store.EXPECT().Write("states", "something", gomock.Any())
 41 | 	err = local.SetDBRole("testing")
 42 | 	if err != nil {
 43 | 		test.Log(err)
 44 | 		test.FailNow()
 45 | 	}
 46 | 
 47 | 	dbRole, err := local.GetDBRole()
 48 | 	if err != nil {
 49 | 		test.Log(err)
 50 | 		test.FailNow()
 51 | 	}
 52 | 	if dbRole != "testing" {
 53 | 		test.Log("wrong dbrole was returned")
 54 | 		test.Fail()
 55 | 	}
 56 | 
 57 | 	if local.Location() != "wherever" {
 58 | 		test.Log("wrong location was returned")
 59 | 		test.Fail()
 60 | 	}
 61 | }
 62 | 
 63 | func TestRpc(test *testing.T) {
 64 | 	ctrl := gomock.NewController(test)
 65 | 	defer ctrl.Finish()
 66 | 
 67 | 	store := mock_state.NewMockStore(ctrl)
 68 | 
 69 | 	store.EXPECT().Read("states", "something", gomock.Any()).Return(fakeErr)
 70 | 	store.EXPECT().Write("states", "something", gomock.Any()).Return(nil)
 71 | 	local, err := state.NewLocalState("something", "wherever", "//here", store)
 72 | 	if err != nil {
 73 | 		test.Log(err)
 74 | 		test.FailNow()
 75 | 	}
 76 | 
 77 | 	_, err = local.ExposeRPCEndpoint("tcp", "127.0.0.1:1234")
 78 | 	if err != nil {
 79 | 		test.Log(err)
 80 | 		test.FailNow()
 81 | 	}
 82 | 	// I don't know why this causes the tests to fail
 83 | 	// defer listen.Close()
 84 | 
 85 | 	client := state.NewRemoteState("tcp", "127.0.0.1:1234", time.Second)
 86 | 
 87 | 	testState(client, store, test)
 88 | 
 89 | 	// now for tests specific to remote states
 90 | 
 91 | 	err = client.SetDBRole("testing")
 92 | 	if err == nil {
 93 | 		test.Log("should not have been able to update the db state from remote")
 94 | 		test.Fail()
 95 | 	}
 96 | 
 97 | 	if client.Location() != "127.0.0.1:1234" {
 98 | 		test.Log("wrong location was returned")
 99 | 		test.Fail()
100 | 	}
101 | }
102 | 
103 | func TestBounce(test *testing.T) {
104 | 	ctrl := gomock.NewController(test)
105 | 	defer ctrl.Finish()
106 | 
107 | 	store := mock_state.NewMockStore(ctrl)
108 | 
109 | 	store.EXPECT().Read("states", "here", gomock.Any()).Return(fakeErr)
110 | 	store.EXPECT().Write("states", "here", gomock.Any()).Return(nil)
111 | 	local, err := state.NewLocalState("here", "right here", "//other", store)
112 | 	if err != nil {
113 | 		test.Log(err)
114 | 		test.FailNow()
115 | 	}
116 | 
117 | 	listen, err := local.ExposeRPCEndpoint("tcp", "127.0.0.1:2345")
118 | 	if err != nil {
119 | 		test.Log(err)
120 | 		test.FailNow()
121 | 	}
122 | 	defer listen.Close()
123 | 
124 | 	store.EXPECT().Read("states", "something", gomock.Any()).Return(fakeErr)
125 | 	store.EXPECT().Write("states", "something", gomock.Any()).Return(nil)
126 | 	remote, err := state.NewLocalState("something", "wherever", "//here", store)
127 | 	if err != nil {
128 | 		test.Log(err)
129 | 		test.FailNow()
130 | 	}
131 | 
132 | 	testState(remote, store, test)
133 | 	test.Logf("now for the remote")
134 | 
135 | 	// this needs to be reset
136 | 	remote.SetSynced(false)
137 | 
138 | 	listen1, err := remote.ExposeRPCEndpoint("tcp", "127.0.0.1:3456")
139 | 	if err != nil {
140 | 		test.Log(err)
141 | 		test.FailNow()
142 | 	}
143 | 	defer listen1.Close()
144 | 
145 | 	client := state.NewRemoteState("tcp", "127.0.0.1:2345", time.Second)
146 | 
147 | 	bounced := client.Bounce("127.0.0.1:3456")
148 | 
149 | 	testState(bounced, store, test)
150 | 
151 | 	// now for tests specific to remote states
152 | 
153 | 	err = bounced.SetDBRole("testing")
154 | 	if err == nil {
155 | 		test.Log("should not have been able to update the db state from remote")
156 | 		test.Fail()
157 | 	}
158 | 
159 | 	if bounced.Location() != "127.0.0.1:3456" {
160 | 		test.Log("wrong location was returned")
161 | 		test.Fail()
162 | 	}
163 | }
164 | 
165 | func testState(client state.State, store *mock_state.MockStore, test *testing.T) {
166 | 	role, err := client.GetRole()
167 | 	if err != nil {
168 | 		test.Log(err)
169 | 		test.FailNow()
170 | 	}
171 | 	if role != "something" {
172 | 		test.Logf("wrong role was returned '%v'", role)
173 | 		test.Fail()
174 | 	}
175 | 
176 | 	dbRole, err := client.GetDBRole()
177 | 	if err != nil {
178 | 		test.Log(err)
179 | 		test.FailNow()
180 | 	}
181 | 	if dbRole != "initialized" {
182 | 		test.Logf("wrong dbrole was returned '%v'", dbRole)
183 | 		test.Fail()
184 | 	}
185 | 
186 | 	synced, err := client.HasSynced()
187 | 	if err != nil {
188 | 		test.Log(err)
189 | 		test.FailNow()
190 | 	}
191 | 
192 | 	if synced {
193 | 		test.Log("it should not have been in sync")
194 | 		test.Fail()
195 | 	}
196 | 
197 | 	err = client.SetSynced(true)
198 | 	if err != nil {
199 | 		test.Log(err)
200 | 		test.FailNow()
201 | 	}
202 | 
203 | 	synced, err = client.HasSynced()
204 | 	if err != nil {
205 | 		test.Log(err)
206 | 		test.FailNow()
207 | 	}
208 | 
209 | 	if !synced {
210 | 		test.Log("it should have been in sync")
211 | 		test.Fail()
212 | 	}
213 | 
214 | 	dir, err := client.GetDataDir()
215 | 	if err != nil {
216 | 		test.Log(err)
217 | 		test.FailNow()
218 | 	}
219 | 
220 | 	if dir != "//here" {
221 | 		test.Logf("got wrong data dir '%v'", dir)
222 | 		test.Fail()
223 | 	}
224 | 
225 | 	// really doesn't do anything...
226 | 	client.Ready()
227 | }
228 | 


--------------------------------------------------------------------------------
/update_mocks.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash -e
2 | 
3 | mkdir -p \
4 |   monitor/mock \
5 |   state/mock
6 | 
7 | mockgen github.com/nanopack/yoke/state State,Store > state/mock/mock.go
8 | mockgen github.com/nanopack/yoke/monitor Performer > monitor/mock/mock.go
9 | 


--------------------------------------------------------------------------------
/yokeadm/commands/clusterList.go:
--------------------------------------------------------------------------------
 1 | package commands
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"net/rpc"
 6 | 	"os"
 7 | 	"regexp"
 8 | 	"time"
 9 | 
10 | 	"github.com/spf13/cobra"
11 | )
12 | 
13 | // Status represents the Status of a node in the cluser
14 | type Status struct {
15 | 	CRole     string    // the nodes 'role' in the cluster (primary, secondary, monitor)
16 | 	DataDir   string    // directory of the postgres database
17 | 	DBRole    string    // the 'role' of the running pgsql instance inside the node (master, slave)
18 | 	Ip        string    // advertise_ip
19 | 	PGPort    int       //
20 | 	State     string    // the current state of the node
21 | 	UpdatedAt time.Time // the last time the node state was updated
22 | }
23 | 
24 | //
25 | var clusterListCmd = &cobra.Command{
26 | 	Use:   "list",
27 | 	Short: "Returns status information for all nodes in the cluster",
28 | 	Long:  ``,
29 | 
30 | 	Run: clusterList,
31 | }
32 | 
33 | // clusterList displays select information about all of the nodes in a cluster
34 | func clusterList(ccmd *cobra.Command, args []string) {
35 | 
36 | 	// create an RPC client that will connect to the designated node
37 | 	client, err := rpc.Dial("tcp", fmt.Sprintf("%s:%s", fHost, fPort))
38 | 	if err != nil {
39 | 		fmt.Println("[cli.ClusterList.run] Failed to dial!", err)
40 | 		os.Exit(1)
41 | 	}
42 | 	defer client.Close()
43 | 
44 | 	// issue a request to the designated node for the status of the cluster
45 | 	var members = &[]Status{}
46 | 	if err := client.Call("Status.RPCCluster", "", members); err != nil {
47 | 		fmt.Println("[cli.ClusterList.run] Failed to call!", err)
48 | 		os.Exit(1)
49 | 	}
50 | 
51 | 	//
52 | 	fmt.Println(`
53 | Cluster Role |   Cluster IP    |     State     |    Status    |  Postgres Role  |  Postgres Port  |      Last Updated
54 | ---------------------------------------------------------------------------------------------------------------------------`)
55 | 	for _, member := range *members {
56 | 
57 | 		state := "--"
58 | 		status := "running"
59 | 
60 | 		//
61 | 		if subMatch := regexp.MustCompile(`^\((.*)\)(.*)$`).FindStringSubmatch(member.State); subMatch != nil {
62 | 			state = subMatch[1]
63 | 			status = subMatch[2]
64 | 		}
65 | 
66 | 		//
67 | 		fmt.Printf("%-12s | %-15s | %-13s | %-12s | %-15s | %-15d | %-25s\n", member.CRole, member.Ip, state, status, member.DBRole, member.PGPort, member.UpdatedAt.Format("01.02.06 (15:04:05) MST"))
68 | 	}
69 | 
70 | 	fmt.Println("")
71 | }
72 | 


--------------------------------------------------------------------------------
/yokeadm/commands/commands.go:
--------------------------------------------------------------------------------
 1 | //
 2 | package commands
 3 | 
 4 | import "github.com/spf13/cobra"
 5 | 
 6 | //
 7 | var (
 8 | 
 9 | 	//
10 | 	YokeCmd = &cobra.Command{
11 | 		Use:   "yoke",
12 | 		Short: "",
13 | 		Long:  ``,
14 | 	}
15 | 
16 | 	// subcommands
17 | 	clusterCmd = &cobra.Command{Use: "cluster", Short: "", Long: ``}
18 | 	memberCmd  = &cobra.Command{Use: "member", Short: "", Long: ``}
19 | 
20 | 	// flags
21 | 	fHost string //
22 | 	fPort string //
23 | )
24 | 
25 | // init creates the list of available nanobox commands and sub commands
26 | func init() {
27 | 
28 | 	// persistent flags
29 | 	YokeCmd.PersistentFlags().StringVarP(&fHost, "host", "H", "localhost", "")
30 | 	YokeCmd.PersistentFlags().StringVarP(&fPort, "port", "p", "4400", "")
31 | 
32 | 	//
33 | 	YokeCmd.AddCommand(clusterCmd)
34 | 	clusterCmd.AddCommand(clusterListCmd)
35 | 
36 | 	//
37 | 	YokeCmd.AddCommand(memberCmd)
38 | 	memberCmd.AddCommand(memberDemoteCmd)
39 | }
40 | 


--------------------------------------------------------------------------------
/yokeadm/commands/memberDemote.go:
--------------------------------------------------------------------------------
 1 | package commands
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"net/rpc"
 6 | 
 7 | 	"github.com/spf13/cobra"
 8 | )
 9 | 
10 | // memberDemoteCmd is used to demote a designated member node in the cluster
11 | var memberDemoteCmd = &cobra.Command{
12 | 	Use:   "demote",
13 | 	Short: "Advises a node to 'demote'",
14 | 	Long:  ``,
15 | 
16 | 	Run: memberDemote,
17 | }
18 | 
19 | // memberDemote demotes the designated member node
20 | func memberDemote(ccmd *cobra.Command, args []string) {
21 | 
22 | 	// create an RPC client that will connect to the designated node
23 | 	client, err := rpc.Dial("tcp", fmt.Sprintf("%s:%s", fHost, fPort))
24 | 	if err != nil {
25 | 		fmt.Printf("[commands/memberDemote] rpc.Dial() failed - %s\n", err.Error())
26 | 		return
27 | 	}
28 | 	defer client.Close()
29 | 
30 | 	fmt.Printf("advising '%s' to demote...\n", fHost)
31 | 
32 | 	// issue a demote to the designated node
33 | 	if err := client.Call("Status.Demote", "", nil); err != nil {
34 | 		fmt.Printf("[commands/memberDemote] client.Call() failed - %s\n", err.Error())
35 | 	}
36 | }
37 | 


--------------------------------------------------------------------------------
/yokeadm/main.go:
--------------------------------------------------------------------------------
1 | package main
2 | 
3 | import "github.com/nanopack/yoke/yokeadm/commands"
4 | 
5 | //
6 | func main() {
7 | 	commands.YokeCmd.Execute()
8 | }
9 | 


--------------------------------------------------------------------------------