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

add: limits.yaml #29

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion .github/workflows/upload.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
upload-features:
runs-on: ubuntu-latest
env:
STRIPE_API_TOKEN: ${{ secrets.STRIPE_API_TOKEN }}
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
steps:
- uses: actions/checkout@v4

Expand Down
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ gen:
go generate ./...

upload-ci-local:
act workflow_dispatch -v -W .github/workflows/upload.yaml -s STRIPE_API_TOKEN=$STRIPE_API_TOKEN
act workflow_dispatch -v -W .github/workflows/upload.yaml -s STRIPE_API_KEY=$STRIPE_API_KEY

# Check struct memory alignment and print potential improvements
[no-exit-message]
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ just gen
```

## Add, remove, edit features
1. Add the features to `pkg/features.yaml`.
1. Add the features to `definitions/features.yaml`.
2. Run
```
just gen
```

## What happens when features are changed
Note: Any mention of "Stripe feature" is talking about a feature that has been created in Stripe. Any mention of "feature" without "Stripe"
immediately in front of it is referring to a feature defined in this repo's [features yaml](./pkg/licenseapi/features.yaml).
immediately in front of it is referring to a feature defined in this repo's [features.yaml](./definitions/features.yaml).

Adding a feature will cause the CI to will create a corresponding Stripe feature. The name of the feature will be the Stripe feature's
`lookup-key` and the `displayName` will be the Stripe feature's `Name`. The Stripe feature's `Name` is the same as the feature's `displayName`
Expand All @@ -40,7 +40,13 @@ Removing a feature will have no effect in Stripe. Stripe features exist indefini

## Test Stripe feature upload CI locally
1. Create token in Stripe sandbox
2. run
2. Install `act`
```bash
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
sudo mv bin/act /usr/local/bin
```
STRIPE_API_TOKEN=<sandbox-token> just upload-ci-local
2. Run
```bahs
export STRIPE_API_KEY=<sandbox-token>
just upload-ci-local
```
2 changes: 1 addition & 1 deletion pkg/licenseapi/features.yaml → definitions/features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ features: # array of features
- name: "auto-ingress-authentication"
displayName: "Automatic Auth For Ingresses"
- name: "oidc-provider"
displayName: "Loft as OIDC Provider"
displayName: "Platform as OIDC Provider"
- name: "multiple-sso-providers"
displayName: "Multiple SSO Providers"
- name: "apps"
Expand Down
3 changes: 3 additions & 0 deletions definitions/limits.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
limits: # array of features
- name: "virtual-cluster"
displayName: "Virtual Cluster" # This name can be changed
10 changes: 8 additions & 2 deletions hack/gen-features/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"regexp"
"strings"

"github.com/loft-sh/admin-apis/hack/internal/featuresyaml"
"github.com/loft-sh/admin-apis/hack/internal/yamlparser"
"github.com/loft-sh/admin-apis/pkg/licenseapi"
)

Expand Down Expand Up @@ -86,11 +86,17 @@ var (
)

func main() {
features, err := featuresyaml.ReadFeaturesYaml("../../pkg/licenseapi/features.yaml")
yamlContent := struct {
Features []*licenseapi.Feature `json:"features"`
}{}

err := yamlparser.ParseYAML("../../definitions/features.yaml", &yamlContent)
if err != nil {
panic(err)
}

features := yamlContent.Features

f, err := os.Create("../../pkg/licenseapi/features.go")
if err != nil {
panic(err)
Expand Down
34 changes: 0 additions & 34 deletions hack/internal/featuresyaml/yaml.go

This file was deleted.

28 changes: 28 additions & 0 deletions hack/internal/yamlparser/yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package yamlparser

import (
"fmt"
"io"
"os"
"reflect"

"github.com/ghodss/yaml"
)

func ParseYAML(yamlPath string, out interface{}) error {
if reflect.ValueOf(out).Kind() != reflect.Pointer {
return fmt.Errorf("function ParseYAML requires to provide a pointer")
}

file, err := os.Open(yamlPath)
if err != nil {
return err
}

bytes, err := io.ReadAll(file)
if err != nil {
return err
}

return yaml.Unmarshal(bytes, &out)
}
79 changes: 51 additions & 28 deletions hack/upstream-features/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"maps"
"os"

"github.com/loft-sh/admin-apis/hack/internal/featuresyaml"
"github.com/loft-sh/admin-apis/hack/internal/yamlparser"
"github.com/loft-sh/admin-apis/pkg/licenseapi"
"github.com/stripe/stripe-go/v81"
stripefeatures "github.com/stripe/stripe-go/v81/entitlements/feature"
Expand All @@ -25,58 +25,78 @@ type syncedFeature struct {
}

func main() {
stripeToken := os.Getenv("STRIPE_API_TOKEN")
stripeToken := os.Getenv("STRIPE_API_KEY")
if stripeToken == "" {
log.Println("stripe token cannot be empty")
os.Exit(1)
log.Fatal("stripe token cannot be empty")
}
stripe.Key = stripeToken

features, err := featuresyaml.ReadFeaturesYaml("pkg/licenseapi/features.yaml")
syncedFeatures := map[string]syncedFeature{}

yamlContent := struct {
Features []*licenseapi.Feature `json:"features"`
Limits []*licenseapi.Feature `json:"limits"`
}{}

err := yamlparser.ParseYAML("definitions/features.yaml", &yamlContent)
if err != nil {
log.Fatal(err)
}

err = createFeatures(yamlContent.Features, &syncedFeatures, false)
if err != nil {
log.Fatal(err)
}

err = yamlparser.ParseYAML("definitions/limits.yaml", &yamlContent)
if err != nil {
log.Println(err)
os.Exit(1)
log.Fatal(err)
}

syncedFeatures := syncStripeFeatures(features)
err = createFeatures(yamlContent.Limits, &syncedFeatures, true)
if err != nil {
log.Fatal(err)
}
}

if err = ensureFeatureProducts(syncedFeatures); err != nil {
log.Println(err)
os.Exit(1)
func createFeatures(features []*licenseapi.Feature, syncedFeatures *map[string]syncedFeature, isLimit bool) error {
err := ensureStripeFeatures(features, syncedFeatures, isLimit)
if err != nil {
return err
}

if err = ensureAttachAll(syncedFeatures); err != nil {
log.Println(err)
os.Exit(1)
if err = ensureFeatureProducts(*syncedFeatures); err != nil {
return err
}

if err = ensureAttachAll(*syncedFeatures); err != nil {
return err
}
return nil
}

func syncStripeFeatures(features []*licenseapi.Feature) map[string]syncedFeature {
syncedFeatures := make(map[string]syncedFeature, len(features))
func ensureStripeFeatures(features []*licenseapi.Feature, syncedFeatures *map[string]syncedFeature, isLimit bool) error {
for _, f := range features {
feature, err := ensureFeatureExists(f.Name, f.DisplayName)
feature, err := ensureFeatureExists(f.Name, f.DisplayName, isLimit)
if err != nil {
log.Println(err)
continue
return err
}
syncedFeatures[feature.stripeID] = feature
(*syncedFeatures)[feature.stripeID] = feature

if !f.Preview {
continue
}

previewFeature, err := ensureFeatureExists(f.Name+"-preview", "Preview: "+f.DisplayName)
previewFeature, err := ensureFeatureExists(f.Name+"-preview", f.DisplayName+" [Preview]", false)
if err != nil {
log.Println(err)
continue
return err
}
syncedFeatures[previewFeature.stripeID] = previewFeature
(*syncedFeatures)[previewFeature.stripeID] = previewFeature
}
return syncedFeatures
return nil
}

func ensureFeatureExists(name, displayName string) (syncedFeature, error) {
func ensureFeatureExists(name, displayName string, isLimit bool) (syncedFeature, error) {
id, exists, err := featureExists(name)
if err != nil {
return syncedFeature{}, err
Expand All @@ -89,6 +109,9 @@ func ensureFeatureExists(name, displayName string) (syncedFeature, error) {
feature, err := stripefeatures.New(&stripe.EntitlementsFeatureParams{
Name: &displayName,
LookupKey: &name,
Metadata: map[string]string{
licenseapi.MetadataKeyFeatureIsLimit: licenseapi.MetadataValueTrue,
},
})
if err != nil {
return syncedFeature{}, fmt.Errorf("failed to create Stripe feature from feature %s: %v\n", name, err)
Expand Down Expand Up @@ -137,7 +160,7 @@ func ensureFeatureProduct(syncedFeature syncedFeature) error {
}

usdCurrencyCode := "usd"
unit := int64(2000000) // =20k, this is in cents
unit := int64(2000000) // =20k, this is in cents (sample placeholder price)
interval := "year"
intervalCount := int64(1)
product, err := stripeproducts.New(&stripe.ProductParams{
Expand Down Expand Up @@ -171,7 +194,7 @@ func ensureFeatureProduct(syncedFeature syncedFeature) error {
func ensureAttachAll(featureIDs map[string]syncedFeature) error {
productSearch := stripeproducts.Search(&stripe.ProductSearchParams{
SearchParams: stripe.SearchParams{
Query: fmt.Sprintf(metadataQueryFmt, licenseapi.MetadataKeyAttachAll, "true"),
Query: fmt.Sprintf(metadataQueryFmt, licenseapi.MetadataKeyAttachAll, licenseapi.MetadataValueTrue),
},
})
if err := productSearch.Err(); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/licenseapi/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const (

AutoIngressAuth FeatureName = "auto-ingress-authentication" // Automatic Auth For Ingresses

OIDCProvider FeatureName = "oidc-provider" // Loft as OIDC Provider
OIDCProvider FeatureName = "oidc-provider" // Platform as OIDC Provider

MultipleSSOProviders FeatureName = "multiple-sso-providers" // Multiple SSO Providers

Expand Down
4 changes: 4 additions & 0 deletions pkg/licenseapi/license_feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ type Feature struct {
// Status shows the status of the feature (see type FeatureStatus)
// +optional
Status string `json:"status,omitempty"`

// Name of the module that this feature belongs to
// +optional
Module string `json:"module,omitempty"`
}
4 changes: 4 additions & 0 deletions pkg/licenseapi/license_limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ type Limit struct {
// Limit specifies the limit for this resource.
// +optional
Quantity *ResourceCount `json:"quantity,omitempty"`

// Name of the module that this limit belongs to
// +optional
Module string `json:"module,omitempty"`
}
2 changes: 1 addition & 1 deletion pkg/licenseapi/license_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func New() *License {
Name: "auto-ingress-authentication",
},
{
DisplayName: "Loft as OIDC Provider",
DisplayName: "Platform as OIDC Provider",
Name: "oidc-provider",
},
{
Expand Down
4 changes: 4 additions & 0 deletions pkg/licenseapi/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ type ButtonName string

// Metadata Keys
const (
/* NEVER CHANGE ANY OF THESE */
MetadataKeyAttachAll = "attach_all_features"
MetadataKeyProductForFeature = "product_for_feature"
MetadataKeyFeatureIsLimit = "is_limit"
MetadataValueTrue = "true"
)

// Products
const (
/* NEVER CHANGE ANY OF THESE */
Loft ProductName = "loft"
VClusterPro ProductName = "vcluster-pro"
DevPodPro ProductName = "devpod-pro"
Expand Down