Skip to content

Commit

Permalink
functional: add automated tests for the API wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
pandatix committed Nov 26, 2023
1 parent 9e98795 commit ecd556a
Show file tree
Hide file tree
Showing 12 changed files with 3,791 additions and 0 deletions.
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.PHONY: tests
tests:
@echo "--- Unitary tests ---"
go test ./api -run=^Test_U_ -json | tee -a gotest.json

@echo "--- Functional tests ---"
go test ./deploy/integration -run=^Test_F_ -json -coverpkg "github.com/ctfer-io/go-ctfd/api" -coverprofile=functional.out | tee -a gotest.json

.PHONY: clean
clean:
rm gotest.json unitary.out functional.out
3 changes: 3 additions & 0 deletions deploy/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: go-ctfd-iac
runtime: go
description: Infrastructure to locally test the go-ctfd API client
160 changes: 160 additions & 0 deletions deploy/components/ctfd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package components

import (
"fmt"

appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1"
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

type (
CTFd struct {
rd *Redis

dep *appsv1.Deployment
svc *corev1.Service

Port pulumi.IntOutput
}

CTFdArgs struct {
Namespace pulumi.StringInput
}
)

// NewCTFd deploys a minimal CTFd configuration with just enough
// Kubernetes infrastructure to test the Go API wrapper.
//
// WARNING: Do not use this component for production purposes.
func NewCTFd(ctx *pulumi.Context, args *CTFdArgs, opts ...pulumi.ResourceOption) (*CTFd, error) {
if args == nil {
args = &CTFdArgs{}
}

ctfd := &CTFd{}

if err := ctfd.provision(ctx, args, opts...); err != nil {
return nil, err
}

ctfd.outputs(ctx)
return ctfd, nil
}

func (ctfd *CTFd) provision(ctx *pulumi.Context, args *CTFdArgs, opts ...pulumi.ResourceOption) (err error) {
uid := randName()

// Dependencies
ctfd.rd, err = NewRedis(ctx, &RedisArgs{
Namespace: args.Namespace,
}, opts...)
if err != nil {
return
}

// Uniquely identify the resources with labels
labels := pulumi.ToStringMap(map[string]string{
"app": "ctfd",
"repository": "github.com_ctfer-io_go-ctfd",
})

// => Deployment
ctfd.dep, err = appsv1.NewDeployment(ctx, "ctfd-dep-"+uid, &appsv1.DeploymentArgs{
Metadata: metav1.ObjectMetaArgs{
Name: pulumi.String("ctfd-dep-" + uid),
Namespace: args.Namespace,
Labels: labels,
},
Spec: appsv1.DeploymentSpecArgs{
Selector: metav1.LabelSelectorArgs{
MatchLabels: labels,
},
Replicas: pulumi.Int(1),
Template: &corev1.PodTemplateSpecArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: args.Namespace,
Labels: labels,
},
Spec: &corev1.PodSpecArgs{
InitContainers: corev1.ContainerArray{
corev1.ContainerArgs{
Name: pulumi.String("wait-for-redis"),
// TODO rebuild image or replace
Image: pulumi.String("goodsmileduck/redis-cli:latest"),
Args: pulumi.StringArray{
pulumi.String("sh"), pulumi.String("-c"),
pulumi.All(ctfd.rd.svc.Metadata, ctfd.rd.svc.Spec).ApplyT(func(args []any) string {
meta := args[0].(metav1.ObjectMeta)
spec := args[1].(corev1.ServiceSpec)

return fmt.Sprintf("until redis-cli -h %s -p %d get hello; do echo \"Sleeping a bit\"; sleep 1; done; echo \"ready!\";", *meta.Name, spec.Ports[0].Port)
}).(pulumi.StringOutput),
},
},
},
Containers: corev1.ContainerArray{
corev1.ContainerArgs{
Name: pulumi.String("ctfd"),
Image: pulumi.String("ctfd/ctfd:3.6.0"),
Ports: corev1.ContainerPortArray{
corev1.ContainerPortArgs{
ContainerPort: pulumi.Int(8000),
},
},
Env: corev1.EnvVarArray{
corev1.EnvVarArgs{
Name: pulumi.String("REDIS_URL"),
Value: ctfd.rd.URL,
},
},
ReadinessProbe: corev1.ProbeArgs{
HttpGet: corev1.HTTPGetActionArgs{
Path: pulumi.String("/setup"),
Port: pulumi.Int(8000),
},
},
},
},
},
},
},
}, opts...)
if err != nil {
return
}

ctfd.svc, err = corev1.NewService(ctx, "ctfd-svc-"+uid, &corev1.ServiceArgs{
Metadata: metav1.ObjectMetaArgs{
Labels: labels,
Name: pulumi.String("ctfd-svc-" + uid),
Namespace: args.Namespace,
},
Spec: &corev1.ServiceSpecArgs{
Selector: labels,
Type: pulumi.String("NodePort"),
Ports: corev1.ServicePortArray{
corev1.ServicePortArgs{
TargetPort: pulumi.Int(8000),
Port: pulumi.Int(8000),
Name: pulumi.String("web"),
},
},
},
}, opts...)
if err != nil {
return
}

return nil
}

func (ctfd *CTFd) outputs(ctx *pulumi.Context) {
ctfd.Port = ctfd.svc.Spec.ApplyT(func(spec corev1.ServiceSpec) int {
if spec.ClusterIP == nil {
return 0
}
return *spec.Ports[0].NodePort
}).(pulumi.IntOutput)
}
12 changes: 12 additions & 0 deletions deploy/components/rand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package components

import (
"crypto/rand"
"encoding/hex"
)

func randName() string {
b := make([]byte, 4)
rand.Read(b)
return hex.EncodeToString(b)
}
194 changes: 194 additions & 0 deletions deploy/components/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package components

import (
"fmt"

appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1"
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
"github.com/pulumi/pulumi-random/sdk/v4/go/random"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

type (
Redis struct {
rand *random.RandomPassword
sec *corev1.Secret
sts *appsv1.StatefulSet
svc *corev1.Service

URL pulumi.StringOutput
}

RedisArgs struct {
Namespace pulumi.StringInput
}
)

func NewRedis(ctx *pulumi.Context, args *RedisArgs, opts ...pulumi.ResourceOption) (*Redis, error) {
if args == nil {
args = &RedisArgs{}
}

rd := &Redis{}

if err := rd.provision(ctx, args, opts...); err != nil {
return nil, err
}

rd.outputs()
return rd, nil
}

func (rd *Redis) provision(ctx *pulumi.Context, args *RedisArgs, opts ...pulumi.ResourceOption) (err error) {
uid := randName()

// Uniquely identify the resources with labels
labels := pulumi.ToStringMap(map[string]string{
"app": "redis",
"repository": "github.com_ctfer-io_go-ctfd",
})

// => Credentials
rd.rand, err = random.NewRandomPassword(ctx, "redis-pass-"+uid, &random.RandomPasswordArgs{
Length: pulumi.Int(64),
Special: pulumi.BoolPtr(false),
}, opts...)
if err != nil {
return err
}

// => Service
rd.svc, err = corev1.NewService(ctx, "redis-svc-"+uid, &corev1.ServiceArgs{
Metadata: metav1.ObjectMetaArgs{
Name: pulumi.String("redis-svc-" + uid),
Labels: labels,
Namespace: args.Namespace,
},
Spec: corev1.ServiceSpecArgs{
Ports: corev1.ServicePortArray{
corev1.ServicePortArgs{
Port: pulumi.Int(6379),
TargetPort: pulumi.Int(6379),
Name: pulumi.String("client"),
},
},
// Headless, for DNS purposes
ClusterIP: pulumi.String("None"),
Selector: labels,
},
}, opts...)
if err != nil {
return err
}

// /!\ Register output URL /!\
rd.URL = pulumi.All(rd.rand.Result, rd.svc.Metadata, rd.svc.Spec).ApplyT(func(args []any) string {
rand := args[0].(string)
meta := args[1].(metav1.ObjectMeta)
spec := args[2].(corev1.ServiceSpec)

return fmt.Sprintf("redis://default:%s@%s:%d", rand, *meta.Name, spec.Ports[0].Port)
}).(pulumi.StringOutput)

// => Secret
rd.sec, err = corev1.NewSecret(ctx, "redis-secret-"+uid, &corev1.SecretArgs{
Metadata: metav1.ObjectMetaArgs{
Labels: labels,
Name: pulumi.String("redis-secret-" + uid),
Namespace: args.Namespace,
},
Type: pulumi.String("Opaque"),
StringData: pulumi.ToStringMapOutput(map[string]pulumi.StringOutput{
"redis-password": rd.rand.Result,
"redis-url": rd.URL,
}),
}, opts...)
if err != nil {
return err
}

// => StatefulSet
rd.sts, err = appsv1.NewStatefulSet(ctx, "redis-sts-"+uid, &appsv1.StatefulSetArgs{
Metadata: metav1.ObjectMetaArgs{
Name: pulumi.String("redis-sts-" + uid),
Labels: labels,
Namespace: args.Namespace,
},
Spec: appsv1.StatefulSetSpecArgs{
ServiceName: rd.svc.Metadata.Name().Elem(),
Replicas: pulumi.Int(1),
Selector: metav1.LabelSelectorArgs{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpecArgs{
Metadata: metav1.ObjectMetaArgs{
Namespace: args.Namespace,
Labels: labels,
},
Spec: corev1.PodSpecArgs{
Containers: corev1.ContainerArray{
corev1.ContainerArgs{
Name: pulumi.String("redis"),
Image: pulumi.String("redis:7.0.10"),
Ports: corev1.ContainerPortArray{
corev1.ContainerPortArgs{
ContainerPort: pulumi.Int(6379),
Name: pulumi.String("client"),
},
},
Args: pulumi.ToStringArray([]string{
"--requirepass",
"$(REDIS_PASSWORD)",
}),
Env: corev1.EnvVarArray{
corev1.EnvVarArgs{
Name: pulumi.String("REDIS_PASSWORD"),
ValueFrom: corev1.EnvVarSourceArgs{
SecretKeyRef: corev1.SecretKeySelectorArgs{
Name: rd.sec.Metadata.Name(),
Key: pulumi.String("redis-password"),
},
},
},
},
VolumeMounts: corev1.VolumeMountArray{
corev1.VolumeMountArgs{
Name: pulumi.String("data"),
MountPath: pulumi.String("/data"),
ReadOnly: pulumi.Bool(false),
},
},
},
},
},
},
VolumeClaimTemplates: corev1.PersistentVolumeClaimTypeArray{
corev1.PersistentVolumeClaimTypeArgs{
Metadata: metav1.ObjectMetaArgs{
Name: pulumi.String("data"),
},
Spec: corev1.PersistentVolumeClaimSpecArgs{
AccessModes: pulumi.ToStringArray([]string{
"ReadWriteOnce",
}),
Resources: corev1.ResourceRequirementsArgs{
Requests: pulumi.ToStringMap(map[string]string{
"storage": "1Gi",
}),
},
},
},
},
},
}, opts...)
if err != nil {
return err
}

return nil
}

func (rd *Redis) outputs() {
// rd.URL has already been registered during provisionning
}
Loading

0 comments on commit ecd556a

Please sign in to comment.