func (s *RedisSuite) TestDumpRestore(t *c.C) { a := s.newCliTestApp(t) res := a.flynn("resource", "add", "redis") t.Assert(res, Succeeds) id := strings.Split(res.Output, " ")[2] release, err := s.controllerClient(t).GetAppRelease(a.id) t.Assert(err, c.IsNil) t.Assert(release.Env["FLYNN_REDIS"], c.Not(c.Equals), "") a.waitForService(release.Env["FLYNN_REDIS"]) t.Assert(a.flynn("redis", "redis-cli", "set", "foo", "bar"), Succeeds) file := filepath.Join(t.MkDir(), "dump.rdb") t.Assert(a.flynn("redis", "dump", "-f", file), Succeeds) t.Assert(a.flynn("redis", "redis-cli", "del", "foo"), Succeeds) a.flynn("redis", "restore", "-f", file) query := a.flynn("redis", "redis-cli", "get", "foo") t.Assert(query, SuccessfulOutputContains, "bar") t.Assert(a.flynn("resource", "remove", "redis", id), Succeeds) }
func (s *CLISuite) TestExportImport(t *c.C) { srcApp := "app-export" + random.String(8) dstApp := "app-import" + random.String(8) // create app r := s.newGitRepo(t, "http") t.Assert(r.flynn("create", srcApp), Succeeds) // exporting the app without a release should work file := filepath.Join(t.MkDir(), "export.tar") t.Assert(r.flynn("export", "-f", file), Succeeds) assertExportContains := func(paths ...string) { cmd := r.sh(fmt.Sprintf("tar --list --file=%s --strip=1 --show-transformed", file)) t.Assert(cmd, Outputs, strings.Join(paths, "\n")+"\n") } assertExportContains("app.json", "routes.json") // exporting the app with an artifact-less release should work t.Assert(r.flynn("env", "set", "FOO=BAR"), Succeeds) t.Assert(r.flynn("export", "-f", file), Succeeds) assertExportContains("app.json", "routes.json", "release.json") // release the app and provision some dbs t.Assert(r.git("push", "flynn", "master"), Succeeds) t.Assert(r.flynn("resource", "add", "postgres"), Succeeds) t.Assert(r.flynn("pg", "psql", "--", "-c", "CREATE table foos (data text); INSERT INTO foos (data) VALUES ('foobar')"), Succeeds) t.Assert(r.flynn("resource", "add", "mysql"), Succeeds) t.Assert(r.flynn("mysql", "console", "--", "-e", "CREATE TABLE foos (data TEXT); INSERT INTO foos (data) VALUES ('foobar')"), Succeeds) // export app t.Assert(r.flynn("export", "-f", file), Succeeds) assertExportContains( "app.json", "routes.json", "release.json", "artifact.json", "formation.json", "slug.tar.gz", "postgres.dump", "mysql.dump", ) // remove db tables from source app t.Assert(r.flynn("pg", "psql", "--", "-c", "DROP TABLE foos"), Succeeds) t.Assert(r.flynn("mysql", "console", "--", "-e", "DROP TABLE foos"), Succeeds) // remove the git remote t.Assert(r.git("remote", "remove", "flynn"), Succeeds) // import app t.Assert(r.flynn("import", "--name", dstApp, "--file", file), Succeeds) // test dbs were imported query := r.flynn("-a", dstApp, "pg", "psql", "--", "-c", "SELECT * FROM foos") t.Assert(query, SuccessfulOutputContains, "foobar") query = r.flynn("-a", dstApp, "mysql", "console", "--", "-e", "SELECT * FROM foos") t.Assert(query, SuccessfulOutputContains, "foobar") // wait for it to start _, err := s.discoverdClient(t).Instances(dstApp+"-web", 10*time.Second) t.Assert(err, c.IsNil) }
func (h *Helper) newGitRepoWithTrace(t *c.C, nameOrURL string, trace bool) *gitRepo { dir := filepath.Join(t.MkDir(), "repo") r := &gitRepo{dir, t, trace} if strings.HasPrefix(nameOrURL, "https://") { t.Assert(run(t, exec.Command("git", "clone", nameOrURL, dir)), Succeeds) return r } else if nameOrURL != "" { t.Assert(run(t, exec.Command("cp", "-r", filepath.Join("apps", nameOrURL), dir)), Succeeds) } else { t.Assert(os.Mkdir(dir, 0755), c.IsNil) t.Assert(ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("app"), 0644), c.IsNil) } t.Assert(r.git("init"), Succeeds) t.Assert(r.git("add", "."), Succeeds) t.Assert(r.git("commit", "-am", "init"), Succeeds) return r }
func (s *MongoDBSuite) TestDumpRestore(t *c.C) { r := s.newGitRepo(t, "empty") t.Assert(r.flynn("create"), Succeeds) res := r.flynn("resource", "add", "mongodb") t.Assert(res, Succeeds) id := strings.Split(res.Output, " ")[2] t.Assert(r.flynn("mongodb", "mongo", "--", "--eval", `db.foos.insert({data: "foobar"})`), Succeeds) file := filepath.Join(t.MkDir(), "db.dump") t.Assert(r.flynn("mongodb", "dump", "-f", file), Succeeds) t.Assert(r.flynn("mongodb", "mongo", "--", "--eval", "db.foos.drop()"), Succeeds) r.flynn("mongodb", "restore", "-f", file) query := r.flynn("mongodb", "mongo", "--", "--eval", "db.foos.find()") t.Assert(query, SuccessfulOutputContains, "foobar") t.Assert(r.flynn("resource", "remove", "mongodb", id), Succeeds) }
func (s *PostgresSuite) TestDumpRestore(t *c.C) { r := s.newGitRepo(t, "empty") t.Assert(r.flynn("create"), Succeeds) res := r.flynn("resource", "add", "postgres") t.Assert(res, Succeeds) id := strings.Split(res.Output, " ")[2] t.Assert(r.flynn("pg", "psql", "--", "-c", "CREATE table foos (data text); INSERT INTO foos (data) VALUES ('foobar')"), Succeeds) file := filepath.Join(t.MkDir(), "db.dump") t.Assert(r.flynn("pg", "dump", "-f", file), Succeeds) t.Assert(r.flynn("pg", "psql", "--", "-c", "DROP TABLE foos"), Succeeds) r.flynn("pg", "restore", "-f", file) query := r.flynn("pg", "psql", "--", "-c", "SELECT * FROM foos") t.Assert(query, SuccessfulOutputContains, "foobar") t.Assert(r.flynn("resource", "remove", "postgres", id), Succeeds) }
func (s *MariaDBSuite) TestDumpRestore(t *c.C) { r := s.newGitRepo(t, "empty") t.Assert(r.flynn("create"), Succeeds) res := r.flynn("resource", "add", "mysql") t.Assert(res, Succeeds) id := strings.Split(res.Output, " ")[2] t.Assert(r.flynn("mysql", "console", "--", "-e", "CREATE TABLE T (F text); INSERT INTO T (F) VALUES ('abc')"), Succeeds) file := filepath.Join(t.MkDir(), "db.dump") t.Assert(r.flynn("mysql", "dump", "-f", file), Succeeds) t.Assert(r.flynn("mysql", "console", "--", "-e", "DROP TABLE T"), Succeeds) r.flynn("mysql", "restore", "-f", file) query := r.flynn("mysql", "console", "--", "-e", "SELECT * FROM T") t.Assert(query, SuccessfulOutputContains, "abc") t.Assert(r.flynn("resource", "remove", "mysql", id), Succeeds) }
func (s *CLISuite) TestDockerExportImport(t *c.C) { // release via docker-receive client := s.controllerClient(t) app := &ct.App{Name: "cli-test-docker-export"} t.Assert(client.CreateApp(app), c.IsNil) repo := "cli-test-export" s.buildDockerImage(t, repo, `CMD ["/bin/pingserv"]`) t.Assert(flynn(t, "/", "-a", app.Name, "docker", "push", repo), Succeeds) t.Assert(flynn(t, "/", "-a", app.Name, "scale", "app=1"), Succeeds) defer flynn(t, "/", "-a", app.Name, "scale", "app=0") // export the app file := filepath.Join(t.MkDir(), "export.tar") t.Assert(flynn(t, "/", "-a", app.Name, "export", "-f", file), Succeeds) // delete the image from the registry release, err := client.GetAppRelease(app.Name) t.Assert(err, c.IsNil) artifact, err := client.GetArtifact(release.ImageArtifactID()) t.Assert(err, c.IsNil) u, err := url.Parse(s.clusterConf(t).DockerPushURL) t.Assert(err, c.IsNil) uri := fmt.Sprintf("http://%s/v2/%s/manifests/%s", u.Host, app.Name, artifact.Meta["docker-receive.digest"]) req, err := http.NewRequest("DELETE", uri, nil) req.SetBasicAuth("", s.clusterConf(t).Key) t.Assert(err, c.IsNil) res, err := http.DefaultClient.Do(req) t.Assert(err, c.IsNil) res.Body.Close() // import to another app importApp := "cli-test-docker-import" t.Assert(flynn(t, "/", "import", "--name", importApp, "--file", file), Succeeds) defer flynn(t, "/", "-a", importApp, "scale", "app=0") // wait for it to start _, err = s.discoverdClient(t).Instances(importApp+"-web", 10*time.Second) t.Assert(err, c.IsNil) }
func (s *CLISuite) TestExportImport(t *c.C) { srcApp := "app-export" + random.String(8) dstApp := "app-import" + random.String(8) // create app r := s.newGitRepo(t, "http") t.Assert(r.flynn("create", srcApp), Succeeds) // exporting the app without a release should work file := filepath.Join(t.MkDir(), "export.tar") t.Assert(r.flynn("export", "-f", file), Succeeds) assertExportContains(t, file, "app.json", "routes.json") // exporting the app with an artifact-less release should work t.Assert(r.flynn("env", "set", "FOO=BAR"), Succeeds) t.Assert(r.flynn("export", "-f", file), Succeeds) assertExportContains(t, file, "app.json", "routes.json", "release.json") // release the app and provision some dbs t.Assert(r.git("push", "flynn", "master"), Succeeds) t.Assert(r.flynn("resource", "add", "postgres"), Succeeds) t.Assert(r.flynn("pg", "psql", "--", "-c", "CREATE table foos (data text); INSERT INTO foos (data) VALUES ('foobar')"), Succeeds) t.Assert(r.flynn("resource", "add", "mysql"), Succeeds) t.Assert(r.flynn("mysql", "console", "--", "-e", "CREATE TABLE foos (data TEXT); INSERT INTO foos (data) VALUES ('foobar')"), Succeeds) // grab the slug details client := s.controllerClient(t) release, err := client.GetAppRelease(srcApp) t.Assert(err, c.IsNil) artifact, err := client.GetArtifact(release.ArtifactIDs[1]) t.Assert(err, c.IsNil) slugLayer := artifact.Manifest().Rootfs[0].Layers[0] // export app t.Assert(r.flynn("export", "-f", file), Succeeds) assertExportContains(t, file, "app.json", "routes.json", "release.json", "artifacts.json", slugLayer.ID+".layer", "formation.json", "postgres.dump", "mysql.dump", ) // remove db tables from source app t.Assert(r.flynn("pg", "psql", "--", "-c", "DROP TABLE foos"), Succeeds) t.Assert(r.flynn("mysql", "console", "--", "-e", "DROP TABLE foos"), Succeeds) // remove the git remote t.Assert(r.git("remote", "remove", "flynn"), Succeeds) // import app t.Assert(r.flynn("import", "--name", dstApp, "--file", file), Succeeds) // test dbs were imported query := r.flynn("-a", dstApp, "pg", "psql", "--", "-c", "SELECT * FROM foos") t.Assert(query, SuccessfulOutputContains, "foobar") query = r.flynn("-a", dstApp, "mysql", "console", "--", "-e", "SELECT * FROM foos") t.Assert(query, SuccessfulOutputContains, "foobar") // wait for it to start _, err = s.discoverdClient(t).Instances(dstApp+"-web", 10*time.Second) t.Assert(err, c.IsNil) }
func (s *CLISuite) TestRelease(t *c.C) { app := s.newCliTestApp(t) defer app.cleanup() release := &ct.Release{ ArtifactIDs: []string{s.createArtifact(t, "test-apps").ID}, Env: map[string]string{"GLOBAL": "FOO"}, Processes: map[string]ct.ProcessType{ "echoer": { Args: []string{"/bin/echoer"}, Env: map[string]string{"ECHOER_ONLY": "BAR"}, }, "env": { Args: []string{"sh", "-c", "env; while true; do sleep 60; done"}, Env: map[string]string{"ENV_ONLY": "BAZ"}, }, }, } client := s.controllerClient(t) t.Assert(client.CreateRelease(release), c.IsNil) t.Assert(client.SetAppRelease(app.id, release.ID), c.IsNil) updateFile := filepath.Join(t.MkDir(), "updates.json") updateJSON := []byte(`{ "processes": { "echoer": { "env": {"ECHOER_ONLY": "BAT"} }, "env": { "env": {"ENV_UPDATE": "QUUX"} } } }`) t.Assert(ioutil.WriteFile(updateFile, updateJSON, 0644), c.IsNil) t.Assert(app.flynn("release", "update", updateFile), Succeeds) resultJSON := []byte(`{ "env": {"GLOBAL": "FOO"}, "processes": { "echoer": { "args": ["/bin/echoer"], "env": { "ECHOER_ONLY": "BAT" } }, "env": { "args": ["sh", "-c", "env; while true; do sleep 60; done"], "env": { "ENV_ONLY": "BAZ", "ENV_UPDATE": "QUUX" } } } }`) result := &ct.Release{} t.Assert(json.Unmarshal(resultJSON, &result), c.IsNil) for typ, proc := range result.Processes { resource.SetDefaults(&proc.Resources) result.Processes[typ] = proc } release, err := s.controller.GetAppRelease(app.name) t.Assert(err, c.IsNil) t.Assert(release.Env, c.DeepEquals, result.Env) t.Assert(release.Processes, c.DeepEquals, result.Processes) scaleCmd := app.flynn("scale", "--no-wait", "env=1", "foo=1") t.Assert(scaleCmd, c.Not(Succeeds)) t.Assert(scaleCmd, OutputContains, "ERROR: unknown process types: \"foo\"") // create a job watcher for the new release watcher, err := client.WatchJobEvents(app.name, release.ID) t.Assert(err, c.IsNil) defer watcher.Close() scaleCmd = app.flynn("scale", "--no-wait", "env=1") t.Assert(watcher.WaitFor(ct.JobEvents{"env": {ct.JobStateUp: 1}}, scaleTimeout, nil), c.IsNil) envLog := app.flynn("log") t.Assert(envLog, Succeeds) t.Assert(envLog, SuccessfulOutputContains, "GLOBAL=FOO") t.Assert(envLog, SuccessfulOutputContains, "ENV_ONLY=BAZ") t.Assert(envLog, SuccessfulOutputContains, "ENV_UPDATE=QUUX") t.Assert(envLog, c.Not(SuccessfulOutputContains), "ECHOER_ONLY=BAR") t.Assert(envLog, c.Not(SuccessfulOutputContains), "ECHOER_UPDATE=BAT") }
func (s *CLISuite) TestCreateAppNoGit(t *c.C) { dir := t.MkDir() name := random.String(30) t.Assert(flynn(t, dir, "create", name), Outputs, fmt.Sprintf("Created %s\n", name)) }
func (s *CLISuite) TestDockerExportImport(t *c.C) { // release via docker-receive client := s.controllerClient(t) app := &ct.App{Name: "cli-test-docker-export"} t.Assert(client.CreateApp(app), c.IsNil) repo := "cli-test-export" s.buildDockerImage(t, repo, `CMD ["/bin/pingserv"]`) t.Assert(flynn(t, "/", "-a", app.Name, "docker", "push", repo), Succeeds) t.Assert(flynn(t, "/", "-a", app.Name, "scale", "app=1"), Succeeds) defer flynn(t, "/", "-a", app.Name, "scale", "app=0") // grab the Flynn image layers release, err := client.GetAppRelease(app.ID) t.Assert(err, c.IsNil) artifact, err := client.GetArtifact(release.ArtifactIDs[0]) t.Assert(err, c.IsNil) layers := artifact.Manifest().Rootfs[0].Layers layerNames := make([]string, len(layers)) for i, layer := range layers { layerNames[i] = layer.ID + ".layer" } // check exporting to stdout works file := filepath.Join(t.MkDir(), "export.tar") cmd := exec.Command("sh", "-c", fmt.Sprintf("%s -a %s export > %s", args.CLI, app.Name, file)) cmd.Env = flynnEnv(flynnrc) var stderr bytes.Buffer cmd.Stderr = &stderr if args.Stream { cmd.Stderr = io.MultiWriter(os.Stderr, &stderr) } if err := cmd.Run(); err != nil { t.Fatalf("error exporting docker app to stdout: %s: %s", err, stderr.String()) } exportFiles := append([]string{ "app.json", "routes.json", "release.json", "artifacts.json", }, append(layerNames, "formation.json")...) assertExportContains(t, file, exportFiles...) // export the app directly to the file t.Assert(flynn(t, "/", "-a", app.Name, "export", "-f", file), Succeeds) assertExportContains(t, file, exportFiles...) // delete the image from the registry u, err := url.Parse(s.clusterConf(t).DockerPushURL) t.Assert(err, c.IsNil) uri := fmt.Sprintf("http://%s/v2/%s/manifests/%s", u.Host, app.Name, artifact.Meta["docker-receive.digest"]) req, err := http.NewRequest("DELETE", uri, nil) req.SetBasicAuth("", s.clusterConf(t).Key) t.Assert(err, c.IsNil) res, err := http.DefaultClient.Do(req) t.Assert(err, c.IsNil) res.Body.Close() // import to another app importApp := "cli-test-docker-import" t.Assert(flynn(t, "/", "import", "--name", importApp, "--file", file), Succeeds) defer flynn(t, "/", "-a", importApp, "scale", "app=0") // wait for it to start _, err = s.discoverdClient(t).Instances(importApp+"-web", 10*time.Second) t.Assert(err, c.IsNil) }
func (s *CLISuite) TestSlugReleaseGarbageCollection(t *c.C) { client := s.controllerClient(t) // create app with gc.max_inactive_slug_releases=3 maxInactiveSlugReleases := 3 app := &ct.App{Meta: map[string]string{"gc.max_inactive_slug_releases": strconv.Itoa(maxInactiveSlugReleases)}} t.Assert(client.CreateApp(app), c.IsNil) // create an image artifact imageArtifact := s.createArtifact(t, "test-apps") // create 5 slug artifacts tmp, err := ioutil.TempFile("", "squashfs-") t.Assert(err, c.IsNil) defer os.Remove(tmp.Name()) defer tmp.Close() t.Assert(exec.Command("mksquashfs", t.MkDir(), tmp.Name(), "-noappend").Run(), c.IsNil) slug, err := ioutil.ReadAll(tmp) t.Assert(err, c.IsNil) slugHash := sha512.Sum512(slug) slugs := []string{ "http://blobstore.discoverd/layer/1.squashfs", "http://blobstore.discoverd/layer/2.squashfs", "http://blobstore.discoverd/layer/3.squashfs", "http://blobstore.discoverd/layer/4.squashfs", "http://blobstore.discoverd/layer/5.squashfs", } slugArtifacts := make([]*ct.Artifact, len(slugs)) put := func(url string, data []byte) { req, err := http.NewRequest("PUT", url, bytes.NewReader(data)) t.Assert(err, c.IsNil) res, err := http.DefaultClient.Do(req) t.Assert(err, c.IsNil) res.Body.Close() t.Assert(res.StatusCode, c.Equals, http.StatusOK) } for i, layerURL := range slugs { manifest := &ct.ImageManifest{ Type: ct.ImageManifestTypeV1, Rootfs: []*ct.ImageRootfs{{ Layers: []*ct.ImageLayer{{ ID: strconv.Itoa(i + 1), Type: ct.ImageLayerTypeSquashfs, Length: int64(len(slug)), Hashes: map[string]string{"sha512": hex.EncodeToString(slugHash[:])}, }}, }}, } data := manifest.RawManifest() url := fmt.Sprintf("http://blobstore.discoverd/image/%s.json", manifest.ID()) put(url, data) put(layerURL, slug) artifact := &ct.Artifact{ Type: ct.ArtifactTypeFlynn, URI: url, Meta: map[string]string{"blobstore": "true"}, RawManifest: data, Hashes: manifest.Hashes(), Size: int64(len(data)), LayerURLTemplate: "http://blobstore.discoverd/layer/{id}.squashfs", } t.Assert(client.CreateArtifact(artifact), c.IsNil) slugArtifacts[i] = artifact } // create 6 releases, the second being scaled up and having the // same slug as the third (so prevents the slug being deleted) releases := make([]*ct.Release, 6) for i, r := range []struct { slug *ct.Artifact active bool }{ {slugArtifacts[0], false}, {slugArtifacts[1], true}, {slugArtifacts[1], false}, {slugArtifacts[2], false}, {slugArtifacts[3], false}, {slugArtifacts[4], false}, } { release := &ct.Release{ ArtifactIDs: []string{imageArtifact.ID, r.slug.ID}, Processes: map[string]ct.ProcessType{ "app": {Args: []string{"/bin/pingserv"}, Ports: []ct.Port{{Proto: "tcp"}}}, }, Meta: map[string]string{"git": "true"}, } t.Assert(client.CreateRelease(release), c.IsNil) procs := map[string]int{"app": 0} if r.active { procs["app"] = 1 } t.Assert(client.PutFormation(&ct.Formation{ AppID: app.ID, ReleaseID: release.ID, Processes: procs, }), c.IsNil) releases[i] = release } // scale the last release so we can deploy it lastRelease := releases[len(releases)-1] watcher, err := client.WatchJobEvents(app.ID, lastRelease.ID) t.Assert(err, c.IsNil) defer watcher.Close() t.Assert(client.PutFormation(&ct.Formation{ AppID: app.ID, ReleaseID: lastRelease.ID, Processes: map[string]int{"app": 1}, }), c.IsNil) t.Assert(watcher.WaitFor(ct.JobEvents{"app": ct.JobUpEvents(1)}, scaleTimeout, nil), c.IsNil) t.Assert(client.SetAppRelease(app.ID, lastRelease.ID), c.IsNil) // subscribe to garbage collection events gcEvents := make(chan *ct.Event) stream, err := client.StreamEvents(ct.StreamEventsOptions{ AppID: app.ID, ObjectTypes: []ct.EventType{ct.EventTypeAppGarbageCollection}, }, gcEvents) t.Assert(err, c.IsNil) defer stream.Close() // deploy a new release with the same slug as the last release timeoutCh := make(chan struct{}) time.AfterFunc(5*time.Minute, func() { close(timeoutCh) }) newRelease := *lastRelease newRelease.ID = "" t.Assert(client.CreateRelease(&newRelease), c.IsNil) t.Assert(client.DeployAppRelease(app.ID, newRelease.ID, timeoutCh), c.IsNil) // wait for garbage collection select { case event, ok := <-gcEvents: if !ok { t.Fatalf("event stream closed unexpectedly: %s", stream.Err()) } var e ct.AppGarbageCollectionEvent t.Assert(json.Unmarshal(event.Data, &e), c.IsNil) if e.Error != "" { t.Fatalf("garbage collection failed: %s", e.Error) } case <-time.After(60 * time.Second): t.Fatal("timed out waiting for garbage collection") } // check we have 4 distinct slug releases (so 5 in total, only 3 are // inactive) list, err := client.AppReleaseList(app.ID) t.Assert(err, c.IsNil) t.Assert(list, c.HasLen, maxInactiveSlugReleases+2) distinctSlugs := make(map[string]struct{}, len(list)) for _, release := range list { t.Assert(release.ArtifactIDs, c.HasLen, 2) distinctSlugs[release.ArtifactIDs[1]] = struct{}{} } t.Assert(distinctSlugs, c.HasLen, maxInactiveSlugReleases+1) // check the first and third releases got deleted, but the rest remain assertDeleted := func(release *ct.Release, deleted bool) { _, err := client.GetRelease(release.ID) if deleted { t.Assert(err, c.Equals, controller.ErrNotFound) } else { t.Assert(err, c.IsNil) } } assertDeleted(releases[0], true) assertDeleted(releases[1], false) assertDeleted(releases[2], true) assertDeleted(releases[3], false) assertDeleted(releases[4], false) assertDeleted(releases[5], false) assertDeleted(&newRelease, false) // check the first slug got deleted, but the rest remain s.assertURI(t, slugs[0], http.StatusNotFound) for i := 1; i < len(slugs); i++ { s.assertURI(t, slugs[i], http.StatusOK) } }
func (s *HostSuite) TestUpdate(t *c.C) { dir := t.MkDir() flynnHost := filepath.Join(dir, "flynn-host") run(t, osexec.Command("cp", args.FlynnHost, flynnHost)) // start flynn-host id := random.String(8) var out bytes.Buffer cmd := osexec.Command( flynnHost, "daemon", "--http-port", "11113", "--state", filepath.Join(dir, "host-state.bolt"), "--id", id, "--backend", "mock", "--vol-provider", "mock", "--volpath", filepath.Join(dir, "volumes"), "--log-dir", filepath.Join(dir, "logs"), ) cmd.Stdout = &out cmd.Stderr = &out defer func() { debug(t, "*** flynn-host output ***") debug(t, out.String()) debug(t, "*************************") }() t.Assert(cmd.Start(), c.IsNil) defer cmd.Process.Kill() httpClient := &http.Client{Transport: &http.Transport{Dial: dialer.Retry.Dial}} client := cluster.NewHost(id, "http://127.0.0.1:11113", httpClient, nil) // exec a program which exits straight away _, err := client.Update("/bin/true") t.Assert(err, c.NotNil) status, err := client.GetStatus() t.Assert(err, c.IsNil) t.Assert(status.ID, c.Equals, id) t.Assert(status.PID, c.Equals, cmd.Process.Pid) // exec a program which reads the control socket but then exits _, err = client.Update("/bin/bash", "-c", "<&4; exit") t.Assert(err, c.NotNil) status, err = client.GetStatus() t.Assert(err, c.IsNil) t.Assert(status.ID, c.Equals, id) t.Assert(status.PID, c.Equals, cmd.Process.Pid) // exec flynn-host and check we get the status from the new daemon pid, err := client.Update( flynnHost, "daemon", "--http-port", "11113", "--state", filepath.Join(dir, "host-state.bolt"), "--id", id, "--backend", "mock", "--vol-provider", "mock", "--volpath", filepath.Join(dir, "volumes"), "--log-dir", filepath.Join(dir, "logs"), ) t.Assert(err, c.IsNil) defer syscall.Kill(pid, syscall.SIGKILL) done := make(chan struct{}) go func() { cmd.Process.Signal(syscall.SIGTERM) syscall.Wait4(cmd.Process.Pid, nil, 0, nil) close(done) }() select { case <-done: case <-time.After(15 * time.Second): t.Fatal("timed out waiting for flynn-host daemon to exit") } // client.GetStatus intermittently returns io.EOF right after the update. We // don't currently understand why (likely due to the way the listener is // passed around), so for now just retry the request. // // TODO(lmars): figure out why and remove this loop. delay := 100 * time.Millisecond for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(delay) { status, err = client.GetStatus() if e, ok := err.(*url.Error); ok && strings.Contains(e.Err.Error(), "EOF") { debugf(t, "got io.EOF from flynn-host, trying again in %s", delay) continue } break } t.Assert(err, c.IsNil) t.Assert(status.ID, c.Equals, id) t.Assert(status.PID, c.Equals, pid) }
func (s *ZZBackupSuite) testClusterBackup(t *c.C, index int, path string) { debugf(t, "restoring cluster backup %s", filepath.Base(path)) // boot the cluster using an RFC 5737 TEST-NET IP, avoiding conflicts // with those used by script/bootstrap-flynn so the test can be run in // development ip := fmt.Sprintf("192.0.2.%d", index+100) device := fmt.Sprintf("eth0:%d", index+10) t.Assert(run(t, exec.Command("sudo", "ifconfig", device, ip)), Succeeds) dir := t.MkDir() debugf(t, "using tempdir %s", dir) debug(t, "starting flynn-host") cmd := exec.Command( "sudo", "../host/bin/flynn-host", "daemon", "--id", fmt.Sprintf("backup%d", index), "--external-ip", ip, "--listen-ip", ip, "--bridge-name", fmt.Sprintf("backupbr%d", index), "--state", filepath.Join(dir, "host-state.bolt"), "--volpath", filepath.Join(dir, "volumes"), "--log-dir", filepath.Join(dir, "logs"), "--flynn-init", "../host/bin/flynn-init", ) out, err := os.Create(filepath.Join(dir, "flynn-host.log")) t.Assert(err, c.IsNil) defer out.Close() cmd.Stdout = out cmd.Stderr = out t.Assert(cmd.Start(), c.IsNil) go cmd.Process.Wait() defer func() { // collect-debug-info if the tests failed then kill flynn-host if t.Failed() { cmd := exec.Command( "sudo", "-E", "../host/bin/flynn-host", "collect-debug-info", "--log-dir", filepath.Join(dir, "logs"), ) cmd.Env = []string{fmt.Sprintf("DISCOVERD=%s:1111", ip)} cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Run() } exec.Command("sudo", "kill", strconv.Itoa(cmd.Process.Pid)).Run() }() debugf(t, "bootstrapping flynn from backup") cmd = exec.Command( "../host/bin/flynn-host", "bootstrap", "--peer-ips", ip, "--from-backup", path, "../bootstrap/bin/manifest.json", ) cmd.Env = []string{ "CLUSTER_DOMAIN=1.localflynn.com", fmt.Sprintf("DISCOVERD=%s:1111", ip), fmt.Sprintf("FLANNEL_NETWORK=100.%d.0.0/16", index+101), } logR, logW := io.Pipe() defer logW.Close() go func() { buf := bufio.NewReader(logR) for { line, err := buf.ReadString('\n') if err != nil { return } debug(t, line[0:len(line)-1]) } }() cmd.Stdout = logW cmd.Stderr = logW t.Assert(cmd.Run(), c.IsNil) debug(t, "waiting for nodejs-web service") disc := discoverd.NewClientWithURL(fmt.Sprintf("http://%s:1111", ip)) _, err = disc.Instances("nodejs-web", 30*time.Second) t.Assert(err, c.IsNil) debug(t, "checking HTTP requests") req, err := http.NewRequest("GET", "http://"+ip, nil) t.Assert(err, c.IsNil) req.Host = "nodejs.1.localflynn.com" var res *http.Response // try multiple times in case we get a 503 from the router as it has // not seen the service yet err = attempt.Strategy{Total: 10 * time.Second, Delay: 100 * time.Millisecond}.Run(func() (err error) { res, err = http.DefaultClient.Do(req) if err != nil { return err } else if res.StatusCode == http.StatusServiceUnavailable { return errors.New("router returned 503") } return nil }) t.Assert(err, c.IsNil) t.Assert(res.StatusCode, c.Equals, http.StatusOK) debug(t, "getting app release") controllerInstances, err := disc.Instances("controller", 30*time.Second) t.Assert(err, c.IsNil) controllerURL := "http://" + controllerInstances[0].Addr controllerKey := controllerInstances[0].Meta["AUTH_KEY"] client, err := controller.NewClient(controllerURL, controllerKey) t.Assert(err, c.IsNil) release, err := client.GetAppRelease("nodejs") t.Assert(err, c.IsNil) debug(t, "configuring flynn CLI") flynnrc := filepath.Join(dir, ".flynnrc") conf := &config.Config{} t.Assert(conf.Add(&config.Cluster{ Name: "default", ControllerURL: controllerURL, Key: controllerKey, }, true), c.IsNil) t.Assert(conf.SaveTo(flynnrc), c.IsNil) flynn := func(cmdArgs ...string) *CmdResult { cmd := exec.Command(args.CLI, cmdArgs...) cmd.Env = flynnEnv(flynnrc) cmd.Env = append(cmd.Env, "FLYNN_APP=nodejs") return run(t, cmd) } if _, ok := release.Env["FLYNN_REDIS"]; ok { debug(t, "checking redis resource") // try multiple times as the Redis resource is not guaranteed to be up yet var redisResult *CmdResult err = attempt.Strategy{Total: 10 * time.Second, Delay: 100 * time.Millisecond}.Run(func() error { redisResult = flynn("redis", "redis-cli", "--", "PING") return redisResult.Err }) t.Assert(err, c.IsNil) t.Assert(redisResult, SuccessfulOutputContains, "PONG") } debug(t, "checking mysql resource") if _, ok := release.Env["FLYNN_MYSQL"]; ok { t.Assert(flynn("mysql", "console", "--", "-e", "SELECT * FROM foos"), SuccessfulOutputContains, "foobar") } else { t.Assert(flynn("resource", "add", "mysql"), Succeeds) } debug(t, "checking mongodb resource") if _, ok := release.Env["FLYNN_MONGO"]; ok { t.Assert(flynn("mongodb", "mongo", "--", "--eval", "db.foos.find()"), SuccessfulOutputContains, "foobar") } else { t.Assert(flynn("resource", "add", "mongodb"), Succeeds) } debug(t, "checking dashboard STATUS_KEY matches status AUTH_KEY") dashboardStatusKeyResult := flynn("-a", "dashboard", "env", "get", "STATUS_KEY") t.Assert(dashboardStatusKeyResult, Succeeds) statusAuthKeyResult := flynn("-a", "status", "env", "get", "AUTH_KEY") t.Assert(statusAuthKeyResult, Succeeds) t.Assert(dashboardStatusKeyResult.Output, c.Equals, statusAuthKeyResult.Output) }