From 2e7da70a4826d02114b6e997f2fd8a2ba934001f Mon Sep 17 00:00:00 2001 From: RouxAntoine Date: Thu, 5 Oct 2023 00:38:19 +0200 Subject: [PATCH] feature: support creating application with multiple deployment, service and expose it with multiple path in ingress configuration --- main.go | 12 ++- pkg/application/generic.go | 172 +++++++++++++++++++++++++------------ 2 files changed, 125 insertions(+), 59 deletions(-) diff --git a/main.go b/main.go index 0b62208..0aff57b 100644 --- a/main.go +++ b/main.go @@ -11,11 +11,19 @@ func main() { nginxApplication, err := application.NewApplication(ctx, &application.Configuration{ Name: "nginx", Namespace: "pulumi-test", - Image: "nginx", + Images: []application.ImagesConfiguration{ + { + Image: "nginx", + }, + { + Image: "nginx", + Path: "/api", + }, + }, Env: map[string]string{ "version": "1.0.0", }, - Dns: pulumi.StringRef("pulumi-test-nginx.localdomain"), + Dns: pulumi.StringRef("pulumi-test-nginx.localdomain"), AllowAllOrigin: true, }) if err != nil { diff --git a/pkg/application/generic.go b/pkg/application/generic.go index 158dc28..ffe8dba 100644 --- a/pkg/application/generic.go +++ b/pkg/application/generic.go @@ -11,6 +11,7 @@ import ( 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" + "slices" ) const ( @@ -18,20 +19,25 @@ const ( defaultRecordTTL = 180 ) +type ImagesConfiguration struct { + Image string + Path string +} + type Configuration struct { Name string Namespace string - Image string + Images []ImagesConfiguration Dns *string Replicas *int Env map[string]string AllowAllOrigin bool } -type internalConfiguration struct { +type configuration struct { Name string Namespace string - Image string + Images []ImagesConfiguration Dns string Replicas int Env map[string]string @@ -42,6 +48,22 @@ type internalConfiguration struct { 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 { pulumi.ResourceState @@ -50,40 +72,55 @@ type Application struct { func NewApplication(ctx *pulumi.Context, publicConfiguration *Configuration) (*Application, error) { - if publicConfiguration.Name != "" && publicConfiguration.Image != "" { - configuration := &internalConfiguration{ + if publicConfiguration.Name != "" && len(publicConfiguration.Images) > 0 { + localConfiguration := &configuration{ Name: publicConfiguration.Name, Namespace: publicConfiguration.Namespace, - Image: publicConfiguration.Image, 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 { - configuration.Replicas = *publicConfiguration.Replicas + localConfiguration.Replicas = *publicConfiguration.Replicas } else { - configuration.Replicas = 1 + localConfiguration.Replicas = 1 } if publicConfiguration.Dns != nil { - configuration.Dns = *publicConfiguration.Dns - configuration.ShouldCreateIngress = true - configuration.ShouldCreateDns = true - configuration.ShouldCreateCertificate = true + localConfiguration.Dns = *publicConfiguration.Dns + localConfiguration.ShouldCreateIngress = true + localConfiguration.ShouldCreateDns = true + localConfiguration.ShouldCreateCertificate = true } if publicConfiguration.AllowAllOrigin { - configuration.ResponseHeaders = &traefikv1alpha1.MiddlewareSpecHeadersArgs{ + localConfiguration.ResponseHeaders = &traefikv1alpha1.MiddlewareSpecHeadersArgs{ AccessControlAllowOriginList: toPulumiStringArray([]string{"*"}), } } - return createResources(ctx, configuration) + return createResources(ctx, localConfiguration) } 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) { +func createResources(ctx *pulumi.Context, configuration *configuration, opts ...pulumi.ResourceOption) (*Application, error) { application := &Application{} err := ctx.RegisterComponentResource("pkg:application:Application", configuration.Name, application) if err != nil { @@ -95,18 +132,37 @@ func createResources(ctx *pulumi.Context, configuration *internalConfiguration, 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() + var ingressesParameter []ingressConfiguration + var deployments []*appsv1.Deployment + for index, image := range configuration.Images { + appLabels := pulumi.StringMap{ + "app.kubernetes.io/name": pulumi.String(configuration.Name), + } - service, err := createService(ctx, configuration, namespace, appLabels, application) - if err != nil { - return nil, err + deploymentParameter := &deploymentConfiguration{ + Name: fmt.Sprintf("%s-%d", configuration.Name, index), + 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) @@ -119,7 +175,7 @@ func createResources(ctx *pulumi.Context, configuration *internalConfiguration, return nil, err } - _, err = createIngress(ctx, configuration, namespace, certificate, service, application, headerMiddleware) + _, err = createIngress(ctx, configuration, namespace, certificate, ingressesParameter, application, headerMiddleware) if err != nil { return nil, err } @@ -129,9 +185,12 @@ func createResources(ctx *pulumi.Context, configuration *internalConfiguration, return nil, err } - err = ctx.RegisterResourceOutputs(application, pulumi.Map{ - "deployment": deployment.Metadata.Name(), - }) + outs := pulumi.Map{} + for i, deployment := range deployments { + outs[fmt.Sprintf("deployment-%d", i)] = deployment.Metadata.Name() + } + + err = ctx.RegisterResourceOutputs(application, outs) if err != nil { return nil, err } @@ -149,7 +208,7 @@ func toPulumiStringArray(values []string) pulumi.StringArray { func createMiddlewareAddResponseHeader( ctx *pulumi.Context, - configuration *internalConfiguration, + configuration *configuration, namespace *corev1.Namespace, application *Application, ) (*traefikv1alpha1.Middleware, error) { @@ -168,19 +227,33 @@ func createMiddlewareAddResponseHeader( }, 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 { 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], - ) + 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"), } + 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{ Metadata: &metav1.ObjectMetaArgs{ Namespace: namespace.Metadata.Name(), @@ -196,20 +269,7 @@ func createIngress(ctx *pulumi.Context, configuration *internalConfiguration, na 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"), - }, - }, - }, - }, - }, + Paths: ingressPaths, }, }, }, @@ -226,7 +286,7 @@ func createIngress(ctx *pulumi.Context, configuration *internalConfiguration, na } 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) { if configuration.ShouldCreateCertificate { return v1.NewCertificate(ctx, configuration.Name, &v1.CertificateArgs{ @@ -253,7 +313,7 @@ func createCertificate( 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 { return dnsv1alpha1.NewDNSEndpoint(ctx, fmt.Sprintf("%s-record", configuration.Name), &dnsv1alpha1.DNSEndpointArgs{ Metadata: &metav1.ObjectMetaArgs{ @@ -280,9 +340,7 @@ func createDNSRecord(ctx *pulumi.Context, configuration *internalConfiguration, return nil, nil } -func createService( - ctx *pulumi.Context, configuration *internalConfiguration, namespace *corev1.Namespace, appLabels pulumi.StringMap, application *Application, -) (*corev1.Service, error) { +func createService(ctx *pulumi.Context, configuration serviceConfiguration, 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(), @@ -307,7 +365,7 @@ func createService( } 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) { env := corev1.EnvVarArray{} for key, value := range configuration.Env { @@ -338,7 +396,7 @@ func createDeployment( Containers: corev1.ContainerArray{ corev1.ContainerArgs{ Name: pulumi.String(configuration.Name), - Image: pulumi.String(configuration.Image), + Image: pulumi.String(configuration.ImageReference.Image), Ports: corev1.ContainerPortArray{ corev1.ContainerPortArgs{ Name: pulumi.String("http"), @@ -355,7 +413,7 @@ func createDeployment( }, 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{ Metadata: &metav1.ObjectMetaArgs{ Name: pulumi.String(configuration.Namespace),