diff --git a/api/api.go b/api/api.go index aa0c9e94..f5ac7142 100644 --- a/api/api.go +++ b/api/api.go @@ -11,7 +11,7 @@ import ( "github.com/camptocamp/terraboard/compare" "github.com/camptocamp/terraboard/db" "github.com/camptocamp/terraboard/state" - "github.com/camptocamp/terraboard/util" + "github.com/gorilla/mux" log "github.com/sirupsen/logrus" ) @@ -27,25 +27,9 @@ func JSONError(w http.ResponseWriter, message string, err error) { } } -// ListStates lists States -func ListStates(w http.ResponseWriter, _ *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") - states := d.ListStates() - - j, err := json.Marshal(states) - if err != nil { - JSONError(w, "Failed to marshal states", err) - return - } - if _, err := io.WriteString(w, string(j)); err != nil { - log.Error(err.Error()) - } -} - // ListTerraformVersionsWithCount lists Terraform versions with their associated // counts, sorted by the 'orderBy' parameter (version by default) func ListTerraformVersionsWithCount(w http.ResponseWriter, r *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") query := r.URL.Query() versions, _ := d.ListTerraformVersionsWithCount(query) @@ -61,7 +45,6 @@ func ListTerraformVersionsWithCount(w http.ResponseWriter, r *http.Request, d *d // ListStateStats returns State information for a given path as parameter func ListStateStats(w http.ResponseWriter, r *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") query := r.URL.Query() states, page, total := d.ListStateStats(query) @@ -82,18 +65,17 @@ func ListStateStats(w http.ResponseWriter, r *http.Request, d *db.Database) { // GetState provides information on a State func GetState(w http.ResponseWriter, r *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") - st := util.TrimBasePath(r, "api/state/") + params := mux.Vars(r) versionID := r.URL.Query().Get("versionid") var err error if versionID == "" { - versionID, err = d.DefaultVersion(st) + versionID, err = d.DefaultVersion(params["lineage"]) if err != nil { JSONError(w, "Failed to retrieve default version", err) return } } - state := d.GetState(st, versionID) + state := d.GetState(params["lineage"], versionID) j, err := json.Marshal(state) if err != nil { @@ -105,11 +87,10 @@ func GetState(w http.ResponseWriter, r *http.Request, d *db.Database) { } } -// GetStateActivity returns the activity (version history) of a State -func GetStateActivity(w http.ResponseWriter, r *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") - st := util.TrimBasePath(r, "api/state/activity/") - activity := d.GetStateActivity(st) +// GetLineageActivity returns the activity (version history) of a Lineage +func GetLineageActivity(w http.ResponseWriter, r *http.Request, d *db.Database) { + params := mux.Vars(r) + activity := d.GetLineageActivity(params["lineage"]) j, err := json.Marshal(activity) if err != nil { @@ -123,14 +104,13 @@ func GetStateActivity(w http.ResponseWriter, r *http.Request, d *db.Database) { // StateCompare compares two versions ('from' and 'to') of a State func StateCompare(w http.ResponseWriter, r *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") - st := util.TrimBasePath(r, "api/state/compare/") + params := mux.Vars(r) query := r.URL.Query() fromVersion := query.Get("from") toVersion := query.Get("to") - from := d.GetState(st, fromVersion) - to := d.GetState(st, toVersion) + from := d.GetState(params["lineage"], fromVersion) + to := d.GetState(params["lineage"], toVersion) compare, err := compare.Compare(from, to) if err != nil { JSONError(w, "Failed to compare state versions", err) @@ -149,7 +129,6 @@ func StateCompare(w http.ResponseWriter, r *http.Request, d *db.Database) { // GetLocks returns information on locked States func GetLocks(w http.ResponseWriter, _ *http.Request, sps []state.Provider) { - w.Header().Set("Access-Control-Allow-Origin", "*") allLocks := make(map[string]state.LockInfo) for _, sp := range sps { locks, err := sp.GetLocks() @@ -175,7 +154,6 @@ func GetLocks(w http.ResponseWriter, _ *http.Request, sps []state.Provider) { // SearchAttribute performs a search on Resource Attributes // by various parameters func SearchAttribute(w http.ResponseWriter, r *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") query := r.URL.Query() result, page, total := d.SearchAttribute(query) @@ -197,7 +175,6 @@ func SearchAttribute(w http.ResponseWriter, r *http.Request, d *db.Database) { // ListResourceTypes lists all Resource types func ListResourceTypes(w http.ResponseWriter, _ *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") result, _ := d.ListResourceTypes() j, err := json.Marshal(result) if err != nil { @@ -211,7 +188,6 @@ func ListResourceTypes(w http.ResponseWriter, _ *http.Request, d *db.Database) { // ListResourceTypesWithCount lists all Resource types with their associated count func ListResourceTypesWithCount(w http.ResponseWriter, _ *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") result, _ := d.ListResourceTypesWithCount() j, err := json.Marshal(result) if err != nil { @@ -225,7 +201,6 @@ func ListResourceTypesWithCount(w http.ResponseWriter, _ *http.Request, d *db.Da // ListResourceNames lists all Resource names func ListResourceNames(w http.ResponseWriter, _ *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") result, _ := d.ListResourceNames() j, err := json.Marshal(result) if err != nil { @@ -240,7 +215,6 @@ func ListResourceNames(w http.ResponseWriter, _ *http.Request, d *db.Database) { // ListAttributeKeys lists all Resource Attribute Keys, // optionally filtered by resource_type func ListAttributeKeys(w http.ResponseWriter, r *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") resourceType := r.URL.Query().Get("resource_type") result, _ := d.ListAttributeKeys(resourceType) j, err := json.Marshal(result) @@ -255,7 +229,6 @@ func ListAttributeKeys(w http.ResponseWriter, r *http.Request, d *db.Database) { // ListTfVersions lists all Terraform versions func ListTfVersions(w http.ResponseWriter, _ *http.Request, d *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") result, _ := d.ListTfVersions() j, err := json.Marshal(result) if err != nil { @@ -269,8 +242,6 @@ func ListTfVersions(w http.ResponseWriter, _ *http.Request, d *db.Database) { // GetUser returns information about the logged user func GetUser(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - name := r.Header.Get("X-Forwarded-User") email := r.Header.Get("X-Forwarded-Email") @@ -289,8 +260,6 @@ func GetUser(w http.ResponseWriter, r *http.Request) { // SubmitPlan inserts a new Terraform plan in the database. // /api/plans POST endpoint callback func SubmitPlan(w http.ResponseWriter, r *http.Request, db *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") - body, err := ioutil.ReadAll(r.Body) if err != nil { log.Errorf("Failed to read body: %v", err) @@ -310,7 +279,6 @@ func SubmitPlan(w http.ResponseWriter, r *http.Request, db *db.Database) { // Sorted by most recent to oldest. // /api/plans GET endpoint callback func GetPlans(w http.ResponseWriter, r *http.Request, db *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") lineage := r.URL.Query().Get("lineage") limit := r.URL.Query().Get("limit") plans := db.GetPlans(lineage, limit) @@ -342,7 +310,6 @@ func ManagePlans(w http.ResponseWriter, r *http.Request, db *db.Database) { // Optional "&limit=X" parameter to limit requested quantity of them. // Sorted by most recent to oldest. func GetLineages(w http.ResponseWriter, r *http.Request, db *db.Database) { - w.Header().Set("Access-Control-Allow-Origin", "*") limit := r.URL.Query().Get("limit") lineages := db.GetLineages(limit) diff --git a/db/db.go b/db/db.go index 8f167456..e4d2d095 100644 --- a/db/db.go +++ b/db/db.go @@ -136,7 +136,7 @@ func (db *Database) stateS3toDB(sf *statefile.File, path string, versionID strin Version: version, TFVersion: sf.TerraformVersion.String(), Serial: int64(sf.Serial), - LineageID: sql.NullInt64{Int64: int64(lineage.ID)}, + LineageID: sql.NullInt64{Int64: int64(lineage.ID), Valid: true}, } for _, m := range sf.State.Modules { @@ -279,25 +279,26 @@ func (db *Database) InsertVersion(version *state.Version) error { } // GetState retrieves a State from the database by its path and versionID -func (db *Database) GetState(path, versionID string) (state types.State) { - db.Joins("JOIN versions on states.version_id=versions.id"). +func (db *Database) GetState(lineage, versionID string) (state types.State) { + db.Joins("JOIN lineages on states.lineage_id=lineages.id"). + Joins("JOIN versions on states.version_id=versions.id"). Preload("Version").Preload("Modules").Preload("Modules.Resources").Preload("Modules.Resources.Attributes"). Preload("Modules.OutputValues"). - Find(&state, "states.path = ? AND versions.version_id = ?", path, versionID) + Find(&state, "lineages.value = ? AND versions.version_id = ?", lineage, versionID) return } -// GetStateActivity returns a slice of StateStat from the Database -// for a given State path representing the State activity over time (Versions) -func (db *Database) GetStateActivity(path string) (states []types.StateStat) { +// GetLineageActivity returns a slice of StateStat from the Database +// for a given lineage representing the State activity over time (Versions) +func (db *Database) GetLineageActivity(lineage string) (states []types.StateStat) { sql := "SELECT t.path, t.serial, t.tf_version, t.version_id, t.last_modified, count(resources.*) as resource_count" + - " FROM (SELECT states.id, states.path, states.serial, states.tf_version, versions.version_id, versions.last_modified FROM states JOIN versions ON versions.id = states.version_id WHERE states.path = ? ORDER BY states.path, versions.last_modified ASC) t" + + " FROM (SELECT states.id, states.path, states.serial, states.tf_version, versions.version_id, versions.last_modified FROM states JOIN lineages ON lineages.id = states.lineage_id JOIN versions ON versions.id = states.version_id WHERE lineages.value = ? ORDER BY states.path, versions.last_modified ASC) t" + " JOIN modules ON modules.state_id = t.id" + " JOIN resources ON resources.module_id = modules.id" + " GROUP BY t.path, t.serial, t.tf_version, t.version_id, t.last_modified" + " ORDER BY last_modified ASC" - db.Raw(sql, path).Find(&states) + db.Raw(sql, lineage).Find(&states) return } @@ -336,7 +337,8 @@ func (db *Database) SearchAttribute(query url.Values) (results []types.SearchRes sqlQuery += " JOIN modules ON states.id = modules.state_id" + " JOIN resources ON modules.id = resources.module_id" + - " JOIN attributes ON resources.id = attributes.resource_id" + " JOIN attributes ON resources.id = attributes.resource_id" + + " JOIN lineages ON lineages.id = states.lineage_id" var where []string var params []interface{} @@ -370,6 +372,10 @@ func (db *Database) SearchAttribute(query url.Values) (results []types.SearchRes where = append(where, fmt.Sprintf("states.tf_version LIKE '%s'", fmt.Sprintf("%%%s%%", v))) } + if v := query.Get("lineage_value"); string(v) != "" { + where = append(where, fmt.Sprintf("lineages.value LIKE '%s'", fmt.Sprintf("%%%s%%", v))) + } + if len(where) > 0 { sqlQuery += " WHERE " + strings.Join(where, " AND ") } @@ -382,11 +388,12 @@ func (db *Database) SearchAttribute(query url.Values) (results []types.SearchRes // Now get results // gorm doesn't support subqueries... - sql := "SELECT states.path, states.version_id, states.tf_version, states.serial, modules.path as module_path, resources.type, resources.name, resources.index, attributes.key, attributes.value" + + sql := "SELECT states.path, states.version_id, states.tf_version, states.serial, lineages.value as lineage_value, modules.path as module_path, resources.type, resources.name, resources.index, attributes.key, attributes.value" + sqlQuery + - " ORDER BY states.path, states.serial, modules.path, resources.type, resources.name, resources.index, attributes.key" + + " ORDER BY states.path, states.serial, lineage_value, modules.path, resources.type, resources.name, resources.index, attributes.key" + " LIMIT ?" + log.Info(sql) params = append(params, pageSize) if v := string(query.Get("page")); v != "" { @@ -422,20 +429,6 @@ func (db *Database) ListStatesVersions() (statesVersions map[string][]string) { return } -// ListStates returns a slice of all State paths from the Database -func (db *Database) ListStates() (states []string) { - rows, _ := db.Table("states").Select("DISTINCT path").Rows() - defer rows.Close() - for rows.Next() { - var state string - if err := rows.Scan(&state); err != nil { - log.Error(err.Error()) - } - states = append(states, state) - } - return -} - // ListTerraformVersionsWithCount returns a slice of maps of Terraform versions // mapped to the count of most recent State paths using them. // ListTerraformVersionsWithCount also takes a query with possible parameter 'orderBy' @@ -475,28 +468,33 @@ func (db *Database) ListTerraformVersionsWithCount(query url.Values) (results [] // ListStateStats returns a slice of StateStat, along with paging information func (db *Database) ListStateStats(query url.Values) (states []types.StateStat, page int, total int) { - row := db.Table("states").Select("count(DISTINCT path)").Row() + row := db.Raw("SELECT count(*) FROM (SELECT DISTINCT lineage_id FROM states) AS t").Row() if err := row.Scan(&total); err != nil { log.Error(err.Error()) } - offset := 0 + var paginationQuery string + var params []interface{} page = 1 if v := string(query.Get("page")); v != "" { page, _ = strconv.Atoi(v) // TODO: err - offset = (page - 1) * pageSize + offset := (page - 1) * pageSize + params = append(params, offset) + paginationQuery = " LIMIT 20 OFFSET ?" + } else { + page = -1 } - sql := "SELECT t.path, t.serial, t.tf_version, t.version_id, t.last_modified, count(resources.*) as resource_count" + - " FROM (SELECT DISTINCT ON(states.path) states.id, states.path, states.serial, states.tf_version, versions.version_id, versions.last_modified FROM states JOIN versions ON versions.id = states.version_id ORDER BY states.path, versions.last_modified DESC) t" + + sql := "SELECT t.path, lineages.value as lineage_value, t.serial, t.tf_version, t.version_id, t.last_modified, count(resources.*) as resource_count" + + " FROM (SELECT DISTINCT ON(states.lineage_id) states.id, states.lineage_id, states.path, states.serial, states.tf_version, versions.version_id, versions.last_modified FROM states JOIN versions ON versions.id = states.version_id ORDER BY states.lineage_id, versions.last_modified DESC) t" + " JOIN modules ON modules.state_id = t.id" + " JOIN resources ON resources.module_id = modules.id" + - " GROUP BY t.path, t.serial, t.tf_version, t.version_id, t.last_modified" + + " JOIN lineages ON lineages.id = t.lineage_id" + + " GROUP BY t.path, lineages.value, t.serial, t.tf_version, t.version_id, t.last_modified" + " ORDER BY last_modified DESC" + - " LIMIT 20" + - " OFFSET ?" + paginationQuery - db.Raw(sql, offset).Find(&states) + db.Raw(sql, params...).Find(&states) return } @@ -682,14 +680,15 @@ func (db *Database) GetLineages(limitStr string) (lineages []types.Lineage) { // DefaultVersion returns the detault VersionID for a given State path // Copied and adapted from github.com/hashicorp/terraform/command/jsonstate/state.go -func (db *Database) DefaultVersion(path string) (version string, err error) { +func (db *Database) DefaultVersion(lineage string) (version string, err error) { sqlQuery := "SELECT versions.version_id FROM" + " (SELECT states.path, max(states.serial) as mx FROM states GROUP BY states.path) t" + " JOIN states ON t.path = states.path AND t.mx = states.serial" + " JOIN versions on states.version_id=versions.id" + - " WHERE states.path = ?" + " JOIN lineages on lineages.id=states.lineage_id" + + " WHERE lineages.value = ?" - row := db.Raw(sqlQuery, path).Row() + row := db.Raw(sqlQuery, lineage).Row() err = row.Scan(&version) return } diff --git a/go.mod b/go.mod index f527daaa..c9ca0b68 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/go-test/deep v1.0.3 github.com/google/go-cmp v0.5.5 + github.com/gorilla/mux v1.8.0 github.com/hashicorp/errwrap v1.1.0 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-hclog v0.15.0 diff --git a/go.sum b/go.sum index a9286a83..3c02170a 100644 --- a/go.sum +++ b/go.sum @@ -315,6 +315,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= diff --git a/main.go b/main.go index b999f1f3..7eded273 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "time" @@ -14,27 +13,11 @@ import ( "github.com/camptocamp/terraboard/db" "github.com/camptocamp/terraboard/state" "github.com/camptocamp/terraboard/util" + "github.com/gorilla/mux" tfversion "github.com/hashicorp/terraform/version" log "github.com/sirupsen/logrus" ) -// idx serves index.html, always, -// so as to let AngularJS manage the app routing. -// The HTML tag is edited on the fly -// to reflect the proper base URL -func idx(w http.ResponseWriter, _ *http.Request) { - idx, err := ioutil.ReadFile("static/index.html") - if err != nil { - log.Errorf("Failed to open index.html: %v", err) - // TODO: Return error page - } - idxStr := string(idx) - idxStr = util.ReplaceBasePath(idxStr, "base href=\"/\"", "base href=\"%s\"") - if _, err := io.WriteString(w, idxStr); err != nil { - log.Error(err.Error()) - } -} - // Pass the DB to API handlers // This takes a callback and returns a HandlerFunc // which calls the callback with the DB @@ -126,8 +109,6 @@ func refreshDB(syncInterval uint16, d *db.Database, sp state.Provider) { var version = "undefined" func getVersion(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - j, err := json.Marshal(map[string]string{ "version": version, "copyright": "Copyright © 2017-2021 Camptocamp", @@ -141,6 +122,15 @@ func getVersion(w http.ResponseWriter, _ *http.Request) { } } +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding") + next.ServeHTTP(w, r) + }) +} + // Main func main() { c := config.LoadConfig(version) @@ -175,34 +165,42 @@ func main() { } defer database.Close() - // Index is a wildcard for all paths - http.HandleFunc(util.GetFullPath(""), idx) + // Instantiate gorilla/mux router instance + r := mux.NewRouter() + + // Handle API endpoints + apiRouter := r.PathPrefix("/api/").Subrouter() + apiRouter.HandleFunc(util.GetFullPath("version"), getVersion) + apiRouter.HandleFunc(util.GetFullPath("user"), api.GetUser) + apiRouter.HandleFunc(util.GetFullPath("lineages"), handleWithDB(api.GetLineages, database)) + apiRouter.HandleFunc(util.GetFullPath("lineages/stats"), handleWithDB(api.ListStateStats, database)) + apiRouter.HandleFunc(util.GetFullPath("lineages/tfversion/count"), + handleWithDB(api.ListTerraformVersionsWithCount, database)) + apiRouter.HandleFunc(util.GetFullPath("lineages/{lineage}"), handleWithDB(api.GetState, database)) + apiRouter.HandleFunc(util.GetFullPath("lineages/{lineage}/activity"), handleWithDB(api.GetLineageActivity, database)) + apiRouter.HandleFunc(util.GetFullPath("lineages/{lineage}/compare"), handleWithDB(api.StateCompare, database)) + apiRouter.HandleFunc(util.GetFullPath("locks"), handleWithStateProviders(api.GetLocks, sps)) + apiRouter.HandleFunc(util.GetFullPath("search/attribute"), handleWithDB(api.SearchAttribute, database)) + apiRouter.HandleFunc(util.GetFullPath("resource/types"), handleWithDB(api.ListResourceTypes, database)) + apiRouter.HandleFunc(util.GetFullPath("resource/types/count"), handleWithDB(api.ListResourceTypesWithCount, database)) + apiRouter.HandleFunc(util.GetFullPath("resource/names"), handleWithDB(api.ListResourceNames, database)) + apiRouter.HandleFunc(util.GetFullPath("attribute/keys"), handleWithDB(api.ListAttributeKeys, database)) + apiRouter.HandleFunc(util.GetFullPath("tf_versions"), handleWithDB(api.ListTfVersions, database)) + apiRouter.HandleFunc(util.GetFullPath("plans"), handleWithDB(api.ManagePlans, database)) // Serve static files (CSS, JS, images) from dir staticFs := http.FileServer(http.Dir("static")) - http.Handle(util.GetFullPath("static/"), http.StripPrefix(util.GetFullPath("static"), staticFs)) - - // Handle API points - http.HandleFunc(util.GetFullPath("api/version"), getVersion) - http.HandleFunc(util.GetFullPath("api/user"), api.GetUser) - http.HandleFunc(util.GetFullPath("api/states"), handleWithDB(api.ListStates, database)) - http.HandleFunc(util.GetFullPath("api/states/stats"), handleWithDB(api.ListStateStats, database)) - http.HandleFunc(util.GetFullPath("api/states/tfversion/count"), - handleWithDB(api.ListTerraformVersionsWithCount, database)) - http.HandleFunc(util.GetFullPath("api/state/"), handleWithDB(api.GetState, database)) - http.HandleFunc(util.GetFullPath("api/state/activity/"), handleWithDB(api.GetStateActivity, database)) - http.HandleFunc(util.GetFullPath("api/state/compare/"), handleWithDB(api.StateCompare, database)) - http.HandleFunc(util.GetFullPath("api/locks"), handleWithStateProviders(api.GetLocks, sps)) - http.HandleFunc(util.GetFullPath("api/search/attribute"), handleWithDB(api.SearchAttribute, database)) - http.HandleFunc(util.GetFullPath("api/resource/types"), handleWithDB(api.ListResourceTypes, database)) - http.HandleFunc(util.GetFullPath("api/resource/types/count"), handleWithDB(api.ListResourceTypesWithCount, database)) - http.HandleFunc(util.GetFullPath("api/resource/names"), handleWithDB(api.ListResourceNames, database)) - http.HandleFunc(util.GetFullPath("api/attribute/keys"), handleWithDB(api.ListAttributeKeys, database)) - http.HandleFunc(util.GetFullPath("api/tf_versions"), handleWithDB(api.ListTfVersions, database)) - http.HandleFunc(util.GetFullPath("api/plans"), handleWithDB(api.ManagePlans, database)) - http.HandleFunc(util.GetFullPath("api/lineages"), handleWithDB(api.GetLineages, database)) + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", staticFs)) + + // Serve index page on all unhandled routes + r.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/index.html") + }) + + // Add CORS Middleware to mux router + r.Use(corsMiddleware) // Start server log.Debugf("Listening on port %d\n", c.Web.Port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", c.Web.Port), nil)) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", c.Web.Port), r)) } diff --git a/state/aws.go b/state/aws.go index 672e89cc..668c0ee6 100644 --- a/state/aws.go +++ b/state/aws.go @@ -204,7 +204,7 @@ func (a *AWS) GetVersions(state string) (versions []Version, err error) { versions = []Version{} if a.noVersioning { versions = append(versions, Version{ - ID: "1", + ID: state, LastModified: time.Now(), }) return diff --git a/static/main.html b/static/main.html index 18a72517..ccab73b1 100644 --- a/static/main.html +++ b/static/main.html @@ -1,36 +1,19 @@
-
- +
+ -

Resource types

-
-
- +

Resource types

+
+
+ -

Terraform versions

-
-
- +

Terraform versions

+
+
+ -

States locked

-
+

States locked

+

@@ -62,15 +45,15 @@

States locked

- {{r.path}} + {{r.path}} {{r.terraform_version}} {{r.serial}} {{r.last_modified | date:'medium'}} {{r.resource_count}} - - + + -
+ \ No newline at end of file diff --git a/static/navbar.html b/static/navbar.html index bc222fef..d7966597 100644 --- a/static/navbar.html +++ b/static/navbar.html @@ -32,7 +32,7 @@ title="Choose a state file"> {{placeholder}} - {{state}} + {{state.path}} diff --git a/static/search.html b/static/search.html index 89361af5..f0cbf135 100644 --- a/static/search.html +++ b/static/search.html @@ -1,89 +1,78 @@ -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
+
-
- - {{$parent.tfVersion}} - - {{k}} - - -
-
- - {{$parent.resType}} - - {{k}} - - -
-
- - {{$parent.resID}} - - {{k}} - - -
-
- - {{$parent.attrKey}} - - {{k}} - - +
+
+ + {{$parent.tfVersion}} + + {{k}} + + +
+
+ + {{$parent.resType}} + + {{k}} + + +
+
+ + {{$parent.resID}} + + {{k}} + + +
+
+ + {{$parent.attrKey}} + + {{k}} + + +
-
- +
+
+ +
+
+ +
-

@@ -120,7 +109,7 @@ - + {{r.path}} {{r.tf_version}} {{r.serial}} diff --git a/static/terraboard.js b/static/terraboard.js index 7a5fed1f..8e617529 100644 --- a/static/terraboard.js +++ b/static/terraboard.js @@ -4,7 +4,7 @@ var app = angular.module("terraboard", ['ngRoute', 'ngSanitize', 'ui.select', 'c $routeProvider.when("/", { templateUrl: "static/main.html", controller: "tbMainCtrl" - }).when("/state/:path*", { + }).when("/lineage/:lineage*", { templateUrl: "static/state.html", controller: "tbStateCtrl", reloadOnSearch: false @@ -45,8 +45,8 @@ app.directive("sparklinechart", function () { element.bind('sparklineClick', function(ev) { var sparkline = ev.sparklines[0], region = sparkline.getCurrentRegionFields(); - var path = element[0].attributes.path.value; - scope.$parent.$parent.goToState(path, region.x); + var lineage = element[0].attributes.lineage.value; + scope.$parent.$parent.goToState(lineage, region.x); }); }); }; @@ -68,7 +68,8 @@ app.directive("hlcode", ['$timeout', function($timeout) { } }]); -app.controller("tbMainCtrl", ['$scope', '$http', '$location', function($scope, $http, $location) { +app.controller("tbMainCtrl", ['$scope', '$http', '$location', '$routeParams', +function($scope, $http, $location) { $scope.itemsPerPage = 20; $scope.getStats = function(page) { var params = {}; @@ -76,7 +77,8 @@ app.controller("tbMainCtrl", ['$scope', '$http', '$location', function($scope, $ params.page = page; } var query = $.param(params); - $http.get('api/states/stats?'+query).then(function(response){ + $http.get('api/lineages/stats?'+query).then(function(response){ + console.log('api/lineages/stats?'+query); $scope.results = response.data; $scope.pages = Math.ceil($scope.results.total / $scope.itemsPerPage); $scope.page = $scope.results.page; @@ -92,15 +94,15 @@ app.controller("tbMainCtrl", ['$scope', '$http', '$location', function($scope, $ // Version map for sparklines click events $scope.versionMap = {}; - $scope.getActivity = function(idx, path) { - $http.get('api/state/activity/'+path).then(function(response){ + $scope.getActivity = function(idx, lineage) { + $http.get('api/lineages/'+lineage+'/activity').then(function(response){ var states = response.data; - $scope.versionMap[path] = {}; + $scope.versionMap[lineage] = {}; var activityData = []; for (i=0; i < states.length; i++) { var date = new Date(states[i].last_modified).getTime() / 1000; activityData.push(date+":"+states[i].resource_count); - $scope.versionMap[path][date] = states[i].version_id; + $scope.versionMap[lineage][date] = states[i].version_id; } var activity = activityData.join(","); @@ -108,9 +110,9 @@ app.controller("tbMainCtrl", ['$scope', '$http', '$location', function($scope, $ }); }; - $scope.goToState = function(path, epoch) { - var versionId = $scope.versionMap[path][epoch]; - var url = 'state/'+path+'?versionid='+versionId; + $scope.goToState = function(lineage, epoch) { + var versionId = $scope.versionMap[lineage][epoch]; + var url = 'lineage/'+lineage+'?versionid='+versionId; $location.url(url); $scope.$apply(); }; @@ -157,7 +159,7 @@ app.controller("tbMainCtrl", ['$scope', '$http', '$location', function($scope, $ pieTfVersionsLabels = [[], [], [], [], [], [], ["Total"]]; pieTfVersionsData = [0, 0, 0, 0, 0, 0, 0]; - $http.get('api/states/tfversion/count?orderBy=version').then(function(response){ + $http.get('api/lineages/tfversion/count?orderBy=version').then(function(response){ data = response.data; angular.forEach(data, function(value, i) { if(i < 6) { @@ -218,8 +220,8 @@ app.controller("tbNavCtrl", } }); - $http.get('api/states').then(function(response){ - $scope.states = response.data; + $http.get('api/lineages/stats').then(function(response){ + $scope.states = response.data.states; }); $http.get('api/user').then(function(response){ @@ -263,7 +265,7 @@ app.controller("tbStateCtrl", * Get versions when URL is loaded */ $scope.$on('$routeChangeSuccess', function() { - $http.get('api/state/activity/'+$routeParams.path).then(function(response){ + $http.get('api/lineages/'+$routeParams.lineage+'/activity').then(function(response){ $scope.versions = []; for (i=0; i