Skip to content

Commit

Permalink
data-api: implement the methods to read in the new JSON api definitio…
Browse files Browse the repository at this point in the history
…ns (#3151)

* implement functions to load and parse the data definitions in json

* change resourceManager to a map of service to service details and load details into resourceManager

* hook up services flag

* clean up code and add some comments

* further clean up

* use defer to close the file when loading json, remove CaseInsensitive from ConstantDetails and lower case compare the definition types when loading files

* remove CaseInsensitive when mapping ConstantDetails

* fix file name and remove IsCaseInsensitive field

* address review comments

* rename resourceManager variable to services
  • Loading branch information
stephybun authored Oct 13, 2023
1 parent d3f884c commit af5dcda
Show file tree
Hide file tree
Showing 15 changed files with 564 additions and 270 deletions.
35 changes: 32 additions & 3 deletions tools/data-api/internal/commands/serve.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,63 @@
package commands

import (
"flag"
"fmt"
"net/http"
"strings"
"time"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/pandora/tools/data-api/internal/endpoints"
"github.com/mitchellh/cli"
)

var _ cli.Command = ServeCommand{}

func NewServeCommand(definitionsDirectory string) func() (cli.Command, error) {
return func() (cli.Command, error) {
return ServeCommand{
DefinitionsDirectory: definitionsDirectory,
Log: hclog.New(&hclog.LoggerOptions{
Level: hclog.DefaultLevel,
Output: hclog.DefaultOutput,
TimeFn: time.Now,
}),
}, nil
}
}

type ServeCommand struct {
Log hclog.Logger
DefinitionsDirectory string
Log hclog.Logger
}

func (ServeCommand) Help() string {
return "Launches the Server"
}

func (c ServeCommand) Run(args []string) int {
port := 8080
// TODO: dynamic ports from args/env
port := 8080

var serviceNamesRaw string

f := flag.NewFlagSet("serve", flag.ExitOnError)
f.StringVar(&serviceNamesRaw, "services", "", "A list of comma separated Service names to load")
f.Parse(args)

var serviceNames *[]string
if serviceNamesRaw != "" {
serviceNames = pointer.To(strings.Split(serviceNamesRaw, ","))
}

c.Log.Debug(fmt.Sprintf("Launching Server on port %d", port))
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Route("/", endpoints.Router)
r.Route("/", endpoints.Router(c.DefinitionsDirectory, serviceNames))
http.ListenAndServe(fmt.Sprintf(":%d", port), r)
return 0
}
Expand Down
59 changes: 32 additions & 27 deletions tools/data-api/internal/endpoints/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,36 @@ import (
"github.com/hashicorp/pandora/tools/data-api/internal/repositories"
)

func Router(router chi.Router) {
router.Route("/infrastructure", infrastructure.Router)
router.Route("/v1/microsoft-graph/beta", func(r chi.Router) {
opts := v1.Options{
ServiceType: repositories.MicrosoftGraphV1BetaServiceType,
UriPrefix: "/v1/microsoft-graph/beta",
UsesCommonTypes: true,
}
v1.Router(r, opts)
})
router.Route("/v1/microsoft-graph/stable-v1", func(r chi.Router) {
opts := v1.Options{
ServiceType: repositories.MicrosoftGraphV1StableServiceType,
UriPrefix: "/v1/microsoft-graph/stable-v1",
UsesCommonTypes: true,
}
v1.Router(r, opts)
})
router.Route("/v1/resource-manager", func(r chi.Router) {
opts := v1.Options{
ServiceType: repositories.ResourceManagerServiceType,
UriPrefix: "/v1/resource-manager",
UsesCommonTypes: false,
}
v1.Router(r, opts)
})
router.Get("/", HomePage)
func Router(directory string, serviceNames *[]string) func(chi.Router) {
return func(router chi.Router) {
router.Route("/infrastructure", infrastructure.Router)
router.Route("/v1/microsoft-graph/beta", func(r chi.Router) {
opts := v1.Options{
ServiceType: repositories.MicrosoftGraphV1BetaServiceType,
UriPrefix: "/v1/microsoft-graph/beta",
UsesCommonTypes: true,
}
serviceRepo := repositories.NewServicesRepository(directory, opts.ServiceType, serviceNames)
v1.Router(r, opts, serviceRepo)
})
router.Route("/v1/microsoft-graph/stable-v1", func(r chi.Router) {
opts := v1.Options{
ServiceType: repositories.MicrosoftGraphV1StableServiceType,
UriPrefix: "/v1/microsoft-graph/stable-v1",
UsesCommonTypes: true,
}
serviceRepo := repositories.NewServicesRepository(directory, opts.ServiceType, serviceNames)
v1.Router(r, opts, serviceRepo)
})
router.Route("/v1/resource-manager", func(r chi.Router) {
opts := v1.Options{
ServiceType: repositories.ResourceManagerServiceType,
UriPrefix: "/v1/resource-manager",
UsesCommonTypes: false,
}
serviceRepo := repositories.NewServicesRepository(directory, opts.ServiceType, serviceNames)
v1.Router(r, opts, serviceRepo)
})
router.Get("/", HomePage)
}
}
9 changes: 4 additions & 5 deletions tools/data-api/internal/endpoints/v1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/hashicorp/pandora/tools/data-api/models"
)

func commonTypes(w http.ResponseWriter, r *http.Request) {
func (api Api) commonTypes(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

opts, ok := ctx.Value("options").(Options)
Expand All @@ -17,7 +17,7 @@ func commonTypes(w http.ResponseWriter, r *http.Request) {
return
}

services, err := servicesRepository.GetAll(opts.ServiceType)
services, err := api.servicesRepository.GetAll(opts.ServiceType)
if err != nil {
internalServerError(w, fmt.Errorf("loading services: %+v", err))
return
Expand Down Expand Up @@ -47,9 +47,8 @@ func commonTypes(w http.ResponseWriter, r *http.Request) {
return
}
payload.Constants[k] = models.ConstantDetails{
CaseInsensitive: v.CaseInsensitive,
Type: models.ConstantType(v.Type),
Values: v.Values,
Type: models.ConstantType(v.Type),
Values: v.Values,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/hashicorp/pandora/tools/data-api/models"
)

func detailsForApiVersion(w http.ResponseWriter, r *http.Request) {
func (api Api) detailsForApiVersion(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

opts, ok := ctx.Value("options").(Options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/hashicorp/pandora/tools/data-api/models"
)

func operationsForApiResource(w http.ResponseWriter, r *http.Request) {
func (api Api) operationsForApiResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

resource, ok := ctx.Value("resourceName").(*repositories.ServiceApiVersionResourceDetails)
Expand Down
30 changes: 19 additions & 11 deletions tools/data-api/internal/endpoints/v1/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,43 @@ type Options struct {
UsesCommonTypes bool
}

func Router(router chi.Router, options Options) {
type Api struct {
servicesRepository repositories.ServicesRepository
}

func Router(router chi.Router, options Options, servicesRepository repositories.ServicesRepository) {
api := Api{
servicesRepository: servicesRepository,
}

router.Use(optionsContext(options))

router.Get("/commonTypes", commonTypes)
router.Get("/commonTypes", api.commonTypes)

router.Route("/services", func(r chi.Router) {
r.Route("/{serviceName}", func(r chi.Router) {
r.Use(serviceRouteContext)
r.Use(api.serviceRouteContext)

r.Get("/", serviceDetails)
r.Get("/", api.serviceDetails)

r.Route("/{serviceApiVersion}", func(r chi.Router) {
r.Use(serviceApiVersionRouteContext)
r.Get("/", detailsForApiVersion)
r.Get("/", api.detailsForApiVersion)

r.Route("/{resourceName}", func(r chi.Router) {
r.Use(apiResourceNameRouteContext)

r.Get("/operations", operationsForApiResource)
r.Get("/schema", schemaForApiResource)
r.Get("/operations", api.operationsForApiResource)
r.Get("/schema", api.schemaForApiResource)
})
})

r.Get("/terraform", terraform)
r.Get("/terraform", api.terraform)
})
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})
r.Get("/", services)
r.Get("/", api.services)
})
}

Expand All @@ -62,7 +70,7 @@ func optionsContext(options Options) func(http.Handler) http.Handler {
}
}

func serviceRouteContext(next http.Handler) http.Handler {
func (api Api) serviceRouteContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
opts, ok := r.Context().Value("options").(Options)
if !ok {
Expand All @@ -76,7 +84,7 @@ func serviceRouteContext(next http.Handler) http.Handler {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

service, err := servicesRepository.GetByName(serviceName, opts.ServiceType)
service, err := api.servicesRepository.GetByName(serviceName, opts.ServiceType)
if err != nil {
internalServerError(w, fmt.Errorf("retrieving service %q: %+v", serviceName, err))
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/hashicorp/pandora/tools/data-api/models"
)

func schemaForApiResource(w http.ResponseWriter, r *http.Request) {
func (api Api) schemaForApiResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

resource, ok := ctx.Value("resourceName").(*repositories.ServiceApiVersionResourceDetails)
Expand All @@ -24,9 +24,8 @@ func schemaForApiResource(w http.ResponseWriter, r *http.Request) {

for k, constant := range resource.Schema.Constants {
constants[k] = models.ConstantDetails{
CaseInsensitive: constant.CaseInsensitive,
Type: models.ConstantType(constant.Type),
Values: constant.Values,
Type: models.ConstantType(constant.Type),
Values: constant.Values,
}
}

Expand Down
2 changes: 1 addition & 1 deletion tools/data-api/internal/endpoints/v1/service_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/hashicorp/pandora/tools/data-api/models"
)

func serviceDetails(w http.ResponseWriter, r *http.Request) {
func (api Api) serviceDetails(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

opts, ok := ctx.Value("options").(Options)
Expand Down
7 changes: 2 additions & 5 deletions tools/data-api/internal/endpoints/v1/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import (
"net/http"

"github.com/go-chi/render"
"github.com/hashicorp/pandora/tools/data-api/internal/repositories"
"github.com/hashicorp/pandora/tools/data-api/models"
)

var servicesRepository = &repositories.ServicesRepositoryImpl{}

func services(w http.ResponseWriter, r *http.Request) {
func (api Api) services(w http.ResponseWriter, r *http.Request) {
opts, ok := r.Context().Value("options").(Options)
if !ok {
internalServerError(w, fmt.Errorf("missing options"))
Expand All @@ -21,7 +18,7 @@ func services(w http.ResponseWriter, r *http.Request) {
payload := models.ServicesDefinition{
Services: make(map[string]models.ServiceSummary, 0),
}
services, err := servicesRepository.GetAll(opts.ServiceType)
services, err := api.servicesRepository.GetAll(opts.ServiceType)
if err != nil {
internalServerError(w, fmt.Errorf("loading services: %+v", err))
return
Expand Down
2 changes: 1 addition & 1 deletion tools/data-api/internal/endpoints/v1/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/hashicorp/pandora/tools/data-api/models"
)

func terraform(w http.ResponseWriter, r *http.Request) {
func (api Api) terraform(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

service, ok := ctx.Value("service").(*repositories.ServiceDetails)
Expand Down
16 changes: 16 additions & 0 deletions tools/data-api/internal/repositories/generator_models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package repositories

// NOTE: this is a copy of models from the package `dataapigeneratorjson` which is in the `importer-rest-api-specs` tool
// these have been duplicated to ease development and will be removed once `dataapigeneratorjson` has been split out

type Constant struct {
Name string `json:"Name"`
Type string `json:"Type"`
Values []Value `json:"Values"`
}

type Value struct {
Key string `json:"Key"`
Value string `json:"Value"`
Description *string `json:"Description,omitempty"`
}
56 changes: 56 additions & 0 deletions tools/data-api/internal/repositories/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package repositories

import (
"fmt"
"io"
"os"
"strings"
)

func listSubDirectories(path string) (*[]string, error) {
directories := make([]string, 0)

contents, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("retrieving list of sub directories under %q: %+v", path, err)
}

for _, c := range contents {
if c.IsDir() {
directories = append(directories, c.Name())
}
}

return &directories, nil
}

func loadJson(path string) (*[]byte, error) {
contents, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("loading %q", path)
}

defer contents.Close()

byteValue, err := io.ReadAll(contents)
if err != nil {
return nil, fmt.Errorf("reading contents of %q", path)
}

return &byteValue, nil
}

// getDefinitionInfo transforms the file names in the api definitions directory into a definition type and a name e.g.
// Model-KeyVaultProperties.json -> type = Model and name = KeyVaultProperties
func getDefinitionInfo(fileName string) (string, string, error) {
if !strings.HasSuffix(fileName, ".json") {
return "", "", fmt.Errorf("file %q has an extensions not supported by the data api", fileName)
}
splitName := strings.Split(fileName, "-")

definitionType := splitName[0]
definitionName := strings.Split(splitName[1], ".")[0]

return definitionType, definitionName, nil

}
Loading

0 comments on commit af5dcda

Please sign in to comment.