Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
tmp
external-crds

# Binaries for programs and plugins
*.exe
Expand Down
36 changes: 36 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ includes:
shared:
taskfile: hack/common/Taskfile_controller.yaml
flatten: true
excludes: # put task names in here which are overwritten in this file
- generate:code
vars:
NESTED_MODULES: ""
API_DIRS: "{{.ROOT_DIR}}/api/..."
MANIFEST_OUT: "{{.ROOT_DIR}}/api/crds/manifests"
CODE_DIRS: "{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/api/..."
COMPONENTS: "usage-operator"
REPO_URL: "https://github.com/openmcp-project/usage-operator"
GENERATE_DOCS_INDEX: "true"
CHART_COMPONENTS: "[]"
ENVTEST_REQUIRED: "true"
common: # imported a second time so that overwriting task definitions can call the overwritten task with a 'c:' prefix
taskfile: hack/common/Taskfile_controller.yaml
internal: true
aliases:
- c
excludes: [] # put task names in here which are overwritten in this file
vars:
NESTED_MODULES: ""
Expand All @@ -15,3 +32,22 @@ includes:
GENERATE_DOCS_INDEX: "true"
CHART_COMPONENTS: "[]"
ENVTEST_REQUIRED: "true"

tasks:
generate:code: # overwrites shared code task to add external API fetching
desc: " Generate code (mainly DeepCopy functions) and fetches external APIs."
aliases:
- gen:code
- g:code
run: once
cmds:
- task: download-crds
- task: c:generate:code

download-crds:
internal: true
cmds:
- curl -s https://api.github.com/repos/openmcp-project/mcp-operator/contents/api/crds/manifests | jq -r '.[] | select(.type=="file") | .download_url' | xargs -n 1 curl -s -O -J
- curl -s https://api.github.com/repos/openmcp-project/project-workspace-operator/contents/api/crds/manifests | jq -r '.[] | select(.type=="file") | .download_url' | xargs -n 1 curl -s -O -J
dir: external-crds
desc: "Download CRD files from mcp-operator GitHub repository"
1 change: 0 additions & 1 deletion api/crds/manifests/usage.openmcp.cloud_mcpusages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ spec:
type: string
required:
- charging_target
- daily_usage
- mcp
- project
- workspace
Expand Down
2 changes: 1 addition & 1 deletion api/usage/v1/mcpusage_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type MCPUsageSpec struct {
Project string `json:"project"`
Workspace string `json:"workspace"`
MCP string `json:"mcp"`
Usage []DailyUsage `json:"daily_usage"`
Usage []DailyUsage `json:"daily_usage,omitempty"`
LastUsageCaptured metav1.Time `json:"last_usage_captured,omitempty"`
MCPCreatedAt metav1.Time `json:"mcp_created_at,omitempty"`
MCPDeletedAt metav1.Time `json:"mcp_deleted_at,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion cmd/usage-operator/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ func (o *RunOptions) Run(ctx context.Context) error {
return fmt.Errorf("unable to create manager: %w", err)
}

usageTracker, err := usage.NewUsageTracker(&o.Log, mgr.GetClient())
usageTracker, err := usage.NewUsageTracker(mgr.GetClient())
if err != nil {
return fmt.Errorf("unable to create usage tracker: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/openmcp-project/usage-operator
go 1.24.4

require (
github.com/go-logr/logr v1.4.3
github.com/google/uuid v1.6.0
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
Expand Down Expand Up @@ -33,7 +34,6 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions internal/controller/managedcontrolplane_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"errors"
"regexp"

"github.com/openmcp-project/controller-utils/pkg/logging"
"github.com/go-logr/logr"
corev1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
Expand Down Expand Up @@ -52,7 +52,7 @@ type ManagedControlPlaneReconciler struct {
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *ManagedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log, err := logging.FromContext(ctx)
log, err := logr.FromContext(ctx)
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
Expand All @@ -75,10 +75,10 @@ func (r *ManagedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.
project := matches[1]
workspace := matches[2]

log.Info("mcp '" + mcp.Name + "' status '" + string(mcp.Status.Status) + "'")
log.Info("reconcile", "mcp", mcp.Name, "status", string(mcp.Status.Status))

if mcp.GetDeletionTimestamp() != nil || mcp.Status.Status == corev1alpha1.MCPStatusDeleting {
log.Info("mcp '" + mcp.Name + "' was deleted. Tracking it...")
log.Info("mcp was deleted", "mcp", mcp.Name)
err := r.UsageTracker.DeletionEvent(ctx, project, workspace, mcp.Name)
if err != nil {
log.Error(err, "error when tracking deletion")
Expand Down
133 changes: 129 additions & 4 deletions internal/controller/managedcontrolplane_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,141 @@ limitations under the License.
package controller

import (
"context"
"strings"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

corev1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1"
pwcorev1alpha1 "github.com/openmcp-project/project-workspace-operator/api/core/v1alpha1"

v1 "github.com/openmcp-project/usage-operator/api/usage/v1"
)

const (
ProjectName = "project"
WorkspaceName = "workspace"
MCPName = "test-mcp"

ChargingTarget = "12345678"

timeout = time.Second * 10
duration = time.Second * 10
interval = time.Millisecond * 250
)

var _ = Describe("ManagedControlPlane Controller", func() {
var (
projectNamespaceName string
workspaceNamespaceName string

mcpUsageName string
)

var _ = Describe("ManagedControlPlane Controller", Ordered, func() {
Context("When reconciling a resource", func() {
BeforeAll(func() {
ctx := context.Background()
projectNamespaceName = "project-" + ProjectName
workspaceNamespaceName = projectNamespaceName + "--ws-" + WorkspaceName
namespaces := []corev1.Namespace{
{
ObjectMeta: metav1.ObjectMeta{
Name: projectNamespaceName,
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: workspaceNamespaceName,
},
},
}
Expect(k8sClient.Create(ctx, &namespaces[0])).To(Succeed())
Expect(k8sClient.Create(ctx, &namespaces[1])).To(Succeed())

project := pwcorev1alpha1.Project{
ObjectMeta: metav1.ObjectMeta{
Name: ProjectName,
Labels: map[string]string{
"openmcp.cloud.sap/charging-target": ChargingTarget,
},
},
}
Expect(k8sClient.Create(ctx, &project)).To(Succeed())
workspace := pwcorev1alpha1.Workspace{
ObjectMeta: metav1.ObjectMeta{
Name: WorkspaceName,
Namespace: projectNamespaceName,
},
}
Expect(k8sClient.Create(ctx, &workspace)).To(Succeed())
mcp := corev1alpha1.ManagedControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: MCPName,
Namespace: workspaceNamespaceName,
},
}
Expect(k8sClient.Create(ctx, &mcp)).To(Succeed())
})

It("should create a mcp usage resource based on a ManagedControlPlane resource", func() {
ctx := context.Background()

var mcpUsages v1.MCPUsageList
Eventually(func(g Gomega) {
g.Expect(k8sClient.List(ctx, &mcpUsages)).To(Succeed())

g.Expect(mcpUsages.Items).Should(HaveLen(1))

mcpUsageName = mcpUsages.Items[0].Name

g.Expect(mcpUsages.Items[0].Spec.ChargingTarget).Should(Equal(ChargingTarget))
}, timeout, interval).Should(Succeed())
})

It("should have set the right charging target", func() {
ctx := context.Background()

mcpUsage := v1.MCPUsage{
ObjectMeta: metav1.ObjectMeta{
Name: mcpUsageName,
},
}
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&mcpUsage), &mcpUsage)).Should(Succeed())

Expect(mcpUsage.Spec.ChargingTarget).Should(Equal(ChargingTarget))
})

It("should mark a mcp usage resource as deleted when ManagedControlPlane is deleted", func() {
ctx := context.Background()

mcpUsage := v1.MCPUsage{
ObjectMeta: metav1.ObjectMeta{
Name: mcpUsageName,
},
}
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&mcpUsage), &mcpUsage)).Should(Succeed())

var mcp corev1alpha1.ManagedControlPlane
Expect(k8sClient.Get(ctx, client.ObjectKey{
Name: strings.ToLower(MCPName),
Namespace: workspaceNamespaceName,
}, &mcp)).Should(Succeed())
mcp.Status.Status = corev1alpha1.MCPStatusDeleting
Expect(k8sClient.Status().Update(ctx, &mcp)).Should(Succeed())

var mcpUsages v1.MCPUsageList
Eventually(func(g Gomega) {
g.Expect(k8sClient.List(ctx, &mcpUsages)).To(Succeed())

It("should successfully reconcile the resource", func() {
g.Expect(mcpUsages.Items).Should(HaveLen(1))

// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
g.Expect(mcpUsages.Items[0].Spec.MCPDeletedAt.IsZero()).Should(BeFalse())
}, timeout, interval).Should(Succeed())
})
})
})
39 changes: 38 additions & 1 deletion internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,23 @@ import (
"path/filepath"
"testing"

ctrl "sigs.k8s.io/controller-runtime"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

corev1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1"
pwcorev1alpha1 "github.com/openmcp-project/project-workspace-operator/api/core/v1alpha1"

"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

v1 "github.com/openmcp-project/usage-operator/api/usage/v1"
"github.com/openmcp-project/usage-operator/internal/usage"
// +kubebuilder:scaffold:imports
)

Expand Down Expand Up @@ -60,12 +67,21 @@ var _ = BeforeSuite(func() {
var err error
err = corev1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = pwcorev1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = v1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())

// +kubebuilder:scaffold:scheme

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "api", "crds", "manifests"),
// Add external CRD directories here:
filepath.Join("..", "..", "external-crds"),
// Add more paths as needed
},
ErrorIfCRDPathMissing: false,
}

Expand All @@ -82,6 +98,27 @@ var _ = BeforeSuite(func() {
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())

k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
Expect(err).ToNot(HaveOccurred())

usageTracker, err := usage.NewUsageTracker(k8sClient)
Expect(err).NotTo(HaveOccurred())

err = (&ManagedControlPlaneReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
UsageTracker: usageTracker,
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

go func() {
defer GinkgoRecover()
err = k8sManager.Start(ctx)
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
}()
})

var _ = AfterSuite(func() {
Expand Down
Loading