Repository: osbuild/osbuild-composer
Pull request #135 (2019-12-02 23:54:34)

weldr: add support for toml blueprints


Patch:
diff --git a/go.mod b/go.mod
index 2bf27de35..e1cdc2cc2 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.12
 
 require (
 	github.com/Azure/azure-storage-blob-go v0.8.0
+	github.com/BurntSushi/toml v0.3.1
 	github.com/aws/aws-sdk-go v1.25.37
 	github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
 	github.com/gobwas/glob v0.2.3
diff --git a/go.sum b/go.sum
index cc48dc82a..56e3f6a60 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ github.com/Azure/azure-pipeline-go v0.2.1 h1:OLBdZJ3yvOn2MezlWvbrBMTEUQC72zAftRZ
 github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
 github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFEY95rZLK0Tj6o=
 github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/aws/aws-sdk-go v1.25.37 h1:gBtB/F3dophWpsUQKN/Kni+JzYEH2mGHF4hWNtfED1w=
 github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index b31a631f7..38d4fe00c 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -2,15 +2,18 @@ package weldr
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"log"
 	"net"
 	"net/http"
+	"net/url"
 	"sort"
 	"strconv"
 	"strings"
 
+	"github.com/BurntSushi/toml"
 	"github.com/google/uuid"
 	"github.com/julienschmidt/httprouter"
 
@@ -701,11 +704,44 @@ func (api *API) blueprintsInfoHandler(writer http.ResponseWriter, request *http.
 		changes = append(changes, change{changed, blueprint.Name})
 	}
 
-	json.NewEncoder(writer).Encode(reply{
-		Blueprints: blueprints,
-		Changes:    changes,
-		Errors:     []responseError{},
-	})
+	q, err := url.ParseQuery(request.URL.RawQuery)
+	if err != nil {
+		errors := responseError{
+			ID:  "InvalidChars",
+			Msg: fmt.Sprintf("invalid query string: %v", err),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
+	}
+
+	format := q.Get("format")
+	if format == "json" || format == "" {
+		json.NewEncoder(writer).Encode(reply{
+			Blueprints: blueprints,
+			Changes:    changes,
+			Errors:     []responseError{},
+		})
+	} else if format == "toml" {
+		// lorax concatenates multiple blueprints with `\n\n` here,
+		// which is never useful. Deviate by only returning the first
+		// blueprint.
+		if len(blueprints) > 1 {
+			errors := responseError{
+				ID:  "HTTPError",
+				Msg: "toml format only supported when requesting one blueprint",
+			}
+			statusResponseError(writer, http.StatusBadRequest, errors)
+			return
+		}
+		toml.NewEncoder(writer).Encode(blueprints[0])
+	} else {
+		errors := responseError{
+			ID:  "InvalidChars",
+			Msg: fmt.Sprintf("invalid `format` parameter: %s", format),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
+	}
 }
 
 func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1030,21 +1066,29 @@ func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.R
 	}
 
 	contentType := request.Header["Content-Type"]
-	if len(contentType) != 1 || contentType[0] != "application/json" {
+	if len(contentType) != 1 {
 		errors := responseError{
 			ID:  "BlueprintsError",
-			Msg: "'blueprint must be json'",
+			Msg: "missing Content-Type header",
 		}
 		statusResponseError(writer, http.StatusBadRequest, errors)
 		return
 	}
 
 	var blueprint blueprint.Blueprint
-	err := json.NewDecoder(request.Body).Decode(&blueprint)
+	var err error
+	if contentType[0] == "application/json" {
+		err = json.NewDecoder(request.Body).Decode(&blueprint)
+	} else if contentType[0] == "text/x-toml" {
+		_, err = toml.DecodeReader(request.Body, &blueprint)
+	} else {
+		err = errors.New("blueprint must be in json or toml format")
+	}
+
 	if err != nil {
 		errors := responseError{
 			ID:  "BlueprintsError",
-			Msg: "400 Bad Request: The browser (or proxy) sent a request that this server could not understand.",
+			Msg: "400 Bad Request: The browser (or proxy) sent a request that this server could not understand: " + err.Error(),
 		}
 		statusResponseError(writer, http.StatusBadRequest, errors)
 		return


Test patch:
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index a3b1f75b2..19d62f8b6 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -1,8 +1,10 @@
 package weldr_test
 
 import (
+	"bytes"
 	"math/rand"
 	"net/http"
+	"net/http/httptest"
 	"os"
 	"strconv"
 	"testing"
@@ -17,6 +19,7 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/test"
 	"github.com/osbuild/osbuild-composer/internal/weldr"
 
+	"github.com/BurntSushi/toml"
 	"github.com/google/go-cmp/cmp"
 )
 
@@ -72,6 +75,29 @@ func TestBlueprintsNew(t *testing.T) {
 	}
 }
 
+func TestBlueprintsNewToml(t *testing.T) {
+	blueprint := `
+name = "test"
+description = "Test"
+version = "0.0.0"
+
+[[packages]]
+name = "httpd"
+version = "2.4.*"`
+
+	req := httptest.NewRequest("POST",  "/api/v0/blueprints/new", bytes.NewReader([]byte(blueprint)))
+	req.Header.Set("Content-Type", "text/x-toml")
+	recorder := httptest.NewRecorder()
+
+	api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
+	api.ServeHTTP(recorder, req)
+
+	r := recorder.Result()
+	if r.StatusCode != http.StatusOK {
+		t.Fatalf("unexpected status %v", r.StatusCode)
+	}
+}
+
 func TestBlueprintsWorkspace(t *testing.T) {
 	var cases = []struct {
 		Method         string
@@ -115,6 +141,42 @@ func TestBlueprintsInfo(t *testing.T) {
 	}
 }
 
+func TestBlueprintsInfoToml(t *testing.T) {
+	api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
+	test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test1","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+
+	req := httptest.NewRequest("GET",  "/api/v0/blueprints/info/test1?format=toml", nil)
+	recorder := httptest.NewRecorder()
+	api.ServeHTTP(recorder, req)
+
+	resp := recorder.Result()
+	if resp.StatusCode != http.StatusOK {
+		t.Fatalf("unexpected status %v", resp.StatusCode)
+	}
+
+	var got blueprint.Blueprint
+	_, err := toml.DecodeReader(resp.Body, &got)
+	if err != nil {
+		t.Fatalf("error decoding toml file: %v", err)
+	}
+
+	t.Logf("%v", got)
+
+	expected := blueprint.Blueprint{
+		Name: "test1",
+		Description: "Test",
+		Version: "0.0.0",
+		Packages: []blueprint.Package{
+			{ "httpd", "2.4.*" },
+		},
+		Groups: []blueprint.Group{},
+		Modules: []blueprint.Package{},
+	}
+	if diff := cmp.Diff(got, expected); diff != "" {
+		t.Fatalf("received unexpected blueprint: %s", diff)
+	}
+}
+
 func TestBlueprintsFreeze(t *testing.T) {
 	var cases = []struct {
 		Fixture        rpmmd_mock.FixtureGenerator