-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: Bot instances advanced filter #59374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2dc2ac2
5af5151
b295941
144c659
ce44caf
877813f
3562bf5
3bd44e6
cc720dd
3b4c162
48207a6
88a29f5
77faebc
3cde394
ba5d656
55d2cca
09a48d4
044bef3
04d30a4
2fbd797
019340f
d1fb2f4
129a4a5
e153fa9
4717dba
15e32e8
322a395
38f5d66
bc59499
e4b2bec
d2f4da2
56689e1
0c7add8
7fcfc0c
95734b1
1804b18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // Teleport | ||
| // Copyright (C) 2025 Gravitational, Inc. | ||
| // | ||
| // This program is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU Affero General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU Affero General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU Affero General Public License | ||
| // along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| package expression | ||
|
|
||
| import ( | ||
| headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" | ||
| machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" | ||
| ) | ||
|
|
||
| // Environment in which expressions will be evaluated. | ||
| type Environment struct { | ||
| Metadata *headerv1.Metadata | ||
| Spec *machineidv1.BotInstanceSpec | ||
| LatestHeartbeat *machineidv1.BotInstanceStatusHeartbeat | ||
| LatestAuthentication *machineidv1.BotInstanceStatusAuthentication | ||
| } | ||
|
|
||
| func (e *Environment) GetMetadata() *headerv1.Metadata { | ||
| if e == nil { | ||
| return nil | ||
| } | ||
| return e.Metadata | ||
| } | ||
|
|
||
| func (e *Environment) GetSpec() *machineidv1.BotInstanceSpec { | ||
| if e == nil { | ||
| return nil | ||
| } | ||
| return e.Spec | ||
| } | ||
|
|
||
| func (e *Environment) GetLatestHeartbeat() *machineidv1.BotInstanceStatusHeartbeat { | ||
| if e == nil { | ||
| return nil | ||
| } | ||
| return e.LatestHeartbeat | ||
| } | ||
|
|
||
| func (e *Environment) GetLatestAuthentication() *machineidv1.BotInstanceStatusAuthentication { | ||
| if e == nil { | ||
| return nil | ||
| } | ||
| return e.LatestAuthentication | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| // Teleport | ||
| // Copyright (C) 2025 Gravitational, Inc. | ||
| // | ||
| // This program is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU Affero General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU Affero General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU Affero General Public License | ||
| // along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| package expression | ||
|
|
||
| import ( | ||
| "github.com/coreos/go-semver/semver" | ||
| "github.com/gravitational/trace" | ||
|
|
||
| "github.com/gravitational/teleport/lib/expression" | ||
| "github.com/gravitational/teleport/lib/utils/typical" | ||
| ) | ||
|
|
||
| func NewBotInstanceExpressionParser() (*typical.Parser[*Environment, bool], error) { | ||
| spec := expression.DefaultParserSpec[*Environment]() | ||
|
|
||
| spec.Variables = map[string]typical.Variable{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the Workload Identity expression language, we did build some tooling for having a dynamic variable which can access any field within a protobuf by name - but I think in some ways, I think I may actually prefer what you've done here since it's restricting them to a subset of fields which is a little bit less scary. I'd maybe think about documenting here that this name should match the proto names, so if we ever did switch to that, it would not be a breaking change.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we were to maintain the same naming as the proto, we'd need a way to access the most recent heartbeat. I've called this
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense to me - I'd misremembered |
||
| "name": typical.DynamicVariable(func(env *Environment) (string, error) { | ||
| return env.GetMetadata().GetName(), nil | ||
| }), | ||
| "metadata.name": typical.DynamicVariable(func(env *Environment) (string, error) { | ||
| return env.GetMetadata().GetName(), nil | ||
| }), | ||
| "spec.bot_name": typical.DynamicVariable(func(env *Environment) (string, error) { | ||
| return env.GetSpec().GetBotName(), nil | ||
| }), | ||
| "spec.instance_id": typical.DynamicVariable(func(env *Environment) (string, error) { | ||
| return env.GetSpec().GetInstanceId(), nil | ||
| }), | ||
| "status.latest_heartbeat.architecture": typical.DynamicVariable(func(env *Environment) (string, error) { | ||
| return env.GetLatestHeartbeat().GetArchitecture(), nil | ||
| }), | ||
| "status.latest_heartbeat.os": typical.DynamicVariable(func(env *Environment) (string, error) { | ||
| return env.GetLatestHeartbeat().GetOs(), nil | ||
| }), | ||
| "status.latest_heartbeat.hostname": typical.DynamicVariable(func(env *Environment) (string, error) { | ||
| return env.GetLatestHeartbeat().GetHostname(), nil | ||
| }), | ||
| "status.latest_heartbeat.one_shot": typical.DynamicVariable(func(env *Environment) (bool, error) { | ||
| return env.GetLatestHeartbeat().GetOneShot(), nil | ||
| }), | ||
| "status.latest_heartbeat.version": typical.DynamicVariable(func(env *Environment) (*semver.Version, error) { | ||
| if env.GetLatestHeartbeat().GetVersion() == "" { | ||
| return nil, nil | ||
| } | ||
| return semver.NewVersion(env.LatestHeartbeat.Version) | ||
| }), | ||
| "status.latest_authentication.join_method": typical.DynamicVariable(func(env *Environment) (string, error) { | ||
| return env.GetLatestAuthentication().GetJoinMethod(), nil | ||
| }), | ||
| } | ||
|
|
||
| // e.g. `more_than(status.latest_heartbeat.version, "19.0.0")` | ||
| spec.Functions["more_than"] = typical.BinaryFunction[*Environment](semverGt) | ||
| // e.g. `less_than(status.latest_heartbeat.version, "19.0.2")` | ||
| spec.Functions["less_than"] = typical.BinaryFunction[*Environment](semverLt) | ||
| // e.g. `between(status.latest_heartbeat.version, "19.0.0", "19.0.2")` | ||
| spec.Functions["between"] = typical.TernaryFunction[*Environment](semverBetween) | ||
| // e.g. `equals(status.latest_heartbeat.version, "19.1.0")` | ||
| spec.Functions["equals"] = typical.BinaryFunction[*Environment](semverEq) | ||
|
|
||
| return typical.NewParser[*Environment, bool](spec) | ||
| } | ||
|
|
||
| func semverGt(a, b any) (bool, error) { | ||
| va, err := toSemver(a) | ||
| if va == nil || err != nil { | ||
| return false, err | ||
| } | ||
| vb, err := toSemver(b) | ||
| if vb == nil || err != nil { | ||
| return false, err | ||
| } | ||
| return va.Compare(*vb) > 0, nil | ||
| } | ||
|
|
||
| func semverLt(a, b any) (bool, error) { | ||
| va, err := toSemver(a) | ||
| if va == nil || err != nil { | ||
| return false, err | ||
| } | ||
| vb, err := toSemver(b) | ||
| if vb == nil || err != nil { | ||
| return false, err | ||
| } | ||
| return va.Compare(*vb) < 0, nil | ||
| } | ||
|
|
||
| func semverEq(a, b any) (bool, error) { | ||
| va, err := toSemver(a) | ||
| if va == nil || err != nil { | ||
| return false, err | ||
| } | ||
| vb, err := toSemver(b) | ||
| if vb == nil || err != nil { | ||
| return false, err | ||
| } | ||
| return va.Compare(*vb) == 0, nil | ||
| } | ||
|
|
||
| func semverBetween(c, a, b any) (bool, error) { | ||
| gt, err := semverGt(c, a) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| eq, err := semverEq(c, a) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| lt, err := semverLt(c, b) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| return (gt || eq) && lt, nil | ||
| } | ||
|
|
||
| func toSemver(anyV any) (*semver.Version, error) { | ||
| switch v := anyV.(type) { | ||
| case *semver.Version: | ||
| return v, nil | ||
| case string: | ||
| return semver.NewVersion(v) | ||
| default: | ||
| return nil, trace.BadParameter("type %T cannot be parsed as semver.Version", v) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remember to backport this PR and the ListBotInstancesV2 PR together so there's never any confusion as to whether the query parameter is supported or not.