Skip to content

Commit

Permalink
add enable-feature command (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
MatousJobanek authored Jul 2, 2024
1 parent ffc4214 commit 9d398a8
Show file tree
Hide file tree
Showing 5 changed files with 369 additions and 3 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/kubesaw/ksctl
go 1.20

require (
github.com/codeready-toolchain/api v0.0.0-20240530120602-c11598ccffb7
github.com/codeready-toolchain/api v0.0.0-20240627084210-f4a765461e75
github.com/codeready-toolchain/toolchain-common v0.0.0-20240530121312-98aad712838f
github.com/ghodss/yaml v1.0.0
github.com/mitchellh/go-homedir v1.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/codeready-toolchain/api v0.0.0-20240530120602-c11598ccffb7 h1:o5JLcHCVS1BlZevw2mh1mH+iKwn9fLUrT1Ek8NFjvPY=
github.com/codeready-toolchain/api v0.0.0-20240530120602-c11598ccffb7/go.mod h1:ie9p4LenCCS0LsnbWp6/xwpFDdCWYE0KWzUO6Sk1g0E=
github.com/codeready-toolchain/api v0.0.0-20240627084210-f4a765461e75 h1:LOrOMTw3bplM+VwzSxi3j9GNseujhcebO/LGr8b95GE=
github.com/codeready-toolchain/api v0.0.0-20240627084210-f4a765461e75/go.mod h1:ie9p4LenCCS0LsnbWp6/xwpFDdCWYE0KWzUO6Sk1g0E=
github.com/codeready-toolchain/toolchain-common v0.0.0-20240530121312-98aad712838f h1:2qfRfyh7wfEnnfxrUtQeQrvhzWlkBCN0B/UXv1YUMiA=
github.com/codeready-toolchain/toolchain-common v0.0.0-20240530121312-98aad712838f/go.mod h1:cyHrUfvBYEtsf+FbqQYmR9y0AQi9QAVtM3SUWLA5bd4=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
Expand Down
103 changes: 103 additions & 0 deletions pkg/cmd/enable-feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package cmd

import (
"fmt"
"strings"

toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/kubesaw/ksctl/pkg/client"
"github.com/kubesaw/ksctl/pkg/configuration"
clicontext "github.com/kubesaw/ksctl/pkg/context"
"github.com/kubesaw/ksctl/pkg/ioutils"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/strings/slices"

"github.com/spf13/cobra"
)

func NewEnableFeatureCmd() *cobra.Command {
return &cobra.Command{
Use: "enable-feature <space-name> <feature-name>",
Short: "Enable a feature for the given Space",
Long: `Enable a feature toggle for the given Space. There are two expected
parameters - the first one is the Space name and the second is the name of the feature toggle that should be enabled for the Space.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout)
ctx := clicontext.NewCommandContext(term, client.DefaultNewClient)
return EnableFeature(ctx, args[0], args[1])
},
}
}

func EnableFeature(ctx *clicontext.CommandContext, spaceName, featureToggleName string) error {
return client.PatchSpace(ctx, spaceName, func(space *toolchainv1alpha1.Space) (bool, error) {
cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName)
if err != nil {
return false, err
}
cl, err := ctx.NewClient(cfg.Token, cfg.ServerAPI)
if err != nil {
return false, err
}

// get ToolchainConfig to check if the feature toggle is supported or not
config := &toolchainv1alpha1.ToolchainConfig{}
namespacedName := types.NamespacedName{Namespace: cfg.OperatorNamespace, Name: "config"}
if err := cl.Get(ctx, namespacedName, config); err != nil {
return false, fmt.Errorf("unable to get ToolchainConfig: %w", err)
}
// if no feature toggle is supported then return an error
if len(config.Spec.Host.Tiers.FeatureToggles) == 0 {
return false, fmt.Errorf("the feature toggle is not supported - the list of supported toggles is empty")
}

supportedFeatureToggles := make([]string, len(config.Spec.Host.Tiers.FeatureToggles))
for i, fToggle := range config.Spec.Host.Tiers.FeatureToggles {
supportedFeatureToggles[i] = fToggle.Name
}

// if the requested feature is not in the list of supported toggles, then print the list of supported ones and return an error
if !slices.Contains(supportedFeatureToggles, featureToggleName) {
ctx.Printlnf("The feature toggle '%s' is not listed as a supported feature toggle in ToolchainConfig CR.", featureToggleName)
fToggleNamesList := "\n"
for _, fToggleName := range supportedFeatureToggles {
fToggleNamesList += fmt.Sprintf("%s\n", fToggleName)
}
ctx.PrintContextSeparatorWithBodyf(fToggleNamesList, "The supported feature toggles are:")
return false, fmt.Errorf("the feature toggle is not supported")
}

// get already enabled features for the space
currentFeatures := strings.TrimSpace(space.Annotations[toolchainv1alpha1.FeatureToggleNameAnnotationKey])
var enabledFeatures []string
if currentFeatures != "" {
enabledFeatures = strings.Split(currentFeatures, ",")
}
if err := ctx.PrintObject(space, "The current Space"); err != nil {
return false, err
}

// check if it's already enabled or not
if slices.Contains(enabledFeatures, featureToggleName) {
ctx.Println("")
ctx.Println("The space has the feature toggle already enabled. There is nothing to do.")
ctx.Println("")
return false, nil
}

confirmation := ctx.AskForConfirmation(ioutils.WithMessagef(
"enable the feature toggle '%s' for the Space '%s'? The already enabled feature toggles are '%s'.",
featureToggleName, spaceName, currentFeatures))

if confirmation {
enabledFeatures = append(enabledFeatures, featureToggleName)
if space.Annotations == nil {
space.Annotations = map[string]string{}
}
space.Annotations[toolchainv1alpha1.FeatureToggleNameAnnotationKey] = strings.Join(enabledFeatures, ",")
return true, nil
}
return false, nil
}, "Successfully enabled feature toggle for the Space")
}
262 changes: 262 additions & 0 deletions pkg/cmd/enable_feature_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package cmd_test

import (
"context"
"fmt"
"testing"

toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/codeready-toolchain/toolchain-common/pkg/test"
"github.com/kubesaw/ksctl/pkg/cmd"
clicontext "github.com/kubesaw/ksctl/pkg/context"
. "github.com/kubesaw/ksctl/pkg/test"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEnableFeatureCmd(t *testing.T) {
// given
SetFileConfig(t, Host())
config := configWithFeatures([]toolchainv1alpha1.FeatureToggle{
{
Name: "feature-x",
},
})

var combinations = []struct {
alreadyEnabled string
afterEnable map[string]string
}{
{
alreadyEnabled: "",
afterEnable: map[string]string{
toolchainv1alpha1.FeatureToggleNameAnnotationKey: "feature-x",
},
},
{
alreadyEnabled: "feature0",
afterEnable: map[string]string{
toolchainv1alpha1.FeatureToggleNameAnnotationKey: "feature0,feature-x",
},
},
{
alreadyEnabled: "feature1,feature2,feature3",
afterEnable: map[string]string{
toolchainv1alpha1.FeatureToggleNameAnnotationKey: "feature1,feature2,feature3,feature-x",
},
},
}

for _, data := range combinations {
t.Run("with the already enabled features: "+data.alreadyEnabled, func(t *testing.T) {
// given
space := newSpace()
if data.alreadyEnabled != "" {
space.Annotations = map[string]string{
toolchainv1alpha1.FeatureToggleNameAnnotationKey: data.alreadyEnabled,
}
}

for _, answer := range []string{"Y", "n"} {

t.Run("when answer is "+answer, func(t *testing.T) {
// given
newClient, fakeClient := NewFakeClients(t, space, config)
term := NewFakeTerminalWithResponse(answer)
ctx := clicontext.NewCommandContext(term, newClient)

// when
err := cmd.EnableFeature(ctx, space.Name, "feature-x")

// then
require.NoError(t, err)

output := term.Output()
assert.Contains(t, output, fmt.Sprintf("enable the feature toggle 'feature-x' for the Space 'testspace'? The already enabled feature toggles are '%s'.", data.alreadyEnabled))
assert.NotContains(t, output, "cool-token")
expectedSpace := newSpace()

if answer == "Y" {
expectedSpace.Annotations = data.afterEnable
assert.Contains(t, output, "Successfully enabled feature toggle for the Space")

} else {
expectedSpace.Annotations = space.Annotations
assert.NotContains(t, output, "Successfully enabled feature toggle for the Space")
}
assertSpaceAnnotations(t, fakeClient, expectedSpace)

})
}
})
}
}

func TestEnableFeatureCmdWhenFeatureIsAlreadyEnabled(t *testing.T) {
// given
SetFileConfig(t, Host())
config := configWithFeatures([]toolchainv1alpha1.FeatureToggle{
{
Name: "feature-x",
},
})

for _, alreadyEnabled := range []string{"feature-x", "feature-x,feature0", "feature1,feature2,feature-x,feature3"} {
t.Run("with the already enabled features: "+alreadyEnabled, func(t *testing.T) {
// given
space := newSpace()
if alreadyEnabled != "" {
space.Annotations = map[string]string{
toolchainv1alpha1.FeatureToggleNameAnnotationKey: alreadyEnabled,
}
}
// given
newClient, fakeClient := NewFakeClients(t, space, config)
term := NewFakeTerminalWithResponse("Y")
ctx := clicontext.NewCommandContext(term, newClient)

// when
err := cmd.EnableFeature(ctx, space.Name, "feature-x")

// then
require.NoError(t, err)
assertSpaceAnnotations(t, fakeClient, space) // no change

output := term.Output()
assert.Contains(t, output, "The space has the feature toggle already enabled. There is nothing to do.")
assert.NotContains(t, output, "enable the feature toggle 'feature-x' for the Space 'testspace'?")
assert.NotContains(t, output, "Successfully enabled feature toggle for the Space")
assert.NotContains(t, output, "cool-token")

})
}
}

func TestEnableFeatureCmdWhenFeatureIsNotSupported(t *testing.T) {
// given
SetFileConfig(t, Host())

var combinations = []struct {
nameList string
supportedFeatures []toolchainv1alpha1.FeatureToggle
}{
{
nameList: "",
supportedFeatures: nil,
},
{
nameList: "feature-0",
supportedFeatures: []toolchainv1alpha1.FeatureToggle{
{
Name: "feature-0",
},
},
},
{
nameList: "feature1\nfeature2\nfeature3",
supportedFeatures: []toolchainv1alpha1.FeatureToggle{
{
Name: "feature1",
},
{
Name: "feature2",
},
{
Name: "feature3",
},
},
},
}

for _, data := range combinations {
t.Run("with the supported features: "+data.nameList, func(t *testing.T) {
// given
space := newSpace()
config := configWithFeatures(data.supportedFeatures)
// given
newClient, fakeClient := NewFakeClients(t, space, config)
term := NewFakeTerminalWithResponse("Y")
ctx := clicontext.NewCommandContext(term, newClient)

// when
err := cmd.EnableFeature(ctx, space.Name, "feature-x")

// then
require.Error(t, err)
output := term.Output()
if data.supportedFeatures == nil {
require.EqualError(t, err, "the feature toggle is not supported - the list of supported toggles is empty")
} else {
require.EqualError(t, err, "the feature toggle is not supported")
assert.Contains(t, output, "The feature toggle 'feature-x' is not listed as a supported feature toggle in ToolchainConfig CR.")
assert.Contains(t, output, "The supported feature toggles are:")
assert.Contains(t, output, data.nameList)
}

assert.NotContains(t, output, "Successfully enabled feature toggle for the Space")
assert.NotContains(t, output, "cool-token")
assertSpaceAnnotations(t, fakeClient, space) // no change
})
}
}

func TestEnableFeatureCmdWhenSpaceNotFound(t *testing.T) {
// given
config := configWithFeatures([]toolchainv1alpha1.FeatureToggle{
{
Name: "feature-x",
},
})
space := newSpace()
newClient, fakeClient := NewFakeClients(t, space, config)
SetFileConfig(t, Host())
term := NewFakeTerminalWithResponse("Y")
ctx := clicontext.NewCommandContext(term, newClient)

// when
err := cmd.EnableFeature(ctx, "another", "feature-x")

// then
require.EqualError(t, err, "spaces.toolchain.dev.openshift.com \"another\" not found")
assertSpaceAnnotations(t, fakeClient, space) // unrelated space should be unchanged
output := term.Output()
assert.NotContains(t, output, "enable the feature toggle 'feature-x' for the Space 'testspace'?")
assert.NotContains(t, output, "Successfully enabled feature toggle for the Space")
assert.NotContains(t, output, "cool-token")
}

func TestEnableFeatureCmdWhenConfigNotFound(t *testing.T) {
// given
space := newSpace()
newClient, fakeClient := NewFakeClients(t, space)
SetFileConfig(t, Host())
term := NewFakeTerminalWithResponse("Y")
ctx := clicontext.NewCommandContext(term, newClient)

// when
err := cmd.EnableFeature(ctx, space.Name, "feature-x")

// then
require.EqualError(t, err, "unable to get ToolchainConfig: toolchainconfigs.toolchain.dev.openshift.com \"config\" not found")
assertSpaceAnnotations(t, fakeClient, space) // no change
}

func assertSpaceAnnotations(t *testing.T, fakeClient *test.FakeClient, expectedSpace *toolchainv1alpha1.Space) {
updatedSpace := &toolchainv1alpha1.Space{}
err := fakeClient.Get(context.TODO(), test.NamespacedName(expectedSpace.Namespace, expectedSpace.Name), updatedSpace)
require.NoError(t, err)
assert.Equal(t, expectedSpace.Annotations, updatedSpace.Annotations)
}

func configWithFeatures(toggles []toolchainv1alpha1.FeatureToggle) *toolchainv1alpha1.ToolchainConfig {
toolchainConfig := &toolchainv1alpha1.ToolchainConfig{
ObjectMeta: metav1.ObjectMeta{
Namespace: test.HostOperatorNs,
Name: "config",
},
}
toolchainConfig.Spec.Host.Tiers.FeatureToggles = toggles
return toolchainConfig
}
1 change: 1 addition & 0 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func init() {
rootCmd.AddCommand(NewLogsCmd())
rootCmd.AddCommand(NewDescribeCmd())
rootCmd.AddCommand(NewDisableUserCmd())
rootCmd.AddCommand(NewEnableFeatureCmd())

// administrative commands
rootCmd.AddCommand(adm.NewAdmCmd())
Expand Down

0 comments on commit 9d398a8

Please sign in to comment.