Skip to content

Commit

Permalink
feat: environment provisioning (#1667)
Browse files Browse the repository at this point in the history
* refactor: remove redundant code, return byte slice insted of strings

* chore: fix tests

* feat: add more assertions for flux installation

* refactor: improve document
  • Loading branch information
Azhovan authored Oct 13, 2023
1 parent 36cb0ca commit d7d7043
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 6 deletions.
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 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")
}
45 changes: 45 additions & 0 deletions apptests/environment/environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package environment

import (
"context"
"testing"

"github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)

func TestProvision(t *testing.T) {
env := Env{}
ctx := context.Background()

err := env.Provision(ctx)
assert.NoError(t, err)
defer env.Destroy(ctx)

selector := labels.SelectorFromSet(map[string]string{
manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue,
manifestgen.InstanceLabelKey: kommanderFluxNamespace,
})

// get flux deployments
deployments, err := env.K8sClient.Clientset().AppsV1().
Deployments(kommanderFluxNamespace).
List(ctx, metav1.ListOptions{
LabelSelector: selector.String(),
})
assert.NoError(t, err)

// assert that there are 3 deployments(helm-controller, kustomize-controller, source-controller)
assert.Equal(t, 3, len(deployments.Items))

// assert that flux deployments are ready
for _, deployment := range deployments.Items {
deploymentObj, err := env.K8sClient.Clientset().AppsV1().Deployments(kommanderFluxNamespace).
Get(ctx, deployment.Name, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, deploymentObj.Status.Replicas, deploymentObj.Status.ReadyReplicas)
}

}
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)
}
6 changes: 3 additions & 3 deletions apptests/kustomize/kustomize.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ func (k *Kustomize) Build() error {
}

// Output returns the YAML representation of the resources map as a string.
func (k *Kustomize) Output() (string, error) {
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

0 comments on commit d7d7043

Please sign in to comment.