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

feat: provisioning environment #1666

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 17 additions & 0 deletions apptests/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cli

import (
"github.com/spf13/pflag"
)

type Settings struct {
Applications []string
}

func New() *Settings {
return &Settings{Applications: make([]string, 0)}
}

func (s *Settings) AddFlags(flg *pflag.FlagSet) {
flg.StringArrayVarP(&s.Applications, "applications", "apps", s.Applications, "comma-separated list of application to test")
}
66 changes: 66 additions & 0 deletions apptests/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cmd

import (
"fmt"

"github.com/mesosphere/kommander-applications/apptests/cli"
"github.com/mesosphere/kommander-applications/apptests/environment"
"github.com/mesosphere/kommander-applications/apptests/scenarios"
"github.com/spf13/cobra"
)

// settings manages the settings and flags for the command.
var settings = cli.New()

// NewCommand creates and returns the root command for application specific testings.
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "apptest --applications=app1,app2,app3,...",
Short: "A CLI tool for applications specific testing",
Long: `The apptest is a CLI tool that allows you to run tests on different applications.
You can specify which applications you want to test using the --applications flag`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

apps := settings.Applications
if len(apps) == 0 {
return fmt.Errorf("at leas one application must be specified")
}

for _, app := range apps {
if !scenarios.Has(app) {
return fmt.Errorf("could not find app: %s", app)
}
}

for _, app := range apps {
// prepare environment
env := &environment.Env{}
err := env.Provision(ctx)
if err != nil {
return err
}

// run the associated scenario with application
sc := scenarios.Get(app)
err = sc.Execute(ctx, env)
if err != nil {
return err
}

// tear down the environment
err = env.Destroy(ctx)
if err != nil {
return err
}
}

return nil
},
}

flags := cmd.PersistentFlags()
settings.AddFlags(flags)

return cmd
}
223 changes: 223 additions & 0 deletions apptests/environment/environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Package environment provides functions to manage, and configure environment for application specific testings.
package environment

import (
"bytes"
"context"
"fmt"
"io"
"path/filepath"
"time"

"github.com/fluxcd/flux2/v2/pkg/manifestgen"
runclient "github.com/fluxcd/pkg/runtime/client"
typedclient "github.com/mesosphere/kommander-applications/apptests/client"
"github.com/mesosphere/kommander-applications/apptests/flux"
"github.com/mesosphere/kommander-applications/apptests/kind"
"github.com/mesosphere/kommander-applications/apptests/kustomize"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/genericclioptions"
genericCLient "sigs.k8s.io/controller-runtime/pkg/client"
)

const (
kommanderFluxNamespace = "kommander-flux"
kommanderNamespace = "kommander"
pollInterval = 2 * time.Second
)

// Env holds the configuration and state for application specific testings.
// It contains the applications to test, the Kubernetes client, and the kind cluster.
type Env struct {
// K8sClient is a reference to the Kubernetes client
// This client is used to interact with the cluster built during the execution of the application specific testing.
K8sClient *typedclient.Client

// Cluster is a dedicated instance of a kind cluster created for running an application specific test.
Cluster *kind.Cluster
}

// Provision creates and configures the environment for application specific testings.
// It calls the provisionEnv function and assigns the returned references to the Environment fields.
// It returns an error if any of the steps fails.
func (e *Env) Provision(ctx context.Context) error {
var err error

kustomizePath, err := AbsolutePathToBase()
if err != nil {
return err
}
// delete the cluster if any error occurs
defer func() {
if err != nil {
e.Destroy(ctx)
}
}()

cluster, k8sClient, err := provisionEnv(ctx)
if err != nil {
return err
}

e.SetK8sClient(k8sClient)
e.SetCluster(cluster)

// apply base Kustomizations
err = e.ApplyKustomizations(ctx, kustomizePath, nil)
if err != nil {
return err
}

return nil
}

// Destroy deletes the provisioned cluster if it exists.
func (e *Env) Destroy(ctx context.Context) error {
if e.Cluster != nil {
return e.Cluster.Delete(ctx)
}

return nil
}

// provisionEnv creates a kind cluster, a Kubernetes client, and installs flux components on the cluster.
// It returns the created cluster and client references, or an error if any of the steps fails.
func provisionEnv(ctx context.Context) (*kind.Cluster, *typedclient.Client, error) {
cluster, err := kind.CreateCluster(ctx, "")
if err != nil {
return nil, nil, err
}

client, err := typedclient.NewClient(cluster.KubeconfigFilePath())
if err != nil {
return nil, nil, err
}

// creating the necessary namespaces
for _, ns := range []string{kommanderNamespace, kommanderFluxNamespace} {
namespaces := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}
if _, err = client.Clientset().
CoreV1().
Namespaces().
Create(ctx, &namespaces, metav1.CreateOptions{}); err != nil {
return nil, nil, err
}
}

components := []string{"source-controller", "kustomize-controller", "helm-controller"}
err = flux.Install(ctx, flux.Options{
KubeconfigArgs: genericclioptions.NewConfigFlags(true),
KubeclientOptions: new(runclient.Options),
Namespace: kommanderFluxNamespace,
Components: components,
})

ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()

err = waitForFluxDeploymentsReady(ctx, client)
if err != nil {
return nil, nil, err
}

return cluster, client, err
}

// waitForFluxDeploymentsReady discovers all flux deployments in the kommander-flux namespace and waits until they get ready
// it returns an error if the context is cancelled or expired, the deployments are missing, or not ready
func waitForFluxDeploymentsReady(ctx context.Context, typedClient *typedclient.Client) error {
selector := labels.SelectorFromSet(map[string]string{
manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue,
manifestgen.InstanceLabelKey: kommanderFluxNamespace,
})

deployments, err := typedClient.Clientset().AppsV1().
Deployments(kommanderFluxNamespace).
List(ctx, metav1.ListOptions{
LabelSelector: selector.String(),
})
if err != nil {
return err
}
if len(deployments.Items) == 0 {
return fmt.Errorf(
"no flux conrollers found in the namespace: %s with the label selector %s",
kommanderFluxNamespace, selector.String())
}

// isDeploymentReady is a condition function that checks a single deployment readiness
isDeploymentReady := func(ctx context.Context, deployment appsv1.Deployment) wait.ConditionWithContextFunc {
return func(ctx context.Context) (done bool, err error) {
deploymentObj, err := typedClient.Clientset().AppsV1().
Deployments(kommanderFluxNamespace).
Get(ctx, deployment.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
return deploymentObj.Status.ReadyReplicas == deploymentObj.Status.Replicas, nil
}
}

for _, deployment := range deployments.Items {
err = wait.PollUntilContextCancel(ctx, pollInterval, false, isDeploymentReady(ctx, deployment))
if err != nil {
return err
}
}

return nil
}

func (e *Env) SetCluster(cluster *kind.Cluster) {
e.Cluster = cluster
}

func (e *Env) SetK8sClient(k8sClient *typedclient.Client) {
e.K8sClient = k8sClient
}

// ApplyKustomizations applies the kustomizations located in the given path.
func (e *Env) ApplyKustomizations(ctx context.Context, path string, substitutions map[string]string) error {
if path == "" {
return fmt.Errorf("requirement argument: path is not specified")
}

kustomizer := kustomize.New(path, substitutions)
if err := kustomizer.Build(); err != nil {
return fmt.Errorf("could not build kustomization manifest for path: %s :%w", path, err)
}
out, err := kustomizer.Output()
if err != nil {
return fmt.Errorf("could not generate YAML manifest for path: %s :%w", path, err)
}

buf := bytes.NewBuffer(out)
dec := yaml.NewYAMLOrJSONDecoder(buf, 1<<20) // default buffer size is 1MB
obj := unstructured.Unstructured{}
if err = dec.Decode(&obj); err != nil && err != io.EOF {
return fmt.Errorf("could not decode kustomization for path: %s :%w", path, err)
}

genericClient, err := genericCLient.New(e.K8sClient.Config(), genericCLient.Options{})
if err != nil {
return fmt.Errorf("could not create the generic client for path: %s :%w", path, err)
}

err = genericClient.Patch(ctx, &obj, genericCLient.Apply, genericCLient.ForceOwnership, genericCLient.FieldOwner("k-cli"))
if err != nil {
return fmt.Errorf("could not patch the kustomization resources for path: %s :%w", path, err)
}

return nil
}

// AbsolutePathToBase returns the absolute path to common/base directory.
func AbsolutePathToBase() (string, error) {
return filepath.Abs("../../common/base")
}
14 changes: 14 additions & 0 deletions apptests/environment/environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package environment

import (
"context"
"testing"

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

func TestProvision(t *testing.T) {
env := Env{}
err := env.Provision(context.Background())
assert.NoError(t, err)
}
3 changes: 2 additions & 1 deletion apptests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/mesosphere/kommander-applications/apptests
go 1.20

require (
github.com/drone/envsubst v1.0.3
github.com/fluxcd/flux2/v2 v2.1.1
github.com/fluxcd/helm-controller/api v0.36.1
github.com/fluxcd/image-automation-controller/api v0.36.1
Expand All @@ -22,6 +23,7 @@ require (
sigs.k8s.io/controller-runtime v0.16.2
sigs.k8s.io/kind v0.20.0
sigs.k8s.io/kustomize/api v0.14.0
sigs.k8s.io/kustomize/kyaml v0.14.3
)

require (
Expand Down Expand Up @@ -110,7 +112,6 @@ require (
k8s.io/kubectl v0.28.2 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/kyaml v0.14.3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
3 changes: 3 additions & 0 deletions apptests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
Expand Down Expand Up @@ -91,6 +93,7 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU=
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
Expand Down
2 changes: 1 addition & 1 deletion apptests/kind/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func CreateCluster(ctx context.Context, name string) (*Cluster, error) {
}

// Delete deletes the cluster and the temporary kubeconfig file.
func (c *Cluster) Delete(ctx context.Context, name string) error {
func (c *Cluster) Delete(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
Expand Down
2 changes: 1 addition & 1 deletion apptests/kind/kind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ func TestCreateCluster(t *testing.T) {
assert.NotEmpty(t, cluster.KubeconfigFilePath())

// delete the cluster
err = cluster.Delete(ctx, name)
err = cluster.Delete(ctx)
assert.NoError(t, err)
}
8 changes: 4 additions & 4 deletions apptests/kustomize/kustomize.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ func (k *Kustomize) Build() error {
return nil
}

// Output returns the YAML representation of the resources map as a string.
func (k *Kustomize) Output() (string, error) {
// Output returns the YAML representation of the resources map as byte slice.
func (k *Kustomize) Output() ([]byte, error) {
yml, err := k.resources.AsYaml()
if err != nil {
return "", err
return nil, err
}
return string(yml), nil
return yml, nil
}

// newResourceFromString converts a given string to a Kubernetes resource.
Expand Down
Loading