Skip to content
Open
3 changes: 1 addition & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ tasks:
if [ "$GITHUB_WORKFLOW" = "ci" ]; then
echo "Running tests in Github Actions"
echo "127.0.0.1 portal.dev.local kcp.api.portal.dev.local" | sudo tee -a /etc/hosts
GH_TOKEN=$WORKFLOW_GITHUB_PAT PATH=$(pwd)/{{.LOCAL_BIN}}:$PATH go test -timeout 25m -coverprofile=cover.out ./... {{.ADDITIONAL_COMMAND_ARGS}} || {
echo $GH_TOKEN
PATH=$(pwd)/{{.LOCAL_BIN}}:$PATH go test -timeout 25m -coverprofile=cover.out ./... {{.ADDITIONAL_COMMAND_ARGS}} || {
echo "---Listing HelmReleases"
kubectl get helmreleases -A
# ...
Expand Down
58 changes: 56 additions & 2 deletions pkg/subroutines/featuretoggles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ package subroutines
import (
"context"
"path/filepath"
"time"

pmconfig "github.com/platform-mesh/golang-commons/config"
"github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject"
"github.com/platform-mesh/golang-commons/errors"
"github.com/platform-mesh/golang-commons/logger"
corev1alpha1 "github.com/platform-mesh/platform-mesh-operator/api/v1alpha1"
"github.com/platform-mesh/platform-mesh-operator/internal/config"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -58,14 +65,45 @@ func (r *FeatureToggleSubroutine) Finalizers() []string { // coverage-ignore

func (r *FeatureToggleSubroutine) Process(ctx context.Context, runtimeObj runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) {
log := logger.LoadLoggerFromContext(ctx).ChildLogger("subroutine", r.GetName())
operatorCfg := pmconfig.LoadConfigFromContext(ctx).(config.OperatorConfig)

// Gate on KCP RootShard readiness
rootShard := &unstructured.Unstructured{}
rootShard.SetGroupVersionKind(schema.GroupVersionKind{Group: "operator.kcp.io", Version: "v1alpha1", Kind: "RootShard"})
if err := r.client.Get(ctx, types.NamespacedName{
Name: operatorCfg.KCP.RootShardName,
Namespace: operatorCfg.KCP.Namespace,
}, rootShard); err != nil {
log.Info().Err(err).Str("name", operatorCfg.KCP.RootShardName).Msg("Failed to get RootShard.. Retry in 5 seconds")
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
if !MatchesCondition(rootShard, "Available") {
log.Info().Str("name", operatorCfg.KCP.RootShardName).Msg("RootShard Available condition not met.. Retry in 5 seconds")
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}

// Gate on KCP FrontProxy readiness
frontProxy := &unstructured.Unstructured{}
frontProxy.SetGroupVersionKind(schema.GroupVersionKind{Group: "operator.kcp.io", Version: "v1alpha1", Kind: "FrontProxy"})
if err := r.client.Get(ctx, types.NamespacedName{
Name: operatorCfg.KCP.FrontProxyName,
Namespace: operatorCfg.KCP.Namespace,
}, frontProxy); err != nil {
log.Info().Err(err).Str("name", operatorCfg.KCP.FrontProxyName).Msg("Failed to get FrontProxy.. Retry in 5 seconds")
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
if !MatchesCondition(frontProxy, "Available") {
log.Info().Str("name", operatorCfg.KCP.FrontProxyName).Msg("FrontProxy Available condition not met.. Retry in 5 seconds")
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}

inst := runtimeObj.(*corev1alpha1.PlatformMesh)
for _, ft := range inst.Spec.FeatureToggles {
switch ft.Name {
case "feature-enable-getting-started":
// Implement the logic to enable the getting started feature
log.Info().Msg("Getting started feature enabled")
return r.FeatureGettingStarted(ctx, inst)
return r.FeatureGettingStarted(ctx, inst, operatorCfg)
default:
log.Warn().Str("featureToggle", ft.Name).Msg("Unknown feature toggle")
}
Expand All @@ -74,12 +112,28 @@ func (r *FeatureToggleSubroutine) Process(ctx context.Context, runtimeObj runtim
return ctrl.Result{}, nil
}

func (r *FeatureToggleSubroutine) FeatureGettingStarted(ctx context.Context, inst *corev1alpha1.PlatformMesh) (ctrl.Result, errors.OperatorError) {
func (r *FeatureToggleSubroutine) FeatureGettingStarted(ctx context.Context, inst *corev1alpha1.PlatformMesh, operatorCfg config.OperatorConfig) (ctrl.Result, errors.OperatorError) {
log := logger.LoadLoggerFromContext(ctx).ChildLogger("subroutine", r.GetName())

// Implement the logic to enable the getting started feature
log.Info().Msg("Getting started feature enabled")

// Ensure the KCP admin secret exists before building kubeconfig
secret := &corev1.Secret{}
if err := r.client.Get(ctx, types.NamespacedName{
Name: operatorCfg.KCP.ClusterAdminSecretName,
Namespace: operatorCfg.KCP.Namespace,
}, secret); err != nil {
if kerrors.IsNotFound(err) {
log.Info().
Str("secret", operatorCfg.KCP.ClusterAdminSecretName).
Str("namespace", operatorCfg.KCP.Namespace).
Msg("KCP admin secret not found yet.. Retry in 5 seconds")
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
return ctrl.Result{}, errors.NewOperatorError(errors.Wrap(err, "Failed to get secret"), true, true)
}

// Build kcp kubeconfig
cfg, err := buildKubeconfig(ctx, r.client, r.kcpUrl)
if err != nil {
Expand Down
72 changes: 65 additions & 7 deletions pkg/subroutines/featuretoggles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/suite"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
Expand Down Expand Up @@ -54,16 +55,20 @@ func (s *FeaturesTestSuite) TearDownTest() {

func (s *FeaturesTestSuite) TestProcess() {
operatorCfg := config.OperatorConfig{}
operatorCfg.KCP.RootShardName = "root-shard"
operatorCfg.KCP.FrontProxyName = "front-proxy"
operatorCfg.KCP.Namespace = "kcp-system"
operatorCfg.KCP.ClusterAdminSecretName = "kcp-admin-kubeconfig"

ctx := context.WithValue(context.Background(), keys.LoggerCtxKey, s.log)
ctx = context.WithValue(ctx, keys.ConfigCtxKey, operatorCfg)

// Mock the kubeconfig secret lookup
s.clientMock.EXPECT().
Get(mock.Anything, types.NamespacedName{
Name: "",
Namespace: "",
}, mock.Anything).
Name: "kcp-admin-kubeconfig",
Namespace: "kcp-system",
}, mock.AnythingOfType("*v1.Secret")).
RunAndReturn(func(ctx context.Context, nn types.NamespacedName, obj client.Object, opts ...client.GetOption) error {
secret := obj.(*corev1.Secret)
secret.Data = map[string][]byte{
Expand All @@ -84,10 +89,63 @@ func (s *FeaturesTestSuite) TestProcess() {
NewKcpClient(mock.Anything, "root:orgs:default").
Return(mockKcpClient, nil)

// Mock patch calls for applying manifests (flexible count)
mockKcpClient.EXPECT().
Patch(mock.Anything, mock.AnythingOfType("*unstructured.Unstructured"), mock.Anything, mock.Anything).
Return(nil).Times(100)
// Mock RootShard lookup
s.clientMock.EXPECT().Get(
mock.Anything,
types.NamespacedName{Name: "root-shard", Namespace: "kcp-system"},
mock.MatchedBy(func(obj client.Object) bool {
u, ok := obj.(*unstructured.Unstructured)
return ok && u.GetKind() == "RootShard"
}),
).RunAndReturn(func(ctx context.Context, nn types.NamespacedName, obj client.Object, opts ...client.GetOption) error {
unstructuredObj := obj.(*unstructured.Unstructured)
unstructuredObj.Object = map[string]interface{}{
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{"type": "Available", "status": "True"},
},
},
}
return nil
})

// Mock FrontProxy lookup
s.clientMock.EXPECT().Get(
mock.Anything,
types.NamespacedName{Name: "front-proxy", Namespace: "kcp-system"},
mock.MatchedBy(func(obj client.Object) bool {
u, ok := obj.(*unstructured.Unstructured)
return ok && u.GetKind() == "FrontProxy"
}),
).RunAndReturn(func(ctx context.Context, nn types.NamespacedName, obj client.Object, opts ...client.GetOption) error {
unstructuredObj := obj.(*unstructured.Unstructured)
unstructuredObj.Object = map[string]interface{}{
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{"type": "Available", "status": "True"},
},
},
}
return nil
})

// Mock unstructured object lookups (for general manifest objects - flexible count)
s.clientMock.EXPECT().Get(mock.Anything, mock.Anything, mock.AnythingOfType("*unstructured.Unstructured")).
RunAndReturn(func(ctx context.Context, nn types.NamespacedName, obj client.Object, opts ...client.GetOption) error {
unstructuredObj := obj.(*unstructured.Unstructured)
unstructuredObj.Object = map[string]interface{}{
"status": map[string]interface{}{
"phase": "Ready",
"conditions": []interface{}{
map[string]interface{}{
"type": "Available",
"status": "True",
},
},
},
}
return nil
}).Times(100)

// Expect multiple Patch calls for applying manifests (flexible count)
mockKcpClient.EXPECT().Patch(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Times(100)
Expand Down
49 changes: 0 additions & 49 deletions test/e2e/kind/suite_kind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package e2e

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -248,40 +246,6 @@ func (s *KindTestSuite) createSecrets(ctx context.Context, dirRootPath []byte) e
return err
}

s.logger.Debug().Str("gh-token", os.Getenv("GH_TOKEN")).Msg("Using GitHub token for Docker secrets")

// create docker secrets
dockerCfg := map[string]interface{}{
"auths": map[string]interface{}{
"ghcr.io": map[string]string{
"username": "platform-mesh-technical-user",
"password": os.Getenv("GH_TOKEN"),
"auth": base64.StdEncoding.EncodeToString([]byte("platform-mesh-technical-user:" + os.Getenv("GH_TOKEN"))),
},
},
}
jsonBytes, _ := json.Marshal(dockerCfg)

github := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "github",
Namespace: "default",
},
Data: map[string][]byte{
".dockerconfigjson": jsonBytes,
},
Type: corev1.SecretTypeDockerConfigJson,
}
github_pms := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "github",
Namespace: "platform-mesh-system",
},
Data: map[string][]byte{
".dockerconfigjson": jsonBytes,
},
Type: corev1.SecretTypeDockerConfigJson,
}
keycloak_admin := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "keycloak-admin",
Expand All @@ -292,16 +256,6 @@ func (s *KindTestSuite) createSecrets(ctx context.Context, dirRootPath []byte) e
},
Type: corev1.SecretTypeOpaque,
}
ocm := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ocm-oci-github-pull",
Namespace: "default",
},
Data: map[string][]byte{
".dockerconfigjson": jsonBytes,
},
Type: corev1.SecretTypeDockerConfigJson,
}
domain_certificate := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "domain-certificate",
Expand Down Expand Up @@ -348,9 +302,6 @@ func (s *KindTestSuite) createSecrets(ctx context.Context, dirRootPath []byte) e
}

secrets := []client.Object{
github,
github_pms,
ocm,
keycloak_admin,
domain_certificate,
rbac_webhook_ca,
Expand Down
Loading