diff --git a/catalog/client.go b/catalog/client.go index 1241387..92b5c22 100644 --- a/catalog/client.go +++ b/catalog/client.go @@ -1,9 +1,10 @@ package catalog import ( + "bytes" "context" + "encoding/json" "fmt" - "io" "net/http" "net/url" ) @@ -68,34 +69,61 @@ func (c *Client) get( path string, query url.Values, fn func(*http.Response) error, -) error { - return c.execute(ctx, http.MethodGet, path, query, nil, fn) +) (err error) { + const method = http.MethodGet + defer func() { + if err != nil { + err = fmt.Errorf("%s %s: %w", method, path, err) + } + }() + requestURL, err := url.Parse(c.config.baseURL + path) + if err != nil { + return err + } + if len(query) > 0 { + requestURL.RawQuery = query.Encode() + } + httpRequest, err := http.NewRequestWithContext(ctx, method, requestURL.String(), nil) + if err != nil { + return err + } + httpResponse, err := c.httpClient.Do(httpRequest) + if err != nil { + return err + } + defer func() { + _ = httpResponse.Body.Close() + }() + if httpResponse.StatusCode != http.StatusOK { + return newStatusError(httpResponse) + } + if fn != nil { + return fn(httpResponse) + } + return nil } -func (c *Client) execute( +func (c *Client) post( ctx context.Context, - method string, path string, - query url.Values, - body io.Reader, + body any, fn func(*http.Response) error, ) (err error) { + const method = http.MethodPost defer func() { if err != nil { err = fmt.Errorf("%s %s: %w", method, path, err) } }() - requestURL, err := url.Parse(c.config.baseURL + path) + bodyData, err := json.Marshal(body) if err != nil { return err } - if len(query) > 0 { - requestURL.RawQuery = query.Encode() - } - httpRequest, err := http.NewRequestWithContext(ctx, method, requestURL.String(), body) + httpRequest, err := http.NewRequestWithContext(ctx, method, c.config.baseURL+path, bytes.NewReader(bodyData)) if err != nil { return err } + httpRequest.Header.Set("Content-Type", "application/json") httpResponse, err := c.httpClient.Do(httpRequest) if err != nil { return err diff --git a/catalog/client_entities_batchgetbyrefs.go b/catalog/client_entities_batchgetbyrefs.go new file mode 100644 index 0000000..62a7729 --- /dev/null +++ b/catalog/client_entities_batchgetbyrefs.go @@ -0,0 +1,59 @@ +package catalog + +import ( + "bytes" + "context" + "encoding/json" + "net/http" +) + +// BatchGetEntitiesByRefsRequest is the request to the [Client.BatchGetEntitiesByRefs] method. +type BatchGetEntitiesByRefsRequest struct { + // EntityRefs to fetch. + // See: https://backstage.io/docs/features/software-catalog/references + EntityRefs []string `json:"entityRefs"` + + // Fields to fetch. + Fields []string `json:"fields,omitempty"` +} + +// BatchGetEntitiesByRefsResponse is the response from the [Client.BatchGetEntitiesByRefs] method. +type BatchGetEntitiesByRefsResponse struct { + // Entities returned. + // Has the same length and the same order as the input entityRefs array. + Entities []*Entity `json:"items"` +} + +// BatchGetEntitiesByRefs gets an entity by its kind, namespace and name. +// See: https://backstage.io/docs/features/software-catalog/software-catalog-api#post-entitiesby-refs +func (c *Client) BatchGetEntitiesByRefs( + ctx context.Context, + request *BatchGetEntitiesByRefsRequest, +) (*BatchGetEntitiesByRefsResponse, error) { + const path = "/api/catalog/entities/by-refs" + var responseBody struct { + RawEntities []json.RawMessage `json:"items"` + } + if err := c.post(ctx, path, request, func(response *http.Response) error { + return json.NewDecoder(response.Body).Decode(&responseBody) + }); err != nil { + return nil, err + } + response := BatchGetEntitiesByRefsResponse{ + Entities: make([]*Entity, 0, len(responseBody.RawEntities)), + } + for _, rawEntity := range responseBody.RawEntities { + if bytes.Equal(rawEntity, []byte("null")) { + response.Entities = append(response.Entities, nil) + } else { + entity := Entity{ + Raw: rawEntity, + } + if err := json.Unmarshal(rawEntity, &entity); err != nil { + return nil, err + } + response.Entities = append(response.Entities, &entity) + } + } + return &response, nil +} diff --git a/cmd/backstage/main.go b/cmd/backstage/main.go index f3c0fef..3cd5f56 100644 --- a/cmd/backstage/main.go +++ b/cmd/backstage/main.go @@ -110,6 +110,7 @@ func newEntitiesCommand() *cobra.Command { cmd.AddCommand(newEntitiesGetByUIDCommand()) cmd.AddCommand(newEntitiesGetByNameCommand()) cmd.AddCommand(newEntitiesDeleteByUIDCommand()) + cmd.AddCommand(newEntitiesBatchGetByRefsCommand()) return cmd } @@ -217,6 +218,37 @@ func newEntitiesDeleteByUIDCommand() *cobra.Command { return cmd } +func newEntitiesBatchGetByRefsCommand() *cobra.Command { + cmd := newCommand() + cmd.Use = "batch-get-by-refs" + cmd.Short = "Batch get entities by their refs" + entityRefs := cmd.Flags().StringSlice("entity-refs", nil, "refs of the entities to get") + _ = cmd.MarkFlagRequired("entity-refs") + fields := cmd.Flags().StringSlice("fields", nil, "select only parts of each entity") + cmd.RunE = func(cmd *cobra.Command, args []string) error { + client, err := newCatalogClient() + if err != nil { + return err + } + response, err := client.BatchGetEntitiesByRefs(cmd.Context(), &catalog.BatchGetEntitiesByRefsRequest{ + EntityRefs: *entityRefs, + Fields: *fields, + }) + if err != nil { + return err + } + for _, entity := range response.Entities { + if entity != nil { + printRawJSON(cmd, entity.Raw) + } else { + cmd.Println("null") + } + } + return nil + } + return cmd +} + func printRawJSON(cmd *cobra.Command, raw json.RawMessage) { var indented bytes.Buffer indented.Grow(len(raw) * 2)