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: environment provisioning #1667

Merged
merged 4 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
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")
}
46 changes: 46 additions & 0 deletions apptests/environment/environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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)

// assert that flux components are ready
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
Loading