diff --git a/Taskfile.yml b/Taskfile.yml index d40d888..ae2fc4b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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 # ... diff --git a/pkg/subroutines/featuretoggles.go b/pkg/subroutines/featuretoggles.go index b074114..09d3900 100644 --- a/pkg/subroutines/featuretoggles.go +++ b/pkg/subroutines/featuretoggles.go @@ -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" + 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" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -58,6 +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 { + if apierrors.IsNotFound(err) { + log.Info().Str("name", operatorCfg.KCP.RootShardName).Msg("RootShard not found yet.. Retry in 5 seconds") + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + log.Error().Err(err).Str("name", operatorCfg.KCP.RootShardName).Msg("Failed to get RootShard") + return ctrl.Result{}, errors.NewOperatorError(err, true, true) + } + 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 { + if apierrors.IsNotFound(err) { + log.Info().Str("name", operatorCfg.KCP.FrontProxyName).Msg("FrontProxy not found yet.. Retry in 5 seconds") + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + log.Error().Err(err).Str("name", operatorCfg.KCP.FrontProxyName).Msg("Failed to get FrontProxy") + return ctrl.Result{}, errors.NewOperatorError(err, true, true) + } + 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 { @@ -65,7 +111,7 @@ func (r *FeatureToggleSubroutine) Process(ctx context.Context, runtimeObj runtim 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") } @@ -74,12 +120,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 apierrors.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 { diff --git a/pkg/subroutines/featuretoggles_test.go b/pkg/subroutines/featuretoggles_test.go index 659937c..701689d 100644 --- a/pkg/subroutines/featuretoggles_test.go +++ b/pkg/subroutines/featuretoggles_test.go @@ -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" @@ -54,6 +55,10 @@ 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) @@ -61,9 +66,9 @@ func (s *FeaturesTestSuite) TestProcess() { // 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{ @@ -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) diff --git a/test/e2e/kind/suite_kind_test.go b/test/e2e/kind/suite_kind_test.go index df4ba91..17da3b8 100644 --- a/test/e2e/kind/suite_kind_test.go +++ b/test/e2e/kind/suite_kind_test.go @@ -2,8 +2,6 @@ package e2e import ( "context" - "encoding/base64" - "encoding/json" "errors" "fmt" "os" @@ -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", @@ -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", @@ -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,