Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 20 additions & 91 deletions doc/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,7 @@ <h2>Table of Contents</h2>


<li>
<a href="#metalstack%2fapi%2fv2%2fmethods.proto">metalstack/api/v2/methods.proto</a>
<a href="#metalstack%2fapi%2fv2%2fmethod.proto">metalstack/api/v2/method.proto</a>
<ul>

<li>
Expand Down Expand Up @@ -2700,10 +2700,9 @@ <h3 id="buf.validate.FieldRules">FieldRules</h3>

```proto
message UpdateRequest {
// The uri rule only applies if the field is populated and not an empty
// string.
optional string url = 1 [
(buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE,
// The uri rule only applies if the field is not an empty string.
string url = 1 [
(buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
(buf.validate.field).string.uri = true
];
}
Expand Down Expand Up @@ -3815,21 +3814,6 @@ <h3 id="buf.validate.MessageRules">MessageRules</h3>
</thead>
<tbody>

<tr>
<td>disabled</td>
<td><a href="#bool">bool</a></td>
<td>optional</td>
<td><p>`disabled` is a boolean flag that, when set to true, nullifies any validation rules for this message.
This includes any fields within the message that would otherwise support validation.

```proto
message MyMessage {
// validation will be bypassed for this message
option (buf.validate.message).disabled = true;
}
``` </p></td>
</tr>

<tr>
<td>cel</td>
<td><a href="#buf.validate.Rule">Rule</a></td>
Expand Down Expand Up @@ -3874,7 +3858,7 @@ <h3 id="buf.validate.MessageRules">MessageRules</h3>
silently ignored when unmarshalling, with only the last field being set when
unmarshalling completes.

Note that adding a field to a `oneof` will also set the IGNORE_IF_UNPOPULATED on the fields. This means
Note that adding a field to a `oneof` will also set the IGNORE_IF_ZERO_VALUE on the fields. This means
only the field that is set will be validated and the unset fields are not validated according to the field rules.
This behavior can be overridden by setting `ignore` against a field.

Expand Down Expand Up @@ -6076,77 +6060,22 @@ <h3 id="buf.validate.Ignore">Ignore</h3>
</tr>

<tr>
<td>IGNORE_IF_UNPOPULATED</td>
<td>IGNORE_IF_ZERO_VALUE</td>
<td>1</td>
<td><p>Ignore rules if the field is unset, also for fields that don&#39;t track
presence.

In proto3, repeated fields, map fields, and fields with scalar types don&#39;t
track presence. Consequently, the following fields are only validated if
they are set:

```proto
syntax=&#34;proto3&#34;;

message RulesApplyIfSet {
// `string.email` is ignored for the empty string.
string link = 1 [
(buf.validate.field).string.email = true,
(buf.validate.field).ignore = IGNORE_IF_UNPOPULATED
];
// `int32.gte` is ignored for the zero value.
int32 age = 2 [
(buf.validate.field).int32.gte = 21,
(buf.validate.field).ignore = IGNORE_IF_UNPOPULATED
];
// `repeated.min_items` is ignored if the list is empty.
repeated string labels = 3 [
(buf.validate.field).repeated.min_items = 3,
(buf.validate.field).ignore = IGNORE_IF_UNPOPULATED
];
}
```

For fields that don&#39;t track presence, the field&#39;s value determines
whether the field is set and rules apply:

- For string and bytes, an empty value is ignored.
- For bool, false is ignored.
- For numeric types, zero is ignored.
- For enums, the first defined enum value is ignored.
- For repeated fields, an empty list is ignored.
- For map fields, an empty map is ignored.
- For message fields, absence of the message (typically a null-value) is
ignored.
<td><p>Ignore rules if the field is unset, or set to the zero value.

The zero value depends on the field type:
- For strings, the zero value is the empty string.
- For bytes, the zero value is empty bytes.
- For bool, the zero value is false.
- For numeric types, the zero value is zero.
- For enums, the zero value is the first defined enum value.
- For repeated fields, the zero is an empty list.
- For map fields, the zero is an empty map.
- For message fields, absence of the message (typically a null-value) is considered zero value.

For fields that track presence (e.g. adding the `optional` label in proto3),
behavior is the same as the default `IGNORE_UNSPECIFIED`.

To learn which fields track presence, see the
[Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).</p></td>
</tr>

<tr>
<td>IGNORE_IF_DEFAULT_VALUE</td>
<td>2</td>
<td><p>Ignore rules if the field is unset, or set to the default value.

The default value depends on the field type:
- For strings, the default value is the empty string.
- For bytes, the default value is empty bytes.
- For bool, the default value is false.
- For numeric types, the default value is zero.
- For enums, the default value is the first defined enum value.
- For repeated fields, the default is an empty list.
- For map fields, the default is an empty map.
- For message fields, Protovalidate treats the empty message as the
default value. All rules of the referenced message are ignored as well.

For some fields, the default value can be overridden with the Protobuf
`default` option.

For fields that don&#39;t track presence and don&#39;t have the `default` option,
behavior is the same as the default `IGNORE_UNSPECIFIED`.</p></td>
this a no-op and behavior is the same as the default `IGNORE_UNSPECIFIED`.</p></td>
</tr>

<tr>
Expand All @@ -6159,7 +6088,7 @@ <h3 id="buf.validate.Ignore">Ignore</h3>

```proto
message MyMessage {
// The field&#39;s rules will always be ignored, including any validation&#39;s
// The field&#39;s rules will always be ignored, including any validations
// on value&#39;s fields.
MyOtherMessage value = 1 [
(buf.validate.field).ignore = IGNORE_ALWAYS];
Expand Down Expand Up @@ -14163,7 +14092,7 @@ <h3 id="metalstack.api.v2.HealthService">HealthService</h3>


<div class="file-heading">
<h2 id="metalstack/api/v2/methods.proto">metalstack/api/v2/methods.proto</h2><a href="#title">Top</a>
<h2 id="metalstack/api/v2/method.proto">metalstack/api/v2/method.proto</h2><a href="#title">Top</a>
</div>
<p></p>

Expand Down
45 changes: 24 additions & 21 deletions examples/python/ip-list.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
#!/usr/bin/env python

from connecpy.context import ClientContext
from connecpy.exceptions import ConnecpyServerException
import os
import sys

from connecpy.exceptions import ConnecpyServerException

from metalstack.api.v2 import ip_pb2, ip_connecpy
from metalstack.client import client as apiclient
from metalstack.api.v2 import ip_pb2
from metalstack.admin.v2 import network_pb2

timeout_s = 5
baseurl = os.environ['METAL_APISERVER_URL']
token = os.environ['API_TOKEN']
project = os.environ['PROJECT_ID']

def main():
with ip_connecpy.IPServiceClient(baseurl, timeout=timeout_s) as client:
try:
response = client.List(
ctx=ClientContext(),
request=ip_pb2.IPServiceListRequest(project=project),
headers={
"Authorization": "Bearer " + token,
}
)
for ip in response.ips:
print(ip.ip, ip.name, ip.project, ip.network)
except ConnecpyServerException as e:
print(e.code, e.message, e.to_dict())


if __name__ == "__main__":
main()
client = apiclient.Client(baseurl=baseurl, token=token, timeout=timeout_s)

try:
resp = client.apiv2().ip().List(request=ip_pb2.IPServiceListRequest(
project=project))
except ConnecpyServerException as e:
print(e.code, e.message, e.to_dict())
sys.exit(1)


for ip in resp.ips:
print(ip.ip, ip.name, ip.project, ip.network)

resp = client.adminv2().network().List(
request=network_pb2.NetworkServiceListRequest())

for nw in resp.networks:
print(nw.id, nw.name, nw.project)
27 changes: 22 additions & 5 deletions generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ var (
mockClientTpl string
//go:embed go_client.tpl
clientTpl string
//go:embed python_client.tpl
pythonClientTpl string
)

type api struct {
Expand Down Expand Up @@ -70,6 +72,11 @@ func main() {
if err != nil {
panic(err)
}

err = writePythonTemplate("../python/metalstack/client/client.py", pythonClientTpl, svcs)
if err != nil {
panic(err)
}
}

func servicePermissions(root string) (*permissions.ServicePermissions, error) {
Expand Down Expand Up @@ -116,22 +123,17 @@ func servicePermissions(root string) (*permissions.ServicePermissions, error) {
}

for _, filename := range files {
filename := filename
fd, err := protoparser.Parse(filename)
if err != nil {
return nil, err
}
for _, serviceDesc := range fd.GetService() {
serviceDesc := serviceDesc
services = append(services, fmt.Sprintf("%s.%s", *fd.Package, *serviceDesc.Name))
for _, method := range serviceDesc.GetMethod() {
method := method
methodName := fmt.Sprintf("/%s.%s/%s", *fd.Package, *serviceDesc.Name, *method.Name)
methodOpts := method.Options.GetUninterpretedOption()
for _, methodOpt := range methodOpts {
methodOpt := methodOpt
for _, namePart := range methodOpt.Name {
namePart := namePart
if !*namePart.IsExtension {
continue
}
Expand Down Expand Up @@ -286,3 +288,18 @@ func writeTemplate(dest, text string, data any) error {

return os.WriteFile(dest, p, 0755) // nolint:gosec
}
func writePythonTemplate(dest, text string, data any) error {
t, err := template.New("").Funcs(sprig.FuncMap()).Parse(text)
if err != nil {
return err
}

var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return err
}

fmt.Println("wrote " + dest)

return os.WriteFile(dest, buf.Bytes(), 0755) // nolint:gosec
}
36 changes: 36 additions & 0 deletions generate/python_client.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Code generated by generate.go. DO NOT EDIT.

import httpx

{{ range $name, $api := . -}}
{{ range $svc := $api.Services -}}
import metalstack.{{ $name | trimSuffix "v2" }}.v2.{{ $svc | trimSuffix "Service" | lower }}_connecpy as {{ $name | trimSuffix "v2" }}_{{ $svc | trimSuffix "Service" | lower }}_connecpy
{{ end }}
{{ end }}

class Client:
def __init__(self, baseurl: str, token: str, timeout: int = 10):
self._baseurl = baseurl

headers = {}
if token:
headers["Authorization"] = "Bearer " + token

self._session = httpx.Client(headers=headers, timeout=timeout)

{{ range $name, $api := . }}
def {{ $name | lower }}(self):
return self._{{ $name | title }}(baseurl=self._baseurl, session=self._session)
{{ end }}

{{ range $name, $api := . }}
class _{{ $name | title }}:
def __init__(self, baseurl: str, session=None):
self._baseurl = baseurl
self._session = session

{{ range $svc := $api.Services }}
def {{ $svc | trimSuffix "Service" | lower }}(self):
return {{ $name | trimSuffix "v2" }}_{{ $svc | trimSuffix "Service" | lower }}_connecpy.{{ $svc }}Client(address=self._baseurl, session=self._session)
{{ end }}
{{ end }}
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ module github.com/metal-stack/api
go 1.24.0

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1
buf.build/go/protovalidate v0.13.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1
buf.build/go/protovalidate v0.14.0
connectrpc.com/connect v1.18.1
github.com/bufbuild/protocompile v0.14.1
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/go-cmp v0.7.0
github.com/klauspost/connect-compress/v2 v2.0.0
github.com/stretchr/testify v1.10.0
Expand All @@ -25,10 +25,10 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/text v0.27.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
24 changes: 12 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1 h1:6tCo3lsKNLqUjRPhyc8JuYWYUiQkulufxSDOfG1zgWQ=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
buf.build/go/protovalidate v0.13.1 h1:6loHDTWdY/1qmqmt1MijBIKeN4T9Eajrqb9isT1W1s8=
buf.build/go/protovalidate v0.13.1/go.mod h1:C/QcOn/CjXRn5udUwYBiLs8y1TGy7RS+GOSKqjS77aU=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1 h1:Lg6klmCi3v7VvpqeeLEER9/m5S8y9e9DjhqQnSCNy4k=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
buf.build/go/protovalidate v0.14.0 h1:kr/rC/no+DtRyYX+8KXLDxNnI1rINz0imk5K44ZpZ3A=
buf.build/go/protovalidate v0.14.0/go.mod h1:+F/oISho9MO7gJQNYC2VWLzcO1fTPmaTA08SDYJZncA=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
Expand All @@ -16,8 +16,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
Expand Down Expand Up @@ -50,16 +50,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
Loading
Loading