Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invitation API and Storage #98

Merged
merged 7 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ spec:
port: 9443
version: v1
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1.user.appuio.io
spec:
insecureSkipTLSVerify: true
group: user.appuio.io
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: apiserver
namespace: default
port: 9443
version: v1
---
apiVersion: v1
kind: Service
metadata:
Expand Down
36 changes: 26 additions & 10 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,15 @@ import (

billingv1 "github.com/appuio/control-api/apis/billing/v1"
orgv1 "github.com/appuio/control-api/apis/organization/v1"
userv1 "github.com/appuio/control-api/apis/user/v1"
"github.com/appuio/control-api/apiserver/authwrapper"
billingStore "github.com/appuio/control-api/apiserver/billing"
"github.com/appuio/control-api/apiserver/billing/odoostorage"
orgStore "github.com/appuio/control-api/apiserver/organization"
"github.com/appuio/control-api/apiserver/secretstorage"
"github.com/appuio/control-api/apiserver/user"
)

type organizationStatusRegisterer struct {
*orgv1.Organization
}

func (o organizationStatusRegisterer) GetGroupVersionResource() schema.GroupVersionResource {
gvr := o.Organization.GetGroupVersionResource()
gvr.Resource = fmt.Sprintf("%s/status", gvr.Resource)
return gvr
}

// APICommand creates a new command allowing to start the API server
func APICommand() *cobra.Command {
roles := []string{}
Expand All @@ -39,11 +32,14 @@ func APICommand() *cobra.Command {

ob := &odooStorageBuilder{}
ost := orgStore.New(&roles, &usernamePrefix, &allowEmptyBillingEntity)
ib := &invitationStorageBuilder{}

cmd, err := builder.APIServer.
WithResourceAndHandler(&orgv1.Organization{}, ost).
WithResourceAndHandler(organizationStatusRegisterer{&orgv1.Organization{}}, ost).
WithResourceAndHandler(&billingv1.BillingEntity{}, ob.Build).
WithResourceAndHandler(&userv1.Invitation{}, ib.Build).
WithResourceAndHandler(secretstorage.NewStatusSubResourceRegisterer(&userv1.Invitation{}), ib.Build).
WithoutEtcd().
ExposeLoopbackAuthorizer().
ExposeLoopbackMasterClientConfig().
Expand All @@ -61,6 +57,8 @@ func APICommand() *cobra.Command {
cmd.Flags().StringVar(&ob.odoo8URL, "billing-entity-odoo8-url", "http://localhost:8069", "URL of the Odoo instance to use for billing entities")
cmd.Flags().BoolVar(&ob.odoo8DebugTransport, "billing-entity-odoo8-debug-transport", false, "Enable debug logging for the Odoo transport")

cmd.Flags().StringVar(&ib.backingNS, "invitation-storage-backing-ns", "default", "Namespace to store invitation secrets in")

rf := cmd.Run
cmd.Run = func(cmd *cobra.Command, args []string) {
ctrl.Log.WithName("setup").WithValues(
Expand Down Expand Up @@ -96,3 +94,21 @@ func (o *odooStorageBuilder) Build(s *runtime.Scheme, g genericregistry.RESTOpti
return nil, fmt.Errorf("unknown billing entity storage: %s", o.billingEntityStorage)
}
}

type invitationStorageBuilder struct {
backingNS string
}

func (i *invitationStorageBuilder) Build(s *runtime.Scheme, g genericregistry.RESTOptionsGetter) (rest.Storage, error) {
return user.NewInvitationStorage(i.backingNS)(s, g)
}

type organizationStatusRegisterer struct {
*orgv1.Organization
}

func (o organizationStatusRegisterer) GetGroupVersionResource() schema.GroupVersionResource {
gvr := o.Organization.GetGroupVersionResource()
gvr.Resource = fmt.Sprintf("%s/status", gvr.Resource)
return gvr
}
21 changes: 21 additions & 0 deletions apis/user/v1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Package v1 contains API Schema definitions for the control-api v1 API group
// +kubebuilder:object:generate=true
// +kubebuilder:skip
// +groupName=user.appuio.io
package v1

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "user.appuio.io", Version: "v1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
137 changes: 137 additions & 0 deletions apis/user/v1/invitation_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package v1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"

"github.com/appuio/control-api/apiserver/secretstorage/status"
)

const (
// ConditionRedeemed is set when the invitation has been redeemed
ConditionRedeemed = "Redeemed"
// ConditionEmailSent is set when the invitation email has been sent
ConditionEmailSent = "EmailSent"
)

// +kubebuilder:object:root=true

// Invitation is a representation of an APPUiO Cloud Invitation
type Invitation struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// Spec holds the desired invitation state
Spec InvitationSpec `json:"spec,omitempty"`
// Status holds the invitation specific status
Status InvitationStatus `json:"status,omitempty"`
}

// InvitationSpec defines the desired state of the Invitation
type InvitationSpec struct {
// Note is a free-form text field to add a note to the invitation
Note string `json:"note,omitempty"`
// Email is the email address of the invited user, used to send the invitation
Email string `json:"email,omitempty"`
// TargetRefs is a list of references to the target resources
TargetRefs []TargetRef `json:"targetRefs,omitempty"`
}

// TargetRef is a reference to a target resource
type TargetRef struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably a good reason why we define TargetRef ourselves, but I'm wondering if we could just use the standard ObjectReference type instead.

Copy link
Contributor Author

@bastjan bastjan Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reason was looking into the RoleBinding subjects instead of other references. Those don't have the right documentation when generated.

Using ObjectReference seems like a good idea but will introduce more validation overhead since there are more fields to respect and we'll most likely never use any of them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with having our own ref type especially since that will give us flexibility if we figure out that we need something that we can't get from ObjectReference. Additionally, there seems to be no clear consensus even in the core K8s types on whether you're supposed to use LocalObjectReference/ObjectReference vs your own ref types

// APIGroup is the API group of the target resource
APIGroup string `json:"apiGroup,omitempty"`
// Kind is the kind of the target resource
Kind string `json:"kind,omitempty"`
// Name is the name of the target resource
Name string `json:"name,omitempty"`
// Namespace is the namespace of the target resource
Namespace string `json:"namespace,omitempty"`
}

// InvitationStatus defines the observed state of the Invitation
type InvitationStatus struct {
// Token is the invitation token
Token string `json:"token"`
// ValidUntil is the time when the invitation expires
ValidUntil metav1.Time `json:"validUntil"`
// Conditions is a list of conditions for the invitation
Conditions []metav1.Condition `json:"conditions"`
}

// Invitation needs to implement the builder resource interface
var _ status.ObjectWithStatusSubResource = &Invitation{}

// GetObjectMeta returns the objects meta reference.
func (o *Invitation) GetObjectMeta() *metav1.ObjectMeta {
return &o.ObjectMeta
}

// GetGroupVersionResource returns the GroupVersionResource for this resource.
// The resource should be the all lowercase and pluralized kind
func (o *Invitation) GetGroupVersionResource() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: GroupVersion.Group,
Version: GroupVersion.Version,
Resource: "invitations",
}
}

// IsStorageVersion returns true if the object is also the internal version -- i.e. is the type defined for the API group or an alias to this object.
// If false, the resource is expected to implement MultiVersionObject interface.
func (o *Invitation) IsStorageVersion() bool {
return true
}

// NamespaceScoped returns true if the object is namespaced
func (o *Invitation) NamespaceScoped() bool {
return false
}

// New returns a new instance of the resource
func (o *Invitation) New() runtime.Object {
return &Invitation{}
}

// NewList return a new list instance of the resource
func (o *Invitation) NewList() runtime.Object {
return &InvitationList{}
}

// SecretStorageGetStatus returns the status of the resource
func (o *Invitation) SecretStorageGetStatus() status.StatusSubResource {
return &o.Status
}

// CopyTo copies the status to the given parent resource
func (s *InvitationStatus) SecretStorageCopyTo(parent status.ObjectWithStatusSubResource) {
parent.(*Invitation).Status = *s.DeepCopy()
}

func (s InvitationStatus) SubResourceName() string {
return "status"
}

// +kubebuilder:object:root=true

// InvitationList contains a list of Invitations
type InvitationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []Invitation `json:"items"`
}

// InvitationList needs to implement the builder resource interface
var _ resource.ObjectList = &InvitationList{}

// GetListMeta returns the list meta reference.
func (in *InvitationList) GetListMeta() *metav1.ListMeta {
return &in.ListMeta
}

func init() {
SchemeBuilder.Register(&Invitation{}, &InvitationList{})
}
128 changes: 128 additions & 0 deletions apis/user/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apiserver/authwrapper/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

"github.com/appuio/control-api/apiserver/authwrapper"
"github.com/appuio/control-api/apiserver/authwrapper/mock"
"github.com/appuio/control-api/apiserver/authwrapper/testresource"
"github.com/appuio/control-api/apiserver/testresource"
)

var gvr = func() metav1.GroupVersionResource {
Expand Down
Loading