Milo: Store console defs as their own entities

BUG=468053

Review-Url: https://codereview.chromium.org/2982183002
diff --git a/milo/api/config/project.pb.go b/milo/api/config/project.pb.go
index 4498728..8361154 100644
--- a/milo/api/config/project.pb.go
+++ b/milo/api/config/project.pb.go
@@ -68,8 +68,9 @@
 	Name string `protobuf:"bytes,2,opt,name=Name" json:"Name,omitempty"`
 	// RepoURL is the URL of the git repository to display as the rows of the console.
 	RepoURL string `protobuf:"bytes,3,opt,name=RepoURL" json:"RepoURL,omitempty"`
-	// Branch is the branch to pull commits from when displaying the console.
-	Branch string `protobuf:"bytes,4,opt,name=Branch" json:"Branch,omitempty"`
+	// Ref is the ref to pull commits from when displaying the console.
+	// Eg. refs/heads/master
+	Ref string `protobuf:"bytes,4,opt,name=Ref" json:"Ref,omitempty"`
 	// ManifestName is the name of the manifest the waterfall looks at.
 	// By convention, Manifest Names can be:
 	// * UNPATCHED - For non patched builds, such as continuous builds
@@ -105,9 +106,9 @@
 	return ""
 }
 
-func (m *Console) GetBranch() string {
+func (m *Console) GetRef() string {
 	if m != nil {
-		return m.Branch
+		return m.Ref
 	}
 	return ""
 }
@@ -177,22 +178,22 @@
 }
 
 var fileDescriptor0 = []byte{
-	// 263 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0xd1, 0x4a, 0xc3, 0x30,
-	0x14, 0x86, 0x69, 0x37, 0xdb, 0xed, 0x28, 0x0a, 0xb9, 0x90, 0x20, 0x5e, 0x8c, 0x5e, 0x0d, 0xc4,
-	0x06, 0x14, 0x5f, 0x60, 0x1b, 0xc2, 0x40, 0x45, 0x22, 0xe2, 0x75, 0x16, 0xb3, 0x36, 0xd2, 0xf6,
-	0x94, 0x34, 0xbd, 0xf0, 0xad, 0x7c, 0x44, 0x31, 0x89, 0xd5, 0xb2, 0x9b, 0x72, 0xfe, 0xef, 0x3f,
-	0x3d, 0x7c, 0x04, 0xee, 0x0a, 0x6d, 0xcb, 0x7e, 0x97, 0x4b, 0xac, 0x59, 0xd5, 0x4b, 0xed, 0x3e,
-	0xd7, 0x05, 0xb2, 0x5a, 0x57, 0xc8, 0x44, 0xab, 0x99, 0xc4, 0x66, 0xaf, 0x0b, 0xd6, 0x1a, 0xfc,
-	0x50, 0xd2, 0xe6, 0xad, 0x41, 0x8b, 0x24, 0xf1, 0x34, 0xbb, 0x87, 0xf4, 0xd9, 0x17, 0xe4, 0x14,
-	0xe2, 0xed, 0x86, 0x46, 0x8b, 0x68, 0x39, 0xe7, 0xf1, 0x76, 0x43, 0xae, 0x60, 0xb6, 0xc6, 0xa6,
-	0xc3, 0x4a, 0x75, 0x34, 0x5e, 0x4c, 0x96, 0xc7, 0x37, 0x67, 0xb9, 0xff, 0x2b, 0x0f, 0x9c, 0x0f,
-	0x0b, 0xd9, 0x57, 0x04, 0x69, 0x08, 0x07, 0x87, 0x08, 0x4c, 0x9f, 0x44, 0xad, 0x68, 0xec, 0x88,
-	0x9b, 0x09, 0x85, 0x94, 0xab, 0x16, 0x5f, 0xf9, 0x03, 0x9d, 0x38, 0xfc, 0x1b, 0xc9, 0x39, 0x24,
-	0x2b, 0x23, 0x1a, 0x59, 0xd2, 0xa9, 0x2b, 0x42, 0x22, 0x19, 0x9c, 0x3c, 0x8a, 0x46, 0xef, 0x55,
-	0x67, 0xdd, 0xb5, 0x23, 0xd7, 0x8e, 0xd8, 0x8f, 0xf2, 0xaa, 0xd7, 0xd5, 0xbb, 0x32, 0x1d, 0x4d,
-	0xc6, 0xca, 0x81, 0xf3, 0x61, 0x21, 0x7b, 0x83, 0x34, 0xcc, 0x83, 0x61, 0xf4, 0xcf, 0xf0, 0x02,
-	0x66, 0x6b, 0x61, 0x55, 0x81, 0xe6, 0x33, 0x98, 0x0f, 0x99, 0x5c, 0xc2, 0xfc, 0xa5, 0x44, 0xe3,
-	0x45, 0xbc, 0xff, 0x1f, 0xd8, 0x25, 0xee, 0x89, 0x6f, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0xfb,
-	0x59, 0x08, 0x73, 0x9b, 0x01, 0x00, 0x00,
+	// 260 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0xc1, 0x4a, 0xc3, 0x40,
+	0x10, 0x86, 0x49, 0x52, 0x93, 0x76, 0x14, 0x95, 0x3d, 0x2d, 0xe2, 0xa1, 0xe4, 0x54, 0x10, 0xb3,
+	0xa0, 0xf8, 0x02, 0xb6, 0x08, 0x05, 0x15, 0x59, 0x11, 0xcf, 0x69, 0xdc, 0xa4, 0x2b, 0x49, 0x26,
+	0x6c, 0x36, 0x07, 0x5f, 0xc9, 0xa7, 0x94, 0xce, 0xae, 0xd1, 0xd2, 0x4b, 0xf8, 0xff, 0x6f, 0x26,
+	0xc3, 0xc7, 0xc2, 0x5d, 0xa5, 0xed, 0x76, 0xd8, 0x64, 0x05, 0x36, 0xa2, 0x1e, 0x0a, 0x4d, 0x9f,
+	0xeb, 0x0a, 0x45, 0xa3, 0x6b, 0x14, 0x79, 0xa7, 0x45, 0x81, 0x6d, 0xa9, 0x2b, 0xd1, 0x19, 0xfc,
+	0x54, 0x85, 0xcd, 0x3a, 0x83, 0x16, 0x59, 0xec, 0x68, 0xfa, 0x00, 0xc9, 0x8b, 0x1b, 0xb0, 0x53,
+	0x08, 0xd7, 0x2b, 0x1e, 0xcc, 0x83, 0xc5, 0x4c, 0x86, 0xeb, 0x15, 0xbb, 0x82, 0xe9, 0x12, 0xdb,
+	0x1e, 0x6b, 0xd5, 0xf3, 0x70, 0x1e, 0x2d, 0x8e, 0x6f, 0xce, 0x32, 0xf7, 0x57, 0xe6, 0xb9, 0x1c,
+	0x17, 0xd2, 0xef, 0x00, 0x12, 0x5f, 0x0e, 0x0e, 0x31, 0x98, 0x3c, 0xe7, 0x8d, 0xe2, 0x21, 0x11,
+	0xca, 0x8c, 0x43, 0x22, 0x55, 0x87, 0x6f, 0xf2, 0x91, 0x47, 0x84, 0x7f, 0x2b, 0x3b, 0x87, 0x48,
+	0xaa, 0x92, 0x4f, 0x88, 0xee, 0x22, 0x4b, 0xe1, 0xe4, 0x29, 0x6f, 0x75, 0xa9, 0x7a, 0x4b, 0x77,
+	0x8e, 0x68, 0xb4, 0xc7, 0x76, 0xb2, 0xf7, 0x83, 0xae, 0x3f, 0x94, 0xe9, 0x79, 0xbc, 0x2f, 0xeb,
+	0xb9, 0x1c, 0x17, 0xd2, 0x77, 0x48, 0x7c, 0x1e, 0xdd, 0x82, 0x7f, 0x6e, 0x17, 0x30, 0x5d, 0xe6,
+	0x56, 0x55, 0x68, 0xbe, 0xbc, 0xf3, 0xd8, 0xd9, 0x25, 0xcc, 0x5e, 0xb7, 0x68, 0x9c, 0x88, 0x33,
+	0xff, 0x03, 0x9b, 0x98, 0x1e, 0xf7, 0xf6, 0x27, 0x00, 0x00, 0xff, 0xff, 0x83, 0x36, 0x82, 0xe0,
+	0x95, 0x01, 0x00, 0x00,
 }
diff --git a/milo/api/config/project.proto b/milo/api/config/project.proto
index 163feff..aa8ad6a 100644
--- a/milo/api/config/project.proto
+++ b/milo/api/config/project.proto
@@ -28,8 +28,9 @@
   // RepoURL is the URL of the git repository to display as the rows of the console.
   string RepoURL = 3;
 
-  // Branch is the branch to pull commits from when displaying the console.
-  string Branch = 4;
+  // Ref is the ref to pull commits from when displaying the console.
+  // Eg. refs/heads/master
+  string Ref = 4;
 
   // ManifestName is the name of the manifest the waterfall looks at.
   // By convention, Manifest Names can be:
diff --git a/milo/api/resp/build.go b/milo/api/resp/build.go
index b316f18..174f328 100644
--- a/milo/api/resp/build.go
+++ b/milo/api/resp/build.go
@@ -315,7 +315,7 @@
 					Name: "REVISION",
 					ID:   []byte(rb.SourceStamp.Revision.Label),
 				})
-				consoles, err := common.GetConsolesForBuilder(c, bs.BuilderID)
+				consoles, err := common.GetAllConsoles(c, bs.BuilderID)
 				if err != nil {
 					return err
 				}
@@ -324,7 +324,7 @@
 					// definitions will specify their manifest as "REVISION", and we'll do
 					// lookups with null URL fields.
 					bs.AddManifestKey(
-						con.ProjectID, con.Console.Name, "REVISION", "", revisionBytes)
+						con.GetProjectName(), con.ID, "REVISION", "", revisionBytes)
 				}
 			}
 		}
diff --git a/milo/buildsource/buildbot/pubsub_test.go b/milo/buildsource/buildbot/pubsub_test.go
index 3952b09..f02b6f0 100644
--- a/milo/buildsource/buildbot/pubsub_test.go
+++ b/milo/buildsource/buildbot/pubsub_test.go
@@ -472,8 +472,6 @@
 			})
 			So(h.Code, ShouldEqual, 200)
 			Convey("And stores correctly", func() {
-				err := common.UpdateProjectConfigs(c)
-				So(err, ShouldBeNil)
 				c = auth.WithState(c, &authtest.FakeState{
 					Identity:       "user:alicebob@google.com",
 					IdentityGroups: []string{"googlers", "all"},
diff --git a/milo/buildsource/console.go b/milo/buildsource/console.go
index e8840c3..f38abdb 100644
--- a/milo/buildsource/console.go
+++ b/milo/buildsource/console.go
@@ -25,7 +25,7 @@
 	"github.com/luci/luci-go/common/errors"
 	"github.com/luci/luci-go/common/sync/parallel"
 
-	"github.com/luci/luci-go/milo/api/config"
+	"github.com/luci/luci-go/milo/common"
 	"github.com/luci/luci-go/milo/common/model"
 )
 
@@ -41,7 +41,7 @@
 // GetConsoleRows returns a row-oriented collection of BuildSummary
 // objects. Each row corresponds to the similarly-indexed commit in the
 // `commits` slice.
-func GetConsoleRows(c context.Context, project string, console *config.Console, commits, builders []string) ([]*ConsoleRow, error) {
+func GetConsoleRows(c context.Context, project string, console *common.Console, commits, builders []string) ([]*ConsoleRow, error) {
 	rawCommits := make([][]byte, len(commits))
 	for i, c := range commits {
 		var err error
@@ -60,7 +60,7 @@
 	if console.ManifestName == "REVISION" {
 		url = ""
 	}
-	partialKey := model.NewPartialManifestKey(project, console.Name, console.ManifestName, url)
+	partialKey := model.NewPartialManifestKey(project, console.ID, console.ManifestName, url)
 	q := datastore.NewQuery("BuildSummary").KeysOnly(true)
 	err := parallel.WorkPool(4, func(ch chan<- func() error) {
 		for i := range rawCommits {
diff --git a/milo/common/acl_test.go b/milo/common/acl_test.go
index 770e8a0..bf8e5ca 100644
--- a/milo/common/acl_test.go
+++ b/milo/common/acl_test.go
@@ -38,7 +38,7 @@
 
 		Convey("Set up projects", func() {
 			c = testconfig.WithCommonClient(c, memcfg.New(aclConfgs))
-			err := UpdateProjectConfigs(c)
+			err := UpdateConsoles(c)
 			So(err, ShouldBeNil)
 
 			Convey("Anon wants to...", func() {
diff --git a/milo/common/config.go b/milo/common/config.go
index 42097a9..dc0ef8d 100644
--- a/milo/common/config.go
+++ b/milo/common/config.go
@@ -16,6 +16,7 @@
 
 import (
 	"fmt"
+	"strings"
 	"time"
 
 	"github.com/golang/protobuf/proto"
@@ -23,24 +24,81 @@
 
 	"github.com/luci/gae/service/datastore"
 	"github.com/luci/gae/service/info"
+	configInterface "github.com/luci/luci-go/common/config"
 	"github.com/luci/luci-go/common/data/caching/proccache"
+	"github.com/luci/luci-go/common/data/stringset"
 	"github.com/luci/luci-go/common/errors"
 	"github.com/luci/luci-go/common/logging"
 	"github.com/luci/luci-go/luci_config/server/cfgclient"
 	"github.com/luci/luci-go/luci_config/server/cfgclient/backend"
-	"github.com/luci/luci-go/luci_config/server/cfgclient/textproto"
 
 	"github.com/luci/luci-go/milo/api/config"
 )
 
-// Project is a LUCI project.
-type Project struct {
-	// ID of the project, as per self defined.  This is the luci-config name.
+// Console is af datastore entity representing a single console.
+type Console struct {
+	// Parent is a key to the parent Project entity where this console was
+	// defined in.
+	Parent *datastore.Key `gae:"$parent"`
+	// ID is the ID of the console.
 	ID string `gae:"$id"`
-	// Data is the Project data in protobuf binary format.
-	Data []byte `gae:",noindex"`
-	// Revision is the latest revision we have of this project's config.
+	// RepoURL and Ref combined defines the commits the show up on the left
+	// hand side of a Console.
+	RepoURL string
+	// RepoURL and Ref combined defines the commits the show up on the left
+	// hand side of a Console.
+	Ref string
+	// ManifestName is the name of the manifest to look for when querying for
+	// builds under this console.
+	ManifestName string
+	// URL is the URL to the luci-config definition of this console.
+	URL string
+	// Revision is the luci-config reivision from when this Console was retrieved.
 	Revision string
+	// Builders is a list of universal builder IDs.
+	Builders []string
+}
+
+// GetProjectName retrieves the project name of the console out of the Console's
+// parent key.
+func (con *Console) GetProjectName() string {
+	return con.Parent.StringID()
+}
+
+// NewConsole creates a fully populated console out of the luci-config proto
+// definition of a console.
+func NewConsole(project *datastore.Key, URL, revision string, con *config.Console) *Console {
+	return &Console{
+		Parent:       project,
+		ID:           con.ID,
+		RepoURL:      con.RepoURL,
+		Ref:          con.Ref,
+		ManifestName: con.ManifestName,
+		Revision:     revision,
+		URL:          URL,
+		Builders:     BuilderFromProto(con.Builders),
+	}
+}
+
+// BuilderFromProto tranforms a luci-config proto builder format into the datastore
+// format.
+func BuilderFromProto(cb []*config.Builder) []string {
+	builders := make([]string, len(cb))
+	for i, b := range cb {
+		builders[i] = b.Name
+	}
+	return builders
+}
+
+// LuciConfigURL returns a user friendly URL that specifies where to view
+// this console definition.
+func LuciConfigURL(c context.Context, configSet, path, revision string) string {
+	// TODO(hinoka): This shouldn't be hardcoded, instead we should get the
+	// luci-config instance from the context.  But we only use this instance at
+	// the moment so it is okay for now.
+	// TODO(hinoka): The UI doesn't allow specifying paths and revision yet.  Add
+	// that in when it is supported.
+	return fmt.Sprintf("https://luci-config.appspot.com/newui#/%s", configSet)
 }
 
 // The key for the service config entity in datastore.
@@ -178,149 +236,144 @@
 	return settings, nil
 }
 
-// UpdateProjectConfigs internal project configuration based off luci-config.
-// update updates Milo's configuration based off luci config.  This includes
-// scanning through all project and extract all console configs.
-func UpdateProjectConfigs(c context.Context) error {
+// updateProjectConsoles updates all of the consoles for a given project,
+// and then returns a set of known console names.
+func updateProjectConsoles(c context.Context, projectName string, cfg *configInterface.Config) (stringset.Set, error) {
+	proj := config.Project{}
+	if err := proto.UnmarshalText(cfg.Content, &proj); err != nil {
+		return nil, errors.Annotate(err, "unmarshalling proto").Err()
+	}
+
+	// Keep a list of known consoles so we can prune deleted ones later.
+	knownConsoles := stringset.New(len(proj.Consoles))
+	// Iterate through all the proto consoles, adding and replacing the
+	// known ones if needed.
+	parentKey := datastore.MakeKey(c, "Project", projectName)
+	for _, pc := range proj.Consoles {
+		knownConsoles.Add(pc.ID)
+		con, err := GetConsole(c, projectName, pc.ID)
+		switch err {
+		case datastore.ErrNoSuchEntity:
+			// continue
+		case nil:
+			// Check if revisions match, if so just skip it.
+			if con.Revision == cfg.Revision {
+				continue
+			}
+		default:
+			return nil, errors.Annotate(err, "checking %s", pc.ID).Err()
+		}
+		URL := LuciConfigURL(c, cfg.ConfigSet, cfg.Path, cfg.Revision)
+		con = NewConsole(parentKey, URL, cfg.Revision, pc)
+		if err = datastore.Put(c, con); err != nil {
+			return nil, errors.Annotate(err, "saving %s", pc.ID).Err()
+		} else {
+			logging.Infof(c, "saved a new %s / %s (revision %s)", projectName, con.ID, cfg.Revision)
+		}
+	}
+	return knownConsoles, nil
+}
+
+// UpdateConsoles updates internal console definitions entities based off luci-config.
+func UpdateConsoles(c context.Context) error {
 	cfgName := info.AppID(c) + ".cfg"
 
-	var (
-		configs []*config.Project
-		metas   []*cfgclient.Meta
-		merr    errors.MultiError
-	)
-
 	logging.Debugf(c, "fetching configs for %s", cfgName)
-	if err := cfgclient.Projects(c, cfgclient.AsService, cfgName, textproto.Slice(&configs), &metas); err != nil {
-		merr = err.(errors.MultiError)
-		// Some configs errored out, but we can continue with the ones that work still.
+	// Acquire the raw config client.
+	lucicfg := backend.Get(c).GetConfigInterface(c, backend.AsService)
+	// Project configs for Milo contains console definitions.
+	configs, err := lucicfg.GetProjectConfigs(c, cfgName, false)
+	if err != nil {
+		return errors.Annotate(err, "while fetching project configs").Err()
 	}
+	logging.Infof(c, "got %d project configs", len(configs))
 
-	// A map of project ID to project.
-	projects := map[string]*Project{}
-	for i, proj := range configs {
-		meta := metas[i]
-		projectName, _, _ := meta.ConfigSet.SplitProject()
-		name := string(projectName)
-		projects[name] = nil
-		if merr != nil && merr[i] != nil {
-			logging.WithError(merr[i]).Warningf(c, "skipping %s due to error", name)
-			continue
+	merr := errors.MultiError{}
+	knownProjects := map[string]stringset.Set{}
+	// Iterate through each project config, extracting the console definition.
+	for _, cfg := range configs {
+		// This looks like "projects/<project name>"
+		splitPath := strings.SplitN(cfg.ConfigSet, "/", 2)
+		if len(splitPath) != 2 {
+			return fmt.Errorf("Invalid config set path %s", cfg.ConfigSet)
 		}
-
-		p := &Project{
-			ID:       name,
-			Revision: meta.Revision,
-		}
-
-		logging.Infof(c, "prossing %s", name)
-
-		var err error
-		p.Data, err = proto.Marshal(proj)
-		if err != nil {
-			logging.WithError(err).Errorf(c, "Encountered error while processing project %s", name)
-			// Set this to nil to signal this project exists but we don't want to update it.
-			projects[name] = nil
-			continue
-		}
-		projects[name] = p
-	}
-
-	// Now load all the data into the datastore.
-	projs := make([]*Project, 0, len(projects))
-	for _, proj := range projects {
-		if proj != nil {
-			projs = append(projs, proj)
+		projectName := splitPath[1]
+		knownProjects[projectName] = nil
+		if kp, err := updateProjectConsoles(c, projectName, &cfg); err != nil {
+			err = errors.Annotate(err, "processing project %s", cfg.ConfigSet).Err()
+			merr = append(merr, err)
+		} else {
+			knownProjects[projectName] = kp
 		}
 	}
-	if err := datastore.Put(c, projs); err != nil {
-		return err
+
+	// Delete all the consoles that no longer exists or are part of deleted projects.
+	toDelete := []*datastore.Key{}
+	err = datastore.Run(c, datastore.NewQuery("Console"), func(key *datastore.Key) error {
+		proj := key.Parent().StringID()
+		id := key.StringID()
+		// If this console is either:
+		// 1. In a project that no longer exists, or
+		// 2. Not in the project, then delete it.
+		knownConsoles, ok := knownProjects[proj]
+		if !ok {
+			logging.Infof(
+				c, "deleting %s/%s because the project no longer exists", proj, id)
+			toDelete = append(toDelete, key)
+			return nil
+		}
+		if knownConsoles == nil {
+			// The project exists but we couldn't check it this time.  Skip it and
+			// try again the next cron cycle.
+			return nil
+		}
+		if !knownConsoles.Has(id) {
+			logging.Infof(
+				c, "deleting %s/%s because the console no longer exists", proj, id)
+			toDelete = append(toDelete, key)
+		}
+		return nil
+	})
+	if err != nil {
+		merr = append(merr, err)
+	} else if err := datastore.Delete(c, toDelete); err != nil {
+		merr = append(merr, err)
 	}
 
-	// Delete entries that no longer exist.
-	q := datastore.NewQuery("Project").KeysOnly(true)
-	allProjs := []Project{}
-	datastore.GetAll(c, q, &allProjs)
-	toDelete := []Project{}
-	for _, proj := range allProjs {
-		if _, ok := projects[proj.ID]; !ok {
-			toDelete = append(toDelete, proj)
-		}
+	// Print some stats.
+	processedConsoles := 0
+	for _, cons := range knownProjects {
+		processedConsoles += cons.Len()
 	}
-	return datastore.Delete(c, toDelete)
+	logging.Infof(
+		c, "processed %d consoles over %d projects", len(knownProjects), processedConsoles)
+
+	if len(merr) == 0 {
+		return nil
+	}
+	return merr
 }
 
-// GetAllProjects returns all registered projects.
-func GetAllProjects(c context.Context) ([]*config.Project, error) {
-	q := datastore.NewQuery("Project")
+// GetAllConsoles returns all registered projects with the builder name.
+// If builderName is empty, then this retrieves all Consoles.
+func GetAllConsoles(c context.Context, builderName string) ([]*Console, error) {
+	q := datastore.NewQuery("Console")
+	if builderName != "" {
+		q = q.Eq("Builders", builderName)
+	}
 	q.Order("ID")
-
-	ps := []*Project{}
-	err := datastore.GetAll(c, q, &ps)
-	if err != nil {
-		return nil, err
-	}
-	results := make([]*config.Project, len(ps))
-	for i, p := range ps {
-		results[i] = &config.Project{}
-		if err := proto.Unmarshal(p.Data, results[i]); err != nil {
-			return nil, err
-		}
-	}
-	return results, nil
+	con := []*Console{}
+	err := datastore.GetAll(c, q, &con)
+	return con, err
 }
 
-// GetProject returns the requested project.
-func GetProject(c context.Context, projName string) (*config.Project, error) {
-	// Next, Try datastore
-	p := Project{ID: projName}
-	if err := datastore.Get(c, &p); err != nil {
-		return nil, err
+// GetConsole returns the requested console.
+func GetConsole(c context.Context, proj, id string) (*Console, error) {
+	// TODO(hinoka): Memcache this.
+	con := Console{
+		Parent: datastore.MakeKey(c, "Project", proj),
+		ID:     id,
 	}
-	mp := config.Project{}
-	if err := proto.Unmarshal(p.Data, &mp); err != nil {
-		return nil, err
-	}
-
-	return &mp, nil
-}
-
-// GetConsole returns the requested console instance.
-func GetConsole(c context.Context, projName, consoleName string) (*config.Console, error) {
-	p, err := GetProject(c, projName)
-	if err != nil {
-		return nil, err
-	}
-	for _, cs := range p.Consoles {
-		if cs.Name == consoleName {
-			return cs, nil
-		}
-	}
-	return nil, fmt.Errorf("Console %s not found in project %s", consoleName, projName)
-}
-
-// ProjectConsole is a simple tuple type for GetConsolesForBuilder.
-type ProjectConsole struct {
-	ProjectID string
-	Console   *config.Console
-}
-
-// GetConsolesForBuilder retrieves all the console definitions that this builder
-// belongs to.
-func GetConsolesForBuilder(c context.Context, builderName string) ([]*ProjectConsole, error) {
-	projs, err := GetAllProjects(c)
-	if err != nil {
-		return nil, err
-	}
-	ret := []*ProjectConsole{}
-	for _, p := range projs {
-		for _, con := range p.Consoles {
-			for _, b := range con.Builders {
-				if b.Name == builderName {
-					ret = append(ret, &ProjectConsole{p.ID, con})
-				}
-			}
-		}
-	}
-	return ret, nil
+	err := datastore.Get(c, &con)
+	return &con, err
 }
diff --git a/milo/common/config_test.go b/milo/common/config_test.go
index ee1863b..89260f3 100644
--- a/milo/common/config_test.go
+++ b/milo/common/config_test.go
@@ -61,19 +61,12 @@
 			_, err := UpdateServiceConfig(c)
 			So(err, ShouldBeNil)
 			// Send update here
-			err = UpdateProjectConfigs(c)
-			So(err, ShouldBeNil)
-
-			Convey("Check Project config updated", func() {
-				p, err := GetProject(c, "foo")
-				So(err, ShouldBeNil)
-				So(p.ID, ShouldEqual, "foo")
-			})
+			So(UpdateConsoles(c), ShouldBeNil)
 
 			Convey("Check Console config updated", func() {
 				cs, err := GetConsole(c, "foo", "default")
 				So(err, ShouldBeNil)
-				So(cs.Name, ShouldEqual, "default")
+				So(cs.ID, ShouldEqual, "default")
 				So(cs.RepoURL, ShouldEqual, "https://chromium.googlesource.com/foo/bar")
 			})
 		})
@@ -83,9 +76,9 @@
 var fooCfg = `
 ID: "foo"
 Consoles: {
-	Name: "default"
+	ID: "default"
 	RepoURL: "https://chromium.googlesource.com/foo/bar"
-	Branch: "master"
+	Ref: "master"
 	Builders: {
 		Name: "buildbucket/luci.foo.something/bar"
 		Category: "main|something"
diff --git a/milo/frontend/appengine/templates/pages/configs.html b/milo/frontend/appengine/templates/pages/configs.html
index 237ec9c..6592983 100644
--- a/milo/frontend/appengine/templates/pages/configs.html
+++ b/milo/frontend/appengine/templates/pages/configs.html
@@ -7,31 +7,17 @@
   <a href="{{ .Navi.SiteTitle.URL }}">{{ .Navi.SiteTitle.Label }}</a>
 </div>
 <div class="content">
-  <h1>All Projects</h1>
-  <h2>These are all of the projects and their settings as defined by luci config</h2>
-  {{ range .Projects }}
-    <h3> {{ .ID }} </h3>
-    <h4> Readers </h4>
-    <ul>
-      {{ range .Readers }}
-        <li> {{ . }} </li>
-      {{ end }}
-    </ul>
-    {{ range .Consoles }}
-      <h4> Console: {{ .Name }} </h4>
-      <p> <b>ID</b>: {{ .ID }} </p>
-      <p> <b>Repo</b>: {{ .RepoURL }} </p>
-      <p> <b>Branch</b>: {{ .Branch}} </p>
-      <ul>
-        {{ range .Builders }}
-        <li> <b>{{ .Module }}/{{ .Name }}</b>: {{ .Category }} - {{ .ShortName }} </li>
-        {{ end }}
-      </ul>
-    {{ else }}
-      <h4> No consoles defined </h4>
-    {{ end }}
+  <h1>All Consoles</h1>
+  {{ range .Consoles }}
+    <h4> {{ .GetProjectName }} / {{ .ID }} </h4>
+    <p> <b>Repo</b>: {{ .RepoURL }} </p>
+    <p> <b>Ref</b>: {{ .Ref }} </p>
+    <p> <b>Manifest Name</b>: {{ .ManifestName }} </p>
+    <p> <b>URL</b>: {{ .URL }} </p>
+    <p> <b>Revision</b>: {{ .Revision }} </p>
+    <ul>{{ range .Builders }}<li>{{ . }}</li>{{ end }}</ul>
   {{ else }}
-    <h3> No projects defined </h3>
+    <h4> No consoles defined </h4>
   {{ end }}
 
   <h1>Service Configs</h1>
diff --git a/milo/frontend/view_config.go b/milo/frontend/view_config.go
index fe6c391..9797016 100644
--- a/milo/frontend/view_config.go
+++ b/milo/frontend/view_config.go
@@ -29,7 +29,7 @@
 
 // ConfigsHandler renders the page showing the currently loaded set of luci-configs.
 func ConfigsHandler(c *router.Context) {
-	projects, err := common.GetAllProjects(c.Context)
+	consoles, err := common.GetAllConsoles(c.Context, "")
 	if err != nil {
 		ErrorHandler(c, errors.Annotate(err, "Error while getting projects").Err())
 		return
@@ -41,7 +41,7 @@
 	}
 
 	templates.MustRender(c.Context, c.Writer, "pages/configs.html", templates.Args{
-		"Projects":      projects,
+		"Consoles":      consoles,
 		"ServiceConfig": sc,
 	})
 }
@@ -51,7 +51,7 @@
 	c, h := ctx.Context, ctx.Writer
 	// Needed to access the PubSub API
 	c = appengine.WithContext(c, ctx.Request)
-	projErr := common.UpdateProjectConfigs(c)
+	projErr := common.UpdateConsoles(c)
 	if projErr != nil {
 		logging.WithError(projErr).Errorf(c, "project update handler encountered error")
 	}
diff --git a/milo/frontend/view_console.go b/milo/frontend/view_console.go
index a5cb312..1150d21 100644
--- a/milo/frontend/view_console.go
+++ b/milo/frontend/view_console.go
@@ -19,7 +19,6 @@
 	"fmt"
 	"html/template"
 	"net/http"
-	"strings"
 
 	"golang.org/x/net/context"
 
@@ -30,7 +29,6 @@
 	"github.com/luci/luci-go/server/router"
 	"github.com/luci/luci-go/server/templates"
 
-	"github.com/luci/luci-go/milo/api/config"
 	"github.com/luci/luci-go/milo/api/resp"
 	"github.com/luci/luci-go/milo/buildsource"
 	"github.com/luci/luci-go/milo/common"
@@ -42,7 +40,7 @@
 // If the user is not a reader of the project, this will return a 404.
 // TODO(hinoka): If the user is not a reader of any of of the builders returned,
 // that builder will be removed from list of results.
-func getConsoleDef(c context.Context, project, name string) (*config.Console, error) {
+func getConsoleDef(c context.Context, project, name string) (*common.Console, error) {
 	cs, err := common.GetConsole(c, project, name)
 	if err != nil {
 		return nil, err
@@ -57,7 +55,7 @@
 	if err != nil {
 		return nil, err
 	}
-	commitInfo, err := git.GetHistory(c, def.RepoURL, def.Branch, 25)
+	commitInfo, err := git.GetHistory(c, def.RepoURL, def.Ref, 25)
 	if err != nil {
 		return nil, err
 	}
@@ -67,10 +65,9 @@
 	builderNames := make([]string, len(def.Builders))
 	builders := make([]resp.BuilderRef, len(def.Builders))
 	for i, b := range def.Builders {
-		builderNames[i] = b.Name
-		builders[i].Name = b.Name
-		builders[i].Category = strings.Split(b.Category, "|")
-		builders[i].ShortName = b.ShortName
+		builderNames[i] = b
+		builders[i].Name = b
+		// TODO(hinoka): Add Categories and ShortNames back in.
 	}
 
 	commitNames := make([]string, len(commitInfo.Commits))
@@ -92,7 +89,7 @@
 			AuthorEmail: commit.AuthorEmail,
 			CommitTime:  google.TimeFromProto(commit.CommitTime),
 			Repo:        def.RepoURL,
-			Branch:      def.Branch,
+			Branch:      def.Ref, // TODO(hinoka): Actually this doesn't match, change branch to ref.
 			Description: commit.Msg,
 			Revision:    resp.NewLink(commitNames[row], def.RepoURL+"/+/"+commitNames[row]),
 		}
@@ -106,7 +103,7 @@
 	}
 
 	return &resp.Console{
-		Name:       def.Name,
+		Name:       def.ID,
 		Commit:     ccb,
 		BuilderRef: builders,
 	}, nil