Skip to content

Commit

Permalink
Merge pull request #42 from controlplaneio-fluxcd/entitlement-reconciler
Browse files Browse the repository at this point in the history
Implement entitlement verification and AWS metering
  • Loading branch information
stefanprodan authored Jun 20, 2024
2 parents 5edca4b + 246e88c commit aca33f2
Show file tree
Hide file tree
Showing 9 changed files with 587 additions and 2 deletions.
36 changes: 34 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ import (

fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1"
"github.com/controlplaneio-fluxcd/flux-operator/internal/controller"
"github.com/controlplaneio-fluxcd/flux-operator/internal/entitlement"
// +kubebuilder:scaffold:imports
)

const controllerName = "flux-controller"
const (
controllerName = "flux-operator"
defaultNamespace = "flux-system"
)

var (
scheme = runtime.NewScheme()
Expand Down Expand Up @@ -70,6 +74,12 @@ func main() {

logger.SetLogger(logger.NewLogger(logOptions))

runtimeNamespace := os.Getenv("RUNTIME_NAMESPACE")
if runtimeNamespace == "" {
runtimeNamespace = defaultNamespace
setupLog.Info("RUNTIME_NAMESPACE env var not set, defaulting to " + defaultNamespace)
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
Expand All @@ -95,7 +105,7 @@ func main() {
// Only the FluxInstance with the name 'flux' can be reconciled.
Field: fields.SelectorFromSet(fields.Set{
"metadata.name": "flux",
"metadata.namespace": os.Getenv("RUNTIME_NAMESPACE"),
"metadata.namespace": runtimeNamespace,
}),
},
},
Expand All @@ -106,6 +116,28 @@ func main() {
os.Exit(1)
}

entitlementClient, err := entitlement.NewClient()
if err != nil {
setupLog.Error(err, "unable to create entitlement client")
os.Exit(1)
}

if err = (&controller.EntitlementReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
StatusPoller: polling.NewStatusPoller(mgr.GetClient(), mgr.GetRESTMapper(), polling.Options{}),
StatusManager: controllerName,
EventRecorder: mgr.GetEventRecorderFor(controllerName),
WatchNamespace: runtimeNamespace,
EntitlementClient: entitlementClient,
}).SetupWithManager(mgr,
controller.EntitlementReconcilerOptions{
RateLimiter: runtimeCtrl.GetRateLimiter(rateLimiterOptions),
}); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Entitlement")
os.Exit(1)
}

if err = (&controller.FluxInstanceReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand Down
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ go 1.22.0

require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/aws/aws-sdk-go-v2 v1.28.0
github.com/aws/aws-sdk-go-v2/config v1.27.19
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.11
github.com/fluxcd/cli-utils v0.36.0-flux.7
github.com/fluxcd/pkg/apis/kustomize v1.5.0
github.com/fluxcd/pkg/apis/meta v1.5.0
github.com/fluxcd/pkg/kustomize v1.11.0
github.com/fluxcd/pkg/runtime v0.47.1
github.com/fluxcd/pkg/ssa v0.39.1
github.com/fluxcd/pkg/tar v0.7.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/go-containerregistry v0.19.1
github.com/onsi/ginkgo/v2 v2.19.0
github.com/onsi/gomega v1.33.1
Expand All @@ -29,6 +33,17 @@ require (
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.19 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down
30 changes: 30 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.28.0 h1:ne6ftNhY0lUvlazMUQF15FF6NH80wKmPRFG7g2q6TCw=
github.com/aws/aws-sdk-go-v2 v1.28.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/config v1.27.19 h1:+DBS8gJP6VsxYkZ6UEV0/VsRM2rYpbQCYsosW9RRmeQ=
github.com/aws/aws-sdk-go-v2/config v1.27.19/go.mod h1:KzZcioJWzy9oV+oS5CobYXlDtU9+eW7bPG1g7gizTW4=
github.com/aws/aws-sdk-go-v2/credentials v1.17.19 h1:R18G7nBBGLby51CFEqUBFF2IVl7LUdCtYj6iosUwh/0=
github.com/aws/aws-sdk-go-v2/credentials v1.17.19/go.mod h1:xr9kUMnaLTB866HItT6pg58JgiBP77fSQLBwIa//zk8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 h1:vVOuhRyslJ6T/HteG71ZWCTas1q2w6f0NKsNbkXHs/A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6/go.mod h1:jimWaqLiT0sJGLh51dKCLLtExRYPtMU7MpxuCgtbkxg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10 h1:LZIUb8sQG2cb89QaVFtMSnER10gyKkqU1k3hP3g9das=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10/go.mod h1:BRIqay//vnIOCZjoXWSLffL2uzbtxEmnSlfbvVh7Z/4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10 h1:HY7CXLA0GiQUo3WYxOP7WYkLcwvRX4cLPf5joUcrQGk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10/go.mod h1:kfRBSxRa+I+VyON7el3wLZdrO91oxUxEwdAaWgFqN90=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12 h1:kO2J7WMroF/OTHN9WTcUtMjPhJ7ZoNxx0dwv6UCXQgY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12/go.mod h1:mrNxrjYvXaSjZe5fkKaWgDnOQ6BExLn/7Ru9OpRsMPY=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.11 h1:hv7JaWwhc6+mwixzbayWMDiMFX591d86VqIRUJUkLqE=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.11/go.mod h1:Z/w6+UQdM5RgBx1111Y2juR+32q16x6s2q+dHywuXWU=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12 h1:FsYii6U+2k8ynYBo+pywlCBY9HNAFRh+iICRHbn+Qyw=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12/go.mod h1:j9Rps+Lcs2A0tYypWsNBeJOjgsIYUf1Styppo9Es0Wo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6 h1:lEE+xEcq3lh9bk362tgErP1+n689q5ERdmTwmF1XT3M=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6/go.mod h1:2tR0x1DCL5IgnVZ1NQNFDNg5/XL/kiQgWI5l7I/N5Js=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13 h1:TSzmuUeruVJ4XWYp3bYzKCXue70ECpJWmbP3UfEvhYY=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13/go.mod h1:FppRtFjBA9mSWTj2cIAWCP66+bbBPMuPpBfWRXC5Yi0=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
Expand Down Expand Up @@ -87,6 +115,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
Expand Down
218 changes: 218 additions & 0 deletions internal/controller/entitlement_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// Copyright 2024 Stefan Prodan.
// SPDX-License-Identifier: AGPL-3.0

package controller

import (
"context"
"fmt"
"time"

"github.com/fluxcd/cli-utils/pkg/kstatus/polling"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
kuberecorder "k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"

"github.com/controlplaneio-fluxcd/flux-operator/internal/entitlement"
)

// EntitlementReconciler reconciles entitlements.
type EntitlementReconciler struct {
client.Client
kuberecorder.EventRecorder

EntitlementClient entitlement.Client
Scheme *runtime.Scheme
StatusPoller *polling.StatusPoller
StatusManager string
WatchNamespace string
}

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *EntitlementReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
log := ctrl.LoggerFrom(ctx)

namespace := &corev1.Namespace{}
if err := r.Get(ctx, req.NamespacedName, namespace); err != nil {
return ctrl.Result{}, err
}

secret, err := r.GetEntitlementSecret(ctx)
if err != nil {
return ctrl.Result{}, err
}

log.Info(fmt.Sprintf("Reconciling entitlement %s/%s", namespace.Name, secret.Name),
entitlement.VendorKey, string(secret.Data[entitlement.VendorKey]))

var token string
id := string(namespace.UID)

// Get the token from the secret if it exists.
if t, found := secret.Data[entitlement.TokenKey]; found {
token = string(t)
}

// Register the usage if the token is missing and update the secret.
if token == "" {
token, err = r.EntitlementClient.RegisterUsage(ctx, id)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to register usage for vendor %s: %w",
r.EntitlementClient.GetVendor(), err)
}

if err := r.UpdateEntitlementSecret(ctx, token); err != nil {
return ctrl.Result{}, err
}

log.Info("Entitlement registered", "vendor", r.EntitlementClient.GetVendor())

// Requeue to verify the token.
return ctrl.Result{Requeue: true}, nil
}

// Verify the token and delete the secret if it is invalid.
valid, err := r.EntitlementClient.Verify(token, id)
if !valid {
if err := r.DeleteEntitlementSecret(ctx, secret); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, fmt.Errorf("failed to verify entitlement: %w", err)
}

log.Info("Entitlement verified", "vendor", r.EntitlementClient.GetVendor())
return ctrl.Result{RequeueAfter: 30 * time.Minute}, nil
}

// EntitlementReconcilerOptions contains options for the reconciler.
type EntitlementReconcilerOptions struct {
RateLimiter ratelimiter.RateLimiter
}

// SetupWithManager sets up the controller with the Manager and initializes the
// entitlement secret in the watch namespace.
func (r *EntitlementReconciler) SetupWithManager(mgr ctrl.Manager, opts EntitlementReconcilerOptions) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

if _, err := r.InitEntitlementSecret(ctx); err != nil {
return err
}

ps, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
MatchLabels: map[string]string{
"kubernetes.io/metadata.name": r.WatchNamespace,
},
})
if err != nil {
return err
}

return ctrl.NewControllerManagedBy(mgr).For(
&corev1.Namespace{},
builder.WithPredicates(ps)).
WithEventFilter(predicate.AnnotationChangedPredicate{}).
WithOptions(controller.Options{RateLimiter: opts.RateLimiter}).
Complete(r)
}

// InitEntitlementSecret creates the entitlement secret if it doesn't exist
// and sets the entitlement vendor if it's missing or different.
func (r *EntitlementReconciler) InitEntitlementSecret(ctx context.Context) (*corev1.Secret, error) {
secretName := fmt.Sprintf("%s-entitlement", r.StatusManager)
secret := &corev1.Secret{}
err := r.Client.Get(ctx, client.ObjectKey{
Namespace: r.WatchNamespace,
Name: secretName,
}, secret)
if err != nil {
if apierrors.IsNotFound(err) {
newSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: r.WatchNamespace,
Labels: map[string]string{
"app.kubernetes.io/name": r.StatusManager,
"app.kubernetes.io/component": "entitlement",
"app.kubernetes.io/managed-by": r.StatusManager,
},
},
Data: map[string][]byte{
entitlement.VendorKey: []byte(r.EntitlementClient.GetVendor()),
},
}
errNew := r.Client.Create(ctx, newSecret)
if errNew != nil {
return nil, fmt.Errorf("failed to create %s: %w", secretName, errNew)
}
return newSecret, nil
} else {
return nil, fmt.Errorf("failed to init %s: %w", secretName, err)
}
}

definedVendor, found := secret.Data[entitlement.VendorKey]
if !found || string(definedVendor) != r.EntitlementClient.GetVendor() {
secret.Data = make(map[string][]byte)
secret.Data[entitlement.VendorKey] = []byte(r.EntitlementClient.GetVendor())
if err := r.Client.Update(ctx, secret); err != nil {
return nil, fmt.Errorf("failed to set vendor in %s: %w", secretName, err)
}
}

return secret, nil
}

// GetEntitlementSecret returns the entitlement secret.
// if the secret doesn't exist, it gets initialized.
func (r *EntitlementReconciler) GetEntitlementSecret(ctx context.Context) (*corev1.Secret, error) {
log := ctrl.LoggerFrom(ctx)
secretName := fmt.Sprintf("%s-entitlement", r.StatusManager)
secret := &corev1.Secret{}
err := r.Client.Get(ctx, client.ObjectKey{
Namespace: r.WatchNamespace,
Name: secretName,
}, secret)
if err != nil {
if apierrors.IsNotFound(err) {
log.Error(err, fmt.Sprintf("Entitlement not found, initializing %s/%s", r.WatchNamespace, secretName))
return r.InitEntitlementSecret(ctx)
}
return nil, fmt.Errorf("failed to get %s: %w", secretName, err)
}

return secret, nil
}

// UpdateEntitlementSecret updates the token in the entitlement secret.
func (r *EntitlementReconciler) UpdateEntitlementSecret(ctx context.Context, token string) error {
secret, err := r.GetEntitlementSecret(ctx)
if err != nil {
return err
}

secret.Data[entitlement.TokenKey] = []byte(token)
if err := r.Client.Update(ctx, secret); err != nil {
return fmt.Errorf("failed to update %s: %w", secret.Name, err)
}

return nil
}

// DeleteEntitlementSecret deletes the entitlement secret.
func (r *EntitlementReconciler) DeleteEntitlementSecret(ctx context.Context, secret *corev1.Secret) error {
if err := r.Client.Delete(ctx, secret); err != nil {
return fmt.Errorf("failed to delete %s: %w", secret.Name, err)
}

return nil
}
Loading

0 comments on commit aca33f2

Please sign in to comment.