Add dynamic marshalers for CertAuthorityOverride (#65858)

This commit is contained in:
Alan Parra
2026-04-17 14:44:02 +00:00
committed by GitHub
parent 3da0f44c80
commit 2edfe25ada
6 changed files with 176 additions and 7 deletions
+3
View File
@@ -26,6 +26,7 @@ import (
autoupdatev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
healthcheckconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/healthcheckconfig/v1"
subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/backend"
@@ -109,6 +110,8 @@ func itemsFromResource(resource types.Resource) ([]backend.Item, error) {
item, err = itemFromHealthCheckConfig(r.UnwrapT())
case types.Resource153UnwrapperT[*workloadidentityv1pb.WorkloadIdentity]:
item, err = itemFromWorkloadIdentity(r.UnwrapT())
case types.Resource153UnwrapperT[*subcav1.CertAuthorityOverride]:
item, err = itemFromCertAuthorityOverride(r.UnwrapT())
default:
return nil, trace.NotImplemented("cannot itemFrom resource of type %T", resource)
}
+26
View File
@@ -262,3 +262,29 @@ func (p *certAuthorityOverrideParser) parse(event backend.Event) (types.Resource
return nil, trace.BadParameter("event %v is not supported", event.Type)
}
}
// itemFromCertAuthorityOverride is used by CreateResources.
func itemFromCertAuthorityOverride(resource *subcav1.CertAuthorityOverride) (*backend.Item, error) {
if _, err := subca.ValidateAndParseCAOverride(resource); err != nil {
return nil, trace.Wrap(err)
}
value, err := services.MarshalCertAuthorityOverride(resource)
if err != nil {
return nil, trace.Wrap(err)
}
expires, err := types.GetExpiry(resource)
if err != nil {
return nil, trace.Wrap(err)
}
key := newCAOverridesPrefix().AppendKey(backend.NewKey(
resource.Metadata.Name,
resource.SubKind,
))
return &backend.Item{
Key: key,
Value: value,
Expires: expires,
Revision: resource.Metadata.Revision,
}, nil
}
+52
View File
@@ -27,6 +27,7 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/backend"
@@ -369,3 +370,54 @@ func TestSubCAService_GetDeleteNotFoundError(t *testing.T) {
assert.ErrorContains(t, err, wantErr)
})
}
func TestCreateResource_CertAuthorityOverride(t *testing.T) {
t.Parallel()
env := subcaenv.New(t, subcaenv.EnvParams{
SkipExternalRoot: true,
})
be := env.Backend
service := env.SubCA
t.Run("invalid", func(t *testing.T) {
t.Parallel()
// An empty resource is not valid.
r := types.Resource153ToLegacy(&subcav1.CertAuthorityOverride{})
assert.ErrorAs(t,
local.CreateResources(t.Context(), be, r),
new(*trace.BadParameterError),
"CreateResources error mismatch")
})
t.Run("ok", func(t *testing.T) {
t.Parallel()
want := &subcav1.CertAuthorityOverride{
Kind: types.KindCertAuthorityOverride,
SubKind: string(types.DatabaseClientCA),
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: env.ClusterName,
},
Spec: &subcav1.CertAuthorityOverrideSpec{},
}
// CreateResources.
r := types.Resource153ToLegacy(want)
require.NoError(t,
local.CreateResources(t.Context(), be, r),
"CreateResources errored")
// Verify resource via service read.
got, err := service.GetCertAuthorityOverride(
t.Context(), local.CertAuthorityOverrideIDFromResource(want))
require.NoError(t, err)
want.Metadata.Revision = got.Metadata.Revision
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("CertAuthorityOverride mismatch (-want +got)\n%s", diff)
}
})
}
+36
View File
@@ -35,10 +35,12 @@ import (
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/subca"
"github.com/gravitational/teleport/lib/utils"
)
@@ -823,6 +825,40 @@ func init() {
}
return types.Resource153ToLegacy(wid), nil
})
// Gate these behind the feature flag, as they have an effect on user-visible product surface
// (ie, "tctl get all").
if subca.Enabled() {
initSubCA()
}
}
func initSubCA() {
// TODO(codingllama): Remove this method and inline calls on init() once the
// feature flag is no more.
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.
+26 -5
View File
@@ -14,7 +14,7 @@
// 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_test
package services
import (
"testing"
@@ -26,10 +26,11 @@ import (
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/services"
)
func TestMarshalCertAuthOverrideRoundtrip(t *testing.T) {
func TestMarshalCertAuthorityOverrideRoundtrip(t *testing.T) {
t.Parallel()
want := &subcav1.CertAuthorityOverride{
Kind: types.KindCertAuthorityOverride,
SubKind: string(types.DatabaseClientCA),
@@ -41,13 +42,33 @@ func TestMarshalCertAuthOverrideRoundtrip(t *testing.T) {
}
t.Run("ok", func(t *testing.T) {
val, err := services.MarshalCertAuthorityOverride(want)
t.Parallel()
val, err := MarshalCertAuthorityOverride(want)
require.NoError(t, err)
got, err := services.UnmarshalCertAuthorityOverride(val)
got, err := UnmarshalCertAuthorityOverride(val)
require.NoError(t, err)
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("CAOverride mismatch (-want +got)\n%s", diff)
}
})
t.Run("dynamic", func(t *testing.T) {
t.Parallel()
initSubCA()
initSubCA() // check that a duplicate call is harmless
val, err := MarshalResource(types.Resource153ToLegacy(want))
require.NoError(t, err)
wrapped, err := UnmarshalResource(types.KindCertAuthorityOverride, val)
require.NoError(t, err)
unwrapper, ok := wrapped.(types.Resource153UnwrapperT[*subcav1.CertAuthorityOverride])
require.True(t, ok, "Wrapped resource has unexpected type: %T", wrapped)
got := unwrapper.UnwrapT()
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("CAOverride mismatch (-want +got)\n%s", diff)
}
})
}
+33 -2
View File
@@ -277,6 +277,15 @@ func (rc *ResourceCommand) Get(ctx context.Context, client *authclient.Client) e
}
func (rc *ResourceCommand) GetMany(ctx context.Context, client *authclient.Client) error {
const skipNotSupported = false
return trace.Wrap(rc.getMany(ctx, client, skipNotSupported))
}
func (rc *ResourceCommand) getMany(
ctx context.Context,
client *authclient.Client,
skipNotSupported bool,
) error {
if rc.format != teleport.YAML {
return trace.BadParameter("mixed resource types only support YAML formatting")
}
@@ -285,6 +294,9 @@ func (rc *ResourceCommand) GetMany(ctx context.Context, client *authclient.Clien
for _, ref := range rc.refs {
rc.ref = ref
collection, err := rc.getCollection(ctx, client)
if skipNotSupported && errors.As(err, new(*errNotSupported)) {
continue
}
if err != nil {
return trace.Wrap(err)
}
@@ -307,7 +319,12 @@ func (rc *ResourceCommand) GetAll(ctx context.Context, client *authclient.Client
allRefs = append(allRefs, ref)
}
rc.refs = services.Refs(allRefs)
return rc.GetMany(ctx, client)
// This lets OSS query Enterprise-only kinds without failing when the
// corresponding RPCs return "NotImplemented".
const skipNotSupported = true
return rc.getMany(ctx, client, skipNotSupported)
}
// Create updates or inserts one or many resources
@@ -842,7 +859,7 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient
coll, err := handler.Get(ctx, client, rc.ref, resources.GetOpts{WithSecrets: rc.withSecrets})
if err != nil {
if trace.IsNotImplemented(err) {
return nil, trace.BadParameter("getting %q is not supported", rc.ref.String())
return nil, &errNotSupported{trace.BadParameter("getting %q is not supported", rc.ref.String())}
}
return nil, trace.Wrap(err, "getting resource %q of type %q", rc.ref.Name, rc.ref.Kind)
}
@@ -1154,6 +1171,20 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient
return nil, trace.BadParameter("getting %q is not supported", rc.ref.String())
}
// errNotSupported is used to mark NotImplemented errors that were transformed
// into BadParameter, so they can later be identified.
type errNotSupported struct {
cause error
}
func (e *errNotSupported) Error() string {
return e.cause.Error()
}
func (e *errNotSupported) Unwrap() error {
return e.cause
}
// UpsertVerb generates the correct string form of a verb based on the action taken
func UpsertVerb(exists bool, force bool) string {
if !force && exists {