Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: appuio/control-api
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 20e309cfedc18ae097d7a124fd0a3ec895f1dcaf
Choose a base ref
..
head repository: appuio/control-api
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0db73625906aa9a26dd7d6cf40e095c84349bf39
Choose a head ref
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ run-api: build ## Starts control api apiserver against the current Kubernetes cl
.PHONY: run-controller
run-controller: build ## Starts control api controller against the current Kubernetes cluster (based on your local config)
$(localenv_make) webhook-certs/tls.key
$(BIN_FILENAME) controller --username-prefix "appuio#" --webhook-cert-dir=./local-env/webhook-certs --webhook-port=9444 --zap-log-level debug
$(BIN_FILENAME) controller --username-prefix "appuio#" --webhook-cert-dir=./local-env/webhook-certs --webhook-port=9444 --zap-log-level debug --billingentity-email-cron-interval "@every 1m"

.PHONY: local-env
local-env-setup: ## Setup local kind-based dev environment
18 changes: 15 additions & 3 deletions api.go
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import (
"github.com/appuio/control-api/apiserver/authwrapper"
billingStore "github.com/appuio/control-api/apiserver/billing"
"github.com/appuio/control-api/apiserver/billing/odoostorage"
"github.com/appuio/control-api/apiserver/billing/odoostorage/odoo/odoo8"
"github.com/appuio/control-api/apiserver/billing/odoostorage/odoo/odoo8/countries"
orgStore "github.com/appuio/control-api/apiserver/organization"
"github.com/appuio/control-api/apiserver/secretstorage"
@@ -60,6 +61,10 @@ func APICommand() *cobra.Command {
cmd.Flags().BoolVar(&ob.odoo8DebugTransport, "billing-entity-odoo8-debug-transport", false, "Enable debug logging for the Odoo transport")
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},...]")

cmd.Flags().StringVar(&ob.odoo8AccountingContactDisplayName, "billing-entity-odoo8-accounting-contact-display-name", "Accounting", "Display name of the accounting contact")
cmd.Flags().StringVar(&ob.odoo8LanguagePreference, "billing-entity-odoo8-language-preference", "en_US", "Language preference of the Odoo record")
cmd.Flags().IntVar(&ob.odoo8PaymentTermID, "billing-entity-odoo8-payment-term-id", 2, "Payment term ID of the Odoo record")

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

rf := cmd.Run
@@ -81,8 +86,10 @@ func APICommand() *cobra.Command {
}

type odooStorageBuilder struct {
billingEntityStorage, odoo8URL, odoo8CountryListPath string
billingEntityFakeMetadataSupport, odoo8DebugTransport bool
billingEntityStorage, odoo8URL, odoo8CountryListPath string
odoo8AccountingContactDisplayName, odoo8LanguagePreference string
odoo8PaymentTermID int
billingEntityFakeMetadataSupport, odoo8DebugTransport bool
}

func (o *odooStorageBuilder) Build(s *runtime.Scheme, g genericregistry.RESTOptionsGetter) (rest.Storage, error) {
@@ -94,7 +101,12 @@ func (o *odooStorageBuilder) Build(s *runtime.Scheme, g genericregistry.RESTOpti
if err != nil {
return nil, err
}
return billingStore.New(odoostorage.NewOdoo8Storage(o.odoo8URL, o.odoo8DebugTransport, countryIDs).(authwrapper.StorageScoper))(s, g)
return billingStore.New(odoostorage.NewOdoo8Storage(o.odoo8URL, o.odoo8DebugTransport, odoo8.Config{
AccountingContactDisplayName: o.odoo8AccountingContactDisplayName,
LanguagePreference: o.odoo8LanguagePreference,
PaymentTermID: o.odoo8PaymentTermID,
CountryIDs: countryIDs,
}).(authwrapper.StorageScoper))(s, g)
default:
return nil, fmt.Errorf("unknown billing entity storage: %s", o.billingEntityStorage)
}
15 changes: 15 additions & 0 deletions apis/billing/v1/billingentity_types.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,13 @@ import (
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
)

const (
// ConditionEmailSent is set when the update notification email has been sent
ConditionEmailSent = "EmailSent"
ConditionReasonSendFailed = "SendFailed"
ConditionReasonUpdated = "Updated"
)

// +kubebuilder:object:root=true

// BillingEntity is a representation of an APPUiO Cloud BillingEntity
@@ -16,6 +23,8 @@ type BillingEntity struct {

// Spec holds the cluster specific metadata.
Spec BillingEntitySpec `json:"spec,omitempty"`

Status BillingEntityStatus `json:"status,omitempty"`
}

// BillingEntitySpec defines the desired state of the BillingEntity
@@ -56,6 +65,12 @@ type BillingEntityContact struct {
Emails []string `json:"emails"`
}

// BillingEntityStatus contains the status conditions of the BillingEntity
type BillingEntityStatus struct {
// Conditions is a list of conditions for the billing entity
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

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

24 changes: 24 additions & 0 deletions apis/billing/v1/zz_generated.deepcopy.go

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

3 changes: 2 additions & 1 deletion apis/user/v1/invitation_types.go
Original file line number Diff line number Diff line change
@@ -16,7 +16,8 @@ const (
// ConditionRedeemed is set when the invitation has been redeemed
ConditionRedeemed = "Redeemed"
// ConditionEmailSent is set when the invitation email has been sent
ConditionEmailSent = "EmailSent"
ConditionEmailSent = "EmailSent"
ConditionReasonSendFailed = "SendFailed"
)

// +kubebuilder:object:root=true
2 changes: 1 addition & 1 deletion apiserver/billing/odoostorage/odoo/odoo8/client/model.go
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ const (
MethodDelete Method = "unlink"
)

// WriteModel is used as "params" in requests to "dataset/create", "dataset/write" or "dataset/unlinke" endpoints.
// WriteModel is used as "params" in requests to "dataset/create", "dataset/write" or "dataset/unlink" endpoints.
type WriteModel struct {
Model string `json:"model"`
Method Method `json:"method"`
Original file line number Diff line number Diff line change
@@ -54,6 +54,9 @@ type Partner struct {

// Inflight allows detecting half-finished creates.
Inflight Nullable[string] `json:"x_control_api_inflight,omitempty" yaml:"x_control_api_inflight,omitempty"`

// Status allows storing status conditions
Status Nullable[string] `json:"x_control_api_meta_status,omitempty" yaml:"x_control_api_meta_status,omitempty"`
}

func (p Partner) Emails() []string {
@@ -102,6 +105,8 @@ var PartnerFields = []string{

"email",
"phone",

"x_control_api_meta_status",
}

// FetchPartnerByID searches for the partner by ID and returns the first entry in the result.
64 changes: 46 additions & 18 deletions apiserver/billing/odoostorage/odoo/odoo8/odoo8.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ const VSHNAccountingContactNameKey = "billing.appuio.io/vshn-accounting-contact-

// Used to identify the accounting contact of a company.
const roleAccountCategory = 7
const companyCategory = 1

// Used to generate the UUID for the .metadata.uid field.
var metaUIDNamespace = uuid.MustParse("51887759-C769-4829-9910-BB9D5F92767D")
@@ -50,21 +51,29 @@ var (
)
accountingContactUpdateAllowedFields = newSet(
"x_invoice_contact",
"x_control_api_meta_status",
"email",
)
)

func NewOdoo8Storage(odooURL string, debugTransport bool, countryIDs map[string]int) odoo.OdooStorage {
type Config struct {
CountryIDs map[string]int
AccountingContactDisplayName string
LanguagePreference string
PaymentTermID int
}

func NewOdoo8Storage(odooURL string, debugTransport bool, conf Config) odoo.OdooStorage {
return &oodo8Storage{
countryIDs: countryIDs,
config: conf,
sessionCreator: func(ctx context.Context) (client.QueryExecutor, error) {
return client.Open(ctx, odooURL, client.ClientOptions{UseDebugLogger: debugTransport})
},
}
}

type oodo8Storage struct {
countryIDs map[string]int
config Config

sessionCreator func(ctx context.Context) (client.QueryExecutor, error)
}
@@ -75,7 +84,7 @@ func (s *oodo8Storage) Get(ctx context.Context, name string) (*billingv1.Billing
return nil, err
}

be := mapPartnersToBillingEntity(company, accountingContact)
be := mapPartnersToBillingEntity(ctx, company, accountingContact)
return &be, nil
}

@@ -158,7 +167,7 @@ func (s *oodo8Storage) List(ctx context.Context) ([]billingv1.BillingEntity, err
l.Info("could not load parent partner (maybe no longer active?)", "parent_id", p.Parent.ID, "id", p.ID)
continue
}
bes = append(bes, mapPartnersToBillingEntity(mp, p))
bes = append(bes, mapPartnersToBillingEntity(ctx, mp, p))
}

return bes, nil
@@ -170,7 +179,7 @@ func (s *oodo8Storage) Create(ctx context.Context, be *billingv1.BillingEntity)
if be == nil {
return errors.New("billing entity is nil")
}
company, accounting, err := mapBillingEntityToPartners(*be, s.countryIDs)
company, accounting, err := mapBillingEntityToPartners(*be, s.config.CountryIDs)
if err != nil {
return fmt.Errorf("failed mapping billing entity to partners: %w", err)
}
@@ -179,8 +188,8 @@ func (s *oodo8Storage) Create(ctx context.Context, be *billingv1.BillingEntity)
l = l.WithValues("debug_inflight", inflight)
company.Inflight = model.NewNullable(inflight)
accounting.Inflight = model.NewNullable(inflight)
setStaticCompanyFields(&company)
setStaticAccountingContactFields(&accounting)
setStaticCompanyFields(s.config, &company)
setStaticAccountingContactFields(s.config, &accounting)

session, err := s.sessionCreator(ctx)
if err != nil {
@@ -223,7 +232,7 @@ func (s *oodo8Storage) Update(ctx context.Context, be *billingv1.BillingEntity)
return errors.New("billing entity is nil")
}

company, accounting, err := mapBillingEntityToPartners(*be, s.countryIDs)
company, accounting, err := mapBillingEntityToPartners(*be, s.config.CountryIDs)
if err != nil {
return fmt.Errorf("failed mapping billing entity to partners: %w", err)
}
@@ -277,8 +286,19 @@ func odooIDToK8sID(id int) string {
return fmt.Sprintf("be-%d", id)
}

func mapPartnersToBillingEntity(company model.Partner, accounting model.Partner) billingv1.BillingEntity {
func mapPartnersToBillingEntity(ctx context.Context, company model.Partner, accounting model.Partner) billingv1.BillingEntity {
l := klog.FromContext(ctx)
name := odooIDToK8sID(accounting.ID)

var status billingv1.BillingEntityStatus
if accounting.Status.Value != "" {
err := json.Unmarshal([]byte(accounting.Status.Value), &status)

if err != nil {
l.Error(err, "Could not unmarshal BillingEntityStatus", "billingEntityName", name, "rawStatus", accounting.Status.Value)
}
}

return billingv1.BillingEntity{
ObjectMeta: metav1.ObjectMeta{
Name: name,
@@ -309,6 +329,7 @@ func mapPartnersToBillingEntity(company model.Partner, accounting model.Partner)
},
LanguagePreference: "",
},
Status: status,
}
}

@@ -318,6 +339,12 @@ func mapBillingEntityToPartners(be billingv1.BillingEntity, countryIDs map[strin
return company, accounting, fmt.Errorf("unknown country %q", be.Spec.Address.Country)
}

st, err := json.Marshal(be.Status)
if err != nil {
return company, accounting, err
}
statusString := string(st)

company = model.Partner{
Name: be.Spec.Name,
Phone: model.NewNullable(be.Spec.Phone),
@@ -332,26 +359,27 @@ func mapBillingEntityToPartners(be billingv1.BillingEntity, countryIDs map[strin

accounting = model.Partner{
InvoiceContactName: model.NewNullable(be.Spec.AccountingContact.Name),
Status: model.NewNullable(statusString),
}
accounting.SetEmails(be.Spec.AccountingContact.Emails)

return company, accounting, nil
}

func setStaticAccountingContactFields(a *model.Partner) {
func setStaticAccountingContactFields(conf Config, a *model.Partner) {
a.CategoryID = []int{roleAccountCategory}
a.Name = "Accounting"
a.Lang = model.NewNullable("en_US")
a.Name = conf.AccountingContactDisplayName
a.Lang = model.NewNullable(conf.LanguagePreference)
a.NotifyEmail = "always"
a.PaymentTerm = model.OdooCompositeID{Valid: true, ID: 2}
a.PaymentTerm = model.OdooCompositeID{Valid: true, ID: conf.PaymentTermID}
a.UseParentAddress = true
}

func setStaticCompanyFields(a *model.Partner) {
a.CategoryID = []int{1}
a.Lang = model.NewNullable("en_US")
func setStaticCompanyFields(conf Config, a *model.Partner) {
a.CategoryID = []int{companyCategory}
a.Lang = model.NewNullable(conf.LanguagePreference)
a.NotifyEmail = "none"
a.PaymentTerm = model.OdooCompositeID{Valid: true, ID: 2}
a.PaymentTerm = model.OdooCompositeID{Valid: true, ID: conf.PaymentTermID}
}

func filterFields(p model.Partner, allowed set) (map[string]any, error) {
Loading