feature: support creating application with multiple deployment, service and expose it with multiple path in ingress configuration

This commit is contained in:
RouxAntoine 2023-10-05 00:38:19 +02:00
parent 53af9043fe
commit 2e7da70a48
Signed by: antoine
GPG Key ID: 098FB66FC0475E70
2 changed files with 125 additions and 59 deletions

12
main.go
View File

@ -11,11 +11,19 @@ func main() {
nginxApplication, err := application.NewApplication(ctx, &application.Configuration{ nginxApplication, err := application.NewApplication(ctx, &application.Configuration{
Name: "nginx", Name: "nginx",
Namespace: "pulumi-test", Namespace: "pulumi-test",
Image: "nginx", Images: []application.ImagesConfiguration{
{
Image: "nginx",
},
{
Image: "nginx",
Path: "/api",
},
},
Env: map[string]string{ Env: map[string]string{
"version": "1.0.0", "version": "1.0.0",
}, },
Dns: pulumi.StringRef("pulumi-test-nginx.localdomain"), Dns: pulumi.StringRef("pulumi-test-nginx.localdomain"),
AllowAllOrigin: true, AllowAllOrigin: true,
}) })
if err != nil { if err != nil {

View File

@ -11,6 +11,7 @@ import (
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/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" netv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/networking/v1"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"slices"
) )
const ( const (
@ -18,20 +19,25 @@ const (
defaultRecordTTL = 180 defaultRecordTTL = 180
) )
type ImagesConfiguration struct {
Image string
Path string
}
type Configuration struct { type Configuration struct {
Name string Name string
Namespace string Namespace string
Image string Images []ImagesConfiguration
Dns *string Dns *string
Replicas *int Replicas *int
Env map[string]string Env map[string]string
AllowAllOrigin bool AllowAllOrigin bool
} }
type internalConfiguration struct { type configuration struct {
Name string Name string
Namespace string Namespace string
Image string Images []ImagesConfiguration
Dns string Dns string
Replicas int Replicas int
Env map[string]string Env map[string]string
@ -42,6 +48,22 @@ type internalConfiguration struct {
ResponseHeaders *traefikv1alpha1.MiddlewareSpecHeadersArgs ResponseHeaders *traefikv1alpha1.MiddlewareSpecHeadersArgs
} }
type deploymentConfiguration struct {
Env map[string]string
Name string
Replicas int
ImageReference ImagesConfiguration
}
type serviceConfiguration struct {
Name string
}
type ingressConfiguration struct {
Service *corev1.Service
ImageReference ImagesConfiguration
}
type Application struct { type Application struct {
pulumi.ResourceState pulumi.ResourceState
@ -50,40 +72,55 @@ type Application struct {
func NewApplication(ctx *pulumi.Context, publicConfiguration *Configuration) (*Application, error) { func NewApplication(ctx *pulumi.Context, publicConfiguration *Configuration) (*Application, error) {
if publicConfiguration.Name != "" && publicConfiguration.Image != "" { if publicConfiguration.Name != "" && len(publicConfiguration.Images) > 0 {
configuration := &internalConfiguration{ localConfiguration := &configuration{
Name: publicConfiguration.Name, Name: publicConfiguration.Name,
Namespace: publicConfiguration.Namespace, Namespace: publicConfiguration.Namespace,
Image: publicConfiguration.Image,
Env: publicConfiguration.Env, Env: publicConfiguration.Env,
} }
var preventDuplicatePath []string
for _, publicImageConfiguration := range publicConfiguration.Images {
localImagesConfiguration := ImagesConfiguration{
Image: publicImageConfiguration.Image,
Path: publicImageConfiguration.Path,
}
if publicImageConfiguration.Path == "" {
localImagesConfiguration.Path = "/"
}
if slices.Contains(preventDuplicatePath, localImagesConfiguration.Path) {
return nil, errors.New("duplicate path in ingress configuration")
}
localConfiguration.Images = append(localConfiguration.Images, localImagesConfiguration)
preventDuplicatePath = append(preventDuplicatePath, localImagesConfiguration.Path)
}
if publicConfiguration.Replicas != nil { if publicConfiguration.Replicas != nil {
configuration.Replicas = *publicConfiguration.Replicas localConfiguration.Replicas = *publicConfiguration.Replicas
} else { } else {
configuration.Replicas = 1 localConfiguration.Replicas = 1
} }
if publicConfiguration.Dns != nil { if publicConfiguration.Dns != nil {
configuration.Dns = *publicConfiguration.Dns localConfiguration.Dns = *publicConfiguration.Dns
configuration.ShouldCreateIngress = true localConfiguration.ShouldCreateIngress = true
configuration.ShouldCreateDns = true localConfiguration.ShouldCreateDns = true
configuration.ShouldCreateCertificate = true localConfiguration.ShouldCreateCertificate = true
} }
if publicConfiguration.AllowAllOrigin { if publicConfiguration.AllowAllOrigin {
configuration.ResponseHeaders = &traefikv1alpha1.MiddlewareSpecHeadersArgs{ localConfiguration.ResponseHeaders = &traefikv1alpha1.MiddlewareSpecHeadersArgs{
AccessControlAllowOriginList: toPulumiStringArray([]string{"*"}), AccessControlAllowOriginList: toPulumiStringArray([]string{"*"}),
} }
} }
return createResources(ctx, configuration) return createResources(ctx, localConfiguration)
} else { } else {
return nil, errors.New("missing required value Name or Image during generic application construction") 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) { func createResources(ctx *pulumi.Context, configuration *configuration, opts ...pulumi.ResourceOption) (*Application, error) {
application := &Application{} application := &Application{}
err := ctx.RegisterComponentResource("pkg:application:Application", configuration.Name, application) err := ctx.RegisterComponentResource("pkg:application:Application", configuration.Name, application)
if err != nil { if err != nil {
@ -95,18 +132,37 @@ func createResources(ctx *pulumi.Context, configuration *internalConfiguration,
return nil, err return nil, err
} }
appLabels := pulumi.StringMap{ var ingressesParameter []ingressConfiguration
"app.kubernetes.io/name": pulumi.String(configuration.Name), var deployments []*appsv1.Deployment
} for index, image := range configuration.Images {
deployment, err := createDeployment(ctx, configuration, namespace, appLabels, application) appLabels := pulumi.StringMap{
if err != nil { "app.kubernetes.io/name": pulumi.String(configuration.Name),
return nil, err }
}
application.DeploymentName = deployment.Metadata.Name().Elem()
service, err := createService(ctx, configuration, namespace, appLabels, application) deploymentParameter := &deploymentConfiguration{
if err != nil { Name: fmt.Sprintf("%s-%d", configuration.Name, index),
return nil, err Env: configuration.Env,
Replicas: configuration.Replicas,
ImageReference: image,
}
deployment, err := createDeployment(ctx, deploymentParameter, namespace, appLabels, application)
if err != nil {
return nil, err
}
application.DeploymentName = deployment.Metadata.Name().Elem()
serviceParameter := serviceConfiguration{
Name: fmt.Sprintf("%s-%d", configuration.Name, index),
}
service, err := createService(ctx, serviceParameter, namespace, appLabels, application)
if err != nil {
return nil, err
}
ingressesParameter = append(ingressesParameter, ingressConfiguration{
Service: service,
ImageReference: image,
})
deployments = append(deployments, deployment)
} }
certificate, err := createCertificate(ctx, configuration, namespace, application) certificate, err := createCertificate(ctx, configuration, namespace, application)
@ -119,7 +175,7 @@ func createResources(ctx *pulumi.Context, configuration *internalConfiguration,
return nil, err return nil, err
} }
_, err = createIngress(ctx, configuration, namespace, certificate, service, application, headerMiddleware) _, err = createIngress(ctx, configuration, namespace, certificate, ingressesParameter, application, headerMiddleware)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -129,9 +185,12 @@ func createResources(ctx *pulumi.Context, configuration *internalConfiguration,
return nil, err return nil, err
} }
err = ctx.RegisterResourceOutputs(application, pulumi.Map{ outs := pulumi.Map{}
"deployment": deployment.Metadata.Name(), for i, deployment := range deployments {
}) outs[fmt.Sprintf("deployment-%d", i)] = deployment.Metadata.Name()
}
err = ctx.RegisterResourceOutputs(application, outs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -149,7 +208,7 @@ func toPulumiStringArray(values []string) pulumi.StringArray {
func createMiddlewareAddResponseHeader( func createMiddlewareAddResponseHeader(
ctx *pulumi.Context, ctx *pulumi.Context,
configuration *internalConfiguration, configuration *configuration,
namespace *corev1.Namespace, namespace *corev1.Namespace,
application *Application, application *Application,
) (*traefikv1alpha1.Middleware, error) { ) (*traefikv1alpha1.Middleware, error) {
@ -168,19 +227,33 @@ func createMiddlewareAddResponseHeader(
}, pulumi.Parent(application)) }, 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) { func createIngress(ctx *pulumi.Context, configuration *configuration, namespace *corev1.Namespace, certificate *v1.Certificate, servicesConfiguration []ingressConfiguration, application *Application, responseHeaderMiddleware *traefikv1alpha1.Middleware) (*netv1.Ingress, error) {
if configuration.ShouldCreateIngress { if configuration.ShouldCreateIngress {
host := pulumi.String(configuration.Dns) host := pulumi.String(configuration.Dns)
middlewares := pulumi.All(namespace.Metadata.Name().Elem(), responseHeaderMiddleware.Metadata.Name().Elem()).ApplyT(func(args []interface{}) string { 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", return fmt.Sprintf("kube-ingress-gzip-compress@kubernetescrd,%s-%s@kubernetescrd", args[0], args[1])
args[0], args[1],
)
}).(pulumi.StringOutput) }).(pulumi.StringOutput)
ingressAnnotations := pulumi.StringMap{ ingressAnnotations := pulumi.StringMap{
"traefik.ingress.kubernetes.io/router.middlewares": middlewares, "traefik.ingress.kubernetes.io/router.middlewares": middlewares,
"traefik.ingress.kubernetes.io/router.entrypoints": pulumi.String("websecure"), "traefik.ingress.kubernetes.io/router.entrypoints": pulumi.String("websecure"),
} }
var ingressPaths netv1.HTTPIngressPathArray
for _, service := range servicesConfiguration {
ingressPaths = append(ingressPaths, netv1.HTTPIngressPathArgs{
Path: pulumi.String(service.ImageReference.Path),
PathType: pulumi.String("Prefix"),
Backend: &netv1.IngressBackendArgs{
Service: &netv1.IngressServiceBackendArgs{
Name: service.Service.Metadata.Name().Elem(),
Port: &netv1.ServiceBackendPortArgs{
Name: pulumi.String("exposed-port"),
},
},
},
})
}
return netv1.NewIngress(ctx, configuration.Name, &netv1.IngressArgs{ return netv1.NewIngress(ctx, configuration.Name, &netv1.IngressArgs{
Metadata: &metav1.ObjectMetaArgs{ Metadata: &metav1.ObjectMetaArgs{
Namespace: namespace.Metadata.Name(), Namespace: namespace.Metadata.Name(),
@ -196,20 +269,7 @@ func createIngress(ctx *pulumi.Context, configuration *internalConfiguration, na
netv1.IngressRuleArgs{ netv1.IngressRuleArgs{
Host: host, Host: host,
Http: &netv1.HTTPIngressRuleValueArgs{ Http: &netv1.HTTPIngressRuleValueArgs{
Paths: &netv1.HTTPIngressPathArray{ Paths: ingressPaths,
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"),
},
},
},
},
},
}, },
}, },
}, },
@ -226,7 +286,7 @@ func createIngress(ctx *pulumi.Context, configuration *internalConfiguration, na
} }
func createCertificate( func createCertificate(
ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, application *Application, ctx *pulumi.Context, configuration *configuration, namespace *corev1.Namespace, application *Application,
) (*v1.Certificate, error) { ) (*v1.Certificate, error) {
if configuration.ShouldCreateCertificate { if configuration.ShouldCreateCertificate {
return v1.NewCertificate(ctx, configuration.Name, &v1.CertificateArgs{ return v1.NewCertificate(ctx, configuration.Name, &v1.CertificateArgs{
@ -253,7 +313,7 @@ func createCertificate(
return nil, nil return nil, nil
} }
func createDNSRecord(ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, application *Application) (*dnsv1alpha1.DNSEndpoint, error) { func createDNSRecord(ctx *pulumi.Context, configuration *configuration, namespace *corev1.Namespace, application *Application) (*dnsv1alpha1.DNSEndpoint, error) {
if configuration.ShouldCreateDns { if configuration.ShouldCreateDns {
return dnsv1alpha1.NewDNSEndpoint(ctx, fmt.Sprintf("%s-record", configuration.Name), &dnsv1alpha1.DNSEndpointArgs{ return dnsv1alpha1.NewDNSEndpoint(ctx, fmt.Sprintf("%s-record", configuration.Name), &dnsv1alpha1.DNSEndpointArgs{
Metadata: &metav1.ObjectMetaArgs{ Metadata: &metav1.ObjectMetaArgs{
@ -280,9 +340,7 @@ func createDNSRecord(ctx *pulumi.Context, configuration *internalConfiguration,
return nil, nil return nil, nil
} }
func createService( func createService(ctx *pulumi.Context, configuration serviceConfiguration, namespace *corev1.Namespace, appLabels pulumi.StringMap, application *Application) (*corev1.Service, error) {
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{ return corev1.NewService(ctx, fmt.Sprintf("%s-service", configuration.Name), &corev1.ServiceArgs{
Metadata: &metav1.ObjectMetaArgs{ Metadata: &metav1.ObjectMetaArgs{
Namespace: namespace.Metadata.Name(), Namespace: namespace.Metadata.Name(),
@ -307,7 +365,7 @@ func createService(
} }
func createDeployment( func createDeployment(
ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, appLabels pulumi.StringMap, application *Application, ctx *pulumi.Context, configuration *deploymentConfiguration, namespace *corev1.Namespace, appLabels pulumi.StringMap, application *Application,
) (*appsv1.Deployment, error) { ) (*appsv1.Deployment, error) {
env := corev1.EnvVarArray{} env := corev1.EnvVarArray{}
for key, value := range configuration.Env { for key, value := range configuration.Env {
@ -338,7 +396,7 @@ func createDeployment(
Containers: corev1.ContainerArray{ Containers: corev1.ContainerArray{
corev1.ContainerArgs{ corev1.ContainerArgs{
Name: pulumi.String(configuration.Name), Name: pulumi.String(configuration.Name),
Image: pulumi.String(configuration.Image), Image: pulumi.String(configuration.ImageReference.Image),
Ports: corev1.ContainerPortArray{ Ports: corev1.ContainerPortArray{
corev1.ContainerPortArgs{ corev1.ContainerPortArgs{
Name: pulumi.String("http"), Name: pulumi.String("http"),
@ -355,7 +413,7 @@ func createDeployment(
}, pulumi.Parent(application)) }, pulumi.Parent(application))
} }
func createNamespace(ctx *pulumi.Context, configuration *internalConfiguration) (*corev1.Namespace, error) { func createNamespace(ctx *pulumi.Context, configuration *configuration) (*corev1.Namespace, error) {
return corev1.NewNamespace(ctx, fmt.Sprintf("%s-namespace", configuration.Name), &corev1.NamespaceArgs{ return corev1.NewNamespace(ctx, fmt.Sprintf("%s-namespace", configuration.Name), &corev1.NamespaceArgs{
Metadata: &metav1.ObjectMetaArgs{ Metadata: &metav1.ObjectMetaArgs{
Name: pulumi.String(configuration.Namespace), Name: pulumi.String(configuration.Namespace),