Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: init new ksctl adm install-operators command #48

Merged
merged 51 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
76b2c6f
init new ksctl adm install-operators command
mfrancisc Jul 11, 2024
65950b1
cleanup
mfrancisc Jul 11, 2024
026a786
Merge branch 'master' into installcmd
mfrancisc Jul 22, 2024
24d1646
add unit tests, fix few things
mfrancisc Jul 23, 2024
0ba7c69
Merge branch 'master' into installcmd
mfrancisc Jul 23, 2024
7e768f0
fix linter
mfrancisc Jul 23, 2024
3758702
Merge remote-tracking branch 'origin/installcmd' into installcmd
mfrancisc Jul 23, 2024
83e88f0
fix operator name
mfrancisc Jul 23, 2024
39e8488
Merge branch 'master' into installcmd
mfrancisc Jul 24, 2024
e80179b
Update install command to work with one operator at the time
mfrancisc Jul 24, 2024
3b12bea
fix lint
mfrancisc Jul 24, 2024
6f6e5fc
fix kubeclient creation with CAData
mfrancisc Jul 29, 2024
f804585
Merge branch 'master' into installcmd
mfrancisc Aug 1, 2024
fcc0d20
Merge branch 'master' into installcmd
mfrancisc Aug 5, 2024
bc10ef1
add some more tests
mfrancisc Aug 5, 2024
a20d02c
Merge branch 'master' into installcmd
mfrancisc Aug 6, 2024
d79bd22
make kubeconfig parameter required
mfrancisc Aug 6, 2024
887c794
create NewKubeClientFromKubeConfig function
mfrancisc Aug 6, 2024
eb100b5
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 6, 2024
f1934b8
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 6, 2024
b56322b
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 6, 2024
f41f209
remove prefixes
mfrancisc Aug 6, 2024
8121d61
Merge remote-tracking branch 'origin/installcmd' into installcmd
mfrancisc Aug 6, 2024
75b7a7f
check that only one operator in namespace
mfrancisc Aug 7, 2024
2873550
Merge branch 'master' into installcmd
mfrancisc Aug 7, 2024
0f0b752
check if operator group is already present
mfrancisc Aug 7, 2024
d4571c2
Merge remote-tracking branch 'origin/installcmd' into installcmd
mfrancisc Aug 7, 2024
2c852fe
filter install plan by olm label
mfrancisc Aug 7, 2024
5b620cc
improve timeout message
mfrancisc Aug 7, 2024
e2c6635
assume defaults when no namespace is provided
mfrancisc Aug 7, 2024
a9252f2
happy linter
mfrancisc Aug 7, 2024
14b516b
replace map with switch
mfrancisc Aug 8, 2024
89aadcc
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 9, 2024
38efc02
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 9, 2024
8136618
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 9, 2024
0f07922
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 9, 2024
a83d3c1
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 9, 2024
754eaca
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 9, 2024
d84f101
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 9, 2024
4d512e8
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 9, 2024
68b5c15
reuse namespaced name, more logs
mfrancisc Aug 9, 2024
c8fce0a
create namespace if it doesn't exist
mfrancisc Aug 9, 2024
eba0d84
use apply client and remove client from context
mfrancisc Aug 9, 2024
f7141d4
use operator name
mfrancisc Aug 9, 2024
949e301
fix linter
mfrancisc Aug 9, 2024
ad35519
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 14, 2024
7ccd7e2
fix operator name
mfrancisc Aug 14, 2024
65d1c2b
simplify tests
mfrancisc Aug 14, 2024
e9f9f2e
Update pkg/cmd/adm/install_operator.go
mfrancisc Aug 14, 2024
29bb820
fix tests
mfrancisc Aug 14, 2024
fe53e0a
simplify tests
mfrancisc Aug 14, 2024
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
22 changes: 22 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"github.com/kubesaw/ksctl/pkg/configuration"
clicontext "github.com/kubesaw/ksctl/pkg/context"
"github.com/kubesaw/ksctl/pkg/ioutils"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"

"github.com/ghodss/yaml"
configv1 "github.com/openshift/api/config/v1"
Expand Down Expand Up @@ -89,6 +90,27 @@
return cl, nil
}

// NewKubeClientFromKubeConfig initializes a runtime client starting from a KubeConfig file path.
func NewKubeClientFromKubeConfig(kubeConfigPath string) (cl runtimeclient.Client, err error) {
var kubeConfig *clientcmdapi.Config
var clientConfig *rest.Config

kubeConfig, err = clientcmd.LoadFromFile(kubeConfigPath)
if err != nil {
return
}
clientConfig, err = clientcmd.NewDefaultClientConfig(*kubeConfig, nil).ClientConfig()
if err != nil {
return

Check warning on line 104 in pkg/client/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/client.go#L104

Added line #L104 was not covered by tests
}
cl, err = NewClientFromRestConfig(clientConfig)
if err != nil {
return

Check warning on line 108 in pkg/client/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/client.go#L108

Added line #L108 was not covered by tests
}

return
}

func newTlsVerifySkippingTransport() http.RoundTripper {
return &http.Transport{
TLSClientConfig: &tls.Config{
Expand Down
31 changes: 29 additions & 2 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import (
toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/codeready-toolchain/toolchain-common/pkg/states"
commontest "github.com/codeready-toolchain/toolchain-common/pkg/test"
"github.com/h2non/gock"
"github.com/kubesaw/ksctl/pkg/client"
clicontext "github.com/kubesaw/ksctl/pkg/context"
. "github.com/kubesaw/ksctl/pkg/test"

"github.com/h2non/gock"
routev1 "github.com/openshift/api/route/v1"
olmv1 "github.com/operator-framework/api/pkg/operators/v1"
olmv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
Expand Down Expand Up @@ -546,6 +545,34 @@ func TestGetRoute(t *testing.T) {
})
}

func TestNewKubeClientFromKubeConfig(t *testing.T) {
// given
t.Cleanup(gock.OffAll)
gock.New("https://cool-server.com").
Get("api").
Persist().
Reply(200).
BodyString("{}")

t.Run("success", func(j *testing.T) {
// when
cl, err := client.NewKubeClientFromKubeConfig(PersistKubeConfigFile(t, HostKubeConfig()))

// then
require.NoError(t, err)
assert.NotNil(t, cl)
})

t.Run("error", func(j *testing.T) {
// when
cl, err := client.NewKubeClientFromKubeConfig("/invalid/kube/config")

// then
require.Error(t, err)
assert.Nil(t, cl)
})
}

func newSubscription(pkg, channel string) *olmv1alpha1.Subscription {
return &olmv1alpha1.Subscription{
TypeMeta: metav1.TypeMeta{
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/adm/adm.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
admCommand.AddCommand(NewRestartCmd())
admCommand.AddCommand(NewUnregisterMemberCmd())
admCommand.AddCommand(NewMustGatherNamespaceCmd())
admCommand.AddCommand(NewInstallOperatorCmd())

Check warning on line 27 in pkg/cmd/adm/adm.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/adm.go#L27

Added line #L27 was not covered by tests

// commands running external script
admCommand.AddCommand(NewRegisterMemberCmd())
Expand Down
266 changes: 266 additions & 0 deletions pkg/cmd/adm/install_operator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package adm

import (
"fmt"
"time"

commonclient "github.com/codeready-toolchain/toolchain-common/pkg/client"
"github.com/kubesaw/ksctl/pkg/client"
"github.com/kubesaw/ksctl/pkg/cmd/flags"
"github.com/kubesaw/ksctl/pkg/configuration"
clicontext "github.com/kubesaw/ksctl/pkg/context"
"github.com/kubesaw/ksctl/pkg/ioutils"
olmv1 "github.com/operator-framework/api/pkg/operators/v1"
olmv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/wait"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/spf13/cobra"
)

type installArgs struct {
kubeConfig string
namespace string
}

func NewInstallOperatorCmd() *cobra.Command {
commandArgs := installArgs{}
cmd := &cobra.Command{
Use: "install-operator <host|member> --kubeconfig <path/to/kubeconfig> --namespace <namespace>",
Short: "install kubesaw operator (host|member)",
Long: `This command installs the latest stable versions of the kubesaw operator using OLM`,
mfrancisc marked this conversation as resolved.
Show resolved Hide resolved
RunE: func(cmd *cobra.Command, args []string) error {
term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout)
kubeClient, err := client.NewKubeClientFromKubeConfig(commandArgs.kubeConfig)
if err != nil {
return err

Check warning on line 41 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L31-L41

Added lines #L31 - L41 were not covered by tests
}

cl := commonclient.NewApplyClient(kubeClient)
ctx := clicontext.NewTerminalContext(term)
return installOperator(ctx, commandArgs, args[0], time.Second*60, cl)

Check warning on line 46 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L44-L46

Added lines #L44 - L46 were not covered by tests
},
}

cmd.Flags().StringVar(&commandArgs.kubeConfig, "kubeconfig", "", "Path to the kubeconfig file to use.")
flags.MustMarkRequired(cmd, "kubeconfig")
cmd.Flags().StringVar(&commandArgs.namespace, "namespace", "", "The namespace where the operator will be installed. Host and Member should be installed in separate namespaces. If the namespace is not provided the standard namespace names are used: toolchain-host|member-operator.")
return cmd

Check warning on line 53 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L50-L53

Added lines #L50 - L53 were not covered by tests
}

func installOperator(ctx *clicontext.TerminalContext, args installArgs, operator string, timeout time.Duration, applyClient *commonclient.ApplyClient) error {
// validate cluster type
if operator != string(configuration.Host) && operator != string(configuration.Member) {
return fmt.Errorf("invalid operator type provided: %s. Valid ones are %s|%s", operator, configuration.Host, configuration.Member)
}

// assume "standard" namespace if not provided
namespace := args.namespace
if args.namespace == "" {
namespace = fmt.Sprintf("toolchain-%s-operator", operator)
}

if !ctx.AskForConfirmation(
ioutils.WithMessagef("install %s in namespace '%s'", operator, namespace)) {
return nil
}

mfrancisc marked this conversation as resolved.
Show resolved Hide resolved
// check if namespace exists
// otherwise create it
if err := createNamespaceIfNotFound(ctx, applyClient, namespace); err != nil {
return err

Check warning on line 76 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L76

Added line #L76 was not covered by tests
}

// check that we don't install both host and member in the same namespace
if err := checkOneOperatorPerNamespace(ctx, applyClient, namespace, operator); err != nil {
return err
}

// install the catalog source
namespacedName := types.NamespacedName{Name: operatorResourceName(operator), Namespace: namespace}
catalogSource := newCatalogSource(namespacedName, operator)
ctx.Println(fmt.Sprintf("Creating CatalogSource %s in namespace %s.", catalogSource.Name, catalogSource.Namespace))
if _, err := applyClient.ApplyObject(ctx.Context, catalogSource, commonclient.SaveConfiguration(false)); err != nil {
return err

Check warning on line 89 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L89

Added line #L89 was not covered by tests
}
ctx.Println(fmt.Sprintf("CatalogSource %s created.", catalogSource.Name))
if err := waitUntilCatalogSourceIsReady(ctx, applyClient, namespacedName, timeout); err != nil {
return err
}
ctx.Printlnf("CatalogSource %s is ready", namespacedName)

// check if operator group is already there
ogs := olmv1.OperatorGroupList{}
if err := applyClient.List(ctx, &ogs, runtimeclient.InNamespace(namespace)); err != nil {
return err

Check warning on line 100 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L100

Added line #L100 was not covered by tests
}
if len(ogs.Items) > 0 {
ctx.Println(fmt.Sprintf("OperatorGroup %s already present in namespace %s. Skipping creation of new operator group.", ogs.Items[0].GetName(), namespace))
} else {
// install operator group
operatorGroup := newOperatorGroup(namespacedName)
ctx.Println(fmt.Sprintf("Creating new operator group %s in namespace %s.", operatorGroup.Name, operatorGroup.Namespace))
if _, err := applyClient.ApplyObject(ctx, operatorGroup, commonclient.SaveConfiguration(false)); err != nil {
return err

Check warning on line 109 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L109

Added line #L109 was not covered by tests
}
ctx.Println(fmt.Sprintf("OperatorGroup %s created.", operatorGroup.Name))
}

// install subscription
operatorName := getOperatorName(operator)
subscription := newSubscription(namespacedName, operatorName, namespacedName.Name)
ctx.Println(fmt.Sprintf("Creating Subscription %s in namespace %s.", subscription.Name, subscription.Namespace))
if _, err := applyClient.ApplyObject(ctx, subscription, commonclient.SaveConfiguration(false)); err != nil {
return err

Check warning on line 119 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L119

Added line #L119 was not covered by tests
}
ctx.Println(fmt.Sprintf("Subcription %s created.", subscription.Name))
if err := waitUntilInstallPlanIsComplete(ctx, applyClient, operatorName, namespace, timeout); err != nil {
return err
}
ctx.Println(fmt.Sprintf("InstallPlan for the %s operator has been completed", operator))
ctx.Println("")
ctx.Println(fmt.Sprintf("The %s operator has been successfully installed in the %s namespace", operator, namespace))
return nil
}

func getOperatorName(operator string) string {
return fmt.Sprintf("toolchain-%s-operator", operator)
}

func createNamespaceIfNotFound(ctx *clicontext.TerminalContext, applyClient *commonclient.ApplyClient, namespace string) error {
ns := &v1.Namespace{}
if err := applyClient.Get(ctx.Context, types.NamespacedName{Name: namespace}, ns); err != nil {
if errors.IsNotFound(err) {
ctx.Println(fmt.Sprintf("Creating namespace %s.", namespace))
ns.Name = namespace
if errNs := applyClient.Create(ctx.Context, ns); errNs != nil {
return errNs

Check warning on line 142 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L142

Added line #L142 was not covered by tests
}
} else {
return err

Check warning on line 145 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L144-L145

Added lines #L144 - L145 were not covered by tests
}
}
ctx.Println(fmt.Sprintf("Namespace %s created.", namespace))
return nil
mfrancisc marked this conversation as resolved.
Show resolved Hide resolved
}

func operatorResourceName(operator string) string {
return fmt.Sprintf("%s-operator", operator)
}

func newCatalogSource(name types.NamespacedName, operator string) *olmv1alpha1.CatalogSource {
return &olmv1alpha1.CatalogSource{
ObjectMeta: metav1.ObjectMeta{
Namespace: name.Namespace,
Name: name.Name,
},
Spec: olmv1alpha1.CatalogSourceSpec{
SourceType: olmv1alpha1.SourceTypeGrpc,
Image: fmt.Sprintf("quay.io/codeready-toolchain/%s-operator-index:latest", operator),
DisplayName: fmt.Sprintf("KubeSaw %s Operator", operator),
Publisher: "Red Hat",
UpdateStrategy: &olmv1alpha1.UpdateStrategy{
RegistryPoll: &olmv1alpha1.RegistryPoll{
Interval: &metav1.Duration{
Duration: 5 * time.Minute,
},
},
},
},
}
}

func newOperatorGroup(name types.NamespacedName) *olmv1.OperatorGroup {
return &olmv1.OperatorGroup{
ObjectMeta: metav1.ObjectMeta{
Namespace: name.Namespace,
Name: name.Name,
},
Spec: olmv1.OperatorGroupSpec{
TargetNamespaces: []string{
name.Namespace,
},
},
}
}

func newSubscription(name types.NamespacedName, operatorName, catalogSourceName string) *olmv1alpha1.Subscription {
return &olmv1alpha1.Subscription{
ObjectMeta: metav1.ObjectMeta{
Namespace: name.Namespace,
Name: name.Name,
},
Spec: &olmv1alpha1.SubscriptionSpec{
Channel: "staging",
InstallPlanApproval: olmv1alpha1.ApprovalAutomatic,
Package: operatorName,
CatalogSource: catalogSourceName,
CatalogSourceNamespace: name.Namespace,
},
}
}

func waitUntilCatalogSourceIsReady(ctx *clicontext.TerminalContext, applyClient *commonclient.ApplyClient, catalogSourceKey runtimeclient.ObjectKey, waitForReadyTimeout time.Duration) error {
cs := &olmv1alpha1.CatalogSource{}
if err := wait.PollImmediate(2*time.Second, waitForReadyTimeout, func() (bool, error) {
ctx.Printlnf("waiting for CatalogSource %s to become ready", catalogSourceKey)
cs = &olmv1alpha1.CatalogSource{}
if err := applyClient.Get(ctx, catalogSourceKey, cs); err != nil {
return false, err

Check warning on line 214 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L214

Added line #L214 was not covered by tests
}

return cs.Status.GRPCConnectionState != nil && cs.Status.GRPCConnectionState.LastObservedState == "READY", nil
}); err != nil {
csString, _ := json.Marshal(cs)
return fmt.Errorf("failed waiting for catalog source to be ready.\n CatalogSource found: %v \n\t", string(csString))
}
return nil
}

func waitUntilInstallPlanIsComplete(ctx *clicontext.TerminalContext, cl runtimeclient.Client, operator, namespace string, waitForReadyTimeout time.Duration) error {
plans := &olmv1alpha1.InstallPlanList{}
if err := wait.PollImmediate(2*time.Second, waitForReadyTimeout, func() (bool, error) {
ctx.Printlnf("waiting for InstallPlans in namespace %s to complete", namespace)
plans = &olmv1alpha1.InstallPlanList{}
if err := cl.List(ctx, plans, runtimeclient.InNamespace(namespace),
mfrancisc marked this conversation as resolved.
Show resolved Hide resolved
runtimeclient.MatchingLabels{fmt.Sprintf("operators.coreos.com/%s.%s", operator, namespace): ""},
mfrancisc marked this conversation as resolved.
Show resolved Hide resolved
); err != nil {
return false, err

Check warning on line 233 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L233

Added line #L233 was not covered by tests
}

for _, ip := range plans.Items {
if ip.Status.Phase != olmv1alpha1.InstallPlanPhaseComplete {
return false, nil
}
}

return len(plans.Items) > 0, nil
}); err != nil {
plansString, _ := json.Marshal(plans)
return fmt.Errorf("failed waiting for install plan to be complete.\n InstallPlans found: %s \n\t", string(plansString))
}
return nil
}

// checkOneOperatorPerNamespace returns an error in case the namespace contains the other operator installed.
// So for host namespace member operator should not be installed in there and vice-versa.
func checkOneOperatorPerNamespace(ctx *clicontext.TerminalContext, applyClient *commonclient.ApplyClient, namespace, operator string) error {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: configuration.ClusterType(operator).TheOtherType().String(),
}
subscription := olmv1alpha1.Subscription{}
if err := applyClient.Get(ctx.Context, namespacedName, &subscription); err != nil {
if errors.IsNotFound(err) {
return nil
}

return err

Check warning on line 263 in pkg/cmd/adm/install_operator.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/adm/install_operator.go#L263

Added line #L263 was not covered by tests
}
return fmt.Errorf("found already installed subscription %s in namespace %s - it's not allowed to have host and member in the same namespace", subscription.GetName(), subscription.GetNamespace())
}
Loading
Loading