Files
Ryan Clark 129d150383 Add Classifier backend storage, cache, gRPC service, and tctl support (#67761)
* Add Classifier backend storage, cache, gRPC service, and tctl support

* Fix formatting in lib/services/summarizer_test.go

* Replace Cache.AllClassifiers with RangeClassifiers using genericLister

* Validate that the filter is a boolean expression

* Code review fixes: sync test, range fallback
2026-06-16 14:47:39 +00:00

1103 lines
38 KiB
Go

/*
* Teleport
* Copyright (C) 2023 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 services
import (
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"github.com/gravitational/trace"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/protoadapt"
"google.golang.org/protobuf/types/known/timestamppb"
autoupdatev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
healthcheckconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/healthcheckconfig/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1"
workloadidentityv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/types"
apiutils "github.com/gravitational/teleport/api/utils"
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
"github.com/gravitational/teleport/lib/utils"
)
// MarshalConfig specifies marshaling options
type MarshalConfig struct {
// Version specifies a particular version we should marshal resources with
Version string
// Revision of the resource to assign.
Revision string
// PreserveRevision preserves revision in resource
// specs when marshaling
PreserveRevision bool
// Expires is an optional expiry time
Expires time.Time
// DisallowUnknown will, for resources stored in protojson, disallow unknown
// fields when unmarshaling. This is useful if a resource is being parsed
// from user-specified data rather than persistent cluster state storage.
DisallowUnknown bool
}
// GetVersion returns explicitly provided version or sets latest as default
func (m *MarshalConfig) GetVersion() string {
if m.Version == "" {
return types.V2
}
return m.Version
}
// MarshalOption sets marshaling option
type MarshalOption func(c *MarshalConfig) error
// CollectOptions collects all options from functional arg and returns config
func CollectOptions(opts []MarshalOption) (*MarshalConfig, error) {
var cfg MarshalConfig
for _, o := range opts {
if err := o(&cfg); err != nil {
return nil, trace.Wrap(err)
}
}
return &cfg, nil
}
// AddOptions adds marshal options and returns a new copy
func AddOptions(opts []MarshalOption, add ...MarshalOption) []MarshalOption {
out := make([]MarshalOption, len(opts), len(opts)+len(add))
copy(out, opts)
return append(opts, add...)
}
// WithRevision assigns Revision to the resource
func WithRevision(rev string) MarshalOption {
return func(c *MarshalConfig) error {
c.Revision = rev
return nil
}
}
// WithExpires assigns expiry value
func WithExpires(expires time.Time) MarshalOption {
return func(c *MarshalConfig) error {
c.Expires = expires
return nil
}
}
// WithVersion sets marshal version
func WithVersion(v string) MarshalOption {
return func(c *MarshalConfig) error {
switch v {
case types.V1, types.V2, types.V3:
c.Version = v
return nil
default:
return trace.BadParameter("version '%v' is not supported", v)
}
}
}
// DisallowUnknown will, for resources stored in protojson, disallow unknown
// fields when unmarshaling. This is useful if a resource is being parsed
// from user-specified data rather than persistent cluster state storage.
func DisallowUnknown() MarshalOption {
return func(c *MarshalConfig) error {
c.DisallowUnknown = true
return nil
}
}
// PreserveRevision preserves revision when
// marshaling value
func PreserveRevision() MarshalOption {
return func(c *MarshalConfig) error {
c.PreserveRevision = true
return nil
}
}
// ParseShortcut parses resource shortcut
// Generally, this should include the plural of a singular resource name or vice
// versa.
func ParseShortcut(in string) (string, error) {
if in == "" {
return "", trace.BadParameter("missing resource name")
}
switch strings.ToLower(in) {
case types.KindRole, "roles":
return types.KindRole, nil
case types.KindNamespace, "namespaces", "ns":
return types.KindNamespace, nil
case types.KindAuthServer, "auth_servers", "auth":
return types.KindAuthServer, nil
case types.KindProxy, "proxies":
return types.KindProxy, nil
case types.KindNode, "nodes":
return types.KindNode, nil
case types.KindOIDCConnector:
return types.KindOIDCConnector, nil
case types.KindSAMLConnector:
return types.KindSAMLConnector, nil
case types.KindGithubConnector:
return types.KindGithubConnector, nil
case types.KindConnectors, "connector":
return types.KindConnectors, nil
case types.KindUser, "users":
return types.KindUser, nil
case types.KindCertAuthority, "cert_authorities", "cas":
return types.KindCertAuthority, nil
case types.KindCertAuthorityOverride, "cert_authority_overrides", "ca_override", "ca_overrides":
return types.KindCertAuthorityOverride, nil
case types.KindReverseTunnel, "reverse_tunnels", "rts":
return types.KindReverseTunnel, nil
case types.KindTrustedCluster, "tc", "cluster", "clusters":
return types.KindTrustedCluster, nil
case types.KindClusterAuthPreference, "cluster_authentication_preferences", "cluster_auth_preferences", "cap":
return types.KindClusterAuthPreference, nil
case types.KindUIConfig, "ui":
return types.KindUIConfig, nil
case types.KindClusterNetworkingConfig, "networking_config", "networking", "net_config", "netconfig":
return types.KindClusterNetworkingConfig, nil
case types.KindSessionRecordingConfig, "recording_config", "session_recording", "rec_config", "recconfig":
return types.KindSessionRecordingConfig, nil
case types.KindExternalAuditStorage:
return types.KindExternalAuditStorage, nil
case types.KindRemoteCluster, "remote_clusters", "rc", "rcs":
return types.KindRemoteCluster, nil
case types.KindSemaphore, "semaphores", "sem", "sems":
return types.KindSemaphore, nil
case types.KindKubernetesCluster, "kube_clusters":
return types.KindKubernetesCluster, nil
case types.KindKubeServer, "kube_servers":
return types.KindKubeServer, nil
case types.KindLock, "locks":
return types.KindLock, nil
case types.KindDatabaseServer, "db_servers":
return types.KindDatabaseServer, nil
case types.KindNetworkRestrictions:
return types.KindNetworkRestrictions, nil
case types.KindDatabase:
return types.KindDatabase, nil
case types.KindApp, "apps":
return types.KindApp, nil
case types.KindAppServer, "app_servers":
return types.KindAppServer, nil
case types.KindWindowsDesktopService, "windows_service", "win_desktop_service", "win_service", "windows_desktop_services":
return types.KindWindowsDesktopService, nil
case types.KindWindowsDesktop, "win_desktop":
return types.KindWindowsDesktop, nil
case types.KindDynamicWindowsDesktop, "dynamic_win_desktop", "dynamic_desktop":
return types.KindDynamicWindowsDesktop, nil
case types.KindToken, "tokens":
return types.KindToken, nil
case types.KindInstaller:
return types.KindInstaller, nil
case types.KindDatabaseService, types.KindDatabaseService + "s":
return types.KindDatabaseService, nil
case types.KindLoginRule, types.KindLoginRule + "s":
return types.KindLoginRule, nil
case types.KindSAMLIdPServiceProvider, types.KindSAMLIdPServiceProvider + "s", "saml_sp", "saml_sps":
return types.KindSAMLIdPServiceProvider, nil
case types.KindUserGroup, types.KindUserGroup + "s", "usergroup", "usergroups":
return types.KindUserGroup, nil
case types.KindDevice, types.KindDevice + "s":
return types.KindDevice, nil
case types.KindOktaImportRule, types.KindOktaImportRule + "s", "oktaimportrule", "oktaimportrules":
return types.KindOktaImportRule, nil
case types.KindOktaAssignment, types.KindOktaAssignment + "s", "oktaassignment", "oktaassignments":
return types.KindOktaAssignment, nil
case types.KindClusterMaintenanceConfig, "cmc":
return types.KindClusterMaintenanceConfig, nil
case types.KindIntegration, types.KindIntegration + "s":
return types.KindIntegration, nil
case types.KindAccessList, types.KindAccessList + "s", "accesslist", "accesslists":
return types.KindAccessList, nil
case types.KindDiscoveryConfig, types.KindDiscoveryConfig + "s", "discoveryconfig", "discoveryconfigs":
return types.KindDiscoveryConfig, nil
case types.KindAuditQuery:
return types.KindAuditQuery, nil
case types.KindSecurityReport:
return types.KindSecurityReport, nil
case types.KindServerInfo:
return types.KindServerInfo, nil
case types.KindBot, "bots":
return types.KindBot, nil
case types.KindBotInstance, types.KindBotInstance + "s":
return types.KindBotInstance, nil
case types.KindDatabaseObjectImportRule, "db_object_import_rules", "database_object_import_rule":
return types.KindDatabaseObjectImportRule, nil
case types.KindAccessMonitoringRule:
return types.KindAccessMonitoringRule, nil
case types.KindDatabaseObject, "database_object":
return types.KindDatabaseObject, nil
case types.KindCrownJewel, "crown_jewels":
return types.KindCrownJewel, nil
case types.KindVnetConfig:
return types.KindVnetConfig, nil
case types.KindAccessRequest, types.KindAccessRequest + "s", "accessrequest", "accessrequests":
return types.KindAccessRequest, nil
case types.KindPlugin, types.KindPlugin + "s":
return types.KindPlugin, nil
case types.KindAccessGraphSettings, "ags":
return types.KindAccessGraphSettings, nil
case types.KindSPIFFEFederation, types.KindSPIFFEFederation + "s":
return types.KindSPIFFEFederation, nil
case types.KindWorkloadIdentity, types.KindWorkloadIdentity + "s", "workload_identities", "workloadidentity", "workloadidentities", "workloadidentitys":
return types.KindWorkloadIdentity, nil
case types.KindStaticHostUser, types.KindStaticHostUser + "s", "host_user", "host_users":
return types.KindStaticHostUser, nil
case types.KindUserTask, types.KindUserTask + "s":
return types.KindUserTask, nil
case types.KindAutoUpdateConfig:
return types.KindAutoUpdateConfig, nil
case types.KindAutoUpdateVersion:
return types.KindAutoUpdateVersion, nil
case types.KindAutoUpdateAgentRollout:
return types.KindAutoUpdateAgentRollout, nil
case types.KindAutoUpdateAgentReport:
return types.KindAutoUpdateAgentReport, nil
case types.KindAutoUpdateBotInstanceReport:
return types.KindAutoUpdateBotInstanceReport, nil
case types.KindGitServer, types.KindGitServer + "s":
return types.KindGitServer, nil
case types.KindWorkloadIdentityX509Revocation, types.KindWorkloadIdentityX509Revocation + "s":
return types.KindWorkloadIdentityX509Revocation, nil
case types.KindWorkloadIdentityX509IssuerOverride, types.KindWorkloadIdentityX509IssuerOverride + "s":
return types.KindWorkloadIdentityX509IssuerOverride, nil
case types.KindSigstorePolicy, "sigstorepolicy", "sigstore_policies", "sigstorepolicies":
return types.KindSigstorePolicy, nil
case types.KindHealthCheckConfig, types.KindHealthCheckConfig + "s", "hcc":
return types.KindHealthCheckConfig, nil
case scopedaccess.KindScopedRole, scopedaccess.KindScopedRole + "s", "scopedrole", "scopedroles":
return scopedaccess.KindScopedRole, nil
case scopedaccess.KindScopedRoleAssignment, scopedaccess.KindScopedRoleAssignment + "s", "scopedroleassignment", "scopedroleassignments", "sra":
return scopedaccess.KindScopedRoleAssignment, nil
case types.KindInferenceModel, "inference_models":
return types.KindInferenceModel, nil
case types.KindInferenceSecret, "inference_secrets":
return types.KindInferenceSecret, nil
case types.KindInferencePolicy, "inference_policies":
return types.KindInferencePolicy, nil
case types.KindClassifier, types.KindClassifier + "s":
return types.KindClassifier, nil
case types.KindRetrievalModel:
return types.KindRetrievalModel, nil
case types.KindRelayServer, types.KindRelayServer + "s":
return types.KindRelayServer, nil
case types.KindAppAuthConfig, types.KindAppAuthConfig + "s", "aac":
return types.KindAppAuthConfig, nil
case types.KindWorkloadCluster, types.KindWorkloadCluster + "s":
return types.KindWorkloadCluster, nil
case scopedaccess.KindScopedToken, scopedaccess.KindScopedToken + "s", "scopedtoken", "scopedtokens":
return scopedaccess.KindScopedToken, nil
}
return "", trace.BadParameter("unsupported resource: %q - resources should be expressed as 'type/name', for example 'connector/github'", in)
}
// ParseRef parses resource reference eg daemonsets/ds1
func ParseRef(ref string) (*Ref, error) {
if ref == "" {
return nil, trace.BadParameter("missing value")
}
parts := strings.FieldsFunc(ref, isDelimiter)
switch len(parts) {
case 1:
shortcut, err := ParseShortcut(parts[0])
if err != nil {
return nil, trace.Wrap(err)
}
return &Ref{Kind: shortcut}, nil
case 2:
shortcut, err := ParseShortcut(parts[0])
if err != nil {
return nil, trace.Wrap(err)
}
return &Ref{Kind: shortcut, Name: parts[1]}, nil
case 3:
shortcut, err := ParseShortcut(parts[0])
if err != nil {
return nil, trace.Wrap(err)
}
return &Ref{Kind: shortcut, SubKind: parts[1], Name: parts[2]}, nil
}
return nil, trace.BadParameter("failed to parse '%v'", ref)
}
// isDelimiter returns true if rune is space or /
func isDelimiter(r rune) bool {
switch r {
case '\t', ' ', '/':
return true
}
return false
}
// Ref is a resource reference. Typically of the form kind/name,
// but sometimes of the form kind/subkind/name.
type Ref struct {
Kind string
SubKind string
Name string
}
// Set sets the name of the resource
func (r *Ref) Set(v string) error {
out, err := ParseRef(v)
if err != nil {
return err
}
*r = *out
return nil
}
func (r *Ref) String() string {
if r.SubKind == "" {
if r.Name == "" {
return r.Kind
}
return fmt.Sprintf("%s/%s", r.Kind, r.Name)
}
return fmt.Sprintf("%s/%s/%s", r.Kind, r.SubKind, r.Name)
}
// Refs is a set of resource references
type Refs []Ref
// ParseRefs parses a comma-separated string of resource references (eg "users/alice,users/bob")
func ParseRefs(refs string) (Refs, error) {
if refs == "all" {
return []Ref{{Kind: "all"}}, nil
}
var escaped bool
isBreak := func(r rune) bool {
brk := false
switch r {
case ',':
brk = true && !escaped
escaped = false
case '\\':
escaped = true && !escaped
default:
escaped = false
}
return brk
}
var parsed []Ref
split := fieldsFunc(strings.TrimSpace(refs), isBreak)
for _, s := range split {
ref, err := ParseRef(strings.ReplaceAll(s, `\,`, `,`))
if err != nil {
return nil, trace.Wrap(err)
}
parsed = append(parsed, *ref)
}
return parsed, nil
}
// Set sets the value of `r` from a comma-separated string of resource
// references (in-place equivalent of `ParseRefs`).
func (r *Refs) Set(v string) error {
refs, err := ParseRefs(v)
if err != nil {
return trace.Wrap(err)
}
*r = refs
return nil
}
// IsAll checks if refs is special wildcard case `all`.
func (r *Refs) IsAll() bool {
refs := *r
if len(refs) != 1 {
return false
}
return refs[0].Kind == "all"
}
func (r *Refs) String() string {
var builder strings.Builder
for i, ref := range *r {
if i > 0 {
builder.WriteRune(',')
}
builder.WriteString(ref.String())
}
return builder.String()
}
// fieldsFunc is an exact copy of the current implementation of `strings.FieldsFunc`.
// The docs of `strings.FieldsFunc` indicate that future implementations may not call
// `f` on every rune, may not preserve ordering, or may panic if `f` does not return the
// same output for every instance of a given rune. All of these changes would break
// our implementation of backslash-escaping, so we're using a local copy.
func fieldsFunc(s string, f func(rune) bool) []string {
// A span is used to record a slice of s of the form s[start:end].
// The start index is inclusive and the end index is exclusive.
type span struct {
start int
end int
}
spans := make([]span, 0, 32)
// Find the field start and end indices.
wasField := false
fromIndex := 0
for i, rune := range s {
if f(rune) {
if wasField {
spans = append(spans, span{start: fromIndex, end: i})
wasField = false
}
} else {
if !wasField {
fromIndex = i
wasField = true
}
}
}
// Last field might end at EOF.
if wasField {
spans = append(spans, span{fromIndex, len(s)})
}
// Create strings from recorded field indices.
a := make([]string, len(spans))
for i, span := range spans {
a[i] = s[span.start:span.end]
}
return a
}
// marshalerMutex is a mutex for resource marshalers/unmarshalers
var marshalerMutex sync.RWMutex
// ResourceMarshaler handles marshaling of a specific resource type.
type ResourceMarshaler func(types.Resource, ...MarshalOption) ([]byte, error)
// ResourceUnmarshaler handles unmarshaling of a specific resource type.
type ResourceUnmarshaler func([]byte, ...MarshalOption) (types.Resource, error)
// resourceMarshalers holds a collection of marshalers organized by kind.
var resourceMarshalers = make(map[string]ResourceMarshaler)
// resourceUnmarshalers holds a collection of unmarshalers organized by kind.
var resourceUnmarshalers = make(map[string]ResourceUnmarshaler)
// GetResourceMarshalerKinds lists all registered resource marshalers by kind.
func GetResourceMarshalerKinds() []string {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
kinds := make([]string, 0, len(resourceMarshalers))
for kind := range resourceMarshalers {
kinds = append(kinds, kind)
}
return kinds
}
// RegisterResourceMarshaler registers a marshaler for resources of a specific kind.
// WARNING!!
// Registering a resource Marshaler requires lib/services/local.CreateResources
// supports the resource kind or the standard backup/restore procedure of using
// `tctl get all` and then BootstrapResources in Teleport will fail.
func RegisterResourceMarshaler(kind string, marshaler ResourceMarshaler) {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
resourceMarshalers[kind] = marshaler
}
// RegisterResourceUnmarshaler registers an unmarshaler for resources of a specific kind.
func RegisterResourceUnmarshaler(kind string, unmarshaler ResourceUnmarshaler) {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
resourceUnmarshalers[kind] = unmarshaler
}
func getResourceMarshaler(kind string) (ResourceMarshaler, bool) {
marshalerMutex.RLock()
defer marshalerMutex.RUnlock()
m, ok := resourceMarshalers[kind]
if !ok {
return nil, false
}
return m, true
}
func getResourceUnmarshaler(kind string) (ResourceUnmarshaler, bool) {
marshalerMutex.RLock()
defer marshalerMutex.RUnlock()
u, ok := resourceUnmarshalers[kind]
if !ok {
return nil, false
}
return u, true
}
func init() {
RegisterResourceMarshaler(types.KindUser, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
user, ok := resource.(types.User)
if !ok {
return nil, trace.BadParameter("expected User, got %T", resource)
}
bytes, err := MarshalUser(user, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindUser, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
user, err := UnmarshalUser(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return user, nil
})
RegisterResourceMarshaler(types.KindCertAuthority, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
certAuthority, ok := resource.(types.CertAuthority)
if !ok {
return nil, trace.BadParameter("expected CertAuthority, got %T", resource)
}
bytes, err := MarshalCertAuthority(certAuthority, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindCertAuthority, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
certAuthority, err := UnmarshalCertAuthority(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return certAuthority, nil
})
RegisterResourceMarshaler(types.KindTrustedCluster, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
trustedCluster, ok := resource.(types.TrustedCluster)
if !ok {
return nil, trace.BadParameter("expected TrustedCluster, got %T", resource)
}
bytes, err := MarshalTrustedCluster(trustedCluster, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindTrustedCluster, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
trustedCluster, err := UnmarshalTrustedCluster(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return trustedCluster, nil
})
RegisterResourceMarshaler(types.KindGithubConnector, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
githubConnector, ok := resource.(types.GithubConnector)
if !ok {
return nil, trace.BadParameter("expected GithubConnector, got %T", resource)
}
bytes, err := MarshalOSSGithubConnector(githubConnector, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindGithubConnector, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
githubConnector, err := UnmarshalOSSGithubConnector(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return githubConnector, nil
})
RegisterResourceMarshaler(types.KindSAMLConnector, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
samlConnector, ok := resource.(types.SAMLConnector)
if !ok {
return nil, trace.BadParameter("expected SAMLConnector, got %T", resource)
}
bytes, err := MarshalSAMLConnector(samlConnector, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindSAMLConnector, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
samlConnector, err := UnmarshalSAMLConnector(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return samlConnector, nil
})
RegisterResourceMarshaler(types.KindOIDCConnector, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
oidConnector, ok := resource.(types.OIDCConnector)
if !ok {
return nil, trace.BadParameter("expected OIDCConnector, got %T", resource)
}
bytes, err := MarshalOIDCConnector(oidConnector, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindOIDCConnector, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
oidcConnector, err := UnmarshalOIDCConnector(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return oidcConnector, nil
})
RegisterResourceMarshaler(types.KindRole, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
role, ok := resource.(types.Role)
if !ok {
return nil, trace.BadParameter("expected Role, got %T", resource)
}
bytes, err := MarshalRole(role, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindRole, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
role, err := UnmarshalRole(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return role, nil
})
RegisterResourceMarshaler(types.KindToken, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
token, ok := resource.(types.ProvisionToken)
if !ok {
return nil, trace.BadParameter("expected Token, got %T", resource)
}
bytes, err := MarshalProvisionToken(token, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindToken, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
token, err := UnmarshalProvisionToken(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return token, nil
})
RegisterResourceMarshaler(types.KindLock, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
lock, ok := resource.(types.Lock)
if !ok {
return nil, trace.BadParameter("expected lock, got %T", resource)
}
bytes, err := MarshalLock(lock, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindLock, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
lock, err := UnmarshalLock(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return lock, nil
})
RegisterResourceMarshaler(types.KindClusterNetworkingConfig, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
cnc, ok := resource.(types.ClusterNetworkingConfig)
if !ok {
return nil, trace.BadParameter("expected cluster_networking_config go %T", resource)
}
bytes, err := MarshalClusterNetworkingConfig(cnc, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindClusterNetworkingConfig, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
cnc, err := UnmarshalClusterNetworkingConfig(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return cnc, nil
})
RegisterResourceMarshaler(types.KindClusterAuthPreference, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
ap, ok := resource.(types.AuthPreference)
if !ok {
return nil, trace.BadParameter("expected cluster_auth_preference go %T", resource)
}
bytes, err := MarshalAuthPreference(ap, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindClusterAuthPreference, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
ap, err := UnmarshalAuthPreference(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return ap, nil
})
RegisterResourceUnmarshaler(types.KindBot, func(bytes []byte, option ...MarshalOption) (types.Resource, error) {
cfg, err := CollectOptions(option)
if err != nil {
return nil, err
}
b := &machineidv1pb.Bot{}
if err := (protojson.UnmarshalOptions{DiscardUnknown: !cfg.DisallowUnknown}).Unmarshal(bytes, b); err != nil {
return nil, trace.Wrap(err)
}
return types.Resource153ToLegacy(b), nil
})
RegisterResourceUnmarshaler(types.KindAutoUpdateConfig, func(bytes []byte, option ...MarshalOption) (types.Resource, error) {
cfg, err := CollectOptions(option)
if err != nil {
return nil, err
}
c := &autoupdatev1pb.AutoUpdateConfig{}
if err := (protojson.UnmarshalOptions{DiscardUnknown: !cfg.DisallowUnknown}).Unmarshal(bytes, c); err != nil {
return nil, trace.Wrap(err)
}
return types.Resource153ToLegacy(c), nil
})
RegisterResourceUnmarshaler(types.KindAutoUpdateVersion, func(bytes []byte, option ...MarshalOption) (types.Resource, error) {
cfg, err := CollectOptions(option)
if err != nil {
return nil, err
}
v := &autoupdatev1pb.AutoUpdateVersion{}
if err := (protojson.UnmarshalOptions{DiscardUnknown: !cfg.DisallowUnknown}).Unmarshal(bytes, v); err != nil {
return nil, trace.Wrap(err)
}
return types.Resource153ToLegacy(v), nil
})
// add health_check_config to tctl get all
RegisterResourceMarshaler(types.KindHealthCheckConfig, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
wrapper, ok := resource.(types.Resource153UnwrapperT[*healthcheckconfigv1.HealthCheckConfig])
if !ok {
return nil, trace.BadParameter("expected health check config, got %T", resource)
}
bytes, err := MarshalHealthCheckConfig(wrapper.UnwrapT(), opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
// support health_check_config --bootstrap and --apply-on-startup
RegisterResourceUnmarshaler(types.KindHealthCheckConfig, func(data []byte, options ...MarshalOption) (types.Resource, error) {
cfg, err := UnmarshalHealthCheckConfig(data, options...)
if err != nil {
return nil, trace.Wrap(err)
}
return types.Resource153ToLegacy(cfg), nil
})
RegisterResourceUnmarshaler(types.KindWorkloadIdentity, func(bytes []byte, option ...MarshalOption) (types.Resource, error) {
cfg, err := CollectOptions(option)
if err != nil {
return nil, err
}
wid := &workloadidentityv1.WorkloadIdentity{}
if err := (protojson.UnmarshalOptions{DiscardUnknown: !cfg.DisallowUnknown}).Unmarshal(bytes, wid); err != nil {
return nil, trace.Wrap(err)
}
return types.Resource153ToLegacy(wid), nil
})
RegisterResourceMarshaler(types.KindCertAuthorityOverride, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
unwrapper, ok := resource.(types.Resource153UnwrapperT[*subcav1.CertAuthorityOverride])
if !ok {
return nil, trace.BadParameter("expected wrapped CertAuthorityOverride resource, got %T", resource)
}
caOverride := unwrapper.UnwrapT()
if caOverride == nil {
return nil, trace.BadParameter("nil CertAuthorityOverride resource")
}
bytes, err := MarshalCertAuthorityOverride(caOverride, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindCertAuthorityOverride, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
caOverride, err := UnmarshalCertAuthorityOverride(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return types.ProtoResource153ToLegacy(caOverride), nil
})
}
// CheckAndSetDefaults calls [r.CheckAndSetDefaults] if r implements the method.
// If r does not implement, then this is a nop.
//
// This method exists for backwards compatibility with old-style resources.
// Prefer using RFD 153 style resources, passing concrete types and running
// validations before storage writes only.
func CheckAndSetDefaults(r any) error {
if r, ok := r.(interface{ CheckAndSetDefaults() error }); ok {
return trace.Wrap(r.CheckAndSetDefaults())
}
return nil
}
// MarshalResource attempts to marshal a resource dynamically, returning NotImplementedError
// if no marshaler has been registered.
//
// NOTE: This function only supports the subset of resources which may be imported/exported
// by users (e.g. via `tctl get`).
func MarshalResource(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
marshal, ok := getResourceMarshaler(resource.GetKind())
if !ok {
return nil, trace.NotImplemented("cannot dynamically marshal resources of kind %q", resource.GetKind())
}
// Handle the case where `resource` was never fully unmarshaled.
if r, ok := resource.(*UnknownResource); ok {
u, err := UnmarshalResource(r.GetKind(), r.Raw, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
resource = u
}
m, err := marshal(resource, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return m, nil
}
// UnmarshalResource attempts to unmarshal a resource dynamically, returning NotImplementedError
// if no unmarshaler has been registered.
//
// NOTE: This function only supports the subset of resources which may be imported/exported
// by users (e.g. via `tctl get`).
func UnmarshalResource(kind string, raw []byte, opts ...MarshalOption) (types.Resource, error) {
unmarshal, ok := getResourceUnmarshaler(kind)
if !ok {
return nil, trace.NotImplemented("cannot dynamically unmarshal resources of kind %q", kind)
}
u, err := unmarshal(raw, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return u, nil
}
// UnknownResource is used to detect resources
type UnknownResource struct {
types.ResourceHeader
// Raw is raw representation of the resource
Raw []byte
}
// UnmarshalJSON unmarshals header and captures raw state
func (u *UnknownResource) UnmarshalJSON(raw []byte) error {
var h types.ResourceHeader
if err := json.Unmarshal(raw, &h); err != nil {
return trace.Wrap(err)
}
u.Raw = make([]byte, len(raw))
u.ResourceHeader = h
copy(u.Raw, raw)
return nil
}
// setResourceName modifies the types.Metadata argument in place, setting the resource name.
// The name is calculated based on nameParts arguments which are joined by hyphens "-".
// If a name override label is present, it will replace the *first* name part.
func setResourceName(overrideLabels []string, meta types.Metadata, firstNamePart string, extraNameParts ...string) types.Metadata {
nameParts := append([]string{firstNamePart}, extraNameParts...)
// apply override
for _, overrideLabel := range overrideLabels {
if override, found := meta.Labels[overrideLabel]; found && override != "" {
nameParts[0] = override
break
}
}
meta.Name = strings.Join(nameParts, "-")
return meta
}
type resetProtoResource interface {
protoadapt.MessageV1
SetRevision(string)
}
// maybeResetProtoRevision returns a clone of [r] with the identifiers reset to default values if
// preserveRevision is true, otherwise this is a nop, and the original value is returned unaltered.
//
// Prefer maybeResetProtoRevisionv2 for newer RFD153-style resources, only one or the other should compile
// for any given type.
func maybeResetProtoRevision[T resetProtoResource](preserveRevision bool, r T) T {
if preserveRevision {
return r
}
cp := apiutils.CloneProtoMsg(r)
cp.SetRevision("")
return cp
}
// ProtoResource abstracts a resource defined as a protobuf message.
type ProtoResource interface {
proto.Message
// GetMetadata returns the generic resource metadata.
GetMetadata() *headerv1.Metadata
}
// ProtoResourcePtr is a ProtoResource that is also a pointer to T.
type ProtoResourcePtr[T any] interface {
*T
ProtoResource
}
// maybeResetProtoRevisionv2 returns a clone of [r] with the identifiers reset to default values if
// preserveRevision is true, otherwise this is a nop, and the original value is returned unaltered.
//
// This is like maybeResetProtoRevision but made for newer RFD153-style resources which implement a
// different interface, only one or the other should compile for any given type.
func maybeResetProtoRevisionv2[T ProtoResource](preserveRevision bool, r T) T {
if preserveRevision {
return r
}
cp := proto.Clone(r).(T)
cp.GetMetadata().SetRevision("")
return cp
}
// MarshalProtoResource marshals a ProtoResource to JSON using [protojson.Marshal] and respecting [opts].
func MarshalProtoResource[T ProtoResource](resource T, opts ...MarshalOption) ([]byte, error) {
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
resource = maybeResetProtoRevisionv2(cfg.PreserveRevision, resource)
data, err := protojson.Marshal(resource)
if err != nil {
return nil, trace.Wrap(err)
}
return data, nil
}
// UnmarshalProtoResource unmarshals a ProtoResource from JSON using [protojson.Unmarshal] and respecting [opts].
// It is paramaterized on types T and U, where T is a pointer type that implements ProtoResource, and U is the
// type that T points to. This is so that it can allocate an instance of U to unmarshal into without
// reflection.
func UnmarshalProtoResource[T ProtoResourcePtr[U], U any](data []byte, opts ...MarshalOption) (T, error) {
if len(data) == 0 {
return nil, trace.BadParameter("nothing to unmarshal")
}
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
var resource T = new(U)
err = protojson.UnmarshalOptions{DiscardUnknown: !cfg.DisallowUnknown}.Unmarshal(data, resource)
if err != nil {
return nil, trace.Wrap(err)
}
if cfg.Revision != "" {
resource.GetMetadata().SetRevision(cfg.Revision)
}
if !cfg.Expires.IsZero() {
resource.GetMetadata().SetExpires(timestamppb.New(cfg.Expires))
}
return resource, nil
}
// UnmarshalProtoResourceArray unmarshals an array of ProtoResources from JSON using [UnmarshalProtoResource] on each
// individual element.
func UnmarshalProtoResourceArray[T ProtoResourcePtr[U], U any](data []byte, opts ...MarshalOption) ([]T, error) {
var msgs []json.RawMessage
if err := json.Unmarshal(data, &msgs); err != nil {
return nil, trace.Wrap(err)
}
resources := make([]T, 0, len(msgs))
for _, msg := range msgs {
resource, err := UnmarshalProtoResource[T](msg, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
resources = append(resources, resource)
}
return resources, nil
}
// FastMarshalProtoResourceDeprecated marshals a ProtoResource to JSON using [utils.FastMarshal] and respecting [opts].
//
// Deprecated: this should not be used for new types, prefer [MarshalProtoResource]. Existing types should not
// be converted to maintain compatibility.
func FastMarshalProtoResourceDeprecated[T ProtoResource](resource T, opts ...MarshalOption) ([]byte, error) {
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
resource = maybeResetProtoRevisionv2(cfg.PreserveRevision, resource)
data, err := utils.FastMarshal(resource)
if err != nil {
return nil, trace.Wrap(err)
}
return data, nil
}
// FastUnmarshalProtoResourceDeprecated unmarshals a ProtoResource from JSON using [utils.FastUnmarshal] and respecting [opts].
// It is paramaterized on types T and U, where T is a pointer type that implements ProtoResource, and U is the
// type that T points to. This is so that it can allocate an instance of U to unmarshal into without
// reflection.
//
// Deprecated: this should not be used for new types, prefer [UnmarshalProtoResource]. Existing types should not
// be converted to maintain compatibility.
func FastUnmarshalProtoResourceDeprecated[T ProtoResourcePtr[U], U any](data []byte, opts ...MarshalOption) (T, error) {
if len(data) == 0 {
return nil, trace.BadParameter("nothing to unmarshal")
}
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
var resource T = new(U)
err = utils.FastUnmarshal(data, resource)
if err != nil {
return nil, trace.Wrap(err)
}
if cfg.Revision != "" {
resource.GetMetadata().SetRevision(cfg.Revision)
}
if !cfg.Expires.IsZero() {
resource.GetMetadata().SetExpires(timestamppb.New(cfg.Expires))
}
return resource, nil
}