-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
functional: add automated tests for the API wrapper
- Loading branch information
Showing
12 changed files
with
3,791 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.