pulumi-library/pkg/application/generic.go

368 lines
12 KiB
Go

package application
import (
v1 "antoine-roux.tk/projects/go/pulumi-library/crds/kubernetes/certmanager/v1"
dnsv1alpha1 "antoine-roux.tk/projects/go/pulumi-library/crds/kubernetes/externaldns/v1alpha1"
traefikv1alpha1 "antoine-roux.tk/projects/go/pulumi-library/crds/kubernetes/traefik/v1alpha1"
"errors"
"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"
netv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/networking/v1"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
const (
loadBalancerAddressName = "internal-lb.localdomain"
defaultRecordTTL = 180
)
type Configuration struct {
Name string
Namespace string
Image string
Dns *string
Replicas *int
Env map[string]string
AllowAllOrigin bool
}
type internalConfiguration struct {
Name string
Namespace string
Image string
Dns string
Replicas int
Env map[string]string
ShouldCreateDns bool
ShouldCreateCertificate bool
ShouldCreateIngress bool
ResponseHeaders *traefikv1alpha1.MiddlewareSpecHeadersArgs
}
type Application struct {
pulumi.ResourceState
DeploymentName pulumi.StringOutput `pulumi:"deployment"`
}
func NewApplication(ctx *pulumi.Context, publicConfiguration *Configuration) (*Application, error) {
if publicConfiguration.Name != "" && publicConfiguration.Image != "" {
configuration := &internalConfiguration{
Name: publicConfiguration.Name,
Namespace: publicConfiguration.Namespace,
Image: publicConfiguration.Image,
Env: publicConfiguration.Env,
}
if publicConfiguration.Replicas != nil {
configuration.Replicas = *publicConfiguration.Replicas
} else {
configuration.Replicas = 1
}
if publicConfiguration.Dns != nil {
configuration.Dns = *publicConfiguration.Dns
configuration.ShouldCreateIngress = true
configuration.ShouldCreateDns = true
configuration.ShouldCreateCertificate = true
}
if publicConfiguration.AllowAllOrigin {
configuration.ResponseHeaders = &traefikv1alpha1.MiddlewareSpecHeadersArgs{
AccessControlAllowOriginList: toPulumiStringArray([]string{"*"}),
}
}
return createResources(ctx, configuration)
} else {
return nil, errors.New("missing required value Name or Image during generic application construction")
}
}
func createResources(ctx *pulumi.Context, configuration *internalConfiguration, opts ...pulumi.ResourceOption) (*Application, error) {
application := &Application{}
err := ctx.RegisterComponentResource("pkg:application:Application", configuration.Name, application)
if err != nil {
return nil, err
}
namespace, err := createNamespace(ctx, configuration)
if err != nil {
return nil, err
}
appLabels := pulumi.StringMap{
"app.kubernetes.io/name": pulumi.String(configuration.Name),
}
deployment, err := createDeployment(ctx, configuration, namespace, appLabels, application)
if err != nil {
return nil, err
}
application.DeploymentName = deployment.Metadata.Name().Elem()
service, err := createService(ctx, configuration, namespace, appLabels, application)
if err != nil {
return nil, err
}
certificate, err := createCertificate(ctx, configuration, namespace, application)
if err != nil {
return nil, err
}
headerMiddleware, err := createMiddlewareAddResponseHeader(ctx, configuration, namespace, application)
if err != nil {
return nil, err
}
_, err = createIngress(ctx, configuration, namespace, certificate, service, application, headerMiddleware)
if err != nil {
return nil, err
}
_, err = createDNSRecord(ctx, configuration, namespace, application)
if err != nil {
return nil, err
}
err = ctx.RegisterResourceOutputs(application, pulumi.Map{
"deployment": deployment.Metadata.Name(),
})
if err != nil {
return nil, err
}
return application, nil
}
func toPulumiStringArray(values []string) pulumi.StringArray {
array := pulumi.StringArray{}
for _, value := range values {
array = append(array, pulumi.String(value))
}
return array
}
func createMiddlewareAddResponseHeader(
ctx *pulumi.Context,
configuration *internalConfiguration,
namespace *corev1.Namespace,
application *Application,
) (*traefikv1alpha1.Middleware, error) {
middlewareName := fmt.Sprintf("%s-response-header-middleware", configuration.Name)
return traefikv1alpha1.NewMiddleware(ctx, middlewareName, &traefikv1alpha1.MiddlewareArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: namespace.Metadata.Name(),
Labels: pulumi.StringMap{
"app.kubernetes.io/part-of": pulumi.String(configuration.Name),
"app.kubernetes.io/managed-by": pulumi.String("pulumi"),
},
},
Spec: &traefikv1alpha1.MiddlewareSpecArgs{
Headers: configuration.ResponseHeaders,
},
}, pulumi.Parent(application))
}
func createIngress(ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, certificate *v1.Certificate, service *corev1.Service, application *Application, responseHeaderMiddleware *traefikv1alpha1.Middleware) (*netv1.Ingress, error) {
if configuration.ShouldCreateIngress {
host := pulumi.String(configuration.Dns)
middlewares := pulumi.All(namespace.Metadata.Name().Elem(), responseHeaderMiddleware.Metadata.Name().Elem()).ApplyT(func(args []interface{}) string {
return fmt.Sprintf("kube-ingress-gzip-compress@kubernetescrd,%s-%s@kubernetescrd",
args[0], args[1],
)
}).(pulumi.StringOutput)
ingressAnnotations := pulumi.StringMap{
"traefik.ingress.kubernetes.io/router.middlewares": middlewares,
"traefik.ingress.kubernetes.io/router.entrypoints": pulumi.String("websecure"),
}
return netv1.NewIngress(ctx, configuration.Name, &netv1.IngressArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: namespace.Metadata.Name(),
Labels: pulumi.StringMap{
"app.kubernetes.io/part-of": pulumi.String(configuration.Name),
"app.kubernetes.io/managed-by": pulumi.String("pulumi"),
},
Annotations: ingressAnnotations,
},
Spec: &netv1.IngressSpecArgs{
IngressClassName: nil,
Rules: &netv1.IngressRuleArray{
netv1.IngressRuleArgs{
Host: host,
Http: &netv1.HTTPIngressRuleValueArgs{
Paths: &netv1.HTTPIngressPathArray{
netv1.HTTPIngressPathArgs{
Path: pulumi.String("/"),
PathType: pulumi.String("Prefix"),
Backend: &netv1.IngressBackendArgs{
Service: &netv1.IngressServiceBackendArgs{
Name: service.Metadata.Name().Elem(),
Port: &netv1.ServiceBackendPortArgs{
Name: pulumi.String("exposed-port"),
},
},
},
},
},
},
},
},
Tls: &netv1.IngressTLSArray{
netv1.IngressTLSArgs{
Hosts: pulumi.StringArray{host},
SecretName: certificate.Spec.SecretName(),
},
},
},
}, pulumi.Parent(application))
}
return nil, nil
}
func createCertificate(
ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, application *Application,
) (*v1.Certificate, error) {
if configuration.ShouldCreateCertificate {
return v1.NewCertificate(ctx, configuration.Name, &v1.CertificateArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: namespace.Metadata.Name(),
Labels: pulumi.StringMap{
"app.kubernetes.io/part-of": pulumi.String(configuration.Name),
"app.kubernetes.io/managed-by": pulumi.String("pulumi"),
},
},
Spec: &v1.CertificateSpecArgs{
SecretName: pulumi.String(fmt.Sprintf("%s-certificate", configuration.Name)),
DnsNames: pulumi.StringArray{
pulumi.String(configuration.Dns),
},
IssuerRef: &v1.CertificateSpecIssuerRefArgs{
Name: pulumi.String("localdomain-issuer"),
Kind: pulumi.String("ClusterIssuer"),
Group: pulumi.String("cfssl-issuer.wikimedia.org"),
},
},
}, pulumi.Parent(application))
}
return nil, nil
}
func createDNSRecord(ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, application *Application) (*dnsv1alpha1.DNSEndpoint, error) {
if configuration.ShouldCreateDns {
return dnsv1alpha1.NewDNSEndpoint(ctx, fmt.Sprintf("%s-record", configuration.Name), &dnsv1alpha1.DNSEndpointArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: namespace.Metadata.Name(),
Labels: pulumi.StringMap{
"app.kubernetes.io/part-of": pulumi.String(configuration.Name),
"app.kubernetes.io/managed-by": pulumi.String("pulumi"),
},
},
Spec: &dnsv1alpha1.DNSEndpointSpecArgs{
Endpoints: &dnsv1alpha1.DNSEndpointSpecEndpointsArray{
&dnsv1alpha1.DNSEndpointSpecEndpointsArgs{
DnsName: pulumi.String(configuration.Dns),
RecordTTL: pulumi.Int(defaultRecordTTL),
RecordType: pulumi.String("CNAME"),
Targets: pulumi.StringArray{
pulumi.String(loadBalancerAddressName),
},
},
},
},
}, pulumi.Parent(application))
}
return nil, nil
}
func createService(
ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, appLabels pulumi.StringMap, application *Application,
) (*corev1.Service, error) {
return corev1.NewService(ctx, fmt.Sprintf("%s-service", configuration.Name), &corev1.ServiceArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: namespace.Metadata.Name(),
Labels: pulumi.StringMap{
"app.kubernetes.io/part-of": pulumi.String(configuration.Name),
"app.kubernetes.io/managed-by": pulumi.String("pulumi"),
},
},
Spec: &corev1.ServiceSpecArgs{
Type: pulumi.String("ClusterIP"),
Selector: appLabels,
Ports: corev1.ServicePortArray{
corev1.ServicePortArgs{
Name: pulumi.String("exposed-port"),
Port: pulumi.Int(8090),
TargetPort: pulumi.String("http"),
Protocol: pulumi.String("TCP"),
},
},
},
}, pulumi.Parent(application))
}
func createDeployment(
ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, appLabels pulumi.StringMap, application *Application,
) (*appsv1.Deployment, error) {
env := corev1.EnvVarArray{}
for key, value := range configuration.Env {
env = append(env, &corev1.EnvVarArgs{
Name: pulumi.String(key),
Value: pulumi.String(value),
})
}
return appsv1.NewDeployment(ctx, fmt.Sprintf("%s-deployment", configuration.Name), &appsv1.DeploymentArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: namespace.Metadata.Name(),
Labels: pulumi.StringMap{
"app.kubernetes.io/part-of": pulumi.String(configuration.Name),
"app.kubernetes.io/managed-by": pulumi.String("pulumi"),
},
},
Spec: appsv1.DeploymentSpecArgs{
Selector: &metav1.LabelSelectorArgs{
MatchLabels: appLabels,
},
Replicas: pulumi.Int(configuration.Replicas),
Template: &corev1.PodTemplateSpecArgs{
Metadata: &metav1.ObjectMetaArgs{
Labels: appLabels,
},
Spec: &corev1.PodSpecArgs{
Containers: corev1.ContainerArray{
corev1.ContainerArgs{
Name: pulumi.String(configuration.Name),
Image: pulumi.String(configuration.Image),
Ports: corev1.ContainerPortArray{
corev1.ContainerPortArgs{
Name: pulumi.String("http"),
ContainerPort: pulumi.Int(80),
Protocol: pulumi.String("TCP"),
},
},
Env: env,
},
},
},
},
},
}, pulumi.Parent(application))
}
func createNamespace(ctx *pulumi.Context, configuration *internalConfiguration) (*corev1.Namespace, error) {
return corev1.NewNamespace(ctx, fmt.Sprintf("%s-namespace", configuration.Name), &corev1.NamespaceArgs{
Metadata: &metav1.ObjectMetaArgs{
Name: pulumi.String(configuration.Namespace),
Labels: pulumi.StringMap{
"app.kubernetes.io/managed-by": pulumi.String("pulumi"),
},
},
})
}