Skip to content

Commit

Permalink
feat(api): add plan submit/get endpoints (camptocamp#175)
Browse files Browse the repository at this point in the history
* feat(api): add plan submit endpoint

* feat(api): add get plan endpoint + models tweaks
* fix: missing omitempty tag on PlanStateResource.SchemaVersion
* fix: replace PlanOutputChange by generic Change (adapt json unmarshalling)

* feat(api): allow new lineage insertion if not exists on plan submit
  • Loading branch information
hbollon committed Jun 25, 2021
1 parent a832bc5 commit f341ffd
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 13 deletions.
53 changes: 53 additions & 0 deletions api/api.go
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"

"github.com/camptocamp/terraboard/auth"
Expand Down Expand Up @@ -278,3 +279,55 @@ func GetUser(w http.ResponseWriter, r *http.Request) {
log.Error(err.Error())
}
}

// 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)
JSONError(w, "Failed to read body during plan submit", err)
return
}

if err = db.InsertPlan(body); err != nil {
log.Errorf("Failed to insert plan to db: %v", err)
JSONError(w, "Failed to insert plan to db", err)
return
}
}

// GetPlans provides all Plan by lineage.
// Optional "&limit=X" parameter to limit requested quantity of plans.
// 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)

j, err := json.Marshal(plans)
if err != nil {
log.Errorf("Failed to marshal plans: %v", err)
JSONError(w, "Failed to marshal plans", err)
return
}
if _, err := io.WriteString(w, string(j)); err != nil {
log.Error(err.Error())
}
}

// ManagePlans is used to route the request to the appropriated handler function
// on /api/plans request
func ManagePlans(w http.ResponseWriter, r *http.Request, db *db.Database) {
if r.Method == "GET" {
GetPlans(w, r, db)
} else if r.Method == "POST" {
SubmitPlan(w, r, db)
} else {
http.Error(w, "Invalid request method.", 405)
}
}
66 changes: 65 additions & 1 deletion db/db.go
Expand Up @@ -61,7 +61,6 @@ func Init(config config.DBConfig, debug bool) *Database {
&types.PlanModel{},
&types.PlanModelVariable{},
&types.PlanOutput{},
&types.PlanOutputChange{},
&types.PlanResourceChange{},
&types.PlanState{},
&types.PlanStateModule{},
Expand Down Expand Up @@ -566,6 +565,71 @@ func (db *Database) ListAttributeKeys(resourceType string) (results []string, er
return
}

// InsertPlan inserts a Terraform plan with associated information in the Database
func (db *Database) InsertPlan(plan []byte) error {
var lineage types.Lineage
if err := json.Unmarshal(plan, &lineage); err != nil {
return err
}

// Recover lineage from db if it's already exists or insert it
res := db.FirstOrCreate(&lineage, lineage)
if res.Error != nil {
return fmt.Errorf("Error on lineage retrival during plan insertion: %v", res.Error)
}

var p types.Plan
if err := json.Unmarshal(plan, &p); err != nil {
return err
}
if err := json.Unmarshal(p.PlanJSON, &p.ParsedPlan); err != nil {
return err
}

p.LineageID = lineage.ID
return db.Create(&p).Error
}

// GetPlans retrieves all Plan of a lineage from the database
func (db *Database) GetPlans(lineage, limitStr string) (plans []types.Plan) {
var limit int
if limitStr == "" {
limit = -1
} else {
var err error
limit, err = strconv.Atoi(limitStr)
if err != nil {
log.Warnf("GetPlans limit ignored: %v", err)
limit = -1
}
}

db.Joins("JOIN lineages on plans.lineage_id=lineages.id").
Preload("ParsedPlan").
Preload("ParsedPlan.PlanStateValue").
Preload("ParsedPlan.PlanStateValue.PlanStateOutputs").
Preload("ParsedPlan.PlanStateValue.PlanStateModule").
Preload("ParsedPlan.PlanStateValue.PlanStateModule.PlanStateResources").
Preload("ParsedPlan.PlanStateValue.PlanStateModule.PlanStateResources.PlanStateResourceAttributes").
Preload("ParsedPlan.PlanStateValue.PlanStateModule.PlanStateModules").
Preload("ParsedPlan.Variables").
Preload("ParsedPlan.PlanResourceChanges").
Preload("ParsedPlan.PlanResourceChanges.Change").
Preload("ParsedPlan.PlanOutputs").
Preload("ParsedPlan.PlanOutputs.Change").
Preload("ParsedPlan.PlanState").
Preload("ParsedPlan.PlanState.PlanStateValue").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateOutputs").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateModule").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateModule.PlanStateResources").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateModule.PlanStateResources.PlanStateResourceAttributes").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateModule.PlanStateModules").
Order("created_at desc").
Limit(limit).
Find(&plans, "lineages.value = ?", lineage)
return
}

// 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) {
Expand Down
1 change: 1 addition & 0 deletions main.go
Expand Up @@ -196,6 +196,7 @@ func main() {
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))

// Start server
log.Debugf("Listening on port %d\n", c.Web.Port)
Expand Down
13 changes: 4 additions & 9 deletions types/db.go
Expand Up @@ -80,6 +80,7 @@ type Attribute struct {
type Plan struct {
gorm.Model
LineageID uint `gorm:"index" json:"-"`
Lineage Lineage `json:"lineage_data"`
TFVersion string `gorm:"varchar(10)" json:"terraform_version"`
GitRemote string `json:"git_remote"`
GitCommit string `gorm:"varchar(50)" json:"git_commit"`
Expand Down Expand Up @@ -214,7 +215,7 @@ type PlanStateResource struct {

// The version of the resource type schema the "values" property
// conforms to.
SchemaVersion uint `json:"schema_version,"`
SchemaVersion uint `json:"schema_version,omitempty"`

// The JSON representation of the attribute values of the resource,
// whose structure depends on the resource type schema. Any unknown
Expand Down Expand Up @@ -289,14 +290,8 @@ type PlanResourceChange struct {

type PlanOutput struct {
gorm.Model
Name string `gorm:"index" json:"key"`
PlanModelID sql.NullInt64 `gorm:"index" json:"-"`
PlanOutputChange PlanOutputChange `json:"value"`
PlanOutputChangeID sql.NullInt64 `gorm:"index" json:"-"`
}

type PlanOutputChange struct {
gorm.Model
Name string `gorm:"index" json:"name"`
PlanModelID sql.NullInt64 `gorm:"index" json:"-"`
// The data describing the change that will be made to this object.
Change Change `json:"change,omitempty"`
ChangeID sql.NullInt64 `gorm:"index" json:"-"`
Expand Down
9 changes: 6 additions & 3 deletions types/json.go
Expand Up @@ -16,16 +16,19 @@ type planStateResourceAttributeList []PlanStateResourceAttribute
type rawJSON string

func (p *planOutputList) UnmarshalJSON(b []byte) error {
tmp := map[string]PlanOutput{}
tmp := map[string]Change{}
err := json.Unmarshal(b, &tmp)
if err != nil {
return err
}

var list planOutputList
for key, value := range tmp {
value.Name = key
list = append(list, value)
output := PlanOutput{
Name: key,
Change: value,
}
list = append(list, output)
}

*p = list
Expand Down

0 comments on commit f341ffd

Please sign in to comment.