diff --git a/Makefile b/Makefile index ac83339..b4d5528 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ dotnet_sdk:: go_sdk:: $(WORKING_DIR)/bin/$(PROVIDER) rm -rf sdk/go pulumi package gen-sdk $(WORKING_DIR)/bin/$(PROVIDER) --language go - pulumi package get-schema $(WORKING_DIR)/bin/$(PROVIDER) > bin/schema-$(PACK).json + pulumi package get-schema $(WORKING_DIR)/bin/$(PROVIDER) > provider/cmd/pulumi-resource-aem/schema.json sed -i.bak 's/"internal"/"github.com\/wttech\/pulumi-aem-native\/sdk\/go\/aem\/internal"/g' sdk/go/$(PACK)/*.go sed -i.bak 's/"internal"/"github.com\/wttech\/pulumi-aem-native\/sdk\/go\/aem\/internal"/g' sdk/go/$(PACK)/$(MOD)/*.go sed -i.bak 's/\/pulumi-aem\/sdk/\/pulumi-aem-native\/sdk/g' sdk/go/$(PACK)/internal/*.go diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000..0f11b13 --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_index.md b/docs/_index.md index 35b0baf..c58ea83 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -16,29 +16,118 @@ It's based on the [AEM Compose](https://github.com/wttech/aemc) tool and aims to ```typescript import * as aem from "@wttech/aem"; -import * as fs from "fs"; +import * as aws from "@pulumi/aws"; -const privateKey = fs.readFileSync("ec2-key.cer", "utf8"); +const workspace = "aemc" +const env = "tf-minimal" +const envType = "aem-single" +const host = "aem-single" +const dataDevice = "/dev/nvme1n1" +const dataDir = "/data" +const composeDir = `${dataDir}/aemc` + +const tags = { + "Workspace": workspace, + "Env": env, + "EnvType": envType, + "Host": host, + "Name": `${workspace}_${envType}_${host}`, +} + +const role = new aws.iam.Role("aem_ec2", { + name: `${workspace}_aem_ec2`, + assumeRolePolicy: JSON.stringify({ + "Version": "2012-10-17", + "Statement": { + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole" + } + }), + tags: tags, +}); + +new aws.iam.RolePolicyAttachment("ssm", { + role: role.name, + policyArn: "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", +}); + +new aws.iam.RolePolicyAttachment("s3", { + role: role.name, + policyArn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess", +}); + +const instanceProfile = new aws.iam.InstanceProfile("aem_ec2", { + name: `${workspace}_aem_ec2`, + role: role.name, + tags: tags, +}); + +const instance = new aws.ec2.Instance("aem_single", { + ami: "ami-043e06a423cbdca17", // RHEL 8 + instanceType: "m5.xlarge", + iamInstanceProfile: instanceProfile.name, + tags: tags, + userData: `#!/bin/bash +sudo dnf install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm`, +}); + +const volume = new aws.ebs.Volume("aem_single_data", { + availabilityZone: instance.availabilityZone, + size: 128, + type: "gp2", + tags: tags, +}); + +const volumeAttachment = new aws.ec2.VolumeAttachment("aem_single_data", { + deviceName: "/dev/xvdf", + volumeId: volume.id, + instanceId: instance.id, +}); const aemInstance = new aem.compose.Instance("aem_instance", { client: { - type: "ssh", + type: "aws-ssm", settings: { - host: "x.x.x.x", - port: "22", - user: "root", - secure: "false", - }, - credentials: { - private_key: privateKey, + instance_id: instance.id, }, }, - files: { - lib: "/data/aemc/aem/home/lib", + system: { + data_dir: composeDir, + bootstrap: { + inline: [ + `sudo mkfs -t ext4 ${dataDevice}`, + `sudo mkdir -p ${dataDir}`, + `sudo mount ${dataDevice} ${dataDir}`, + `echo '${dataDevice} ${dataDir} ext4 defaults 0 0' | sudo tee -a /etc/fstab`, + "sudo yum install -y unzip", + "curl 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip' -o 'awscliv2.zip'", + "unzip -q awscliv2.zip", + "sudo ./aws/install --update", + ], + }, }, -}); + compose: { + create: { + inline: [ + `mkdir -p '${composeDir}/aem/home/lib'`, + `aws s3 cp --recursive --no-progress 's3://aemc/instance/classic/' '${composeDir}/aem/home/lib'`, + "sh aemw instance init", + "sh aemw instance create", + ], + }, + configure: { + inline: [ + "sh aemw osgi config save --pid 'org.apache.sling.jcr.davex.impl.servlets.SlingDavExServlet' --input-string 'alias: /crx/server'", + "sh aemw repl agent setup -A --location 'author' --name 'publish' --input-string '{enabled: true, transportUri: \"http://localhost:4503/bin/receive?sling:authRequestLogin=1\", transportUser: admin, transportPassword: admin, userId: admin}'", + "sh aemw package deploy --file 'aem/home/lib/aem-service-pkg-6.5.*.0.zip'", + ], + }, + } +}, {dependsOn: [instance, volumeAttachment]}); export const output = { + instanceIp: instance.publicIp, aemInstances: aemInstance.instances, }; ``` @@ -50,38 +139,150 @@ export const output = { package main import ( - _ "embed" + "fmt" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ebs" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/wttech/pulumi-aem-native/sdk/go/aem/compose" ) -//go:embed ec2-key.cer -var privateKey string - func main() { + workspace := "aemc" + env := "tf-minimal" + envType := "aem-single" + host := "aem-single" + dataDevice := "/dev/nvme1n1" + dataDir := "/data" + composeDir := fmt.Sprintf("%s/aemc", dataDir) + + tags := pulumi.StringMap{ + "Workspace": pulumi.String(workspace), + "Env": pulumi.String(env), + "EnvType": pulumi.String(envType), + "Host": pulumi.String(host), + "Name": pulumi.Sprintf("%s_%s_%s", workspace, envType, host), + } + pulumi.Run(func(ctx *pulumi.Context) error { + role, err := iam.NewRole(ctx, "aem_ec2", &iam.RoleArgs{ + Name: pulumi.Sprintf("%s_aem_ec2", workspace), + AssumeRolePolicy: pulumi.String(`{ + "Version": "2012-10-17", + "Statement": { + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole" + } +}`), + Tags: tags, + }) + if err != nil { + return err + } + + _, err = iam.NewRolePolicyAttachment(ctx, "ssm", &iam.RolePolicyAttachmentArgs{ + Role: role.Name, + PolicyArn: pulumi.String("arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"), + }) + if err != nil { + return err + } + + _, err = iam.NewRolePolicyAttachment(ctx, "s3", &iam.RolePolicyAttachmentArgs{ + Role: role.Name, + PolicyArn: pulumi.String("arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"), + }) + if err != nil { + return err + } + + instanceProfile, err := iam.NewInstanceProfile(ctx, "aem_ec2", &iam.InstanceProfileArgs{ + Name: pulumi.Sprintf("%s_aem_ec2", workspace), + Role: role.Name, + Tags: tags, + }) + if err != nil { + return err + } + + instance, err := ec2.NewInstance(ctx, "aem_single", &ec2.InstanceArgs{ + Ami: pulumi.String("ami-043e06a423cbdca17"), // RHEL 8 + InstanceType: pulumi.String("m5.xlarge"), + IamInstanceProfile: instanceProfile.Name, + Tags: tags, + UserData: pulumi.String(`#!/bin/bash +sudo dnf install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm`), + }) + if err != nil { + return err + } + + volume, err := ebs.NewVolume(ctx, "aem_single_data", &ebs.VolumeArgs{ + AvailabilityZone: instance.AvailabilityZone, + Size: pulumi.Int(128), + Type: pulumi.String("gp2"), + Tags: tags, + }) + if err != nil { + return err + } + + volumeAttachment, err := ec2.NewVolumeAttachment(ctx, "aem_single_data", &ec2.VolumeAttachmentArgs{ + DeviceName: pulumi.String("/dev/xvdf"), + VolumeId: volume.ID(), + InstanceId: instance.ID(), + }) + if err != nil { + return err + } + aemInstance, err := compose.NewInstance(ctx, "aem_instance", &compose.InstanceArgs{ Client: compose.ClientArgs{ - Type: pulumi.String("ssh"), + Type: pulumi.String("aws-ssm"), Settings: pulumi.StringMap{ - "host": pulumi.String("x.x.x.x"), - "port": pulumi.String("22"), - "user": pulumi.String("root"), - "secure": pulumi.String("false"), + "instance_id": instance.ID(), }, - Credentials: pulumi.StringMap{ - "private_key": pulumi.String(privateKey), + }, + System: compose.SystemArgs{ + Data_dir: pulumi.String(composeDir), + Bootstrap: compose.InstanceScriptArgs{ + Inline: pulumi.StringArray{ + pulumi.Sprintf("sudo mkfs -t ext4 %s", dataDevice), + pulumi.Sprintf("sudo mkdir -p %s", dataDir), + pulumi.Sprintf("sudo mount %s %s", dataDevice, dataDir), + pulumi.Sprintf("echo '%s %s ext4 defaults 0 0' | sudo tee -a /etc/fstab", dataDevice, dataDir), + pulumi.String("sudo yum install -y unzip"), + pulumi.String("curl 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip' -o 'awscliv2.zip'"), + pulumi.String("unzip -q awscliv2.zip"), + pulumi.String("sudo ./aws/install --update"), + }, }, }, - Files: pulumi.StringMap{ - "lib": pulumi.String("/data/aemc/aem/home/lib"), + Compose: compose.ComposeArgs{ + Create: compose.InstanceScriptArgs{ + Inline: pulumi.StringArray{ + pulumi.Sprintf("mkdir -p '%s/aem/home/lib'", composeDir), + pulumi.Sprintf("aws s3 cp --recursive --no-progress 's3://aemc/instance/classic/' '%s/aem/home/lib'", composeDir), + pulumi.String("sh aemw instance init"), + pulumi.String("sh aemw instance create"), + }, + }, + Configure: compose.InstanceScriptArgs{ + Inline: pulumi.StringArray{ + pulumi.String("sh aemw osgi config save --pid 'org.apache.sling.jcr.davex.impl.servlets.SlingDavExServlet' --input-string 'alias: /crx/server'"), + pulumi.String("sh aemw repl agent setup -A --location 'author' --name 'publish' --input-string '{enabled: true, transportUri: \"http://localhost:4503/bin/receive?sling:authRequestLogin=1\", transportUser: admin, transportPassword: admin, userId: admin}'"), + pulumi.String("sh aemw package deploy --file 'aem/home/lib/aem-service-pkg-6.5.*.0.zip'"), + }, + }, }, - }) + }, pulumi.DependsOn([]pulumi.Resource{instance, volumeAttachment})) if err != nil { return err } ctx.Export("output", pulumi.Map{ + "instanceIp": instance.PublicIp, "aemInstances": aemInstance.Instances, }) return nil diff --git a/provider/cmd/pulumi-resource-aem/schema.json b/provider/cmd/pulumi-resource-aem/schema.json new file mode 100644 index 0000000..3483a39 --- /dev/null +++ b/provider/cmd/pulumi-resource-aem/schema.json @@ -0,0 +1,229 @@ +{ + "name": "aem", + "version": "0.0.1-alpha.1711067432+259a1b2e.dirty", + "meta": { + "moduleFormat": "(.*)" + }, + "config": {}, + "types": { + "aem:compose:Client": { + "properties": { + "action_timeout": { + "type": "string", + "description": "Used when trying to connect to the AEM instance machine (often right after creating it). Need to be enough long because various types of connections (like AWS SSM or SSH) may need some time to boot up the agent." + }, + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Credentials for the connection type" + }, + "settings": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Settings for the connection type" + }, + "state_timeout": { + "type": "string", + "description": "Used when reading the AEM instance state when determining the plan." + }, + "type": { + "type": "string", + "description": "Type of connection to use to connect to the machine on which AEM instance will be running." + } + }, + "type": "object", + "required": [ + "settings", + "type" + ] + }, + "aem:compose:Compose": { + "properties": { + "config": { + "type": "string", + "description": "Contents of the AEM Compose YML configuration file." + }, + "configure": { + "$ref": "#/types/aem:compose:InstanceScript", + "description": "Script(s) for configuring a launched instance. Must be idempotent as it is executed always when changed. Typically used for installing AEM service packs, setting up replication agents, etc." + }, + "create": { + "$ref": "#/types/aem:compose:InstanceScript", + "description": "Script(s) for creating an instance or restoring it from a backup. Typically customized to provide AEM library files (quickstart.jar, license.properties, service packs) from alternative sources (e.g., AWS S3, Azure Blob Storage). Instance recreation is forced if changed." + }, + "delete": { + "$ref": "#/types/aem:compose:InstanceScript", + "description": "Script(s) for deleting a stopped instance." + }, + "download": { + "type": "boolean", + "description": "Toggle automatic AEM Compose CLI wrapper download. If set to false, assume the wrapper is present in the data directory." + }, + "version": { + "type": "string", + "description": "Version of AEM Compose tool to use on remote machine." + } + }, + "type": "object" + }, + "aem:compose:InstanceModel": { + "properties": { + "aem_version": { + "type": "string", + "description": "Version of the AEM instance. Reflects service pack installations." + }, + "attributes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A brief description of the state details for a specific AEM instance. Possible states include 'created', 'uncreated', 'running', 'unreachable', 'up-to-date', and 'out-of-date'." + }, + "dir": { + "type": "string", + "description": "Remote path in which AEM instance is stored." + }, + "id": { + "type": "string", + "description": "Unique identifier of AEM instance defined in the configuration." + }, + "run_modes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of run modes for a specific AEM instance." + }, + "url": { + "type": "string", + "description": "The machine-internal HTTP URL address used for communication with the AEM instance." + } + }, + "type": "object", + "required": [ + "aem_version", + "attributes", + "dir", + "id", + "run_modes", + "url" + ] + }, + "aem:compose:InstanceScript": { + "properties": { + "inline": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Inline shell commands to be executed" + }, + "script": { + "type": "string", + "description": "Multiline shell script to be executed" + } + }, + "type": "object" + }, + "aem:compose:System": { + "properties": { + "bootstrap": { + "$ref": "#/types/aem:compose:InstanceScript", + "description": "Script executed once upon instance connection, often for mounting on VM data volumes from attached disks (e.g., AWS EBS, Azure Disk Storage). This script runs only once, even during instance recreation, as changes are typically persistent and system-wide. If re-execution is needed, it is recommended to set up a new machine." + }, + "data_dir": { + "type": "string", + "description": "Remote root path in which AEM Compose files and unpacked AEM instances will be stored." + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables for AEM instances." + }, + "service_config": { + "type": "string", + "description": "Contents of the AEM system service definition file (systemd)." + }, + "user": { + "type": "string", + "description": "System user under which AEM instance will be running. By default, the same as the user used to connect to the machine." + }, + "work_dir": { + "type": "string", + "description": "Remote root path where provider-related files will be stored." + } + }, + "type": "object" + } + }, + "provider": { + "type": "object" + }, + "resources": { + "aem:compose:Instance": { + "properties": { + "client": { + "$ref": "#/types/aem:compose:Client", + "description": "Connection settings used to access the machine on which the AEM instance will be running." + }, + "compose": { + "$ref": "#/types/aem:compose:Compose", + "description": "AEM Compose CLI configuration. See documentation(https://github.com/wttech/aemc#configuration)." + }, + "files": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Files or directories to be copied into the machine." + }, + "instances": { + "type": "array", + "items": { + "$ref": "#/types/aem:compose:InstanceModel" + }, + "description": "Current state of the configured AEM instances." + }, + "system": { + "$ref": "#/types/aem:compose:System", + "description": "Operating system configuration for the machine on which AEM instance will be running." + } + }, + "type": "object", + "required": [ + "client", + "instances" + ], + "inputProperties": { + "client": { + "$ref": "#/types/aem:compose:Client", + "description": "Connection settings used to access the machine on which the AEM instance will be running." + }, + "compose": { + "$ref": "#/types/aem:compose:Compose", + "description": "AEM Compose CLI configuration. See documentation(https://github.com/wttech/aemc#configuration)." + }, + "files": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Files or directories to be copied into the machine." + }, + "system": { + "$ref": "#/types/aem:compose:System", + "description": "Operating system configuration for the machine on which AEM instance will be running." + } + }, + "requiredInputs": [ + "client" + ] + } + } +}