func (s *CLISuite) TestReleaseRollback(t *c.C) { // create an app and release it r := s.newGitRepo(t, "http") app := "release-rollback-" + random.String(8) t.Assert(r.flynn("create", app), Succeeds) t.Assert(r.git("push", "flynn", "master"), Succeeds) // check that rollback fails when there's only a single release res := r.flynn("release", "rollback", "--yes") t.Assert(res, c.Not(Succeeds)) // create a second release t.Assert(r.git("commit", "--allow-empty", "--message", "empty commit"), Succeeds) t.Assert(r.git("push", "flynn", "master"), Succeeds) // get the releases client := s.controllerClient(t) releases, err := client.AppReleaseList(app) t.Assert(err, c.IsNil) t.Assert(releases, c.HasLen, 2) // rollback to the second release res = r.flynn("release", "rollback", "--yes") t.Assert(res, Succeeds) // revert rollback res = r.flynn("release", "rollback", "--yes", releases[0].ID) t.Assert(res, Succeeds) // check that attempting to rollback to the current release fails res = r.flynn("release", "rollback", "--yes", releases[0].ID) t.Assert(res, c.Not(Succeeds)) }
func (s *CLISuite) TestReleaseDelete(t *c.C) { // create an app and release it twice r := s.newGitRepo(t, "http") app := "release-delete-" + random.String(8) t.Assert(r.flynn("create", app), Succeeds) t.Assert(r.git("push", "flynn", "master"), Succeeds) t.Assert(r.git("commit", "--allow-empty", "--message", "empty commit"), Succeeds) t.Assert(r.git("push", "flynn", "master"), Succeeds) // get the releases client := s.controllerClient(t) releases, err := client.AppReleaseList(app) t.Assert(err, c.IsNil) t.Assert(releases, c.HasLen, 2) // check the current release cannot be deleted res := r.flynn("release", "delete", "--yes", releases[0].ID) t.Assert(res, c.Not(Succeeds)) t.Assert(res.Output, c.Equals, "validation_error: cannot delete current app release\n") // associate the initial release with another app otherApp := &ct.App{Name: "release-delete-" + random.String(8)} t.Assert(client.CreateApp(otherApp), c.IsNil) t.Assert(client.PutFormation(&ct.Formation{AppID: otherApp.ID, ReleaseID: releases[1].ID}), c.IsNil) // check deleting the initial release just deletes the formation res = r.flynn("release", "delete", "--yes", releases[1].ID) t.Assert(res, Succeeds) t.Assert(res.Output, c.Equals, "Release scaled down for app but not fully deleted (still associated with 1 other apps)\n") // check the slug artifact still exists slugArtifact, err := client.GetArtifact(releases[1].ArtifactIDs[1]) t.Assert(err, c.IsNil) s.assertURI(t, slugArtifact.URI, http.StatusOK) slugLayerURL := slugArtifact.LayerURL(slugArtifact.Manifest().Rootfs[0].Layers[0]) s.assertURI(t, slugLayerURL, http.StatusOK) // check the inital release can now be deleted res = r.flynn("-a", otherApp.ID, "release", "delete", "--yes", releases[1].ID) t.Assert(res, Succeeds) t.Assert(res.Output, c.Equals, fmt.Sprintf("Deleted release %s (deleted 2 files)\n", releases[1].ID)) // check the slug artifact was deleted _, err = client.GetArtifact(slugArtifact.ID) t.Assert(err, c.Equals, controller.ErrNotFound) s.assertURI(t, slugArtifact.URI, http.StatusNotFound) s.assertURI(t, slugLayerURL, http.StatusNotFound) // check the image artifact was not deleted (since it is shared between both releases) _, err = client.GetArtifact(releases[1].ArtifactIDs[0]) 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 *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 := &ct.Artifact{Type: host.ArtifactTypeDocker, URI: imageURIs["test-apps"]} t.Assert(client.CreateArtifact(imageArtifact), c.IsNil) // create 5 slug artifacts var slug bytes.Buffer gz := gzip.NewWriter(&slug) t.Assert(tar.NewWriter(gz).Close(), c.IsNil) t.Assert(gz.Close(), c.IsNil) slugs := []string{ "http://blobstore.discoverd/1/slug.tgz", "http://blobstore.discoverd/2/slug.tgz", "http://blobstore.discoverd/3/slug.tgz", "http://blobstore.discoverd/4/slug.tgz", "http://blobstore.discoverd/5/slug.tgz", } slugArtifacts := make([]*ct.Artifact, len(slugs)) for i, uri := range slugs { req, err := http.NewRequest("PUT", uri, bytes.NewReader(slug.Bytes())) 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) artifact := &ct.Artifact{ Type: host.ArtifactTypeFile, URI: uri, Meta: map[string]string{"blobstore": "true"}, } 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"}}}, }, } 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 { files := release.FileArtifactIDs() t.Assert(files, c.HasLen, 1) distinctSlugs[files[0]] = 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 *ControllerSuite) TestAppDeleteCleanup(t *c.C) { app := "app-delete-cleanup-" + random.String(8) client := s.controllerClient(t) // create and push app r := s.newGitRepo(t, "http") t.Assert(r.flynn("create", app), Succeeds) t.Assert(r.git("push", "flynn", "master"), Succeeds) // wait for it to start service := app + "-web" _, err := s.discoverdClient(t).Instances(service, 10*time.Second) t.Assert(err, c.IsNil) t.Assert(r.flynn("scale", "another-web=1"), Succeeds) _, err = s.discoverdClient(t).Instances(app+"-another-web", 10*time.Second) t.Assert(err, c.IsNil) // create some routes routes := []string{"foo.example.com", "bar.example.com", "another.example.com"} for _, route := range routes { if route == "another.example.com" { t.Assert(r.flynn("route", "add", "http", "-s", app+"-another-web", route), Succeeds) } else { t.Assert(r.flynn("route", "add", "http", route), Succeeds) } } routeList, err := client.RouteList(app) t.Assert(err, c.IsNil) numRoutes := len(routes) + 1 // includes default app route t.Assert(routeList, c.HasLen, numRoutes) assertRouteStatus := func(route string, status int) { req, err := http.NewRequest("GET", "http://"+routerIP, nil) t.Assert(err, c.IsNil) req.Host = route res, err := http.DefaultClient.Do(req) t.Assert(err, c.IsNil) t.Assert(res.StatusCode, c.Equals, status) } for _, route := range routes { assertRouteStatus(route, 200) } // provision resources t.Assert(r.flynn("resource", "add", "postgres"), Succeeds) resources, err := client.AppResourceList(app) t.Assert(err, c.IsNil) numResources := 1 t.Assert(resources, c.HasLen, numResources) // create another release t.Assert(r.git("commit", "--allow-empty", "--message", "deploy"), Succeeds) t.Assert(r.git("push", "flynn", "master"), Succeeds) releases, err := client.AppReleaseList(app) t.Assert(err, c.IsNil) // delete app cmd := r.flynn("delete", "--yes") t.Assert(cmd, Succeeds) // check route cleanup t.Assert(cmd, OutputContains, fmt.Sprintf("removed %d routes", numRoutes)) for _, route := range routes { assertRouteStatus(route, 404) } // check release cleanup t.Assert(cmd, OutputContains, fmt.Sprintf("deleted %d releases", len(releases))) for _, release := range releases { _, err := client.GetRelease(release.ID) t.Assert(err, c.Equals, controller.ErrNotFound) } // check resource cleanup t.Assert(cmd, OutputContains, fmt.Sprintf("deprovisioned %d resources", numResources)) // check creating and pushing same app name succeeds t.Assert(os.RemoveAll(r.dir), c.IsNil) r = s.newGitRepo(t, "http") t.Assert(r.flynn("create", app), Succeeds) t.Assert(r.git("push", "flynn", "master"), Succeeds) }