This patch refactors the API following the [API Best Practices][api]
documentation. The UpdatePlatform method is modeled after a mutating
operation described [by Netflix][nflx] instead of using a REST resource
representation. This makes it much easier to iterate over the fields
that need to be updated as the PlatformUpdateOperation is a flat data
structure while a Platform resource may have nested fields. Nested
fields are more complicated and less clear to handle with a FieldMask.
This patch also adds a snapckbar message on save. Previously, the save
button didn't give any indication of success or failure. This patch
fixes the problem by adding a snackbar message that pop up at the bottom
of the screen nicely.
When the snackbar message is dismissed or times out the save button is
re-enabled.
[api]: https://protobuf.dev/programming-guides/api/
[nflx]: https://netflixtechblog.com/practical-api-design-at-netflix-part-2-protobuf-fieldmask-for-mutation-operations-2e75e1d230e4
Examples:
FieldMask for ListPlatforms
```
grpcurl -H "x-oidc-id-token: $(holos token)" -d @ ${HOLOS_SERVER##*/} holos.platform.v1alpha1.PlatformService.ListPlatforms <<EOF
{
"org_id": "018f36fb-e3f7-7f7f-a1c5-c85fb735d215",
"field_mask": { "paths": ["id","name"] }
}
EOF
```
```json
{
"platforms": [
{
"id": "018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94",
"name": "bare"
},
{
"id": "018f6b06-9e57-7223-91a9-784e145d998c",
"name": "gary"
},
{
"id": "018f6b06-9e53-7223-8ae1-1ad53d46b158",
"name": "jeff"
},
{
"id": "018f6b06-9e5b-7223-8b8b-ea62618e8200",
"name": "nate"
}
]
}
```
Closes: #171
This patch restructures the bare platform in preparation for a
`Platform` kind of output from CUE in addition to the existing
`BuildPlan` kind.
This patch establishes a pattern where our own CUE defined code goes
into the two CUE module paths:
1. `internal/platforms/cue.mod/gen/github.com/holos-run/holos/api/v1alpha1`
2. `internal/platforms/cue.mod/pkg/github.com/holos-run/holos/api/v1alpha1`
3. `internal/platforms/cue.mod/usr/github.com/holos-run/holos/api/v1alpha1`
The first path is automatically generated from Go structs. The second
path is where we override and provide additional cue level integration.
The third path is reserved for the end user to further refine and
constrain our definitions.
The way we were organizing fields into section broke Formly validation.
This patch fixes the problem by using the recommended approach of
[Nested Forms][1].
This patch also refactors the PlatformService API to clean it up.
GetForm / PutForm are separated from the Platform methods. Similarly
GetModel / PutModel are separated out and are specific to get and put
the model data.
NOTE: I'm not sure we should have separated out the platform service
into it's own protobuf package. Seems maybe unnecessary.
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94"}' jeff.app.dev.k2.holos.run:443 holos.platform.v1alpha1.PlatformService.GetModel
{
"model": {
"org": {
"contactEmail": "platform@openinfrastructure.co",
"displayName": "Open Infrastructure Services LLC",
"domain": "ois.run",
"name": "ois"
},
"privacy": {
"country": "earth",
"regions": [
"us-east-2",
"us-west-2"
]
},
"terms": {
"didAgree": true
}
}
}
[1]: https://formly.dev/docs/examples/other/nested-formly-forms
Problem:
The use of google.protobuf.Any was making it awkward to work with the
data provided by the user. The structure of the form data is defined by
the platform engineer, so the intent of Any was to wrap the data in a
way we can pass over the network and persist in the database.
The escaped JSON encoding was problematic and error prone to decode on
the other end.
Solution:
Define the Platform values as a two level map with string keys, but with
protobuf message fields "sections" and "fields" respectively. Use
google.protobuf.Value from the struct package to encode the actual
value.
Result:
In TypeScript, google.protobuf.Value encodes and decodes easily to a
JSON value. On the go side, connect correctly handles the value as
well.
No more ugly error prone escaping:
```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"'${platformId}'"}' $host holos.v1alpha1.PlatformService.GetConfig
{
"sections": {
"org": {
"fields": {
"contactEmail": "jeff@openinfrastructure.co",
"displayName": "Open Infrastructure Services LLC",
"domain": "ois.run",
"name": "ois"
}
}
}
}
```
This return value is intended to be directly usable in the CUE code, so
we may further nest the values into a platform.spec key.
This patch changes the backend to store the platform config form
definition and the config values supplied by the form as JSON in the
database.
The gRPC API does not change with this patch, but may need to depending
on how this works and how easy it is to evolve the data model and add
features.
This patch is a work in progress wiring up the form to put the values to
the holos server using grpc.
In an effort to simplify the platform configuration, the structure is a
two level map with the top level being configuration sections and the
second level being the fields associated with the config section.
To support multiple kinds of values and field controls, the values are
serialized to JSON for rpc over the network and for storage in the
database. When they values are used, either by the UI or by the `holos
render` command, they're to be unmarshalled and in-lined into the
Platform Config data structure.
Pick back up ensuring the Platform rpc handler correctly encodes and
decodes the structure to the database.
Consider changing the config_form and config_values fields to JSON field
types in the database. It will likely make working with this a lot
easier.
With this patch we're ready to wire up the holos render command to fetch
the platform configuration and create the end to end demo.
Here's essentially what the render command will fetch and lay down as a
json file for CUE:
```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"018f2c4e-ecde-7bcb-8b89-27a99e6cc7a1"}' jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.GetPlatform | jq .platform.config.values
{
"sections": {
"org": {
"values": {
"contactEmail": "\"platform@openinfrastructure.co\"",
"displayName": "\"Open Infrastructure Services LLC\"",
"domain": "\"ois.run\"",
"name": "\"ois\""
}
}
}
}
```
This patch adds a /platform/:id route path to a PlatformDetail
component. The platform detail component calls the GetPlatform method
given the platform ID and renders the platform config form on the detail
tab.
The submit button is not yet wired up.
The API for adding platforms changes, allowing raw json bytes using the
RawConfig. The raw bytes are not presented on the read path though,
calling GetPlatforms provides the platform and the config form inline in
the response.
Use the `raw_config` field instead of `config` when creating the form
data.
```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d @ jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.AddPlatform <<EOF
{
"platform": {
"org_id": "018f27cd-e5ac-7f98-bfe1-2dbab208a48c",
"name": "bare2",
"raw_config": {
"form": "$(cue export ./forms/platform/ --out json | jq -cM | base64 -w0)"
}
}
}
EOF
```
This patch adds 4 fields to the Platform table:
1. Config Form represents the JSON FormlyFieldConfig for the UI.
2. Config CUE represents the CUE file containing a definition the
Config Values must unify with.
3. Config Definition is the CUE definition variable name used to unify
the values with the cue code. Should be #PlatformSpec in most
cases.
4. Config Values represents the JSON values provided by the UI.
The use case is the platform engineer defines the #PlatformSpec in cue,
and provides the form field config. The platform engineer then provides
1-3 above when adding or updating a Platform.
The UI then presents the form to the end user and provides values for 4
when the user submits the form.
This patch also refactors the AddPlatform method to accept a Platform
message. To do so we make the id field optional since it is server
assigned.
The patch also adds a database constraint to ensure platform names are
unique within the scope of an organization.
Results:
Note how the CUE representation of the Platform Form is exported to JSON
then converted to a base64 encoded string, which is the protobuf JSON
representation of a bytes[] value.
```
grpcurl -H "x-oidc-id-token: $(holos token)" -d @ jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.AddPlatform <<EOF
{
"platform": {
"id": "0d3dc0c0-bbc8-41f8-8c6e-75f0476509d6",
"org_id": "018f27cd-e5ac-7f98-bfe1-2dbab208a48c",
"name": "bare",
"config": {
"form": "$(cd internal/platforms/bare && cue export ./forms/platform/ --out json | jq -cM | base64 -w0)"
}
}
}
EOF
```
Note the requested platform ID is ignored.
```
{
"platforms": [
{
"id": "018f2af9-f7ba-772a-9db6-f985ece8fed1",
"timestamps": {
"createdAt": "2024-04-29T17:49:36.058379Z",
"updatedAt": "2024-04-29T17:49:36.058379Z"
},
"name": "bare",
"creator": {
"id": "018f27cd-e591-7f98-a9d2-416167282d37"
},
"config": {
"form": "eyJhcGlWZXJzaW9uIjoiZm9ybXMuaG9sb3MucnVuL3YxYWxwaGExIiwia2luZCI6IlBsYXRmb3JtRm9ybSIsIm1ldGFkYXRhIjp7Im5hbWUiOiJiYXJlIn0sInNwZWMiOnsic2VjdGlvbnMiOlt7Im5hbWUiOiJvcmciLCJkaXNwbGF5TmFtZSI6Ik9yZ2FuaXphdGlvbiIsImRlc2NyaXB0aW9uIjoiT3JnYW5pemF0aW9uIGNvbmZpZyB2YWx1ZXMgYXJlIHVzZWQgdG8gZGVyaXZlIG1vcmUgc3BlY2lmaWMgY29uZmlndXJhdGlvbiB2YWx1ZXMgdGhyb3VnaG91dCB0aGUgcGxhdGZvcm0uIiwiZmllbGRDb25maWdzIjpbeyJrZXkiOiJuYW1lIiwidHlwZSI6ImlucHV0IiwicHJvcHMiOnsibGFiZWwiOiJOYW1lIiwicGxhY2Vob2xkZXIiOiJleGFtcGxlIiwiZGVzY3JpcHRpb24iOiJETlMgbGFiZWwsIGUuZy4gJ2V4YW1wbGUnIiwicmVxdWlyZWQiOnRydWV9fSx7ImtleSI6ImRvbWFpbiIsInR5cGUiOiJpbnB1dCIsInByb3BzIjp7ImxhYmVsIjoiRG9tYWluIiwicGxhY2Vob2xkZXIiOiJleGFtcGxlLmNvbSIsImRlc2NyaXB0aW9uIjoiRE5TIGRvbWFpbiwgZS5nLiAnZXhhbXBsZS5jb20nIiwicmVxdWlyZWQiOnRydWV9fSx7ImtleSI6ImRpc3BsYXlOYW1lIiwidHlwZSI6ImlucHV0IiwicHJvcHMiOnsibGFiZWwiOiJEaXNwbGF5IE5hbWUiLCJwbGFjZWhvbGRlciI6IkV4YW1wbGUgT3JnYW5pemF0aW9uIiwiZGVzY3JpcHRpb24iOiJEaXNwbGF5IG5hbWUsIGUuZy4gJ0V4YW1wbGUgT3JnYW5pemF0aW9uJyIsInJlcXVpcmVkIjp0cnVlfX0seyJrZXkiOiJjb250YWN0RW1haWwiLCJ0eXBlIjoiaW5wdXQiLCJwcm9wcyI6eyJsYWJlbCI6IkNvbnRhY3QgRW1haWwiLCJwbGFjZWhvbGRlciI6InBsYXRmb3JtLXRlYW1AZXhhbXBsZS5jb20iLCJkZXNjcmlwdGlvbiI6IlRlY2huaWNhbCBjb250YWN0IGVtYWlsIGFkZHJlc3MiLCJyZXF1aXJlZCI6dHJ1ZX19XX1dfX0K"
}
}
]
}
```
This patch adds a basic AddPlatform method that adds a platform with a
name and a display name.
Next steps are to add fields for the Platform Config Form definition and
the Platform Config values submitted from the form.
Next step: AddPlatform
Also consider extracting the queries to get the requested org_id to a
helper function. This will likely eventually move to an interceptor
because every request is org scoped and needs authorization checks
against the org.
```
grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"org_id":"018f27cd-e5ac-7f98-bfe1-2dbab208a48c"}' jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.GetPlatforms
```
Problem:
The default gateway in one cluster gets server entries for all hosts in
the problem. This makes the list unnecessarily large with entries for
clusters that should not be handled on the current cluster.
For example, the k2 cluster has gateway entries to route hosts for k1,
k3, k4, k5, etc...
Solution:
Add a field to the CertInfo definition representing which clusters the
host is valid on.
Result:
Hosts which are valid on all clusters, e.g. login.ois.run, have all
project clusters added to the clusters field of the CertInfo. Hosts
which are valid on a single cluster have the coresponding single entry
added.
When building resources, holos components should check if `#ClusterName`
is a valid field of the CertInfo.clusters field. If so, the host is
valid for the current cluster. If not, the host should be omitted from
the current cluster.