mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-29 17:58:15 +08:00
Add ServerInfo type (#25281)
This change adds a new ServerInfo type as described in RFD 105.
This commit is contained in:
@@ -5497,3 +5497,36 @@ message WatchStatusSpecV1 {
|
||||
(gogoproto.jsontag) = "kinds"
|
||||
];
|
||||
}
|
||||
|
||||
// ServerInfoV1 contains info that should be applied to joining Nodes.
|
||||
message ServerInfoV1 {
|
||||
// Kind is the resource kind.
|
||||
string Kind = 1 [(gogoproto.jsontag) = "kind"];
|
||||
// SubKind is an optional resource subkind. Currently unused for this resource.
|
||||
string SubKind = 2 [(gogoproto.jsontag) = "sub_kind,omitempty"];
|
||||
// Version is the resource version.
|
||||
string Version = 3 [(gogoproto.jsontag) = "version"];
|
||||
// Metadata is the resource metadata.
|
||||
Metadata Metadata = 4 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.jsontag) = "metadata"
|
||||
];
|
||||
// Spec is the resource spec.
|
||||
ServerInfoSpecV1 Spec = 5 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.jsontag) = "spec"
|
||||
];
|
||||
}
|
||||
|
||||
// ServerInfoSpecV1 contains fields used to match Nodes to this ServerInfo.
|
||||
message ServerInfoSpecV1 {
|
||||
// AWSInfo contains attributes to match to an EC2 instance.
|
||||
message AWSInfo {
|
||||
// AccountID is an AWS account ID.
|
||||
string AccountID = 1 [(gogoproto.jsontag) = "account_id"];
|
||||
// InstanceID is an EC2 instance ID.
|
||||
string InstanceID = 2 [(gogoproto.jsontag) = "instance_id"];
|
||||
}
|
||||
// AWS matches an EC2 instance.
|
||||
AWSInfo AWS = 1 [(gogoproto.jsontag) = "aws,omitempty"];
|
||||
}
|
||||
|
||||
@@ -347,6 +347,9 @@ const (
|
||||
// KindClusterMaintenanceConfig determines maintenance times for the cluster.
|
||||
KindClusterMaintenanceConfig = "cluster_maintenance_config"
|
||||
|
||||
// KindServerInfo contains info that should be applied to joining Nodes.
|
||||
KindServerInfo = "server_info"
|
||||
|
||||
// MetaNameClusterMaintenanceConfig is the only allowed metadata.name value for the maintenance
|
||||
// window singleton resource.
|
||||
MetaNameClusterMaintenanceConfig = "cluster-maintenance-config"
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
Copyright 2023 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/utils"
|
||||
)
|
||||
|
||||
// ServerInfo represents info that should be applied to joining Nodes.
|
||||
type ServerInfo interface {
|
||||
// ResourceWithLabels provides common resource headers
|
||||
ResourceWithLabels
|
||||
}
|
||||
|
||||
// NewServerInfo creates an instance of ServerInfo.
|
||||
func NewServerInfo(meta Metadata, spec ServerInfoSpecV1) (ServerInfo, error) {
|
||||
si := &ServerInfoV1{
|
||||
Metadata: meta,
|
||||
Spec: spec,
|
||||
}
|
||||
if err := si.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return si, nil
|
||||
}
|
||||
|
||||
// GetKind returns resource kind
|
||||
func (s *ServerInfoV1) GetKind() string {
|
||||
return s.Kind
|
||||
}
|
||||
|
||||
// GetSubKind returns resource subkind
|
||||
func (s *ServerInfoV1) GetSubKind() string {
|
||||
return s.SubKind
|
||||
}
|
||||
|
||||
// SetSubKind sets resource subkind
|
||||
func (s *ServerInfoV1) SetSubKind(subkind string) {
|
||||
s.SubKind = subkind
|
||||
}
|
||||
|
||||
// GetVersion returns resource version
|
||||
func (s *ServerInfoV1) GetVersion() string {
|
||||
return s.Version
|
||||
}
|
||||
|
||||
// GetName returns the name of the resource
|
||||
func (s *ServerInfoV1) GetName() string {
|
||||
return s.Metadata.Name
|
||||
}
|
||||
|
||||
// SetName sets the name of the resource
|
||||
func (s *ServerInfoV1) SetName(name string) {
|
||||
s.Metadata.Name = name
|
||||
}
|
||||
|
||||
// Expiry returns object expiry setting
|
||||
func (s *ServerInfoV1) Expiry() time.Time {
|
||||
return s.Metadata.Expiry()
|
||||
}
|
||||
|
||||
// SetExpiry sets object expiry
|
||||
func (s *ServerInfoV1) SetExpiry(expiry time.Time) {
|
||||
s.Metadata.SetExpiry(expiry)
|
||||
}
|
||||
|
||||
// GetMetadata returns object metadata
|
||||
func (s *ServerInfoV1) GetMetadata() Metadata {
|
||||
return s.Metadata
|
||||
}
|
||||
|
||||
// GetResourceID returns resource ID
|
||||
func (s *ServerInfoV1) GetResourceID() int64 {
|
||||
return s.Metadata.ID
|
||||
}
|
||||
|
||||
// SetResourceID sets resource ID
|
||||
func (s *ServerInfoV1) SetResourceID(id int64) {
|
||||
s.Metadata.ID = id
|
||||
}
|
||||
|
||||
// Origin returns the origin value of the resource.
|
||||
func (s *ServerInfoV1) Origin() string {
|
||||
return s.Metadata.Origin()
|
||||
}
|
||||
|
||||
// SetOrigin sets the origin value of the resource.
|
||||
func (s *ServerInfoV1) SetOrigin(o string) {
|
||||
s.Metadata.SetOrigin(o)
|
||||
}
|
||||
|
||||
// GetLabel retrieves the label with the provided key.
|
||||
func (s *ServerInfoV1) GetLabel(key string) (string, bool) {
|
||||
value, ok := s.Metadata.Labels[key]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// GetAllLabels returns all resource's labels.
|
||||
func (s *ServerInfoV1) GetAllLabels() map[string]string {
|
||||
return s.Metadata.Labels
|
||||
}
|
||||
|
||||
// GetStaticLabels returns the resource's static labels.
|
||||
func (s *ServerInfoV1) GetStaticLabels() map[string]string {
|
||||
return s.Metadata.Labels
|
||||
}
|
||||
|
||||
// SetStaticLabels sets the resource's static labels.
|
||||
func (s *ServerInfoV1) SetStaticLabels(sl map[string]string) {
|
||||
s.Metadata.Labels = sl
|
||||
}
|
||||
|
||||
// MatchSearch goes through select field values of a resource
|
||||
// and tries to match against the list of search values.
|
||||
func (s *ServerInfoV1) MatchSearch(searchValues []string) bool {
|
||||
fieldVals := append(
|
||||
utils.MapToStrings(s.GetAllLabels()),
|
||||
s.GetName(),
|
||||
)
|
||||
if s.Spec.AWS != nil {
|
||||
fieldVals = append(fieldVals, s.Spec.AWS.AccountID, s.Spec.AWS.InstanceID)
|
||||
}
|
||||
return MatchSearch(fieldVals, searchValues, nil)
|
||||
}
|
||||
|
||||
func (s *ServerInfoV1) setStaticFields() {
|
||||
s.Kind = KindServerInfo
|
||||
s.Version = V1
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults validates the Resource and sets any empty fields to
|
||||
// default values.
|
||||
func (s *ServerInfoV1) CheckAndSetDefaults() error {
|
||||
s.setStaticFields()
|
||||
return trace.Wrap(s.Metadata.CheckAndSetDefaults())
|
||||
}
|
||||
+2022
-1251
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
"github.com/gravitational/teleport/api/internalutils/stream"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/retryutils"
|
||||
"github.com/gravitational/teleport/lib/backend"
|
||||
@@ -138,6 +139,84 @@ func (s *PresenceService) DeleteNamespace(namespace string) error {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// GetServerInfos returns a stream of ServerInfos.
|
||||
func (s *PresenceService) GetServerInfos(ctx context.Context) stream.Stream[types.ServerInfo] {
|
||||
startKey := backend.ExactKey(serverInfoPrefix)
|
||||
endKey := backend.RangeEnd(startKey)
|
||||
items := backend.StreamRange(ctx, s, startKey, endKey, apidefaults.DefaultChunkSize)
|
||||
return stream.FilterMap(items, func(item backend.Item) (types.ServerInfo, bool) {
|
||||
si, err := services.UnmarshalServerInfo(
|
||||
item.Value,
|
||||
services.WithResourceID(item.ID),
|
||||
services.WithExpires(item.Expires),
|
||||
)
|
||||
if err != nil {
|
||||
s.log.Warnf("Skipping server info at %s, failed to unmarshal: %v", item.Key, err)
|
||||
return nil, false
|
||||
}
|
||||
return si, true
|
||||
})
|
||||
}
|
||||
|
||||
// GetServerInfo returns a ServerInfo by name.
|
||||
func (s *PresenceService) GetServerInfo(ctx context.Context, name string) (types.ServerInfo, error) {
|
||||
if name == "" {
|
||||
return nil, trace.BadParameter("missing server info name")
|
||||
}
|
||||
item, err := s.Get(ctx, backend.Key(serverInfoPrefix, name))
|
||||
if err != nil {
|
||||
if trace.IsNotFound(err) {
|
||||
return nil, trace.NotFound("server info %q is not found", name)
|
||||
}
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
si, err := services.UnmarshalServerInfo(
|
||||
item.Value, services.WithResourceID(item.ID), services.WithExpires(item.Expires),
|
||||
)
|
||||
return si, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// DeleteAllServerInfos deletes all ServerInfos.
|
||||
func (s *PresenceService) DeleteAllServerInfos(ctx context.Context) error {
|
||||
startKey := backend.ExactKey(serverInfoPrefix)
|
||||
return trace.Wrap(s.DeleteRange(ctx, startKey, backend.RangeEnd(startKey)))
|
||||
}
|
||||
|
||||
// UpsertServerInfo upserts a ServerInfo.
|
||||
func (s *PresenceService) UpsertServerInfo(ctx context.Context, si types.ServerInfo) error {
|
||||
if err := si.CheckAndSetDefaults(); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
value, err := services.MarshalServerInfo(si)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
item := backend.Item{
|
||||
Key: backend.Key(serverInfoPrefix, si.GetName()),
|
||||
Value: value,
|
||||
Expires: si.Expiry(),
|
||||
ID: si.GetResourceID(),
|
||||
}
|
||||
|
||||
_, err = s.Put(ctx, item)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// DeleteServerInfo deletes a ServerInfo by name.
|
||||
func (s *PresenceService) DeleteServerInfo(ctx context.Context, name string) error {
|
||||
if name == "" {
|
||||
return trace.BadParameter("missing server info name")
|
||||
}
|
||||
err := s.Delete(ctx, backend.Key(serverInfoPrefix, name))
|
||||
if err != nil {
|
||||
if trace.IsNotFound(err) {
|
||||
return trace.NotFound("server info %q is not found", name)
|
||||
}
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PresenceService) getServers(ctx context.Context, kind, prefix string) ([]types.Server, error) {
|
||||
result, err := s.GetRange(ctx, backend.Key(prefix), backend.RangeEnd(backend.Key(prefix)), backend.NoLimit)
|
||||
if err != nil {
|
||||
@@ -1740,4 +1819,5 @@ const (
|
||||
semaphoresPrefix = "semaphores"
|
||||
windowsDesktopServicesPrefix = "windowsDesktopServices"
|
||||
loginTimePrefix = "hostuser_interaction_time"
|
||||
serverInfoPrefix = "serverInfos"
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
"github.com/gravitational/teleport/api/internalutils/stream"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/backend"
|
||||
"github.com/gravitational/teleport/lib/backend/lite"
|
||||
@@ -1310,3 +1311,84 @@ func TestListResources_DuplicateResourceFilterByLabel(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerInfoCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
bk, err := lite.New(ctx, backend.Params{"path": t.TempDir()})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, bk.Close()) })
|
||||
|
||||
presence := NewPresenceService(bk)
|
||||
|
||||
serverInfoA, err := types.NewServerInfo(types.Metadata{
|
||||
Name: "server1",
|
||||
Labels: map[string]string{
|
||||
"a": "b",
|
||||
"c": "d",
|
||||
},
|
||||
}, types.ServerInfoSpecV1{
|
||||
AWS: &types.ServerInfoSpecV1_AWSInfo{
|
||||
AccountID: "abcd",
|
||||
InstanceID: "1234",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
serverInfoB, err := types.NewServerInfo(types.Metadata{
|
||||
Name: "server2",
|
||||
}, types.ServerInfoSpecV1{
|
||||
AWS: &types.ServerInfoSpecV1_AWSInfo{
|
||||
AccountID: "efgh",
|
||||
InstanceID: "5678",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// No infos present initially.
|
||||
out, err := stream.Collect(presence.GetServerInfos(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
|
||||
// Create infos.
|
||||
require.NoError(t, presence.UpsertServerInfo(ctx, serverInfoA))
|
||||
require.NoError(t, presence.UpsertServerInfo(ctx, serverInfoB))
|
||||
|
||||
// Get server infos.
|
||||
out, err = stream.Collect(presence.GetServerInfos(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff([]types.ServerInfo{serverInfoA, serverInfoB}, out,
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "ID")))
|
||||
|
||||
outInfo, err := presence.GetServerInfo(ctx, serverInfoA.GetName())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(serverInfoA, outInfo, cmpopts.IgnoreFields(types.Metadata{}, "ID")))
|
||||
|
||||
_, err = presence.GetServerInfo(ctx, "nonexistant")
|
||||
require.True(t, trace.IsNotFound(err))
|
||||
|
||||
// Delete a server info.
|
||||
require.NoError(t, presence.DeleteServerInfo(ctx, serverInfoA.GetName()))
|
||||
out, err = stream.Collect(presence.GetServerInfos(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff([]types.ServerInfo{serverInfoB}, out,
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "ID")))
|
||||
|
||||
// Update server info.
|
||||
serverInfoB.SetStaticLabels(map[string]string{
|
||||
"e": "f",
|
||||
"g": "h",
|
||||
})
|
||||
require.NoError(t, presence.UpsertServerInfo(ctx, serverInfoB))
|
||||
out, err = stream.Collect(presence.GetServerInfos(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff([]types.ServerInfo{serverInfoB}, out,
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "ID")))
|
||||
|
||||
// Delete all server infos.
|
||||
require.NoError(t, presence.DeleteAllServerInfos(ctx))
|
||||
out, err = stream.Collect(presence.GetServerInfos(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
Copyright 2023 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
apiutils "github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
// UnmarshalServerInfo unmarshals the ServerInfo resource from JSON.
|
||||
func UnmarshalServerInfo(bytes []byte, opts ...MarshalOption) (types.ServerInfo, error) {
|
||||
cfg, err := CollectOptions(opts)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if len(bytes) == 0 {
|
||||
return nil, trace.BadParameter("missing server info data")
|
||||
}
|
||||
|
||||
var si types.ServerInfoV1
|
||||
if err := utils.FastUnmarshal(bytes, &si); err != nil {
|
||||
return nil, trace.BadParameter(err.Error())
|
||||
}
|
||||
if err := si.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if cfg.ID != 0 {
|
||||
si.SetResourceID(cfg.ID)
|
||||
}
|
||||
if !cfg.Expires.IsZero() {
|
||||
si.SetExpiry(cfg.Expires)
|
||||
}
|
||||
if si.Metadata.Expires != nil {
|
||||
apiutils.UTC(si.Metadata.Expires)
|
||||
}
|
||||
|
||||
return &si, nil
|
||||
}
|
||||
|
||||
// MarshalServerInfo marshals the ServerInfo resource to JSON.
|
||||
func MarshalServerInfo(si types.ServerInfo, opts ...MarshalOption) ([]byte, error) {
|
||||
if err := si.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
cfg, err := CollectOptions(opts)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
switch si := si.(type) {
|
||||
case *types.ServerInfoV1:
|
||||
if !cfg.PreserveResourceID {
|
||||
// avoid modifying the original object
|
||||
// to prevent unexpected data races
|
||||
copy := *si
|
||||
copy.SetResourceID(0)
|
||||
si = ©
|
||||
}
|
||||
bytes, err := utils.FastMarshal(si)
|
||||
return bytes, trace.Wrap(err)
|
||||
default:
|
||||
return nil, trace.BadParameter("unrecognized server info version %T", si)
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalServerInfos unmarshals a list of ServerInfo resources.
|
||||
func UnmarshalServerInfos(bytes []byte) ([]types.ServerInfo, error) {
|
||||
var serverInfos []types.ServerInfoV1
|
||||
|
||||
err := utils.FastUnmarshal(bytes, &serverInfos)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
out := make([]types.ServerInfo, len(serverInfos))
|
||||
for i, v := range serverInfos {
|
||||
out[i] = types.ServerInfo(&v)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MarshalServerInfos marshals a list of ServerInfo resources.
|
||||
func MarshalServerInfos(si []types.ServerInfo) ([]byte, error) {
|
||||
bytes, err := utils.FastMarshal(si)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user