diff --git a/api/gen/proto/go/teleport/backendinfo/v1/backendinfo.pb.go b/api/gen/proto/go/teleport/backendinfo/v1/backendinfo.pb.go new file mode 100644 index 0000000000000..03f9282f009df --- /dev/null +++ b/api/gen/proto/go/teleport/backendinfo/v1/backendinfo.pb.go @@ -0,0 +1,250 @@ +// Copyright 2025 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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc (unknown) +// source: teleport/backendinfo/v1/backendinfo.proto + +package backendinfov1 + +import ( + v1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// BackendInfo is a singleton resource that holds meta-information for the cluster's auth service. +// It is used to store the auth instance last known version and is managed by the major version +// check validator. After a cluster upgrade with a new version of the auth service, this +// information is overridden with data from the new auth instance. +type BackendInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` + SubKind string `protobuf:"bytes,2,opt,name=sub_kind,json=subKind,proto3" json:"sub_kind,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Metadata *v1.Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` + Spec *BackendInfoSpec `protobuf:"bytes,5,opt,name=spec,proto3" json:"spec,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BackendInfo) Reset() { + *x = BackendInfo{} + mi := &file_teleport_backendinfo_v1_backendinfo_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BackendInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackendInfo) ProtoMessage() {} + +func (x *BackendInfo) ProtoReflect() protoreflect.Message { + mi := &file_teleport_backendinfo_v1_backendinfo_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BackendInfo.ProtoReflect.Descriptor instead. +func (*BackendInfo) Descriptor() ([]byte, []int) { + return file_teleport_backendinfo_v1_backendinfo_proto_rawDescGZIP(), []int{0} +} + +func (x *BackendInfo) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *BackendInfo) GetSubKind() string { + if x != nil { + return x.SubKind + } + return "" +} + +func (x *BackendInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *BackendInfo) GetMetadata() *v1.Metadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *BackendInfo) GetSpec() *BackendInfoSpec { + if x != nil { + return x.Spec + } + return nil +} + +// BackendInfoSpec encodes the parameters auth server meta-information. +type BackendInfoSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // teleport_version advertises the version of the auth server, e.g., "17.3.3" (without the leading "v"). + TeleportVersion string `protobuf:"bytes,1,opt,name=teleport_version,json=teleportVersion,proto3" json:"teleport_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BackendInfoSpec) Reset() { + *x = BackendInfoSpec{} + mi := &file_teleport_backendinfo_v1_backendinfo_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BackendInfoSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BackendInfoSpec) ProtoMessage() {} + +func (x *BackendInfoSpec) ProtoReflect() protoreflect.Message { + mi := &file_teleport_backendinfo_v1_backendinfo_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BackendInfoSpec.ProtoReflect.Descriptor instead. +func (*BackendInfoSpec) Descriptor() ([]byte, []int) { + return file_teleport_backendinfo_v1_backendinfo_proto_rawDescGZIP(), []int{1} +} + +func (x *BackendInfoSpec) GetTeleportVersion() string { + if x != nil { + return x.TeleportVersion + } + return "" +} + +var File_teleport_backendinfo_v1_backendinfo_proto protoreflect.FileDescriptor + +var file_teleport_backendinfo_v1_backendinfo_proto_rawDesc = string([]byte{ + 0x0a, 0x29, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x69, 0x6e, 0x66, 0x6f, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x66, + 0x6f, 0x2e, 0x76, 0x31, 0x1a, 0x21, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xce, 0x01, 0x0a, 0x0b, 0x42, 0x61, 0x63, 0x6b, + 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, + 0x75, 0x62, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, + 0x75, 0x62, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3c, 0x0a, 0x04, 0x73, 0x70, + 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x66, 0x6f, 0x2e, + 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x53, 0x70, + 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0x3c, 0x0a, 0x0f, 0x42, 0x61, 0x63, 0x6b, + 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x53, 0x70, 0x65, 0x63, 0x12, 0x29, 0x0a, 0x10, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x5a, 0x5a, 0x58, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x66, + 0x6f, 0x2f, 0x76, 0x31, 0x3b, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x66, 0x6f, + 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_teleport_backendinfo_v1_backendinfo_proto_rawDescOnce sync.Once + file_teleport_backendinfo_v1_backendinfo_proto_rawDescData []byte +) + +func file_teleport_backendinfo_v1_backendinfo_proto_rawDescGZIP() []byte { + file_teleport_backendinfo_v1_backendinfo_proto_rawDescOnce.Do(func() { + file_teleport_backendinfo_v1_backendinfo_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_teleport_backendinfo_v1_backendinfo_proto_rawDesc), len(file_teleport_backendinfo_v1_backendinfo_proto_rawDesc))) + }) + return file_teleport_backendinfo_v1_backendinfo_proto_rawDescData +} + +var file_teleport_backendinfo_v1_backendinfo_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_teleport_backendinfo_v1_backendinfo_proto_goTypes = []any{ + (*BackendInfo)(nil), // 0: teleport.backendinfo.v1.BackendInfo + (*BackendInfoSpec)(nil), // 1: teleport.backendinfo.v1.BackendInfoSpec + (*v1.Metadata)(nil), // 2: teleport.header.v1.Metadata +} +var file_teleport_backendinfo_v1_backendinfo_proto_depIdxs = []int32{ + 2, // 0: teleport.backendinfo.v1.BackendInfo.metadata:type_name -> teleport.header.v1.Metadata + 1, // 1: teleport.backendinfo.v1.BackendInfo.spec:type_name -> teleport.backendinfo.v1.BackendInfoSpec + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_teleport_backendinfo_v1_backendinfo_proto_init() } +func file_teleport_backendinfo_v1_backendinfo_proto_init() { + if File_teleport_backendinfo_v1_backendinfo_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_backendinfo_v1_backendinfo_proto_rawDesc), len(file_teleport_backendinfo_v1_backendinfo_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_teleport_backendinfo_v1_backendinfo_proto_goTypes, + DependencyIndexes: file_teleport_backendinfo_v1_backendinfo_proto_depIdxs, + MessageInfos: file_teleport_backendinfo_v1_backendinfo_proto_msgTypes, + }.Build() + File_teleport_backendinfo_v1_backendinfo_proto = out.File + file_teleport_backendinfo_v1_backendinfo_proto_goTypes = nil + file_teleport_backendinfo_v1_backendinfo_proto_depIdxs = nil +} diff --git a/api/proto/teleport/backendinfo/v1/backendinfo.proto b/api/proto/teleport/backendinfo/v1/backendinfo.proto new file mode 100644 index 0000000000000..95039896bf72f --- /dev/null +++ b/api/proto/teleport/backendinfo/v1/backendinfo.proto @@ -0,0 +1,40 @@ +// Copyright 2025 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. + +syntax = "proto3"; + +package teleport.backendinfo.v1; + +import "teleport/header/v1/metadata.proto"; + +option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1;backendinfov1"; + +// BackendInfo is a singleton resource that holds meta-information for the cluster's auth service. +// It is used to store the auth instance last known version and is managed by the major version +// check validator. After a cluster upgrade with a new version of the auth service, this +// information is overridden with data from the new auth instance. +message BackendInfo { + string kind = 1; + string sub_kind = 2; + string version = 3; + teleport.header.v1.Metadata metadata = 4; + + BackendInfoSpec spec = 5; +} + +// BackendInfoSpec encodes the parameters auth server meta-information. +message BackendInfoSpec { + // teleport_version advertises the version of the auth server, e.g., "17.3.3" (without the leading "v"). + string teleport_version = 1; +} diff --git a/api/types/backendinfo/info.go b/api/types/backendinfo/info.go new file mode 100644 index 0000000000000..b1114e05f7e45 --- /dev/null +++ b/api/types/backendinfo/info.go @@ -0,0 +1,59 @@ +/* +Copyright 2025 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 backendinfo + +import ( + "github.com/gravitational/trace" + + backendinfov1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1" + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + "github.com/gravitational/teleport/api/types" +) + +// NewBackendInfo creates a new auth info resource. +func NewBackendInfo(spec *backendinfov1.BackendInfoSpec) (*backendinfov1.BackendInfo, error) { + info := &backendinfov1.BackendInfo{ + Kind: types.KindBackendInfo, + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: types.MetaNameBackendInfo, + }, + Spec: spec, + } + if err := ValidateBackendInfo(info); err != nil { + return nil, trace.Wrap(err) + } + + return info, nil +} + +// ValidateBackendInfo checks that required parameters are set +// for the specified BackendInfo. +func ValidateBackendInfo(info *backendinfov1.BackendInfo) error { + switch { + case info.GetKind() != types.KindBackendInfo: + return trace.BadParameter("wrong BackendInfo Kind: %+q", info.Kind) + case info.GetMetadata().Name != types.MetaNameBackendInfo: + return trace.BadParameter("wrong BackendInfo Metadata name: %+q", info.GetMetadata().Name) + case info.GetSubKind() != "": + return trace.BadParameter("wrong BackendInfo SubKind: %+q", info.GetSubKind()) + case info.GetVersion() != types.V1: + return trace.BadParameter("wrong BackendInfo Version: %+q", info.GetVersion()) + default: + return nil + } +} diff --git a/api/types/constants.go b/api/types/constants.go index 1487924445517..26cb22578f5ee 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -500,6 +500,12 @@ const ( // KindServerInfo contains info that should be applied to joining Nodes. KindServerInfo = "server_info" + // KindBackendInfo contains backend info. + KindBackendInfo = "backend_info" + + // MetaNameBackendInfo name backend info entity. + MetaNameBackendInfo = "backend-info" + // SubKindCloudInfo is a ServerInfo that was created by the Discovery // service to match with a single discovered instance. SubKindCloudInfo = "cloud_info" diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 886cce88e8e91..28810e2e098f3 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -346,6 +346,12 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { return nil, trace.Wrap(err, "creating SPIFFEFederation service") } } + if cfg.BackendInfo == nil { + cfg.BackendInfo, err = local.NewBackendInfoService(cfg.Backend) + if err != nil { + return nil, trace.Wrap(err, "creating BackendInfo service") + } + } limiter, err := limiter.NewConnectionsLimiter(limiter.Config{ MaxConnections: defaults.LimiterMaxConcurrentSignatures, @@ -435,6 +441,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { AccessMonitoringRules: cfg.AccessMonitoringRules, CrownJewels: cfg.CrownJewels, SPIFFEFederations: cfg.SPIFFEFederations, + BackendInfoService: cfg.BackendInfo, } as := Server{ @@ -636,6 +643,7 @@ type Services struct { services.AccessGraphSecretsGetter services.DevicesGetter services.AutoUpdateService + services.BackendInfoService } // GetWebSession returns existing web session described by req. diff --git a/lib/auth/helpers.go b/lib/auth/helpers.go index bbf074d0bc1b9..3def53813d4b4 100644 --- a/lib/auth/helpers.go +++ b/lib/auth/helpers.go @@ -1189,12 +1189,17 @@ func NewFakeTeleportVersion() *FakeTeleportVersion { } // GetTeleportVersion returns current Teleport version. -func (s FakeTeleportVersion) GetTeleportVersion(_ context.Context) (*semver.Version, error) { - return teleport.SemVersion, nil +func (s FakeTeleportVersion) GetTeleportVersion(_ context.Context) (semver.Version, error) { + return *teleport.SemVersion, nil } // WriteTeleportVersion stub function for writing. -func (s FakeTeleportVersion) WriteTeleportVersion(_ context.Context, _ *semver.Version) error { +func (s FakeTeleportVersion) WriteTeleportVersion(_ context.Context, _ semver.Version) error { + return nil +} + +// DeleteTeleportVersion error stub function for deleting. +func (s FakeTeleportVersion) DeleteTeleportVersion(_ context.Context) error { return nil } diff --git a/lib/auth/init.go b/lib/auth/init.go index 96664254ea054..b6db4caaa491e 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -78,9 +78,11 @@ var log = logrus.WithFields(logrus.Fields{ // VersionStorage local storage for saving the version. type VersionStorage interface { // GetTeleportVersion reads the last known Teleport version from storage. - GetTeleportVersion(ctx context.Context) (*semver.Version, error) + GetTeleportVersion(ctx context.Context) (semver.Version, error) // WriteTeleportVersion writes the last known Teleport version to the storage. - WriteTeleportVersion(ctx context.Context, version *semver.Version) error + WriteTeleportVersion(ctx context.Context, version semver.Version) error + // DeleteTeleportVersion removes the last known Teleport version in storage. + DeleteTeleportVersion(ctx context.Context) error } // InitConfig is auth server init config @@ -322,6 +324,12 @@ type InitConfig struct { // SPIFFEFederations is a service that manages storing SPIFFE federations. SPIFFEFederations services.SPIFFEFederations + + // BackendInfo is a service of backend information. + BackendInfo services.BackendInfoService + + // SkipVersionCheck skips version check during major version upgrade/downgrade. + SkipVersionCheck bool } // Init instantiates and configures an instance of AuthServer @@ -369,8 +377,14 @@ func initCluster(ctx context.Context, cfg InitConfig, asrv *Server) error { if err != nil { return trace.Wrap(err) } - if err := validateAndUpdateTeleportVersion(ctx, cfg.VersionStorage, teleport.SemVersion, firstStart); err != nil { - return trace.Wrap(err) + if cfg.SkipVersionCheck { + if err := upsertTeleportVersion(ctx, cfg.VersionStorage, asrv.Services.BackendInfoService, *teleport.SemVersion); err != nil { + return trace.Wrap(err) + } + } else { + if err := validateAndUpdateTeleportVersion(ctx, cfg.VersionStorage, asrv.Services.BackendInfoService, *teleport.SemVersion); err != nil { + return trace.Wrap(err) + } } // if bootstrap resources are supplied, use them to bootstrap backend state diff --git a/lib/auth/init_test.go b/lib/auth/init_test.go index e6f8a1e8948c4..7b6b75f7dffaf 100644 --- a/lib/auth/init_test.go +++ b/lib/auth/init_test.go @@ -43,7 +43,9 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/constants" + backendinfov1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/backendinfo" apisshutils "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth/keystore" @@ -55,6 +57,7 @@ import ( "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/observability/tracing" "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local" "github.com/gravitational/teleport/lib/services/suite" "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/sshutils" @@ -1787,11 +1790,12 @@ func TestTeleportProcessAuthVersionUpgradeCheck(t *testing.T) { defer lib.SetInsecureDevMode(false) tests := []struct { - name string - initialVersion string - expectedVersion string - expectError bool - skipCheck bool + name string + initialVersion string + initialProcVersion string + expectedVersion string + expectError bool + skipCheck bool }{ { name: "first-launch", @@ -1805,16 +1809,33 @@ func TestTeleportProcessAuthVersionUpgradeCheck(t *testing.T) { expectedVersion: teleport.Version, expectError: false, }, + { + name: "old-version-upgrade-proc", + initialProcVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major-1), + expectedVersion: teleport.Version, + expectError: false, + }, { name: "major-upgrade-fail", initialVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major-2), expectedVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major-2), expectError: true, }, + { + name: "major-upgrade-fail-proc", + initialProcVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major-2), + expectError: true, + }, + { + name: "major-upgrade-fail-proc-with-skip-check", + initialProcVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major-2), + expectedVersion: teleport.Version, + skipCheck: true, + }, { name: "major-upgrade-with-dev-skip-check", initialVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major-2), - expectedVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major-2), + expectedVersion: teleport.Version, expectError: false, skipCheck: true, }, @@ -1824,10 +1845,21 @@ func TestTeleportProcessAuthVersionUpgradeCheck(t *testing.T) { expectedVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major+2), expectError: true, }, + { + name: "major-downgrade-fail-proc", + initialProcVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major+2), + expectError: true, + }, + { + name: "major-downgrade-fail-proc-with-skip-check", + initialProcVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major+2), + expectedVersion: teleport.Version, + skipCheck: true, + }, { name: "major-downgrade-with-dev-skip-check", initialVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major+2), - expectedVersion: fmt.Sprintf("%d.0.0", teleport.SemVersion.Major+2), + expectedVersion: teleport.Version, expectError: false, skipCheck: true, }, @@ -1838,25 +1870,42 @@ func TestTeleportProcessAuthVersionUpgradeCheck(t *testing.T) { defer cancel() authCfg := setupConfig(t) + service, err := local.NewBackendInfoService(authCfg.Backend) + require.NoError(t, err) + if test.initialProcVersion != "" { + err = authCfg.VersionStorage.WriteTeleportVersion(ctx, *semver.New(test.initialProcVersion)) + require.NoError(t, err) + } if test.initialVersion != "" { - err := authCfg.VersionStorage.WriteTeleportVersion(ctx, semver.New(test.initialVersion)) + backendInfo, err := backendinfo.NewBackendInfo(&backendinfov1.BackendInfoSpec{ + TeleportVersion: semver.New(test.initialVersion).String(), + }) + require.NoError(t, err) + _, err = service.CreateBackendInfo(ctx, backendInfo) require.NoError(t, err) } if test.skipCheck { - t.Setenv(skipVersionUpgradeCheckEnv, "yes") + authCfg.SkipVersionCheck = true } - _, err := Init(ctx, authCfg) + _, err = Init(ctx, authCfg) if test.expectError { require.Error(t, err) } else { require.NoError(t, err) } + // Verifies that version is removed from process storage. + if test.initialProcVersion != "" && !test.expectError { + _, err := authCfg.VersionStorage.GetTeleportVersion(ctx) + require.True(t, trace.IsNotFound(err)) + } - lastKnownVersion, err := authCfg.VersionStorage.GetTeleportVersion(ctx) - require.NoError(t, err) - require.Equal(t, test.expectedVersion, lastKnownVersion.String()) + if test.expectedVersion != "" { + backendInfo, err := service.GetBackendInfo(ctx) + require.NoError(t, err) + require.Equal(t, test.expectedVersion, backendInfo.GetSpec().GetTeleportVersion()) + } }) } } diff --git a/lib/auth/storage/storage.go b/lib/auth/storage/storage.go index 625cc393f8698..6d5f6f30c516b 100644 --- a/lib/auth/storage/storage.go +++ b/lib/auth/storage/storage.go @@ -211,24 +211,34 @@ func (p *ProcessStorage) WriteIdentity(name string, id state.Identity) error { } // GetTeleportVersion reads the last known Teleport version from storage. -func (p *ProcessStorage) GetTeleportVersion(ctx context.Context) (*semver.Version, error) { - item, err := p.stateStorage.Get(ctx, backend.NewKey(teleportPrefix, lastKnownVersion)) +func (p *ProcessStorage) GetTeleportVersion(ctx context.Context) (semver.Version, error) { + item, err := p.BackendStorage.Get(ctx, backend.NewKey(teleportPrefix, lastKnownVersion)) if err != nil { - return nil, trace.Wrap(err) + return semver.Version{}, trace.Wrap(err) + } + version, err := semver.NewVersion(string(item.Value)) + if err != nil { + return semver.Version{}, trace.Wrap(err) } - return semver.NewVersion(string(item.Value)) + return *version, nil } // WriteTeleportVersion writes the last known Teleport version to the storage. -func (p *ProcessStorage) WriteTeleportVersion(ctx context.Context, version *semver.Version) error { - if version == nil { - return trace.BadParameter("wrong version parameter") - } +func (p *ProcessStorage) WriteTeleportVersion(ctx context.Context, version semver.Version) error { item := backend.Item{ Key: backend.NewKey(teleportPrefix, lastKnownVersion), Value: []byte(version.String()), } - _, err := p.stateStorage.Put(ctx, item) + _, err := p.BackendStorage.Put(ctx, item) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +// DeleteTeleportVersion removes last known Teleport version from the process storage. +func (p *ProcessStorage) DeleteTeleportVersion(ctx context.Context) error { + err := p.BackendStorage.Delete(ctx, backend.NewKey(teleportPrefix, lastKnownVersion)) if err != nil { return trace.Wrap(err) } diff --git a/lib/auth/version.go b/lib/auth/version.go index b601898060030..279f26816e0e3 100644 --- a/lib/auth/version.go +++ b/lib/auth/version.go @@ -20,72 +20,136 @@ package auth import ( "context" - "os" + "errors" + "log/slog" "github.com/coreos/go-semver/semver" "github.com/gravitational/trace" + + backendinfov1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1" + "github.com/gravitational/teleport/api/types/backendinfo" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/services" ) const ( - // majorVersionConstraint is the major version constraint when previous major version must be - // present in the storage, if not - we must refuse to start. - // TODO(vapopov): DELETE IN 18.0.0 - majorVersionConstraint = 18 - - // skipVersionUpgradeCheckEnv is environment variable key for disabling the check - // major version upgrade check. - skipVersionUpgradeCheckEnv = "TELEPORT_UNSTABLE_SKIP_VERSION_UPGRADE_CHECK" + // versionUpgradeCheckMaxWriteRetry is the number of retries for conditional updates of the BackendInfo resource. + versionUpgradeCheckMaxWriteRetry = 5 ) // validateAndUpdateTeleportVersion validates that the major version persistent in the backend // meets our upgrade compatibility guide. func validateAndUpdateTeleportVersion( ctx context.Context, - storage VersionStorage, - currentVersion *semver.Version, - firstTimeStart bool, + procStorage VersionStorage, + backendStorage services.BackendInfoService, + currentVersion semver.Version, ) error { - if skip := os.Getenv(skipVersionUpgradeCheckEnv); skip != "" { - return nil + // TODO(vapopov): DELETE IN v19.0.0 – the last known version should already be migrated to backend storage. + // Fallback to local process storage for backward compatibility with previous versions. + cleanProcVersion := true + teleportVersion, err := procStorage.GetTeleportVersion(ctx) + if trace.IsNotFound(err) { + cleanProcVersion = false + teleportVersion = currentVersion + } else if err != nil { + return trace.Wrap(err) } - lastKnownVersion, err := storage.GetTeleportVersion(ctx) - if trace.IsNotFound(err) { - // When this is not the first start, we have to ensure that previous versions, - // introduced before this check, were also verified. Therefore, not having a version - // in the database means the last known version is = majorVersionConstraint && !firstTimeStart { - return trace.BadParameter("Unsupported upgrade path detected: to %v. "+ - "Teleport supports direct upgrades to the next major version only.\n "+ - "For instance, if you have version 15.x.x, you must upgrade to version 16.x.x first. "+ - "See compatibility guarantees for details: "+ + var createNewResource bool + for range versionUpgradeCheckMaxWriteRetry { + backendInfo, err := backendStorage.GetBackendInfo(ctx) + if trace.IsNotFound(err) { + createNewResource = true + backendInfo, err = backendinfo.NewBackendInfo(&backendinfov1.BackendInfoSpec{TeleportVersion: teleportVersion.String()}) + if err != nil { + return trace.Wrap(err) + } + } else if err != nil { + return trace.Wrap(err) + } + + lastKnownVersion, err := semver.NewVersion(backendInfo.GetSpec().GetTeleportVersion()) + if err != nil { + return trace.Wrap(err, "failed to parse teleport version: %+q", backendInfo.GetSpec().GetTeleportVersion()) + } + // The last known version is already updated to the current one. + // Skip any further checks and resource updates. + if !createNewResource && lastKnownVersion.Equal(currentVersion) { + return nil + } + if currentVersion.Major-lastKnownVersion.Major > 1 { + return trace.BadParameter("Unsupported upgrade path detected: from %v to %v. "+ + "Teleport supports direct upgrades to the next major version only.\n Please upgrade "+ + "your cluster to version %d.x.x first. See compatibility guarantees for details: "+ "https://goteleport.com/docs/upgrading/overview/#component-compatibility.", - currentVersion.String()) + lastKnownVersion, currentVersion.String(), lastKnownVersion.Major+1) } - if err := storage.WriteTeleportVersion(ctx, currentVersion); err != nil { - return trace.Wrap(err) + if lastKnownVersion.Major > currentVersion.Major { + return trace.BadParameter("Unsupported downgrade path detected: from %v to %v. "+ + "Teleport doesn't support major version downgrade.\n Please downgrade "+ + "your cluster to version %d.x.x first. See compatibility guarantees for details: "+ + "https://goteleport.com/docs/upgrading/overview/#component-compatibility.", + lastKnownVersion, currentVersion.String(), lastKnownVersion.Major-1) + } + + backendInfo.GetSpec().TeleportVersion = currentVersion.String() + + if createNewResource { + _, err = backendStorage.CreateBackendInfo(ctx, backendInfo) + if trace.IsAlreadyExists(err) { + err = trace.Wrap(err) + slog.WarnContext(ctx, "Failed to create BackendInfo resource", "error", err) + continue + } else if err != nil { + return trace.Wrap(err) + } + } else { + _, err = backendStorage.UpdateBackendInfo(ctx, backendInfo) + if errors.Is(err, backend.ErrIncorrectRevision) || trace.IsNotFound(err) { + err = trace.Wrap(err) + slog.WarnContext(ctx, "Failed to update BackendInfo resource", "error", err) + continue + } else if err != nil { + return trace.Wrap(err) + } + } + + // TODO(vapopov): DELETE IN v19.0.0 – the last known version should already be migrated to backend storage. + if cleanProcVersion { + if err := procStorage.DeleteTeleportVersion(ctx); err != nil && !trace.IsNotFound(err) { + return trace.Wrap(err) + } } + return nil - } else if err != nil { - return trace.Wrap(err) } + return err +} - if currentVersion.Major-lastKnownVersion.Major > 1 { - return trace.BadParameter("Unsupported upgrade path detected: from %v to %v. "+ - "Teleport supports direct upgrades to the next major version only.\n Please upgrade "+ - "your cluster to version %d.x.x first. See compatibility guarantees for details: "+ - "https://goteleport.com/docs/upgrading/overview/#component-compatibility.", - lastKnownVersion, currentVersion.String(), lastKnownVersion.Major+1) - } - if lastKnownVersion.Major-currentVersion.Major > 1 { - return trace.BadParameter("Unsupported downgrade path detected: from %v to %v. "+ - "Teleport doesn't support major version downgrade.\n Please downgrade "+ - "your cluster to version %d.x.x first. See compatibility guarantees for details: "+ - "https://goteleport.com/docs/upgrading/overview/#component-compatibility.", - lastKnownVersion, currentVersion.String(), lastKnownVersion.Major-1) +// upsertTeleportVersion overrides major version persistent in the backend. +func upsertTeleportVersion( + ctx context.Context, + procStorage VersionStorage, + backendStorage services.BackendInfoService, + currentVersion semver.Version, +) error { + backendInfo, err := backendinfo.NewBackendInfo(&backendinfov1.BackendInfoSpec{ + TeleportVersion: currentVersion.String(), + }) + if err != nil { + return trace.Wrap(err) } - if err := storage.WriteTeleportVersion(ctx, currentVersion); err != nil { + if _, err = backendStorage.UpsertBackendInfo(ctx, backendInfo); err != nil { return trace.Wrap(err) } + slog.WarnContext(ctx, "Version check skipped, Teleport might perform unsupported backend version transitions", + "upgrade_version", currentVersion.String()) + + // TODO(vapopov): DELETE IN v19.0.0 – the last known version should already be migrated to backend storage. + if err := procStorage.DeleteTeleportVersion(ctx); err != nil && !trace.IsNotFound(err) { + slog.ErrorContext(ctx, "Failed to delete Teleport version from process storage", "error", err) + } + return nil } diff --git a/lib/service/service.go b/lib/service/service.go index 5c55da251ce82..973b4d6a0d056 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -2073,12 +2073,17 @@ func (process *TeleportProcess) initAuthService() error { keystoreConfig.AWSKMS.CloudClients = cloudClients } + // Environment variable for disabling the check major version upgrade check and overrides + // latest known version in backend. + skipVersionCheckFromEnv := os.Getenv("TELEPORT_UNSTABLE_SKIP_VERSION_UPGRADE_CHECK") != "" + // first, create the AuthServer authServer, err := auth.Init( process.ExitContext(), auth.InitConfig{ Backend: b, VersionStorage: process.storage, + SkipVersionCheck: cfg.SkipVersionCheck || skipVersionCheckFromEnv, Authority: cfg.Keygen, ClusterConfiguration: cfg.ClusterConfiguration, AutoUpdateService: cfg.AutoUpdateService, diff --git a/lib/services/backendinfo.go b/lib/services/backendinfo.go new file mode 100644 index 0000000000000..2d3bd147b920a --- /dev/null +++ b/lib/services/backendinfo.go @@ -0,0 +1,39 @@ +/* + * 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 . + */ + +package services + +import ( + "context" + + backendinfov1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1" +) + +// BackendInfoService stores information about the backend. +type BackendInfoService interface { + // GetBackendInfo gets the BackendInfo singleton resource. + GetBackendInfo(ctx context.Context) (*backendinfov1.BackendInfo, error) + // CreateBackendInfo creates the BackendInfo singleton resource. + CreateBackendInfo(ctx context.Context, info *backendinfov1.BackendInfo) (*backendinfov1.BackendInfo, error) + // UpdateBackendInfo updates the BackendInfo singleton resource. + UpdateBackendInfo(ctx context.Context, info *backendinfov1.BackendInfo) (*backendinfov1.BackendInfo, error) + // UpsertBackendInfo create or update the BackendInfo singleton resource. + UpsertBackendInfo(ctx context.Context, info *backendinfov1.BackendInfo) (*backendinfov1.BackendInfo, error) + // DeleteBackendInfo deletes the BackendInfo singleton resource. + DeleteBackendInfo(ctx context.Context) error +} diff --git a/lib/services/local/backendinfo.go b/lib/services/local/backendinfo.go new file mode 100644 index 0000000000000..1e083413b3b5f --- /dev/null +++ b/lib/services/local/backendinfo.go @@ -0,0 +1,95 @@ +/* + * 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 . + */ + +package local + +import ( + "context" + + "github.com/gravitational/trace" + + backendinfov1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1" + "github.com/gravitational/teleport/api/types" + backendinfotype "github.com/gravitational/teleport/api/types/backendinfo" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local/generic" +) + +const ( + // backendInfoKeyComponent is the name of the backend item for storing the backend information. + backendInfoKeyComponent = "backend_info" +) + +// BackendInfoService is responsible for managing the information about backend. +type BackendInfoService struct { + backendInfo *generic.ServiceWrapper[*backendinfov1.BackendInfo] +} + +// NewBackendInfoService returns a new BackendInfoService. +func NewBackendInfoService(b backend.Backend) (*BackendInfoService, error) { + backendInfo, err := generic.NewServiceWrapper( + generic.ServiceWrapperConfig[*backendinfov1.BackendInfo]{ + Backend: b, + ResourceKind: types.KindBackendInfo, + BackendPrefix: backendInfoKeyComponent, + MarshalFunc: services.MarshalProtoResource[*backendinfov1.BackendInfo], + UnmarshalFunc: services.UnmarshalProtoResource[*backendinfov1.BackendInfo], + ValidateFunc: backendinfotype.ValidateBackendInfo, + KeyFunc: func(*backendinfov1.BackendInfo) string { + return types.MetaNameBackendInfo + }, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + return &BackendInfoService{ + backendInfo: backendInfo, + }, nil +} + +// GetBackendInfo gets the BackendInfo singleton resource. +func (s *BackendInfoService) GetBackendInfo(ctx context.Context) (*backendinfov1.BackendInfo, error) { + info, err := s.backendInfo.GetResource(ctx, types.MetaNameBackendInfo) + return info, trace.Wrap(err) +} + +// CreateBackendInfo creates the BackendInfo singleton resource. +func (s *BackendInfoService) CreateBackendInfo(ctx context.Context, info *backendinfov1.BackendInfo) (*backendinfov1.BackendInfo, error) { + info, err := s.backendInfo.CreateResource(ctx, info) + return info, trace.Wrap(err) +} + +// UpdateBackendInfo updates the BackendInfo singleton resource. +func (s *BackendInfoService) UpdateBackendInfo(ctx context.Context, info *backendinfov1.BackendInfo) (*backendinfov1.BackendInfo, error) { + info, err := s.backendInfo.ConditionalUpdateResource(ctx, info) + return info, trace.Wrap(err) +} + +// UpsertBackendInfo creates or updates the BackendInfo singleton resource. +func (s *BackendInfoService) UpsertBackendInfo(ctx context.Context, info *backendinfov1.BackendInfo) (*backendinfov1.BackendInfo, error) { + info, err := s.backendInfo.UpsertResource(ctx, info) + return info, trace.Wrap(err) +} + +// DeleteBackendInfo deletes the BackendInfo singleton resource. +func (s *BackendInfoService) DeleteBackendInfo(ctx context.Context) error { + err := s.backendInfo.DeleteResource(ctx, types.MetaNameBackendInfo) + return trace.Wrap(err) +} diff --git a/lib/services/local/backendinfo_test.go b/lib/services/local/backendinfo_test.go new file mode 100644 index 0000000000000..cfb2102b18ba9 --- /dev/null +++ b/lib/services/local/backendinfo_test.go @@ -0,0 +1,93 @@ +/* + * 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 . + */ + +package local + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + + backendinfo1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1" + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend/memory" +) + +// TestBackendInfoServiceCRUD verifies create/read/update/delete methods of the backend service +// for BackendInfo resource. +func TestBackendInfoServiceCRUD(t *testing.T) { + t.Parallel() + + bk, err := memory.New(memory.Config{}) + require.NoError(t, err) + + service, err := NewBackendInfoService(bk) + require.NoError(t, err) + + ctx := context.Background() + info := &backendinfo1pb.BackendInfo{ + Kind: types.KindBackendInfo, + Version: types.V1, + Metadata: &headerv1.Metadata{Name: types.MetaNameBackendInfo}, + Spec: &backendinfo1pb.BackendInfoSpec{ + TeleportVersion: "1.2.3", + }, + } + + created, err := service.CreateBackendInfo(ctx, info) + require.NoError(t, err) + diff := cmp.Diff(info, created, + cmpopts.IgnoreFields(headerv1.Metadata{}, "Revision"), + protocmp.Transform(), + ) + require.Empty(t, diff) + require.NotEmpty(t, created.GetMetadata().GetRevision()) + + got, err := service.GetBackendInfo(ctx) + require.NoError(t, err) + diff = cmp.Diff(info, got, + cmpopts.IgnoreFields(headerv1.Metadata{}, "Revision"), + protocmp.Transform(), + ) + require.Empty(t, diff) + require.Equal(t, created.GetMetadata().GetRevision(), got.GetMetadata().GetRevision()) + + info.Spec.TeleportVersion = "3.2.1" + updated, err := service.UpdateBackendInfo(ctx, info) + require.NoError(t, err) + require.NotEqual(t, got.GetSpec().GetTeleportVersion(), updated.GetSpec().GetTeleportVersion()) + + err = service.DeleteBackendInfo(ctx) + require.NoError(t, err) + + _, err = service.GetBackendInfo(ctx) + var notFoundError *trace.NotFoundError + require.ErrorAs(t, err, ¬FoundError) + + // If we try to conditionally update a missing resource, we receive + // a CompareFailed instead of a NotFound. + var revisionMismatchError *trace.CompareFailedError + _, err = service.UpdateBackendInfo(ctx, info) + require.ErrorAs(t, err, &revisionMismatchError) +}