Skip to content

Commit 58bd1ca

Browse files
authored
Create/Update support for BillingEntities (#146)
1 parent 8fee950 commit 58bd1ca

24 files changed

+1358
-169
lines changed

.goreleaser.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@ dockers:
2727
- "--platform=linux/amd64"
2828
image_templates:
2929
- "ghcr.io/appuio/control-api:v{{ .Version }}-amd64"
30+
extra_files:
31+
- countries.yaml
3032

3133
- goarch: arm64
3234
use: buildx
3335
build_flag_templates:
3436
- "--platform=linux/arm64/v8"
3537
image_templates:
3638
- "ghcr.io/appuio/control-api:v{{ .Version }}-arm64"
39+
extra_files:
40+
- countries.yaml
3741

3842
docker_manifests:
3943
# For prereleases, updating `latest` does not make sense.

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ RUN \
1111
mkdir /.cache && chmod -R g=u /.cache
1212

1313
COPY control-api /usr/local/bin/
14+
COPY countries.yaml .
1415

1516
RUN chmod a+x /usr/local/bin/control-api
1617

api.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/appuio/control-api/apiserver/authwrapper"
2020
billingStore "github.com/appuio/control-api/apiserver/billing"
2121
"github.com/appuio/control-api/apiserver/billing/odoostorage"
22+
"github.com/appuio/control-api/apiserver/billing/odoostorage/odoo/odoo8/countries"
2223
orgStore "github.com/appuio/control-api/apiserver/organization"
2324
"github.com/appuio/control-api/apiserver/secretstorage"
2425
"github.com/appuio/control-api/apiserver/user"
@@ -57,6 +58,7 @@ func APICommand() *cobra.Command {
5758
cmd.Flags().BoolVar(&ob.billingEntityFakeMetadataSupport, "billing-entity-fake-metadata-support", false, "Enable metadata support for the fake storage backend")
5859
cmd.Flags().StringVar(&ob.odoo8URL, "billing-entity-odoo8-url", "http://localhost:8069", "URL of the Odoo instance to use for billing entities")
5960
cmd.Flags().BoolVar(&ob.odoo8DebugTransport, "billing-entity-odoo8-debug-transport", false, "Enable debug logging for the Odoo transport")
61+
cmd.Flags().StringVar(&ob.odoo8CountryListPath, "billing-entity-odoo8-country-list", "countries.yaml", "Path to the country list file in the format of [{name: \"Germany\", code: \"DE\", id: 81},...]")
6062

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

@@ -79,18 +81,20 @@ func APICommand() *cobra.Command {
7981
}
8082

8183
type odooStorageBuilder struct {
82-
billingEntityStorage, odoo8URL string
84+
billingEntityStorage, odoo8URL, odoo8CountryListPath string
8385
billingEntityFakeMetadataSupport, odoo8DebugTransport bool
8486
}
8587

8688
func (o *odooStorageBuilder) Build(s *runtime.Scheme, g genericregistry.RESTOptionsGetter) (rest.Storage, error) {
87-
fmt.Printf("Building storage with options: %#v\n", o)
88-
8989
switch o.billingEntityStorage {
9090
case "fake":
9191
return billingStore.New(odoostorage.NewFakeStorage(o.billingEntityFakeMetadataSupport).(authwrapper.StorageScoper))(s, g)
9292
case "odoo8":
93-
return billingStore.New(odoostorage.NewOdoo8Storage(o.odoo8URL, o.odoo8DebugTransport).(authwrapper.StorageScoper))(s, g)
93+
countryIDs, err := countries.LoadCountryIDs(o.odoo8CountryListPath)
94+
if err != nil {
95+
return nil, err
96+
}
97+
return billingStore.New(odoostorage.NewOdoo8Storage(o.odoo8URL, o.odoo8DebugTransport, countryIDs).(authwrapper.StorageScoper))(s, g)
9498
default:
9599
return nil, fmt.Errorf("unknown billing entity storage: %s", o.billingEntityStorage)
96100
}

apiserver/billing/odoostorage/odoo/fake/fake.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"sync"
77
"sync/atomic"
88

9+
"github.com/google/uuid"
910
"golang.org/x/exp/slices"
1011
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
apitypes "k8s.io/apimachinery/pkg/types"
1113

1214
billingv1 "github.com/appuio/control-api/apis/billing/v1"
1315
"github.com/appuio/control-api/apiserver/billing/odoostorage/odoo"
@@ -45,6 +47,7 @@ func (s *fakeOdooStorage) Create(ctx context.Context, be *billingv1.BillingEntit
4547
id := formatID(s.nextID())
4648

4749
be.Name = id
50+
be.UID = apitypes.UID(uuid.NewString())
4851

4952
s.cleanMetadata(be)
5053

@@ -104,10 +107,13 @@ func (s *fakeOdooStorage) nextID() uint64 {
104107
func (s *fakeOdooStorage) cleanMetadata(be *billingv1.BillingEntity) {
105108
meta := metav1.ObjectMeta{
106109
Name: be.Name,
110+
// Without UID patch requests fail with a 404 error.
111+
UID: be.UID,
107112
}
108113
if s.metadataSupport {
109114
meta = metav1.ObjectMeta{
110115
Name: be.Name,
116+
UID: be.UID,
111117
ResourceVersion: be.ResourceVersion,
112118
Annotations: be.Annotations,
113119
Labels: be.Labels,

apiserver/billing/odoostorage/odoo/odoo8/client/clientmock/session.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package model_test
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/appuio/control-api/apiserver/billing/odoostorage/odoo/odoo8/client/model"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestCategoryIDs_MarshalJSON(t *testing.T) {
12+
m, err := json.Marshal(model.CategoryIDs{1, 2, 3})
13+
14+
require.NoError(t, err)
15+
require.Equal(t, `[[6,false,[1,2,3]]]`, string(m))
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package model
2+
3+
import "encoding/json"
4+
5+
type CategoryIDs []int
6+
7+
func (t CategoryIDs) MarshalJSON() ([]byte, error) {
8+
// Values observed on test and prod instances.
9+
// Seems to be some kinda update join table command.
10+
// Also observed in other n..m relations.
11+
return json.Marshal([][]any{{6, false, []int(t)}})
12+
}

apiserver/billing/odoostorage/odoo/odoo8/client/model/nullable.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ type Nullable[T any] struct {
1313
Valid bool
1414
}
1515

16+
// NewNullable creates a new Nullable[T] with the given value.
17+
func NewNullable[T any](v T) Nullable[T] {
18+
return Nullable[T]{
19+
Value: v,
20+
Valid: true,
21+
}
22+
}
23+
1624
func (t *Nullable[T]) UnmarshalJSON(b []byte) error {
1725
// Odoo returns false (not null) if a field is not set.
1826
if bytes.Equal(b, []byte("false")) {

apiserver/billing/odoostorage/odoo/odoo8/client/model/nullable_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import (
99
"github.com/stretchr/testify/require"
1010
)
1111

12+
func Test_NewNullable(t *testing.T) {
13+
subject := model.NewNullable("test")
14+
require.True(t, subject.Valid)
15+
require.Equal(t, "test", subject.Value)
16+
}
17+
1218
func Test_Nullable(t *testing.T) {
1319
type mt struct {
1420
NullableString model.Nullable[string] `json:"nullable_string"`

apiserver/billing/odoostorage/odoo/odoo8/client/model/odoo_composite_id.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ type OdooCompositeID struct {
2424
Name string
2525
}
2626

27+
// NewCompositeID creates a new, valid OdooCompositeID.
28+
func NewCompositeID(id int, name string) OdooCompositeID {
29+
return OdooCompositeID{
30+
Valid: true,
31+
ID: id,
32+
Name: name,
33+
}
34+
}
35+
2736
// UnmarshalJSON handles deserialization of OdooCompositeID.
2837
func (t *OdooCompositeID) UnmarshalJSON(b []byte) error {
2938
// Odoo returns false (not null) if a field is not set.
@@ -66,5 +75,10 @@ func (t *OdooCompositeID) UnmarshalJSON(b []byte) error {
6675

6776
// MarshalJSON handles serialization of OdooCompositeID.
6877
func (t OdooCompositeID) MarshalJSON() ([]byte, error) {
69-
return json.Marshal([...]any{t.ID, t.Name})
78+
if !t.Valid {
79+
return []byte("false"), nil
80+
}
81+
82+
// Write path wants just the ID, not the tuple.
83+
return json.Marshal(t.ID)
7084
}

0 commit comments

Comments
 (0)