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: httpbin addon #127

Merged
merged 8 commits into from
Sep 30, 2021
Merged
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
12 changes: 12 additions & 0 deletions internal/cmd/ktf/environments.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ import (
"github.com/blang/semver/v4"
"github.com/spf13/cobra"

"github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/httpbin"
"github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/istio"
"github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/kong"
"github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/metallb"
"github.com/kong/kubernetes-testing-framework/pkg/clusters/types/kind"
@@ -110,6 +112,16 @@ func configureAddons(cmd *cobra.Command, builder *environments.Builder, addons [
builder = builder.WithAddons(metallb.New())
case "kong":
builder = configureKongAddon(cmd, builder)
case "istio":
istioAddon := istio.NewBuilder().
WithGrafana().
WithJaeger().
WithKiali().
WithPrometheus().
Build()
builder = builder.WithAddons(istioAddon)
case "httpbin":
builder = builder.WithAddons(httpbin.New())
default:
invalid = append(invalid, addon)
}
25 changes: 12 additions & 13 deletions internal/utils/namespaces.go
Original file line number Diff line number Diff line change
@@ -11,15 +11,14 @@ import (
"github.com/kong/kubernetes-testing-framework/pkg/clusters"
)

// IsNamespaceReady checks for all Daemonsets, Deployment and Services
// in a given namespace to see if they are ready. It reports on readiness
// but also will provide a list of runtime objects that are being waited
// on still if the namespace is not ready yet.
// IsNamespaceAvailable checks for all Daemonsets, Deployment and Services
// in a given namespace to see if they are available (ready for minimum number
// of seconds).
//
// Keep in mind that this specifically checks on readiness, not availability
// (uptime) for speed reasons during tests.
func IsNamespaceReady(ctx context.Context, cluster clusters.Cluster, namespace string) (waitForObjects []runtime.Object, ready bool, err error) {
// check daemonsets for readiness
// If the namespace is not yet available a list of the components being waited
// on will be provided.
func IsNamespaceAvailable(ctx context.Context, cluster clusters.Cluster, namespace string) (waitForObjects []runtime.Object, available bool, err error) {
// check daemonsets for availability
var daemonsets *appsv1.DaemonSetList
daemonsets, err = cluster.Client().AppsV1().DaemonSets(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
@@ -28,12 +27,12 @@ func IsNamespaceReady(ctx context.Context, cluster clusters.Cluster, namespace s

for i := 0; i < len(daemonsets.Items); i++ {
daemonset := &(daemonsets.Items[i])
if daemonset.Status.NumberReady < 1 {
if daemonset.Status.NumberAvailable < 1 {
waitForObjects = append(waitForObjects, daemonset)
}
}

// check deployments for readiness
// check deployments for availability
var deployments *appsv1.DeploymentList
deployments, err = cluster.Client().AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
@@ -42,12 +41,12 @@ func IsNamespaceReady(ctx context.Context, cluster clusters.Cluster, namespace s

for i := 0; i < len(deployments.Items); i++ {
deployment := &(deployments.Items[i])
if deployment.Status.ReadyReplicas != *deployment.Spec.Replicas {
if deployment.Status.AvailableReplicas != *deployment.Spec.Replicas {
waitForObjects = append(waitForObjects, deployment)
}
}

// check services for readiness
// check services for availability
var services *corev1.ServiceList
services, err = cluster.Client().CoreV1().Services(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
@@ -61,6 +60,6 @@ func IsNamespaceReady(ctx context.Context, cluster clusters.Cluster, namespace s
}
}

ready = len(waitForObjects) == 0
available = len(waitForObjects) == 0
return
}
152 changes: 152 additions & 0 deletions pkg/clusters/addons/httpbin/addon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package httpbin

import (
"context"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"

"github.com/google/uuid"
"github.com/kong/kubernetes-testing-framework/internal/utils"
"github.com/kong/kubernetes-testing-framework/pkg/clusters"
"github.com/kong/kubernetes-testing-framework/pkg/utils/kubernetes/generators"
)

// -----------------------------------------------------------------------------
// HttpBin Addon
// -----------------------------------------------------------------------------

const (
// AddonName is the unique name of the HttpBin cluster.Addon
AddonName clusters.AddonName = "httpbin"

// DefaultNamespace is the namespace that the Addon components will be deployed
DefaultNamespace = "httpbin"

// Image is the container image that will be used by default.
Image = "kennethreitz/httpbin"

// DefaultPort is the port that will be used for the HttpBin endpoint
// on pods and services unless otherwise specified.
DefaultPort = 80
)

// Addon is a Kong Proxy addon which can be deployed on a clusters.Cluster.
type Addon struct {
name string
namespace string
generateNamespace bool
ingressAnnotations map[string]string
path string
}

// New produces a new clusters.Addon for Kong but uses a very opionated set of
// default configurations (see the defaults() function for more details).
// If you need to customize your Kong deployment, use the kong.Builder instead.
func New() *Addon {
return NewBuilder().Build()
}

// Namespace indicates the namespace where the HttpBin addon components are to be
// deployed and managed.
func (a *Addon) Namespace() string {
return a.namespace
}

// -----------------------------------------------------------------------------
// HttpBin Addon - Public Methods
// -----------------------------------------------------------------------------

// Path provides the URL path which the addon can be reached via Ingress.
func (a *Addon) Path() string {
return a.path
}

// -----------------------------------------------------------------------------
// HttpBin Addon - Addon Implementation
// -----------------------------------------------------------------------------

func (a *Addon) Name() clusters.AddonName {
return AddonName
}

func (a *Addon) Deploy(ctx context.Context, cluster clusters.Cluster) error {
// generate a namespace name if the caller optioned for that
if a.generateNamespace {
a.namespace = uuid.New().String()
}
namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: a.namespace}}

// ensure the namespace for this addon is available
_, err := cluster.Client().CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{})
if err != nil {
if !errors.IsAlreadyExists(err) {
return err
}
}

// determine the kubernetes cluster version so we know if we need to use a legacy ingress API
kubernetesVersion, err := cluster.Version()
if err != nil {
return err
}

// generate a container, deployment, service and ingress resource for the HttpBin addon
a.path = fmt.Sprintf("/%s", a.name)
container := generators.NewContainer(a.name, Image, DefaultPort)
deployment, service, ingress := generators.NewIngressForContainerWithDeploymentAndService(
kubernetesVersion,
container,
corev1.ServiceTypeClusterIP,
a.ingressAnnotations,
a.path,
)

// deploy the httpbin deployment
_, err = cluster.Client().AppsV1().Deployments(a.namespace).Create(ctx, deployment, metav1.CreateOptions{})
if err != nil {
if !errors.IsAlreadyExists(err) {
return err
}
}

// expose httpbin inside the cluster via service
_, err = cluster.Client().CoreV1().Services(a.namespace).Create(ctx, service, metav1.CreateOptions{})
if err != nil {
if !errors.IsAlreadyExists(err) {
return err
}
}

// expose httpbin outside the cluster via ingress
if err := clusters.DeployIngress(ctx, cluster, a.namespace, ingress); err != nil {
if !errors.IsAlreadyExists(err) {
return err
}
}

return nil
}

func (a *Addon) Delete(ctx context.Context, cluster clusters.Cluster) error {
for {
select {
case <-ctx.Done():
return fmt.Errorf("context completed before addon could be deleted: %w", ctx.Err())
default:
if err := cluster.Client().CoreV1().Namespaces().Delete(ctx, a.namespace, metav1.DeleteOptions{}); err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
}
}
}

func (a *Addon) Ready(ctx context.Context, cluster clusters.Cluster) (waitForObjects []runtime.Object, ready bool, err error) {
return utils.IsNamespaceAvailable(ctx, cluster, a.namespace)
}
77 changes: 77 additions & 0 deletions pkg/clusters/addons/httpbin/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package httpbin

// -----------------------------------------------------------------------------
// Kong Addon - Builder
// -----------------------------------------------------------------------------

// Builder is a configuration tool to generate HttpBin cluster addons.
type Builder struct {
name string
namespace string
generateNamespace bool
ingressAnnotations map[string]string
}

// NewBuilder provides a new Builder object for configuring HttpBin cluster addons.
func NewBuilder() *Builder {
return &Builder{
name: string(AddonName),
namespace: DefaultNamespace,
ingressAnnotations: make(map[string]string),
}
}

// WithName indicates the name of the Addon which is useful if the caller intends
// to deploy multiple copies of the addon into a single namespace.
func (b *Builder) WithName(name string) *Builder {
b.name = name
return b
}

// WithNamespace allows the namespace where the addon should be deployed to be
// overridden from the default.
func (b *Builder) WithNamespace(namespace string) *Builder {
b.namespace = namespace
return b
}

// WithGeneratedNamespace indicates that a uniquely named namespace should be
// used. Helpful when deploying multiple copies of HttpBin to the cluster.
func (b *Builder) WithGeneratedNamespace() *Builder {
b.generateNamespace = true
return b
}

// WithIngressAnnotations allows injecting the annotations that will be placed
// on the HttpBin Ingress resource for things like deciding the ingress.class.
// This will override values for new keys provided, but will combine with any
// other previously existing keys.
func (b *Builder) WithIngressAnnotations(anns map[string]string) *Builder {
for k, v := range anns {
b.ingressAnnotations[k] = v
}
return b
}

// Build generates a new kong cluster.Addon which can be loaded and deployed
// into a test Environment's cluster.Cluster.
func (b *Builder) Build() *Addon {
// if no ingress.class annotations are provided we'll assume Kong should
// be the ingress class.
if _, ok := b.ingressAnnotations["networking.knative.dev/ingress.class"]; !ok {
b.ingressAnnotations["networking.knative.dev/ingress.class"] = "kong"
}
if _, ok := b.ingressAnnotations["kubernetes.io/ingress.class"]; !ok {
b.ingressAnnotations["kubernetes.io/ingress.class"] = "kong"
}
if _, ok := b.ingressAnnotations["konghq.com/strip-path"]; !ok {
b.ingressAnnotations["konghq.com/strip-path"] = "true"
}

return &Addon{
name: b.name,
namespace: b.namespace,
generateNamespace: b.generateNamespace,
ingressAnnotations: b.ingressAnnotations,
}
}
2 changes: 1 addition & 1 deletion pkg/clusters/addons/istio/addon.go
Original file line number Diff line number Diff line change
@@ -202,7 +202,7 @@ func (a *Addon) Delete(ctx context.Context, cluster clusters.Cluster) error {
}

func (a *Addon) Ready(ctx context.Context, cluster clusters.Cluster) (waitForObjects []runtime.Object, ready bool, err error) {
return utils.IsNamespaceReady(ctx, cluster, Namespace)
return utils.IsNamespaceAvailable(ctx, cluster, Namespace)
}

// -----------------------------------------------------------------------------
2 changes: 1 addition & 1 deletion pkg/clusters/addons/knative/knative.go
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ func (a *addon) Ready(ctx context.Context, cluster clusters.Cluster) ([]runtime.
var waitingForObjects []runtime.Object
for i := 0; i < len(deploymentList.Items); i++ {
deployment := &(deploymentList.Items[i])
if deployment.Status.ReadyReplicas != *deployment.Spec.Replicas {
if deployment.Status.AvailableReplicas != *deployment.Spec.Replicas {
waitingForObjects = append(waitingForObjects, deployment)
}
}
2 changes: 1 addition & 1 deletion pkg/clusters/addons/kong/addon.go
Original file line number Diff line number Diff line change
@@ -293,7 +293,7 @@ func (a *Addon) Delete(ctx context.Context, cluster clusters.Cluster) error {
}

func (a *Addon) Ready(ctx context.Context, cluster clusters.Cluster) (waitForObjects []runtime.Object, ready bool, err error) {
return utils.IsNamespaceReady(ctx, cluster, a.namespace)
return utils.IsNamespaceAvailable(ctx, cluster, a.namespace)
}

// -----------------------------------------------------------------------------
2 changes: 1 addition & 1 deletion pkg/clusters/addons/metallb/metallb.go
Original file line number Diff line number Diff line change
@@ -94,7 +94,7 @@ func (a *addon) Ready(ctx context.Context, cluster clusters.Cluster) ([]runtime.
return nil, false, err
}

if deployment.Status.ReadyReplicas != *deployment.Spec.Replicas {
if deployment.Status.AvailableReplicas != *deployment.Spec.Replicas {
return []runtime.Object{deployment}, false, nil
}

2 changes: 1 addition & 1 deletion pkg/environments/implementation.go
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ func (env *environment) Ready(ctx context.Context) (waitForObjects []runtime.Obj

for i := 0; i < len(deployments.Items); i++ {
deployment := &(deployments.Items[i])
if deployment.Status.ReadyReplicas != *deployment.Spec.Replicas {
if deployment.Status.AvailableReplicas != *deployment.Spec.Replicas {
waitForObjects = append(waitForObjects, deployment)
}
}
19 changes: 19 additions & 0 deletions pkg/utils/kubernetes/generators/ingress.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package generators

import (
"github.com/blang/semver/v4"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
netv1beta1 "k8s.io/api/networking/v1beta1"
@@ -88,3 +89,21 @@ func NewLegacyIngressForService(path string, annotations map[string]string, s *c
},
}
}

// NewIngressForContainerWithDeploymentAndService generates a Deployment, Service, and Ingress given a container.
// The idea is that if you have a container that provides an HTTP endpoint, this function can be used to generate
// everything you need to deploy to the cluster to start accessing that HTTP server from outside the cluster via Ingress.
// This effectively just compiles together multiple generators for convenience, look at the individual generators
// used here if you're looking for something more granular.
func NewIngressForContainerWithDeploymentAndService(
kubernetesVersion semver.Version,
c corev1.Container,
serviceType corev1.ServiceType,
annotations map[string]string,
path string,
) (*appsv1.Deployment, *corev1.Service, runtime.Object) {
deployment := NewDeploymentForContainer(c)
service := NewServiceForDeployment(deployment, serviceType)
ingress := NewIngressForServiceWithClusterVersion(kubernetesVersion, path, annotations, service)
return deployment, service, ingress
}
Loading