func watch( client *consulapi.Client, prefix string, path string, token string, pairCh chan<- consulapi.KVPairs, errCh chan<- error, quitCh <-chan struct{}) { // Create the root for KVs, if necessary mkdirp.Mk(path, 0777) // Get the initial list of k/v pairs. We don't do a retryableList // here because we want a fast fail if the initial request fails. opts := &consulapi.QueryOptions{Token: token} pairs, meta, err := client.KV().List(prefix, opts) if err != nil { errCh <- err return } // Send the initial list out right away pairCh <- pairs // Loop forever (or until quitCh is closed) and watch the keys // for changes. curIndex := meta.LastIndex for { select { case <-quitCh: return default: } pairs, meta, err = retryableList( func() (consulapi.KVPairs, *consulapi.QueryMeta, error) { opts = &consulapi.QueryOptions{WaitIndex: curIndex, Token: token} return client.KV().List(prefix, opts) }) if err != nil { // This happens when the connection to the consul agent dies. Build in a retry by looping after a delay. log.Warn("Error communicating with consul agent.") continue } pairCh <- pairs log.WithFields(log.Fields{ "curIndex": curIndex, "lastIndex": meta.LastIndex, }).Debug("Potential index update observed") curIndex = meta.LastIndex } }
func main() { reader := bufio.NewReader(os.Stdin) fmt.Println("aiw3/maps 0.1.1") fmt.Print("Please type which kind of files you want to download [client/server]: ") cur_type, err := reader.ReadString('\n') cur_type = strings.Trim(cur_type, "\n\r") if err != nil { fmt.Println(err) os.Exit(1) } if cur_type != "client" && cur_type != "server" { fmt.Println("Invalid type") os.Exit(1) } fmt.Println("Loading packages list...") resp, err := http.Get("https://cloudrack.io/aiw3/maps.json") if err != nil { fmt.Printf("Error while downloading map data; %s", err) os.Exit(1) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Printf("Error while downloading map data; %s", err) os.Exit(1) } var maps Maps err = json.Unmarshal(body, &maps) if err != nil { fmt.Printf("Error while parsing map data; %s", err) os.Exit(1) } fmt.Println("Verifying repository's integrity...") for _, pkg := range maps.Packages { for name, _ := range pkg.Files { if _, exists := maps.Files[name]; !exists { fmt.Printf("Invalid repository; File %s does not exist (required by %s).\n", name, pkg.Id) os.Exit(1) } } } fmt.Println("Choose the map to install:") for i, pkg := range maps.Packages { fmt.Printf("[%d] %s (%s)\n", i, pkg.Name, pkg.Id) } fmt.Printf("[%d] All files (both client and server)\n", len(maps.Packages)) fmt.Print("Type in the ID of the map you want to install: ") input, err := reader.ReadString('\n') if err != nil { fmt.Println(err) os.Exit(1) } i, err := strconv.Atoi(strings.Trim(input, "\r\n")) if err != nil { fmt.Println(err) os.Exit(1) } if i > len(maps.Packages) || i < 0 { fmt.Println("Package does not exist.") os.Exit(1) } if i < len(maps.Packages) { pkg := maps.Packages[i] for name, side := range pkg.Files { if side == "shared" || side == cur_type { abs, err := filepath.Abs(name) if err != nil { fmt.Println(err) os.Exit(1) } if utility.FileExists(abs) { fmt.Println(name + " already exists") continue } dir := filepath.Dir(abs) err = mkdirp.Mk(dir, 755) if err != nil { fmt.Println(err) os.Exit(1) } fmt.Printf("Downloading %s\n", name) var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("megatools/megadl", "--path="+dir, maps.Files[name]) } else { cmd = exec.Command("megadl", "--path="+dir, maps.Files[name]) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { fmt.Println(err) os.Exit(1) } } } } else { for name, url := range maps.Files { abs, err := filepath.Abs(name) if err != nil { fmt.Println(err) os.Exit(1) } if utility.FileExists(abs) { fmt.Println(name + " already exists") continue } dir := filepath.Dir(abs) err = mkdirp.Mk(dir, 755) if err != nil { fmt.Println(err) os.Exit(1) } fmt.Printf("Downloading %s\n", name) cmd := exec.Command("megatools/megadl", "--path="+dir, url) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { fmt.Println(err) os.Exit(1) } } } fmt.Println("Install completed.") }
// Connects to Consul and watches a given K/V prefix and uses that to // write to the filesystem. func watchMappingAndExec(config *WatchConfig, mappingConfig *MappingConfig) (int, error) { client, err := buildConsulClient(config.Consul) if err != nil { return 0, err } // If prefix starts with /, trim it. if mappingConfig.Prefix[0] == '/' { mappingConfig.Prefix = mappingConfig.Prefix[1:] } // If the config path is lacking a trailing separator, add it. if mappingConfig.Path[len(mappingConfig.Path)-1] != os.PathSeparator { mappingConfig.Path += string(os.PathSeparator) } isWindows := os.PathSeparator != '/' // Remove an unhandled trailing quote, which presented itself on Windows when // the given path contained spaces (requiring quotes) and also had a trailing // backslash. if mappingConfig.Path[len(mappingConfig.Path)-1] == 34 { mappingConfig.Path = mappingConfig.Path[:len(mappingConfig.Path)-1] } // Start the watcher goroutine that watches for changes in the // K/V and notifies us on a channel. errCh := make(chan error, 1) pairCh := make(chan consulapi.KVPairs) quitCh := make(chan struct{}) // Defer close of quitCh if we're running more than once if !config.RunOnce { defer close(quitCh) } go watch( client, mappingConfig.Prefix, mappingConfig.Path, config.Consul.Token, pairCh, errCh, quitCh) var env map[string]string for { var pairs consulapi.KVPairs // Wait for new pairs to come on our channel or an error // to occur. select { case pairs = <-pairCh: case err := <-errCh: return 0, err } newEnv := make(map[string]string) for _, pair := range pairs { log.WithFields(log.Fields{ "key": pair.Key, }).Debug("Key present in source") k := strings.TrimPrefix(pair.Key, mappingConfig.Prefix) k = strings.TrimLeft(k, "/") newEnv[k] = string(pair.Value) } // If the variables didn't actually change, // then don't do anything. if reflect.DeepEqual(env, newEnv) { continue } // Iterate over all objects in the current env. If they are not in the newEnv, they // were deleted from Consul and should be deleted from disk. for k := range env { if _, ok := newEnv[k]; !ok { log.WithFields(log.Fields{ "key": k, }).Debug("Key no longer present locally") // Write file to disk keyfile := fmt.Sprintf("%s%s", mappingConfig.Path, k) if isWindows { keyfile = strings.Replace(keyfile, "/", "\\", -1) } err := os.Remove(keyfile) if err != nil { log.WithFields(log.Fields{ "error": err, }).Error("Failed to remove key") } } } // Replace the env so we can detect future changes env = newEnv // Write the updated keys to the filesystem at the specified path for k, v := range newEnv { // Write file to disk keyfile := fmt.Sprintf("%s%s", mappingConfig.Path, k) // if Windows, replace / with windows path delimiter if isWindows { keyfile = strings.Replace(keyfile, "/", "\\", -1) // mkdirp the file's path err := mkdirp.Mk(keyfile[:strings.LastIndex(keyfile, "\\")], 0777) if err != nil { log.WithFields(log.Fields{ "error": err, }).Error("Failed to create parent directory for key") } } else { // mkdirp the file's path err := mkdirp.Mk(keyfile[:strings.LastIndex(keyfile, "/")], 0777) if err != nil { log.WithFields(log.Fields{ "error": err, }).Error("Failed to create parent directory for key") } } f, err := os.Create(keyfile) if err != nil { log.WithFields(log.Fields{ "error": err, "file": keyfile, }).Error("Failed to create file") continue } defer f.Close() log.WithFields(log.Fields{ "length": len(v), }).Debug("Input value length") buff := new(bytes.Buffer) buff.Write([]byte(v)) if len(mappingConfig.Keystore) > 0 { decryptedValue, err := gosecret.DecryptTags([]byte(v), mappingConfig.Keystore) if err != nil { log.WithFields(log.Fields{ "error": err, }).Error("Failed to decrypt value") continue } log.WithFields(log.Fields{ "length": len(decryptedValue), }).Debug("Output value length") data := string(decryptedValue) funcs := template.FuncMap{ // Template functions "goDecrypt": goDecryptFunc(mappingConfig.Keystore), } tmpl, err := template.New("decryption").Funcs(funcs).Parse(data) if err != nil { log.WithFields(log.Fields{ "error": err, }).Error("Could not parse template") continue } // Run the template to verify the output. buff = new(bytes.Buffer) err = tmpl.Execute(buff, nil) if err != nil { log.WithFields(log.Fields{ "error": err, }).Error("Could not execute template") continue } } wrote, err := f.Write(buff.Bytes()) if err != nil { log.WithFields(log.Fields{ "error": err, "file": keyfile, }).Error("Failed to write to file") continue } log.WithFields(log.Fields{ "length": wrote, "file": keyfile, }).Debug("Successfully wrote value to file") err = f.Sync() if err != nil { log.WithFields(log.Fields{ "error": err, "file": keyfile, }).Error("Failed to sync file") } err = f.Close() if err != nil { log.WithFields(log.Fields{ "error": err, "file": keyfile, }).Error("Failed to close file") } } // Configuration changed, run our onchange command, if one was specified. if mappingConfig.OnChange != nil { var cmd = exec.Command(mappingConfig.OnChange[0], mappingConfig.OnChange[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // Always wait for the forked process to exit. We may wish to revisit this, but I think // it's the safest approach since it avoids a case where rapid key updates DOS a system // by slurping all proc handles. err = cmd.Run() if err != nil { return 111, err } } // If we are only running once, close the channel on this watcher. if config.RunOnce { close(quitCh) return 0, nil } } }