Skip to content

Commit

Permalink
Add API types for configuration metadata (#14132)
Browse files Browse the repository at this point in the history
This is so that we can manage configuration metadata with API extensions
as we do with any other API response over `/1.0`

* Adds an API extension for adding entity metadata to `GET
/1.0/configuration/metadata`.
* Adds types under `shared/api` for configuration metadata.
* Updates entity metadata such that each entity maps to a JSON object,
with `requires_project` and `entitlements` fields.
* Enforces that generated metadata conforms to the new API type.
* Updates the swagger doc for `GET /1.0/configuration/metadata` to
include the newly defined type.
  • Loading branch information
tomponline committed Sep 26, 2024
2 parents cb8a515 + d594de4 commit d16275f
Show file tree
Hide file tree
Showing 8 changed files with 877 additions and 625 deletions.
5 changes: 5 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2475,3 +2475,8 @@ for a project, the pool is excluded from `lxc storage list` in that project.

Adds a new {config:option}`instance-miscellaneous:ubuntu_pro.guest_attach` configuration option for instances.
When set to `on`, if the host has guest attachment enabled, the guest can request a guest token for Ubuntu Pro via `devlxd`.

## `metadata_configuration_entity_types`

This adds entity type metadata to `GET /1.0/metadata/configuration`.
The entity type metadata is a JSON object under the `entities` key.
108 changes: 106 additions & 2 deletions doc/rest-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2774,6 +2774,111 @@ definitions:
title: InstancesPut represents the fields available for a mass update.
type: object
x-go-package: github.com/canonical/lxd/shared/api
MetadataConfiguration:
properties:
configs:
additionalProperties:
additionalProperties:
$ref: '#/definitions/MetadataConfigurationConfigKeys'
type: object
description: Configs contains all server configuration metadata.
type: object
x-go-name: Configs
entities:
additionalProperties:
$ref: '#/definitions/MetadataConfigurationEntity'
description: |-
Entities contains all authorization related metadata.

API extension: metadata_configuration_entity_types
type: object
x-go-name: Entities
title: MetadataConfiguration contains metadata about the LXD server configuration options.
type: object
x-go-package: github.com/canonical/lxd/shared/api
MetadataConfigurationConfigKey:
properties:
condition:
description: Condition describes conditions under which the configuration key can be applied.
example: Virtual machines only.
type: string
x-go-name: Condition
defaultdesc:
description: DefaultDescription contains a description of the configuration key.
example: A general description of a configuration key.
type: string
x-go-name: DefaultDescription
longdesc:
description: LongDescription contains a long-form description of the configuration key.
example: A much more in-depth description of the configuration key, including where and how it is used.
type: string
x-go-name: LongDescription
managed:
description: Managed describes whether the configuration key is managed by LXD.
example: yes.
type: string
x-go-name: Managed
required:
description: Required describes conditions under which the configuration key is required.
example: On device creation.
type: string
x-go-name: Required
shortdesc:
description: ShortDescription contains a short-form description of the configuration key.
example: A key for doing X.
type: string
x-go-name: ShortDescription
type:
description: Type describes the type of the key.
example: Comma delimited CIDR format subnets.
type: string
x-go-name: Type
title: MetadataConfigurationConfigKey contains metadata about a LXD server configuration option.
type: object
x-go-package: github.com/canonical/lxd/shared/api
MetadataConfigurationConfigKeys:
properties:
keys:
items:
additionalProperties:
$ref: '#/definitions/MetadataConfigurationConfigKey'
type: object
type: array
x-go-name: Keys
title: MetadataConfigurationConfigKeys contains metadata about LXD server configuration options.
type: object
x-go-package: github.com/canonical/lxd/shared/api
MetadataConfigurationEntity:
properties:
entitlements:
description: Entitlements contains a list of entitlements that apply to a specific entity type.
items:
$ref: '#/definitions/MetadataConfigurationEntityEntitlement'
type: array
x-go-name: Entitlements
project_specific:
description: ProjectSpecific indicates whether the entity is project specific.
example: true
type: boolean
x-go-name: ProjectSpecific
title: MetadataConfigurationEntity contains metadata about LXD server entities and available entitlements for authorization.
type: object
x-go-package: github.com/canonical/lxd/shared/api
MetadataConfigurationEntityEntitlement:
properties:
description:
description: Description describes the entitlement.
example: Grants permission to do X, Y, and Z.
type: string
x-go-name: Description
name:
description: Name contains the name of the entitlement.
example: can_edit
type: string
x-go-name: Name
title: MetadataConfigurationEntityEntitlement contains metadata about a LXD server entitlement.
type: object
x-go-package: github.com/canonical/lxd/shared/api
Network:
description: Network represents a LXD network
properties:
Expand Down Expand Up @@ -11381,8 +11486,7 @@ paths:
description: Sync response
properties:
metadata:
description: The generated metadata configuration
type: string
$ref: '#/definitions/MetadataConfiguration'
status:
description: Status description
example: Success
Expand Down
21 changes: 20 additions & 1 deletion lxd/auth/generate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"strings"
"unicode"

"github.com/canonical/lxd/shared/api"
"github.com/canonical/lxd/shared/entity"
"github.com/canonical/lxd/shared/logger"
)
Expand Down Expand Up @@ -92,7 +93,25 @@ func main() {
return fmt.Errorf("Failed to close OpenFGA model file: %w", err)
}

err = json.NewEncoder(os.Stdout).Encode(entityToEntitlements)
metadata := make(map[string]api.MetadataConfigurationEntity)
for eType, entitlements := range entityToEntitlements {
projectSpecific, _ := eType.RequiresProject()

apiEntitlements := make([]api.MetadataConfigurationEntityEntitlement, 0, len(entitlements))
for _, e := range entitlements {
apiEntitlements = append(apiEntitlements, api.MetadataConfigurationEntityEntitlement{
Name: e.Relation,
Description: e.Description,
})
}

metadata[string(eType)] = api.MetadataConfigurationEntity{
ProjectSpecific: projectSpecific,
Entitlements: apiEntitlements,
}
}

err = json.NewEncoder(os.Stdout).Encode(metadata)
if err != nil {
return fmt.Errorf("Failed to write entitlement json to stdout: %w", err)
}
Expand Down
6 changes: 3 additions & 3 deletions lxd/documentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"

"github.com/canonical/lxd/lxd/response"
"github.com/canonical/lxd/shared/api"
)

var metadataConfigurationCmd = APIEndpoint{
Expand Down Expand Up @@ -46,8 +47,7 @@ var generatedDoc embed.FS
// description: Status code
// example: 200
// metadata:
// type: string
// description: The generated metadata configuration
// $ref: "#/definitions/MetadataConfiguration"
// "403":
// $ref: "#/responses/Forbidden"
// "500":
Expand All @@ -58,7 +58,7 @@ func metadataConfigurationGet(d *Daemon, r *http.Request) response.Response {
return response.SmartError(err)
}

var data map[string]any
var data api.MetadataConfiguration
err = json.Unmarshal(file, &data)
if err != nil {
return response.SmartError(err)
Expand Down
52 changes: 14 additions & 38 deletions lxd/lxd-metadata/lxd_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import (
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"

"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"

"gopkg.in/yaml.v2"
)
Expand All @@ -43,37 +43,6 @@ type doc struct {
Entities json.RawMessage `json:"entities"`
}

// detectType detects the type of a string and returns the corresponding value.
func detectType(s string) any {
i, err := strconv.Atoi(s)
if err == nil {
return i
}

b, err := strconv.ParseBool(s)
if err == nil {
return b
}

f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f
}

t, err := time.Parse(time.RFC3339, s)
if err == nil {
return t
}

// special characters handling
if s == "-" {
return ""
}

// If all conversions fail, it's a string
return s
}

// sortConfigKeys alphabetically sorts the entries by key (config option key) within each config group in an entity.
func sortConfigKeys(allEntries map[string]map[string]map[string][]any) {
for _, entityValue := range allEntries {
Expand Down Expand Up @@ -285,7 +254,7 @@ func parse(path string, outputJSONPath string, excludedPaths []string, substitut
continue
}

configKeyEntry[metadataMap["key"]].(map[string]any)[dataKVMatch[1]] = detectType(dataKVMatch[2])
configKeyEntry[metadataMap["key"]].(map[string]any)[dataKVMatch[1]] = dataKVMatch[2]
}

// There can be multiple entities for a given group
Expand Down Expand Up @@ -340,6 +309,13 @@ func parse(path string, outputJSONPath string, excludedPaths []string, substitut
return nil, fmt.Errorf("Error while marshaling project documentation: %v", err)
}

// Validate that what we've generated is valid against our API definition.
var metadataConfiguration api.MetadataConfiguration
err = json.Unmarshal(data, &metadataConfiguration)
if err != nil {
return nil, fmt.Errorf("Failed to unmarshal generated metadata into MetadataConfiguration API type: %w", err)
}

if outputJSONPath != "" {
buf := bytes.NewBufferString("")
_, err = buf.Write(data)
Expand Down Expand Up @@ -494,7 +470,7 @@ func writeDocFile(inputJSONPath, outputTxtPath string) error {
}
}

entities := make(map[string][]map[string]string)
entities := make(map[string]api.MetadataConfigurationEntity)
err = json.Unmarshal(jsonDoc.Entities, &entities)
if err != nil {
return err
Expand All @@ -508,11 +484,11 @@ func writeDocFile(inputJSONPath, outputTxtPath string) error {
sort.Strings(sortedEntityNames)

for _, entityName := range sortedEntityNames {
entitlements := entities[entityName]
entity := entities[entityName]
buffer.WriteString(fmt.Sprintf("<!-- entity group %s start -->\n", entityName))
for _, entitlement := range entitlements {
buffer.WriteString(fmt.Sprintf("`%s`\n", entitlement["name"]))
buffer.WriteString(fmt.Sprintf(": %s\n\n", entitlement["description"]))
for _, entitlement := range entity.Entitlements {
buffer.WriteString(fmt.Sprintf("`%s`\n", entitlement.Name))
buffer.WriteString(fmt.Sprintf(": %s\n\n", entitlement.Description))
}

buffer.WriteString("\n")
Expand Down
Loading

0 comments on commit d16275f

Please sign in to comment.