puls-dev 0.2.7 ā 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/checker.js +71 -0
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +11 -1
- package/dist/core/context.d.ts +14 -0
- package/dist/core/context.js +2 -0
- package/dist/core/decorators.d.ts +4 -0
- package/dist/core/decorators.js +56 -30
- package/dist/core/hooks.d.ts +21 -0
- package/dist/core/hooks.js +116 -0
- package/dist/core/hooks.test.d.ts +1 -0
- package/dist/core/hooks.test.js +194 -0
- package/dist/core/multiregion.test.d.ts +1 -0
- package/dist/core/multiregion.test.js +87 -0
- package/dist/core/output.d.ts +2 -0
- package/dist/core/output.js +9 -2
- package/dist/core/parallel.test.d.ts +1 -0
- package/dist/core/parallel.test.js +215 -0
- package/dist/core/parser.d.ts +10 -0
- package/dist/core/parser.js +140 -0
- package/dist/core/parser.test.d.ts +1 -0
- package/dist/core/parser.test.js +117 -0
- package/dist/core/production.test.d.ts +1 -0
- package/dist/core/production.test.js +189 -0
- package/dist/core/provisioner.d.ts +4 -0
- package/dist/core/provisioner.js +123 -0
- package/dist/core/resource.d.ts +23 -0
- package/dist/core/resource.js +54 -0
- package/dist/core/retry.d.ts +9 -0
- package/dist/core/retry.js +28 -0
- package/dist/core/retry.test.d.ts +1 -0
- package/dist/core/retry.test.js +66 -0
- package/dist/core/secret.d.ts +41 -0
- package/dist/core/secret.js +105 -0
- package/dist/core/secret.test.d.ts +1 -0
- package/dist/core/secret.test.js +166 -0
- package/dist/core/stack.d.ts +4 -3
- package/dist/core/stack.js +322 -48
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/providers/aws/api.js +97 -17
- package/dist/providers/aws/ec2.d.ts +51 -0
- package/dist/providers/aws/ec2.js +331 -0
- package/dist/providers/aws/ec2.test.d.ts +1 -0
- package/dist/providers/aws/ec2.test.js +281 -0
- package/dist/providers/aws/index.d.ts +4 -0
- package/dist/providers/aws/index.js +4 -0
- package/dist/providers/aws/route53.d.ts +1 -0
- package/dist/providers/aws/route53.js +15 -2
- package/dist/providers/aws/route53.test.js +47 -0
- package/dist/providers/aws/template.d.ts +34 -0
- package/dist/providers/aws/template.js +252 -0
- package/dist/providers/aws/template.test.d.ts +1 -0
- package/dist/providers/aws/template.test.js +208 -0
- package/dist/providers/do/api.d.ts +3 -1
- package/dist/providers/do/api.js +126 -27
- package/dist/providers/do/app.d.ts +26 -0
- package/dist/providers/do/app.js +124 -0
- package/dist/providers/do/app.test.d.ts +1 -0
- package/dist/providers/do/app.test.js +268 -0
- package/dist/providers/do/database.d.ts +44 -0
- package/dist/providers/do/database.js +208 -0
- package/dist/providers/do/database.test.d.ts +1 -0
- package/dist/providers/do/database.test.js +293 -0
- package/dist/providers/do/domain.d.ts +2 -0
- package/dist/providers/do/domain.js +30 -0
- package/dist/providers/do/domain.test.js +49 -0
- package/dist/providers/do/droplet.d.ts +9 -0
- package/dist/providers/do/droplet.js +146 -8
- package/dist/providers/do/droplet.test.js +228 -1
- package/dist/providers/do/firewall.d.ts +2 -1
- package/dist/providers/do/firewall.js +23 -9
- package/dist/providers/do/firewall.test.js +54 -0
- package/dist/providers/do/index.d.ts +11 -0
- package/dist/providers/do/index.js +8 -0
- package/dist/providers/do/spaces.d.ts +27 -0
- package/dist/providers/do/spaces.js +142 -0
- package/dist/providers/do/spaces.test.d.ts +1 -0
- package/dist/providers/do/spaces.test.js +180 -0
- package/dist/providers/do/spaces_api.d.ts +2 -0
- package/dist/providers/do/spaces_api.js +20 -0
- package/dist/providers/do/vpc.d.ts +30 -0
- package/dist/providers/do/vpc.js +128 -0
- package/dist/providers/do/vpc.test.d.ts +1 -0
- package/dist/providers/do/vpc.test.js +258 -0
- package/dist/providers/firebase/api.js +92 -29
- package/dist/providers/firebase/list.d.ts +2 -0
- package/dist/providers/firebase/list.js +25 -0
- package/dist/providers/gcp/api.js +88 -14
- package/dist/providers/gcp/clouddns.d.ts +1 -0
- package/dist/providers/gcp/clouddns.js +15 -2
- package/dist/providers/gcp/clouddns.test.js +45 -0
- package/dist/providers/gcp/index.d.ts +5 -1
- package/dist/providers/gcp/index.js +5 -1
- package/dist/providers/gcp/list.d.ts +2 -0
- package/dist/providers/gcp/list.js +55 -0
- package/dist/providers/gcp/secrets.js +1 -1
- package/dist/providers/gcp/template.d.ts +32 -0
- package/dist/providers/gcp/template.js +252 -0
- package/dist/providers/gcp/template.test.d.ts +1 -0
- package/dist/providers/gcp/template.test.js +227 -0
- package/dist/providers/gcp/vm.d.ts +48 -0
- package/dist/providers/gcp/vm.js +375 -0
- package/dist/providers/gcp/vm.test.d.ts +1 -0
- package/dist/providers/gcp/vm.test.js +321 -0
- package/dist/providers/proxmox/api.d.ts +1 -0
- package/dist/providers/proxmox/api.js +72 -16
- package/dist/providers/proxmox/index.d.ts +2 -0
- package/dist/providers/proxmox/index.js +2 -0
- package/dist/providers/proxmox/template.d.ts +44 -0
- package/dist/providers/proxmox/template.js +349 -0
- package/dist/providers/proxmox/template.test.d.ts +1 -0
- package/dist/providers/proxmox/template.test.js +179 -0
- package/dist/providers/proxmox/vm.d.ts +7 -4
- package/dist/providers/proxmox/vm.js +57 -102
- package/dist/providers/proxmox/vm.test.js +77 -0
- package/dist/types/inventory.d.ts +44 -1
- package/package.json +3 -1
|
@@ -15,26 +15,106 @@ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
|
|
|
15
15
|
import { CloudWatchClient } from "@aws-sdk/client-cloudwatch";
|
|
16
16
|
import { SNSClient } from "@aws-sdk/client-sns";
|
|
17
17
|
import { Config } from "../../core/config.js";
|
|
18
|
+
import { withRetry } from "../../core/retry.js";
|
|
19
|
+
import { resourceContextStorage } from "../../core/context.js";
|
|
18
20
|
function getRegion() {
|
|
19
21
|
const region = Config.get().providers.aws?.region;
|
|
20
|
-
if (!region)
|
|
22
|
+
if (!region) {
|
|
23
|
+
if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
|
|
24
|
+
return "us-east-1";
|
|
25
|
+
}
|
|
21
26
|
throw new Error('AWS region not configured. Call AWS.init({ region: "..." })');
|
|
27
|
+
}
|
|
22
28
|
return region;
|
|
23
29
|
}
|
|
24
|
-
|
|
30
|
+
function createAwsOfflineMock(command) {
|
|
31
|
+
const name = command.constructor.name;
|
|
32
|
+
if (name.includes("RunInstances")) {
|
|
33
|
+
return {
|
|
34
|
+
Instances: [
|
|
35
|
+
{
|
|
36
|
+
InstanceId: "i-mock1234567890abcdef0",
|
|
37
|
+
State: { Name: "running" },
|
|
38
|
+
PublicIpAddress: "54.210.12.34",
|
|
39
|
+
PrivateIpAddress: "10.0.1.10",
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (name.includes("CreateVpc")) {
|
|
45
|
+
return { Vpc: { VpcId: "vpc-mock123456" } };
|
|
46
|
+
}
|
|
47
|
+
if (name.includes("CreateSubnet")) {
|
|
48
|
+
return { Subnet: { SubnetId: "subnet-mock123456" } };
|
|
49
|
+
}
|
|
50
|
+
if (name.includes("CreateSecurityGroup")) {
|
|
51
|
+
return { GroupId: "sg-mock123456" };
|
|
52
|
+
}
|
|
53
|
+
if (name.includes("CreateBucket")) {
|
|
54
|
+
return { Location: "/mock-bucket" };
|
|
55
|
+
}
|
|
56
|
+
if (name.includes("CreateKeyPair")) {
|
|
57
|
+
return { KeyMaterial: "mock-private-key", KeyName: "mock-key" };
|
|
58
|
+
}
|
|
59
|
+
const mockProxy = new Proxy({}, {
|
|
60
|
+
get(target, prop) {
|
|
61
|
+
if (prop === "then")
|
|
62
|
+
return undefined;
|
|
63
|
+
if (prop === "CertificateArn")
|
|
64
|
+
return "arn:aws:acm:us-east-1:123456789012:certificate/mock-cert-uuid";
|
|
65
|
+
if (prop === "HostedZoneId")
|
|
66
|
+
return "Z2FDTNDATAQYW2";
|
|
67
|
+
if (prop === "Id" || prop === "id")
|
|
68
|
+
return "mock-id-12345";
|
|
69
|
+
if (prop === "Arn" || prop === "arn")
|
|
70
|
+
return `arn:aws:mock:::resource/mock-id`;
|
|
71
|
+
if (prop === "Status" || prop === "status")
|
|
72
|
+
return "Active";
|
|
73
|
+
if (prop === "DNSName")
|
|
74
|
+
return "mock.cloudfront.net";
|
|
75
|
+
if (prop.endsWith("s"))
|
|
76
|
+
return [];
|
|
77
|
+
return `mock-${prop.toLowerCase()}`;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return mockProxy;
|
|
81
|
+
}
|
|
82
|
+
function wrapClient(client) {
|
|
83
|
+
const originalSend = client.send;
|
|
84
|
+
client.send = function (command, options) {
|
|
85
|
+
const context = resourceContextStorage.getStore();
|
|
86
|
+
const abortSignal = context?.abortSignal;
|
|
87
|
+
if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
|
|
88
|
+
return Promise.resolve(createAwsOfflineMock(command));
|
|
89
|
+
}
|
|
90
|
+
const opts = abortSignal ? { abortSignal, ...options } : options;
|
|
91
|
+
return withRetry(() => originalSend.call(client, command, opts), {
|
|
92
|
+
retryable: (err) => {
|
|
93
|
+
const code = err.name || err.code;
|
|
94
|
+
const status = err.$metadata?.httpStatusCode;
|
|
95
|
+
return (code === "ThrottlingException" ||
|
|
96
|
+
code === "ProvisionedThroughputExceededException" ||
|
|
97
|
+
code === "RequestLimitExceeded" ||
|
|
98
|
+
(status && status >= 500));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
return client;
|
|
103
|
+
}
|
|
104
|
+
export const getS3Client = (region) => wrapClient(new S3Client({ region: region ?? getRegion() }));
|
|
25
105
|
// CloudFront, Route53, ACM, Route53 Domains, and IAM are all global - must use us-east-1
|
|
26
|
-
export const getCFClient = () => new CloudFrontClient({ region: "us-east-1" });
|
|
27
|
-
export const getR53Client = () => new Route53Client({ region: "us-east-1" });
|
|
28
|
-
export const getR53DomainsClient = () => new Route53DomainsClient({ region: "us-east-1" });
|
|
29
|
-
export const getACMClient = () => new ACMClient({ region: "us-east-1" });
|
|
30
|
-
export const getIAMClient = () => new IAMClient({ region: "us-east-1" });
|
|
31
|
-
export const getLambdaClient = (region) => new LambdaClient({ region: region ?? getRegion() });
|
|
32
|
-
export const getAPIGWClient = (region) => new ApiGatewayV2Client({ region: region ?? getRegion() });
|
|
33
|
-
export const getECSClient = (region) => new ECSClient({ region: region ?? getRegion() });
|
|
34
|
-
export const getEC2Client = (region) => new EC2Client({ region: region ?? getRegion() });
|
|
35
|
-
export const getCWLogsClient = (region) => new CloudWatchLogsClient({ region: region ?? getRegion() });
|
|
36
|
-
export const getRDSClient = (region) => new RDSClient({ region: region ?? getRegion() });
|
|
37
|
-
export const getSQSClient = (region) => new SQSClient({ region: region ?? getRegion() });
|
|
38
|
-
export const getSecretsClient = (region) => new SecretsManagerClient({ region: region ?? getRegion() });
|
|
39
|
-
export const getCWClient = (region) => new CloudWatchClient({ region: region ?? getRegion() });
|
|
40
|
-
export const getSNSClient = (region) => new SNSClient({ region: region ?? getRegion() });
|
|
106
|
+
export const getCFClient = () => wrapClient(new CloudFrontClient({ region: "us-east-1" }));
|
|
107
|
+
export const getR53Client = () => wrapClient(new Route53Client({ region: "us-east-1" }));
|
|
108
|
+
export const getR53DomainsClient = () => wrapClient(new Route53DomainsClient({ region: "us-east-1" }));
|
|
109
|
+
export const getACMClient = () => wrapClient(new ACMClient({ region: "us-east-1" }));
|
|
110
|
+
export const getIAMClient = () => wrapClient(new IAMClient({ region: "us-east-1" }));
|
|
111
|
+
export const getLambdaClient = (region) => wrapClient(new LambdaClient({ region: region ?? getRegion() }));
|
|
112
|
+
export const getAPIGWClient = (region) => wrapClient(new ApiGatewayV2Client({ region: region ?? getRegion() }));
|
|
113
|
+
export const getECSClient = (region) => wrapClient(new ECSClient({ region: region ?? getRegion() }));
|
|
114
|
+
export const getEC2Client = (region) => wrapClient(new EC2Client({ region: region ?? getRegion() }));
|
|
115
|
+
export const getCWLogsClient = (region) => wrapClient(new CloudWatchLogsClient({ region: region ?? getRegion() }));
|
|
116
|
+
export const getRDSClient = (region) => wrapClient(new RDSClient({ region: region ?? getRegion() }));
|
|
117
|
+
export const getSQSClient = (region) => wrapClient(new SQSClient({ region: region ?? getRegion() }));
|
|
118
|
+
export const getSecretsClient = (region) => wrapClient(new SecretsManagerClient({ region: region ?? getRegion() }));
|
|
119
|
+
export const getCWClient = (region) => wrapClient(new CloudWatchClient({ region: region ?? getRegion() }));
|
|
120
|
+
export const getSNSClient = (region) => wrapClient(new SNSClient({ region: region ?? getRegion() }));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { Output } from "../../core/output.js";
|
|
3
|
+
import { EC2TemplateBuilder } from "./template.js";
|
|
4
|
+
export declare class EC2VMBuilder extends BaseBuilder {
|
|
5
|
+
readonly out: {
|
|
6
|
+
ip: Output<string>;
|
|
7
|
+
id: Output<string>;
|
|
8
|
+
};
|
|
9
|
+
private _instanceType;
|
|
10
|
+
private _ami;
|
|
11
|
+
private _templateSource?;
|
|
12
|
+
private _keyName?;
|
|
13
|
+
private _subnetId?;
|
|
14
|
+
private _securityGroupIds?;
|
|
15
|
+
private _userData?;
|
|
16
|
+
private _sshPrivateKeyPath?;
|
|
17
|
+
private _provision;
|
|
18
|
+
private _forceConfigCheck;
|
|
19
|
+
private resolvedInstanceId?;
|
|
20
|
+
private resolvedIp?;
|
|
21
|
+
constructor(name: string);
|
|
22
|
+
instanceType(type: string): this;
|
|
23
|
+
ami(amiId: string): this;
|
|
24
|
+
fromTemplate(template: EC2TemplateBuilder): this;
|
|
25
|
+
keyName(name: string): this;
|
|
26
|
+
subnetId(id: string): this;
|
|
27
|
+
securityGroupIds(ids: string[]): this;
|
|
28
|
+
userData(data: string): this;
|
|
29
|
+
sshPrivateKey(path: string): this;
|
|
30
|
+
provision(...playbookPaths: (string | string[])[]): this;
|
|
31
|
+
forceConfigCheck(): this;
|
|
32
|
+
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
33
|
+
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
34
|
+
private discoverVM;
|
|
35
|
+
deploy(): Promise<{
|
|
36
|
+
name: string;
|
|
37
|
+
id: string;
|
|
38
|
+
ip?: undefined;
|
|
39
|
+
} | {
|
|
40
|
+
name: string;
|
|
41
|
+
id: string | undefined;
|
|
42
|
+
ip: string | undefined;
|
|
43
|
+
} | null>;
|
|
44
|
+
destroy(): Promise<{
|
|
45
|
+
destroyed: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
destroyed: string;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
50
|
+
export declare function parseAwsTagsForProvision(tags?: any[]): Record<string, string>;
|
|
51
|
+
export declare function mergeAwsTagsForProvision(metadata: Record<string, string>): string;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { DescribeInstancesCommand, RunInstancesCommand, TerminateInstancesCommand, StopInstancesCommand, StartInstancesCommand, ModifyInstanceAttributeCommand, CreateTagsCommand, } from "@aws-sdk/client-ec2";
|
|
2
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
3
|
+
import { Output } from "../../core/output.js";
|
|
4
|
+
import { getEC2Client } from "./api.js";
|
|
5
|
+
import { checkPort, runProvisioner } from "../../core/provisioner.js";
|
|
6
|
+
import { getFileHash } from "../proxmox/hash.js";
|
|
7
|
+
import { resourceContextStorage } from "../../core/context.js";
|
|
8
|
+
export class EC2VMBuilder extends BaseBuilder {
|
|
9
|
+
out = {
|
|
10
|
+
ip: new Output(),
|
|
11
|
+
id: new Output(),
|
|
12
|
+
};
|
|
13
|
+
_instanceType = "t3.micro";
|
|
14
|
+
_ami = "ami-0c55b159cbfafe1f0"; // Default standard Ubuntu 22.04 LTS in us-east-1
|
|
15
|
+
_templateSource;
|
|
16
|
+
_keyName;
|
|
17
|
+
_subnetId;
|
|
18
|
+
_securityGroupIds;
|
|
19
|
+
_userData;
|
|
20
|
+
_sshPrivateKeyPath;
|
|
21
|
+
_provision = [];
|
|
22
|
+
_forceConfigCheck = false;
|
|
23
|
+
resolvedInstanceId;
|
|
24
|
+
resolvedIp;
|
|
25
|
+
constructor(name) {
|
|
26
|
+
super(name);
|
|
27
|
+
this.discoveryPromise = this.discoverVM();
|
|
28
|
+
}
|
|
29
|
+
instanceType(type) {
|
|
30
|
+
this._instanceType = type;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
ami(amiId) {
|
|
34
|
+
this._ami = amiId;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
fromTemplate(template) {
|
|
38
|
+
this._templateSource = template;
|
|
39
|
+
this.dependsOn(template);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
keyName(name) {
|
|
43
|
+
this._keyName = name;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
subnetId(id) {
|
|
47
|
+
this._subnetId = id;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
securityGroupIds(ids) {
|
|
51
|
+
this._securityGroupIds = ids;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
userData(data) {
|
|
55
|
+
this._userData = data;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
sshPrivateKey(path) {
|
|
59
|
+
this._sshPrivateKeyPath = path;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
provision(...playbookPaths) {
|
|
63
|
+
this._provision.push(...playbookPaths.flat());
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
forceConfigCheck() {
|
|
67
|
+
this._forceConfigCheck = true;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
async checkPort(ip, port) {
|
|
71
|
+
return checkPort(ip, port);
|
|
72
|
+
}
|
|
73
|
+
async runProvisioner(ip, script) {
|
|
74
|
+
if (!this._sshPrivateKeyPath) {
|
|
75
|
+
throw new Error(`[EC2VMBuilder:${this.name}] sshPrivateKey(path) is required to run playbook provisioning.`);
|
|
76
|
+
}
|
|
77
|
+
// Default user is 'ubuntu' for standard Ubuntu images on AWS EC2
|
|
78
|
+
return runProvisioner(ip, "ubuntu", this._sshPrivateKeyPath, script);
|
|
79
|
+
}
|
|
80
|
+
async discoverVM() {
|
|
81
|
+
try {
|
|
82
|
+
const client = getEC2Client();
|
|
83
|
+
const result = await client.send(new DescribeInstancesCommand({
|
|
84
|
+
Filters: [
|
|
85
|
+
{ Name: "tag:Name", Values: [this.name] },
|
|
86
|
+
{
|
|
87
|
+
Name: "instance-state-name",
|
|
88
|
+
Values: ["pending", "running", "stopping", "stopped"],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}));
|
|
92
|
+
const reservation = result.Reservations?.[0];
|
|
93
|
+
const instance = reservation?.Instances?.[0];
|
|
94
|
+
if (instance) {
|
|
95
|
+
this.resolvedInstanceId = instance.InstanceId;
|
|
96
|
+
this.resolvedIp = instance.PublicIpAddress ?? instance.PrivateIpAddress ?? undefined;
|
|
97
|
+
if (instance.InstanceId)
|
|
98
|
+
this.out.id.resolve(instance.InstanceId);
|
|
99
|
+
if (this.resolvedIp)
|
|
100
|
+
this.out.ip.resolve(this.resolvedIp);
|
|
101
|
+
}
|
|
102
|
+
return instance ?? null;
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (e.name === "CredentialsProviderError" ||
|
|
106
|
+
e.message?.includes("credentials not configured")) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async deploy() {
|
|
113
|
+
const dryRun = this.isDryRunActive();
|
|
114
|
+
const existing = await this.discoveryPromise;
|
|
115
|
+
const client = getEC2Client();
|
|
116
|
+
const hasChanges = existing ? existing.InstanceType !== this._instanceType : true;
|
|
117
|
+
if (await this.checkProtection(hasChanges))
|
|
118
|
+
return null;
|
|
119
|
+
// Parse applied playbooks metadata from EC2 Tags
|
|
120
|
+
const appliedHashes = parseAwsTagsForProvision(existing?.Tags);
|
|
121
|
+
const declaredPlaybooksWithHashes = this._provision.map((p) => {
|
|
122
|
+
const baseName = p.split("/").pop() ?? p;
|
|
123
|
+
const slug = baseName.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
124
|
+
return { path: p, slug, hash: getFileHash(p) };
|
|
125
|
+
});
|
|
126
|
+
const playbooksToRun = this._forceConfigCheck
|
|
127
|
+
? declaredPlaybooksWithHashes
|
|
128
|
+
: declaredPlaybooksWithHashes.filter((p) => {
|
|
129
|
+
const appliedHash = appliedHashes[p.slug];
|
|
130
|
+
return !appliedHash || appliedHash !== p.hash;
|
|
131
|
+
});
|
|
132
|
+
const playbookRunRequired = playbooksToRun.length > 0;
|
|
133
|
+
if (dryRun) {
|
|
134
|
+
console.log(`\nš [DRY RUN] AWS EC2 VM "${this.name}"...`);
|
|
135
|
+
if (!existing) {
|
|
136
|
+
const sourceLabel = this._templateSource ? `Template: ${this._templateSource.name}` : `AMI ${this._ami}`;
|
|
137
|
+
console.log(` š Plan: Create AWS EC2 Instance`);
|
|
138
|
+
const details = [
|
|
139
|
+
`Name: ${this.name}`,
|
|
140
|
+
`Instance Type: ${this._instanceType}`,
|
|
141
|
+
`Source: ${sourceLabel}`,
|
|
142
|
+
];
|
|
143
|
+
if (this._provision.length > 0) {
|
|
144
|
+
details.push(`Provision: ${this._provision.join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
for (let i = 0; i < details.length; i++) {
|
|
147
|
+
const prefix = i === details.length - 1 ? " āā " : " āā ";
|
|
148
|
+
console.log(`${prefix}${details[i]}`);
|
|
149
|
+
}
|
|
150
|
+
this.out.id.resolve("PENDING");
|
|
151
|
+
this.out.ip.resolve("0.0.0.0");
|
|
152
|
+
}
|
|
153
|
+
else if (hasChanges || playbookRunRequired) {
|
|
154
|
+
if (hasChanges) {
|
|
155
|
+
console.log(` š Plan: Stop, Resize, and Start EC2 VM ${this.name} (${existing.InstanceType} ā ${this._instanceType})`);
|
|
156
|
+
}
|
|
157
|
+
if (playbookRunRequired) {
|
|
158
|
+
console.log(` š [PLAN] Run ${playbooksToRun.length} playbook changes on existing EC2 VM:`);
|
|
159
|
+
for (const p of playbooksToRun) {
|
|
160
|
+
console.log(` āā Playbook: ${p.path} (hash: ${p.hash})`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.log(` ā
AWS EC2 VM "${this.name}" is up to date.`);
|
|
166
|
+
}
|
|
167
|
+
return { name: this.name, id: this.resolvedInstanceId ?? "PENDING" };
|
|
168
|
+
}
|
|
169
|
+
console.log(`\nā³ Finalizing AWS EC2 VM "${this.name}"...`);
|
|
170
|
+
if (!existing) {
|
|
171
|
+
const initialHashes = {};
|
|
172
|
+
for (const p of declaredPlaybooksWithHashes) {
|
|
173
|
+
initialHashes[p.slug] = p.hash;
|
|
174
|
+
}
|
|
175
|
+
const initialMetadataVal = mergeAwsTagsForProvision(initialHashes);
|
|
176
|
+
let activeAmi = this._ami;
|
|
177
|
+
if (this._templateSource) {
|
|
178
|
+
activeAmi = await this._templateSource.out.amiId.get();
|
|
179
|
+
}
|
|
180
|
+
console.log(`š Creating AWS EC2 VM Instance "${this.name}"...`);
|
|
181
|
+
const result = await client.send(new RunInstancesCommand({
|
|
182
|
+
ImageId: activeAmi,
|
|
183
|
+
InstanceType: this._instanceType,
|
|
184
|
+
MinCount: 1,
|
|
185
|
+
MaxCount: 1,
|
|
186
|
+
KeyName: this._keyName,
|
|
187
|
+
SubnetId: this._subnetId,
|
|
188
|
+
SecurityGroupIds: this._securityGroupIds,
|
|
189
|
+
UserData: this._userData ? Buffer.from(this._userData).toString("base64") : undefined,
|
|
190
|
+
TagSpecifications: [
|
|
191
|
+
{
|
|
192
|
+
ResourceType: "instance",
|
|
193
|
+
Tags: [
|
|
194
|
+
{ Key: "Name", Value: this.name },
|
|
195
|
+
...(initialMetadataVal ? [{ Key: "puls-provision", Value: initialMetadataVal }] : []),
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
}));
|
|
200
|
+
const instanceId = result.Instances?.[0]?.InstanceId;
|
|
201
|
+
if (!instanceId)
|
|
202
|
+
throw new Error("Failed to retrieve instance ID from RunInstancesCommand");
|
|
203
|
+
this.resolvedInstanceId = instanceId;
|
|
204
|
+
this.out.id.resolve(instanceId);
|
|
205
|
+
// Poll until instance is running and has an IP address
|
|
206
|
+
await this.waitFor(`AWS EC2 VM "${this.name}" to start running`, async () => {
|
|
207
|
+
const current = await this.discoverVM();
|
|
208
|
+
return current && current.State?.Name === "running" && !!this.resolvedIp;
|
|
209
|
+
}, { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
210
|
+
console.log(`š AWS EC2 VM "${this.name}" is now running.`);
|
|
211
|
+
if (this._provision.length > 0) {
|
|
212
|
+
const activeIp = this.resolvedIp ?? "0.0.0.0";
|
|
213
|
+
if (activeIp === "0.0.0.0") {
|
|
214
|
+
throw new Error(`Failed to resolve IP for new AWS EC2 VM "${this.name}" to run playbooks`);
|
|
215
|
+
}
|
|
216
|
+
await this.waitFor(`SSH on ${activeIp} to be ready`, () => this.checkPort(activeIp, 22), { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
217
|
+
for (const playbook of this._provision) {
|
|
218
|
+
await this.runProvisioner(activeIp, playbook);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const instanceId = existing.InstanceId;
|
|
224
|
+
this.resolvedInstanceId = instanceId;
|
|
225
|
+
if (hasChanges) {
|
|
226
|
+
console.log(`⨠Resizing AWS EC2 VM ${this.name} (${existing.InstanceType} ā ${this._instanceType})...`);
|
|
227
|
+
console.log(` š Stopping EC2 VM to perform resize...`);
|
|
228
|
+
await client.send(new StopInstancesCommand({ InstanceIds: [instanceId] }));
|
|
229
|
+
await this.waitFor(`VM "${this.name}" to stop`, async () => {
|
|
230
|
+
const current = await this.discoverVM();
|
|
231
|
+
return current && current.State?.Name === "stopped";
|
|
232
|
+
}, { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
233
|
+
// Perform resize
|
|
234
|
+
await client.send(new ModifyInstanceAttributeCommand({
|
|
235
|
+
InstanceId: instanceId,
|
|
236
|
+
InstanceType: { Value: this._instanceType },
|
|
237
|
+
}));
|
|
238
|
+
// Restart VM
|
|
239
|
+
console.log(` š Restarting EC2 VM...`);
|
|
240
|
+
await client.send(new StartInstancesCommand({ InstanceIds: [instanceId] }));
|
|
241
|
+
await this.waitFor(`VM "${this.name}" to restart`, async () => {
|
|
242
|
+
const current = await this.discoverVM();
|
|
243
|
+
return current && current.State?.Name === "running" && !!this.resolvedIp;
|
|
244
|
+
}, { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
245
|
+
console.log(` ā
AWS EC2 VM resized and restarted successfully.`);
|
|
246
|
+
}
|
|
247
|
+
if (playbookRunRequired) {
|
|
248
|
+
console.log(` š Running ${playbooksToRun.length} playbook changes on AWS EC2 VM...`);
|
|
249
|
+
const activeIp = this.resolvedIp ?? "0.0.0.0";
|
|
250
|
+
if (activeIp === "0.0.0.0") {
|
|
251
|
+
throw new Error(`Failed to resolve IP for AWS EC2 VM "${this.name}" to run playbooks`);
|
|
252
|
+
}
|
|
253
|
+
await this.waitFor(`SSH on ${activeIp} to be ready`, () => this.checkPort(activeIp, 22), { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
254
|
+
for (const p of playbooksToRun) {
|
|
255
|
+
await this.runProvisioner(activeIp, p.path);
|
|
256
|
+
appliedHashes[p.slug] = p.hash;
|
|
257
|
+
}
|
|
258
|
+
// Update EC2 Tags with new hashes
|
|
259
|
+
const newValue = mergeAwsTagsForProvision(appliedHashes);
|
|
260
|
+
await client.send(new CreateTagsCommand({
|
|
261
|
+
Resources: [instanceId],
|
|
262
|
+
Tags: [{ Key: "puls-provision", Value: newValue }],
|
|
263
|
+
}));
|
|
264
|
+
console.log(` ā
Playbooks applied successfully and EC2 Tags updated.`);
|
|
265
|
+
}
|
|
266
|
+
if (!hasChanges && !playbookRunRequired) {
|
|
267
|
+
console.log(` ā
AWS EC2 VM "${this.name}" is up to date.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const context = resourceContextStorage.getStore();
|
|
271
|
+
if (context && context.hosts) {
|
|
272
|
+
const activeIp = this.resolvedIp ?? "0.0.0.0";
|
|
273
|
+
if (!context.hosts.some(h => h.name === this.name)) {
|
|
274
|
+
context.hosts.push({
|
|
275
|
+
name: this.name,
|
|
276
|
+
ip: activeIp,
|
|
277
|
+
user: "ubuntu",
|
|
278
|
+
sshKey: this._sshPrivateKeyPath,
|
|
279
|
+
provider: "aws"
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
await this.deploySidecars();
|
|
284
|
+
return {
|
|
285
|
+
name: this.name,
|
|
286
|
+
id: this.resolvedInstanceId,
|
|
287
|
+
ip: this.resolvedIp,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
async destroy() {
|
|
291
|
+
const dryRun = this.isDryRunActive();
|
|
292
|
+
const existing = await this.discoveryPromise;
|
|
293
|
+
const client = getEC2Client();
|
|
294
|
+
console.log(`\nšļø Destroying AWS EC2 VM "${this.name}"...`);
|
|
295
|
+
if (!existing) {
|
|
296
|
+
console.log(` ā AWS EC2 VM "${this.name}" not found`);
|
|
297
|
+
return { destroyed: false };
|
|
298
|
+
}
|
|
299
|
+
if (dryRun) {
|
|
300
|
+
console.log(` š [PLAN] Delete AWS EC2 VM "${this.name}" (id=${existing.InstanceId})`);
|
|
301
|
+
return { destroyed: this.name };
|
|
302
|
+
}
|
|
303
|
+
console.log(` š Terminating AWS EC2 VM "${this.name}" (id=${existing.InstanceId})...`);
|
|
304
|
+
await client.send(new TerminateInstancesCommand({ InstanceIds: [existing.InstanceId] }));
|
|
305
|
+
console.log(` šļø Removed AWS EC2 VM "${this.name}"`);
|
|
306
|
+
await this.destroySidecars();
|
|
307
|
+
return { destroyed: this.name };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export function parseAwsTagsForProvision(tags) {
|
|
311
|
+
const tag = (tags ?? []).find((t) => t.Key === "puls-provision");
|
|
312
|
+
if (!tag?.Value)
|
|
313
|
+
return {};
|
|
314
|
+
const record = {};
|
|
315
|
+
const entries = tag.Value.split(",");
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
const parts = entry.trim().split("=");
|
|
318
|
+
if (parts.length === 2) {
|
|
319
|
+
const [name, hash] = parts;
|
|
320
|
+
if (name && hash) {
|
|
321
|
+
record[name.trim()] = hash.trim();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return record;
|
|
326
|
+
}
|
|
327
|
+
export function mergeAwsTagsForProvision(metadata) {
|
|
328
|
+
return Object.entries(metadata)
|
|
329
|
+
.map(([name, hash]) => `${name}=${hash}`)
|
|
330
|
+
.join(",");
|
|
331
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|