Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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:
- gh api 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
- gh api 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
Loading