Add AuthenticatableType duck type (#3056)

* Add AuthenticatableType

* Add Resolver for AuthenticatableType

* Run gofmt and goimports

* Fix linter issues
This commit is contained in:
Christoph Stäbler 2024-06-14 15:52:39 +02:00 committed by GitHub
parent 15e6cdf2f3
commit 339c22b821
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 655 additions and 0 deletions

View File

@ -16,6 +16,21 @@ limitations under the License.
package v1
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"knative.dev/pkg/apis"
"knative.dev/pkg/apis/duck/ducktypes"
"knative.dev/pkg/kmeta"
"knative.dev/pkg/ptr"
)
// +genduck
// AuthStatus is meant to provide the generated service account name
// in the resource status.
type AuthStatus struct {
@ -28,3 +43,81 @@ type AuthStatus struct {
// when the component uses multiple identities (e.g. in case of a Parallel).
ServiceAccountNames []string `json:"serviceAccountNames,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// AuthenticatableType is a skeleton type wrapping AuthStatus in the manner we expect
// resource writers defining compatible resources to embed it. We will
// typically use this type to deserialize AuthenticatableType ObjectReferences and
// access the AuthenticatableType data. This is not a real resource.
type AuthenticatableType struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Status AuthenticatableStatus `json:"status"`
}
type AuthenticatableStatus struct {
// Auth contains the service account name for the subscription
// +optional
Auth *AuthStatus `json:"auth,omitempty"`
}
var (
// AuthStatus is a Convertible type.
_ apis.Convertible = (*AuthStatus)(nil)
// Verify AuthenticatableType resources meet duck contracts.
_ apis.Listable = (*AuthenticatableType)(nil)
_ ducktypes.Populatable = (*AuthenticatableType)(nil)
_ kmeta.OwnerRefable = (*AuthenticatableType)(nil)
)
// GetFullType implements duck.Implementable
func (*AuthStatus) GetFullType() ducktypes.Populatable {
return &AuthenticatableType{}
}
// ConvertTo implements apis.Convertible
func (a *AuthStatus) ConvertTo(_ context.Context, to apis.Convertible) error {
return fmt.Errorf("v1 is the highest known version, got: %T", to)
}
// ConvertFrom implements apis.Convertible
func (a *AuthStatus) ConvertFrom(_ context.Context, from apis.Convertible) error {
return fmt.Errorf("v1 is the highest known version, got: %T", from)
}
// Populate implements duck.Populatable
func (t *AuthenticatableType) Populate() {
t.Status = AuthenticatableStatus{
Auth: &AuthStatus{
// Populate ALL fields
ServiceAccountName: ptr.String("foo"),
ServiceAccountNames: []string{
"bar",
"baz",
},
},
}
}
// GetGroupVersionKind implements kmeta.OwnerRefable
func (t *AuthenticatableType) GetGroupVersionKind() schema.GroupVersionKind {
return t.GroupVersionKind()
}
// GetListType implements apis.Listable
func (*AuthenticatableType) GetListType() runtime.Object {
return &AuthenticatableTypeList{}
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// AuthenticatableTypeList is a list of AuthenticatableType resources
type AuthenticatableTypeList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []AuthenticatableType `json:"items"`
}

View File

@ -176,6 +176,87 @@ func (in *AuthStatus) DeepCopy() *AuthStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthenticatableStatus) DeepCopyInto(out *AuthenticatableStatus) {
*out = *in
if in.Auth != nil {
in, out := &in.Auth, &out.Auth
*out = new(AuthStatus)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticatableStatus.
func (in *AuthenticatableStatus) DeepCopy() *AuthenticatableStatus {
if in == nil {
return nil
}
out := new(AuthenticatableStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthenticatableType) DeepCopyInto(out *AuthenticatableType) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Status.DeepCopyInto(&out.Status)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticatableType.
func (in *AuthenticatableType) DeepCopy() *AuthenticatableType {
if in == nil {
return nil
}
out := new(AuthenticatableType)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AuthenticatableType) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthenticatableTypeList) DeepCopyInto(out *AuthenticatableTypeList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]AuthenticatableType, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticatableTypeList.
func (in *AuthenticatableTypeList) DeepCopy() *AuthenticatableTypeList {
if in == nil {
return nil
}
out := new(AuthenticatableTypeList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AuthenticatableTypeList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Binding) DeepCopyInto(out *Binding) {
*out = *in

View File

@ -0,0 +1,60 @@
/*
Copyright 2022 The Knative Authors
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 injection-gen. DO NOT EDIT.
package authstatus
import (
context "context"
duck "knative.dev/pkg/apis/duck"
v1 "knative.dev/pkg/apis/duck/v1"
controller "knative.dev/pkg/controller"
injection "knative.dev/pkg/injection"
dynamicclient "knative.dev/pkg/injection/clients/dynamicclient"
logging "knative.dev/pkg/logging"
)
func init() {
injection.Default.RegisterDuck(WithDuck)
}
// Key is used for associating the Informer inside the context.Context.
type Key struct{}
func WithDuck(ctx context.Context) context.Context {
dc := dynamicclient.Get(ctx)
dif := &duck.CachedInformerFactory{
Delegate: &duck.TypedInformerFactory{
Client: dc,
Type: (&v1.AuthStatus{}).GetFullType(),
ResyncPeriod: controller.GetResyncPeriod(ctx),
StopChannel: ctx.Done(),
},
}
return context.WithValue(ctx, Key{}, dif)
}
// Get extracts the typed informer from the context.
func Get(ctx context.Context) duck.InformerFactory {
untyped := ctx.Value(Key{})
if untyped == nil {
logging.FromContext(ctx).Panic(
"Unable to fetch knative.dev/pkg/apis/duck.InformerFactory from context.")
}
return untyped.(duck.InformerFactory)
}

View File

@ -0,0 +1,30 @@
/*
Copyright 2022 The Knative Authors
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 injection-gen. DO NOT EDIT.
package fake
import (
authstatus "knative.dev/pkg/client/injection/ducks/duck/v1/authstatus"
injection "knative.dev/pkg/injection"
)
var Get = authstatus.Get
func init() {
injection.Fake.RegisterDuck(authstatus.WithDuck)
}

View File

@ -0,0 +1,117 @@
/*
Copyright 2024 The Knative Authors
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 resolver
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/tools/cache"
corev1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"knative.dev/pkg/client/injection/ducks/duck/v1/authstatus"
"knative.dev/pkg/controller"
pkgapisduck "knative.dev/pkg/apis/duck"
duckv1 "knative.dev/pkg/apis/duck/v1"
"knative.dev/pkg/tracker"
)
// AuthenticatableResolver resolves ObjectReferences into a AuthenticatableType.
type AuthenticatableResolver struct {
tracker tracker.Interface
listerFactory func(schema.GroupVersionResource) (cache.GenericLister, error)
}
// NewAuthenticatableResolverFromTracker constructs a new AuthenticatableResolver with context and a tracker.
func NewAuthenticatableResolverFromTracker(ctx context.Context, t tracker.Interface) *AuthenticatableResolver {
ret := &AuthenticatableResolver{
tracker: t,
}
informerFactory := &pkgapisduck.CachedInformerFactory{
Delegate: &pkgapisduck.EnqueueInformerFactory{
Delegate: authstatus.Get(ctx),
EventHandler: controller.HandleAll(ret.tracker.OnChanged),
},
}
ret.listerFactory = func(gvr schema.GroupVersionResource) (cache.GenericLister, error) {
_, l, err := informerFactory.Get(ctx, gvr)
return l, err
}
return ret
}
// AuthStatusFromObjectReference returns the AuthStatus from an object
func (r *AuthenticatableResolver) AuthStatusFromObjectReference(ref *corev1.ObjectReference, parent interface{}) (*duckv1.AuthStatus, error) {
if ref == nil {
return nil, apierrs.NewBadRequest("ref is nil")
}
authenticatable, err := r.authenticatableFromObjectReference(ref, parent)
if err != nil {
return nil, fmt.Errorf("failed to get authenticatable %s/%s: %w", ref.Namespace, ref.Name, err)
}
if authenticatable.Status.Auth == nil {
return nil, fmt.Errorf(".status.auth is missing in object %s/%s", ref.Namespace, ref.Name)
}
return authenticatable.Status.Auth, nil
}
// authenticatableFromObjectReference resolves an object reference into an AuthenticatableType
func (r *AuthenticatableResolver) authenticatableFromObjectReference(ref *corev1.ObjectReference, parent interface{}) (*duckv1.AuthenticatableType, error) {
if ref == nil {
return nil, apierrs.NewBadRequest("ref is nil")
}
gvr, _ := meta.UnsafeGuessKindToResource(ref.GroupVersionKind())
if err := r.tracker.TrackReference(tracker.Reference{
APIVersion: ref.APIVersion,
Kind: ref.Kind,
Namespace: ref.Namespace,
Name: ref.Name,
}, parent); err != nil {
return nil, fmt.Errorf("failed to track reference %s %s/%s: %w", gvr.String(), ref.Namespace, ref.Name, err)
}
lister, err := r.listerFactory(gvr)
if err != nil {
return nil, fmt.Errorf("failed to get lister for %s: %w", gvr.String(), err)
}
obj, err := lister.ByNamespace(ref.Namespace).Get(ref.Name)
if err != nil {
return nil, fmt.Errorf("failed to get object %s/%s: %w", ref.Namespace, ref.Name, err)
}
authenticatable, ok := obj.(*duckv1.AuthenticatableType)
if !ok {
return nil, apierrs.NewBadRequest(fmt.Sprintf("%s(%T) is not an AuthenticatableType", ref, ref))
}
// Do not modify informer copy.
authenticatable = authenticatable.DeepCopy()
return authenticatable, nil
}

View File

@ -0,0 +1,274 @@
/*
Copyright 2024 The Knative Authors
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 resolver_test
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
duckv1 "knative.dev/pkg/apis/duck/v1"
fakedynamicclient "knative.dev/pkg/injection/clients/dynamicclient/fake"
"knative.dev/pkg/ptr"
"knative.dev/pkg/resolver"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
duckv1alpha1 "knative.dev/pkg/apis/duck/v1alpha1"
duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1"
"knative.dev/pkg/client/injection/ducks/duck/v1/authstatus"
"knative.dev/pkg/tracker"
)
const (
authenticatableName = "testsource"
authenticatableKind = "Source"
authenticatableAPIVersion = "duck.knative.dev/v1"
authenticatableResource = "sources.duck.knative.dev"
authenticatable2Name = "test2-source"
authenticatable2Kind = "AnotherSource"
authenticatable2APIVersion = "duck.knative.dev/v1"
authenticatable2Resource = "anothersources.duck.knative.dev"
unauthenticatableName = "testunauthenticatable"
unauthenticatableKind = "KResource"
unauthenticatableAPIVersion = "duck.knative.dev/v1alpha1"
authenticatableServiceAccountName = "my-service-account"
authenticatableServiceAccountName1 = "my-service-account-1"
authenticatableServiceAccountName2 = "my-service-account-2"
authenticatable2ServiceAccountName = "service-account-of-2nd-authenticatable"
)
func init() {
// Add types to scheme
duckv1alpha1.AddToScheme(scheme.Scheme)
duckv1beta1.AddToScheme(scheme.Scheme)
scheme.Scheme.AddKnownTypeWithName(
schema.FromAPIVersionAndKind(unauthenticatableAPIVersion, unauthenticatableKind),
&unstructured.Unstructured{},
)
scheme.Scheme.AddKnownTypeWithName(
schema.FromAPIVersionAndKind(unauthenticatableAPIVersion, unauthenticatableKind+"List"),
&unstructured.UnstructuredList{},
)
scheme.Scheme.AddKnownTypeWithName(
schema.FromAPIVersionAndKind(authenticatableAPIVersion, authenticatableKind),
&unstructured.Unstructured{},
)
scheme.Scheme.AddKnownTypeWithName(
schema.FromAPIVersionAndKind(authenticatableAPIVersion, authenticatableKind+"List"),
&unstructured.UnstructuredList{},
)
}
func TestAuthenticatableResolver_AuthStatusFromObjectReference(t *testing.T) {
tests := []struct {
name string
objects []runtime.Object
objectRef *corev1.ObjectReference
want *duckv1.AuthStatus
wantErr string
}{
{
name: "nil everything",
wantErr: "ref is nil",
}, {
name: "Valid authenticatable",
objects: []runtime.Object{
getAuthenticatable(),
},
objectRef: authenticatableRef(),
want: &duckv1.AuthStatus{
ServiceAccountName: ptr.String(authenticatableServiceAccountName),
},
}, {
name: "Valid authenticatable in multiple objects",
objects: []runtime.Object{
getUnauthenticatable(),
getAuthenticatable(),
getAuthenticatable2(),
},
objectRef: authenticatable2Ref(),
want: &duckv1.AuthStatus{
ServiceAccountName: ptr.String(authenticatable2ServiceAccountName),
},
}, {
name: "Valid authenticatable multiple SAs",
objects: []runtime.Object{
getAuthenticatableWithMultipleSAs(),
},
objectRef: authenticatableRef(),
want: &duckv1.AuthStatus{
ServiceAccountNames: []string{
authenticatableServiceAccountName1,
authenticatableServiceAccountName2,
},
},
}, {
name: "Unauthenticatable",
objects: []runtime.Object{
getUnauthenticatable(),
},
objectRef: unauthenticatableRef(),
wantErr: fmt.Sprintf(".status.auth is missing in object %s/%s", testNS, unauthenticatableName),
}, {
name: "Authenticatable not found",
objects: []runtime.Object{
getUnauthenticatable(),
},
objectRef: authenticatableRef(),
wantErr: fmt.Sprintf("failed to get authenticatable %s/%s: failed to get object %s/%s: %s %q not found", testNS, authenticatableName, testNS, authenticatableName, authenticatableResource, authenticatableName),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, tt.objects...)
ctx = authstatus.WithDuck(ctx)
r := resolver.NewAuthenticatableResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0))
// Run it twice since this should be idempotent. AuthenticatableResolver should
// not modify the cache's copy.
_, _ = r.AuthStatusFromObjectReference(tt.objectRef, getAuthenticatable())
authStatus, gotErr := r.AuthStatusFromObjectReference(tt.objectRef, getAuthenticatable())
if gotErr != nil {
if tt.wantErr != "" {
if got, want := gotErr.Error(), tt.wantErr; got != want {
t.Errorf("Unexpected error (-want, +got) =\n%s", cmp.Diff(want, got))
}
} else {
t.Error("Unexpected error:", gotErr)
}
return
}
if got, want := authStatus, tt.want; !cmp.Equal(got, want) {
t.Errorf("Unexpected object (-want, +got) =\n%s", cmp.Diff(got, want))
}
})
}
}
func getAuthenticatable() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": authenticatableAPIVersion,
"kind": authenticatableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": authenticatableName,
},
"status": map[string]interface{}{
"auth": map[string]interface{}{
"serviceAccountName": authenticatableServiceAccountName,
},
},
},
}
}
func getAuthenticatable2() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": authenticatable2APIVersion,
"kind": authenticatable2Kind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": authenticatable2Name,
},
"status": map[string]interface{}{
"auth": map[string]interface{}{
"serviceAccountName": authenticatable2ServiceAccountName,
},
},
},
}
}
func getAuthenticatableWithMultipleSAs() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": authenticatableAPIVersion,
"kind": authenticatableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": authenticatableName,
},
"status": map[string]interface{}{
"auth": map[string]interface{}{
"serviceAccountNames": []interface{}{
authenticatableServiceAccountName1,
authenticatableServiceAccountName2,
},
},
},
},
}
}
func getUnauthenticatable() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": unauthenticatableAPIVersion,
"kind": unauthenticatableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": unauthenticatableName,
},
"status": map[string]interface{}{
"something": map[string]interface{}{
"foo": "bar",
},
},
},
}
}
func authenticatableRef() *corev1.ObjectReference {
return &corev1.ObjectReference{
Kind: authenticatableKind,
Name: authenticatableName,
APIVersion: authenticatableAPIVersion,
Namespace: testNS,
}
}
func authenticatable2Ref() *corev1.ObjectReference {
return &corev1.ObjectReference{
Kind: authenticatable2Kind,
Name: authenticatable2Name,
APIVersion: authenticatable2APIVersion,
Namespace: testNS,
}
}
func unauthenticatableRef() *corev1.ObjectReference {
return &corev1.ObjectReference{
Kind: unauthenticatableKind,
Name: unauthenticatableName,
APIVersion: unauthenticatableAPIVersion,
Namespace: testNS,
}
}