Add ServerInfo type (#25281)

This change adds a new ServerInfo type as described in RFD 105.
This commit is contained in:
Andrew Burke
2023-05-09 17:17:40 +00:00
committed by GitHub
parent 22f0fac57c
commit 3349c6932c
7 changed files with 2484 additions and 1251 deletions
@@ -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"];
}
+3
View File
@@ -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"
+154
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+80
View File
@@ -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"
)
+82
View File
@@ -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)
}
+110
View File
@@ -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 = &copy
}
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
}