Skip to content

Commit

Permalink
Cache human readable billing entity name in Organization (#90)
Browse files Browse the repository at this point in the history
* Cache human readable billing entity name in Organization

- Implement /status subresource handling for Organizations
- Add controller writing this status
  • Loading branch information
bastjan authored Jan 13, 2023
1 parent d877caf commit 3c7e4c5
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 16 deletions.
15 changes: 14 additions & 1 deletion api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
genericregistry "k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
"sigs.k8s.io/apiserver-runtime/pkg/builder"
Expand All @@ -20,16 +21,28 @@ import (
orgStore "github.com/appuio/control-api/apiserver/organization"
)

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{}
usernamePrefix := ""
var allowEmptyBillingEntity bool

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

cmd, err := builder.APIServer.
WithResourceAndHandler(&orgv1.Organization{}, orgStore.New(&roles, &usernamePrefix, &allowEmptyBillingEntity)).
WithResourceAndHandler(&orgv1.Organization{}, ost).
WithResourceAndHandler(organizationStatusRegisterer{&orgv1.Organization{}}, ost).
WithResourceAndHandler(&billingv1.BillingEntity{}, ob.Build).
WithoutEtcd().
ExposeLoopbackAuthorizer().
Expand Down
17 changes: 16 additions & 1 deletion apis/organization/v1/organization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ var (
DisplayNameKey = "organization.appuio.io/display-name"
// BillingEntityRefKey is the annotation key that stores the billing entity reference
BillingEntityRefKey = "organization.appuio.io/billing-entity-ref"
// BillingEntityNameKey is the annotation key that stores the billing entity name
BillingEntityNameKey = "status.organization.appuio.io/billing-entity-name"
)

// NewOrganizationFromNS returns an Organization based on the given namespace
Expand All @@ -27,21 +29,26 @@ func NewOrganizationFromNS(ns *corev1.Namespace) *Organization {
if ns == nil || ns.Labels == nil || ns.Labels[TypeKey] != OrgType {
return nil
}
var displayName, billingEntityRef string
var displayName, billingEntityRef, billingEntityName string
if ns.Annotations != nil {
displayName = ns.Annotations[DisplayNameKey]
billingEntityRef = ns.Annotations[BillingEntityRefKey]
billingEntityName = ns.Annotations[BillingEntityNameKey]
}
org := &Organization{
ObjectMeta: *ns.ObjectMeta.DeepCopy(),
Spec: OrganizationSpec{
DisplayName: displayName,
BillingEntityRef: billingEntityRef,
},
Status: OrganizationStatus{
BillingEntityName: billingEntityName,
},
}
if org.Annotations != nil {
delete(org.Annotations, DisplayNameKey)
delete(org.Annotations, BillingEntityRefKey)
delete(org.Annotations, BillingEntityNameKey)
delete(org.Labels, TypeKey)
}
return org
Expand All @@ -56,6 +63,8 @@ type Organization struct {

// Spec holds the cluster specific metadata.
Spec OrganizationSpec `json:"spec,omitempty"`
// Status holds the organization specific status
Status OrganizationStatus `json:"status,omitempty"`
}

// OrganizationSpec defines the desired state of the Organization
Expand All @@ -67,6 +76,11 @@ type OrganizationSpec struct {
BillingEntityRef string `json:"billingEntityRef,omitempty"`
}

type OrganizationStatus struct {
// BillingEntityName is the name of the billing entity
BillingEntityName string `json:"billingEntityName,omitempty"`
}

// Organization needs to implement the builder resource interface
var _ resource.Object = &Organization{}

Expand Down Expand Up @@ -138,6 +152,7 @@ func (o *Organization) ToNamespace() *corev1.Namespace {
ns.Labels[TypeKey] = OrgType
ns.Annotations[DisplayNameKey] = o.Spec.DisplayName
ns.Annotations[BillingEntityRefKey] = o.Spec.BillingEntityRef
ns.Annotations[BillingEntityNameKey] = o.Status.BillingEntityName
return ns
}

Expand Down
15 changes: 10 additions & 5 deletions apis/organization/v1/organization_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ func TestOrganization_ToNamespace(t *testing.T) {
DisplayName: "Foo Bar Inc.",
BillingEntityRef: "be-1234",
},
Status: OrganizationStatus{
BillingEntityName: "Fooaccounting",
},
},
namespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -116,8 +119,9 @@ func TestOrganization_ToNamespace(t *testing.T) {
TypeKey: OrgType,
},
Annotations: map[string]string{
DisplayNameKey: "Foo Bar Inc.",
BillingEntityRefKey: "be-1234",
DisplayNameKey: "Foo Bar Inc.",
BillingEntityRefKey: "be-1234",
BillingEntityNameKey: "Fooaccounting",
},
},
},
Expand Down Expand Up @@ -145,9 +149,10 @@ func TestOrganization_ToNamespace(t *testing.T) {
"foo": "bar",
},
Annotations: map[string]string{
DisplayNameKey: "Foo Bar Inc.",
BillingEntityRefKey: "",
"bar": "buzz",
DisplayNameKey: "Foo Bar Inc.",
BillingEntityRefKey: "",
BillingEntityNameKey: "",
"bar": "buzz",
},
},
},
Expand Down
16 changes: 16 additions & 0 deletions apis/organization/v1/zz_generated.deepcopy.go

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

3 changes: 3 additions & 0 deletions apiserver/organization/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ func (s *organizationStorage) Create(ctx context.Context, obj runtime.Object, cr
return nil, fmt.Errorf("not an organization: %#v", obj)
}

// Status can only be updated (not created) though the status subresource
org.Status = orgv1.OrganizationStatus{}

// Validate Org
if err := createValidation(ctx, obj); err != nil {
return nil, err
Expand Down
1 change: 1 addition & 0 deletions apiserver/organization/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func TestOrganizationStorage_Create(t *testing.T) {
organizationIn: func() *orgv1.Organization {
fooOrg := fooOrg.DeepCopy()
fooOrg.Spec.BillingEntityRef = "foo"
fooOrg.Status.BillingEntityName = "Foorg"
return fooOrg
}(),
authDecision: authResponse{
Expand Down
28 changes: 28 additions & 0 deletions apiserver/organization/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package organization

import (
"context"
"errors"
"fmt"

orgv1 "github.com/appuio/control-api/apis/organization/v1"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
)

Expand All @@ -27,6 +29,8 @@ func (s *organizationStorage) Update(ctx context.Context, name string, objInfo r
return nil, false, fmt.Errorf("old object is not an organization")
}

objInfo = rest.WrapUpdatedObjectInfo(objInfo, filterStatusUpdates)

newObj, err := objInfo.UpdatedObject(ctx, oldObj)
if err != nil {
return nil, false, err
Expand All @@ -49,3 +53,27 @@ func (s *organizationStorage) Update(ctx context.Context, name string, objInfo r

return newOrg, false, convertNamespaceError(s.namepaces.UpdateNamespace(ctx, newOrg.ToNamespace(), options))
}

func filterStatusUpdates(ctx context.Context, newObj, oldObj runtime.Object) (transformedNewObj runtime.Object, err error) {
requestInfo, found := request.RequestInfoFrom(ctx)
if !found {
return nil, errors.New("no RequestInfo found in the context")
}

oldOrg, ok := oldObj.(*orgv1.Organization)
if !ok {
return nil, fmt.Errorf("old object is not an organization")
}
newOrg, ok := newObj.(*orgv1.Organization)
if !ok {
return nil, fmt.Errorf("new object is not an organization")
}

if requestInfo.Subresource == "status" {
withUpdatedStatus := oldOrg.DeepCopy()
withUpdatedStatus.Status = newOrg.Status
return withUpdatedStatus, nil
}
newOrg.Status = oldOrg.Status
return newOrg, nil
}
41 changes: 37 additions & 4 deletions apiserver/organization/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,16 @@ func TestOrganizationStorage_Update(t *testing.T) {

organization *orgv1.Organization
err error

subresource string
}{
"GivenUpdateOrg_ThenSuccess": {
name: "foo",
updateFunc: func(obj runtime.Object) runtime.Object {
org := obj.(*orgv1.Organization).DeepCopy()
org.Spec.DisplayName = "New Foo Inc."
// This can only be changed though the status subresource
org.Status.BillingEntityName = "New Foo Inc., Accounting"
return org
},

Expand All @@ -68,6 +72,34 @@ func TestOrganizationStorage_Update(t *testing.T) {
},
},
},
"GivenUpdateOrgStatus_ThenSuccess": {
name: "foo",
updateFunc: func(obj runtime.Object) runtime.Object {
org := obj.(*orgv1.Organization).DeepCopy()
// Status subresource can only change the fields in the status
org.Spec.DisplayName = "New Foo Inc."
org.Status.BillingEntityName = "New Foo Inc., Accounting"
return org
},

namespace: fooNs,
authDecision: authResponse{
decision: authorizer.DecisionAllow,
},

organization: &orgv1.Organization{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Spec: fooOrg.Spec,
Status: orgv1.OrganizationStatus{
BillingEntityName: "New Foo Inc., Accounting",
},
},
subresource: "status",
},
"GivenUpdateOrg_ValidBillingEntity_ThenSuccess": {
name: "foo",
updateFunc: func(obj runtime.Object) runtime.Object {
Expand Down Expand Up @@ -192,10 +224,11 @@ func TestOrganizationStorage_Update(t *testing.T) {
request.WithUser(
request.WithRequestInfo(request.NewContext(),
&request.RequestInfo{
Verb: "update",
APIGroup: orgv1.GroupVersion.Group,
Resource: "organizations",
Name: tc.name,
Verb: "update",
APIGroup: orgv1.GroupVersion.Group,
Resource: "organizations",
Name: tc.name,
Subresource: tc.subresource,
}),
&user.DefaultInfo{
Name: "appuio#foo",
Expand Down
32 changes: 32 additions & 0 deletions config/rbac/controller/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ rules:
- get
- patch
- update
- apiGroups:
- billing.appuio.io
resources:
- billingentities
verbs:
- get
- list
- watch
- apiGroups:
- organization.appuio.io
resources:
Expand All @@ -74,6 +82,22 @@ rules:
- patch
- update
- watch
- apiGroups:
- organization.appuio.io
resources:
- organizations/status
verbs:
- get
- patch
- update
- apiGroups:
- rbac.appuio.io
resources:
- billingentities
verbs:
- get
- list
- watch
- apiGroups:
- rbac.appuio.io
resources:
Expand All @@ -86,6 +110,14 @@ rules:
- patch
- update
- watch
- apiGroups:
- rbac.appuio.io
resources:
- organizations/status
verbs:
- get
- patch
- update
- apiGroups:
- rbac.authorization.k8s.io
resources:
Expand Down
Loading

0 comments on commit 3c7e4c5

Please sign in to comment.