@daisy-workflow/plugin-aws-s3 0.1.0
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/.dockerignore +9 -0
- package/.github/workflows/release.yml +185 -0
- package/Dockerfile +20 -0
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/index.js +51 -0
- package/lib/actions.js +403 -0
- package/lib/client.js +169 -0
- package/lib/sigv4.js +176 -0
- package/manifest.json +185 -0
- package/package.json +17 -0
- package/publish-docker.sh +97 -0
package/lib/sigv4.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// AWS Signature Version 4 — minimal implementation supporting both
|
|
2
|
+
// header-based auth (Authorization header) and query-string auth
|
|
3
|
+
// (presigned URLs).
|
|
4
|
+
//
|
|
5
|
+
// Spec: https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
|
6
|
+
// Validated against AWS's published S3 test vector
|
|
7
|
+
// ("Example: GET Object" — examplebucket/test.txt).
|
|
8
|
+
//
|
|
9
|
+
// Built with node:crypto — no external deps.
|
|
10
|
+
|
|
11
|
+
import crypto from "node:crypto";
|
|
12
|
+
|
|
13
|
+
const ALGO = "AWS4-HMAC-SHA256";
|
|
14
|
+
const TERMINATOR = "aws4_request";
|
|
15
|
+
const SERVICE = "s3";
|
|
16
|
+
|
|
17
|
+
const sha256Hex = (data) =>
|
|
18
|
+
crypto.createHash("sha256").update(data).digest("hex");
|
|
19
|
+
const hmac = (key, data) =>
|
|
20
|
+
crypto.createHmac("sha256", key).update(data).digest();
|
|
21
|
+
|
|
22
|
+
// RFC 3986 URI encoding. encodeURIComponent already does most of this;
|
|
23
|
+
// patch up the characters it leaves alone but the spec says to escape.
|
|
24
|
+
function uriEncode(str) {
|
|
25
|
+
return encodeURIComponent(str)
|
|
26
|
+
.replace(/!/g, "%21")
|
|
27
|
+
.replace(/\*/g, "%2A")
|
|
28
|
+
.replace(/'/g, "%27")
|
|
29
|
+
.replace(/\(/g, "%28")
|
|
30
|
+
.replace(/\)/g, "%29");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function canonicalUri(pathname) {
|
|
34
|
+
if (!pathname || pathname === "/") return "/";
|
|
35
|
+
return pathname
|
|
36
|
+
.split("/")
|
|
37
|
+
.map(seg => uriEncode(decodeURIComponent(seg)))
|
|
38
|
+
.join("/");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function canonicalQuery(searchParams) {
|
|
42
|
+
if (!searchParams) return "";
|
|
43
|
+
const entries = [];
|
|
44
|
+
for (const [k, v] of searchParams) entries.push([k, v]);
|
|
45
|
+
entries.sort((a, b) => a[0] === b[0] ? (a[1] < b[1] ? -1 : 1) : (a[0] < b[0] ? -1 : 1));
|
|
46
|
+
return entries.map(([k, v]) => `${uriEncode(k)}=${uriEncode(v ?? "")}`).join("&");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function canonicalHeaders(headers) {
|
|
50
|
+
const lower = {};
|
|
51
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
52
|
+
if (v == null) continue;
|
|
53
|
+
lower[k.toLowerCase()] = String(v).trim().replace(/\s+/g, " ");
|
|
54
|
+
}
|
|
55
|
+
const keys = Object.keys(lower).sort();
|
|
56
|
+
const canonical = keys.map(k => `${k}:${lower[k]}\n`).join("");
|
|
57
|
+
const signed = keys.join(";");
|
|
58
|
+
return { canonical, signed };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function derivSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
62
|
+
const kDate = hmac(`AWS4${secretAccessKey}`, dateStamp);
|
|
63
|
+
const kRegion = hmac(kDate, region);
|
|
64
|
+
const kService = hmac(kRegion, service);
|
|
65
|
+
return hmac(kService, TERMINATOR);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function amzTimestamp(d) {
|
|
69
|
+
return d.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── header-based signing ──────────────────────────────────────────────
|
|
73
|
+
// Returns headers to send (caller headers + Authorization + amz-date +
|
|
74
|
+
// content-sha256, plus security-token if STS).
|
|
75
|
+
export function signRequest({
|
|
76
|
+
method, url, headers = {}, body = "",
|
|
77
|
+
region, service = SERVICE,
|
|
78
|
+
accessKeyId, secretAccessKey, sessionToken = null,
|
|
79
|
+
nowMs,
|
|
80
|
+
}) {
|
|
81
|
+
const u = new URL(url);
|
|
82
|
+
const now = new Date(nowMs ?? Date.now());
|
|
83
|
+
const amzDate = amzTimestamp(now);
|
|
84
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
85
|
+
|
|
86
|
+
const payloadHash = sha256Hex(body || "");
|
|
87
|
+
|
|
88
|
+
const signedHeaders = {
|
|
89
|
+
...headers,
|
|
90
|
+
host: u.host,
|
|
91
|
+
"x-amz-date": amzDate,
|
|
92
|
+
"x-amz-content-sha256": payloadHash,
|
|
93
|
+
};
|
|
94
|
+
if (sessionToken) signedHeaders["x-amz-security-token"] = sessionToken;
|
|
95
|
+
|
|
96
|
+
const { canonical: canonHeaders, signed: signedHeaderList } = canonicalHeaders(signedHeaders);
|
|
97
|
+
|
|
98
|
+
const canonicalRequest = [
|
|
99
|
+
method.toUpperCase(),
|
|
100
|
+
canonicalUri(u.pathname),
|
|
101
|
+
canonicalQuery(u.searchParams),
|
|
102
|
+
canonHeaders,
|
|
103
|
+
signedHeaderList,
|
|
104
|
+
payloadHash,
|
|
105
|
+
].join("\n");
|
|
106
|
+
|
|
107
|
+
const credentialScope = `${dateStamp}/${region}/${service}/${TERMINATOR}`;
|
|
108
|
+
const stringToSign = [ALGO, amzDate, credentialScope, sha256Hex(canonicalRequest)].join("\n");
|
|
109
|
+
|
|
110
|
+
const kSigning = derivSigningKey(secretAccessKey, dateStamp, region, service);
|
|
111
|
+
const signature = hmac(kSigning, stringToSign).toString("hex");
|
|
112
|
+
|
|
113
|
+
const authorization = [
|
|
114
|
+
`${ALGO} Credential=${accessKeyId}/${credentialScope}`,
|
|
115
|
+
`SignedHeaders=${signedHeaderList}`,
|
|
116
|
+
`Signature=${signature}`,
|
|
117
|
+
].join(", ");
|
|
118
|
+
|
|
119
|
+
return { ...signedHeaders, Authorization: authorization };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── presigned URL signing ─────────────────────────────────────────────
|
|
123
|
+
// Returns a URL with X-Amz-* query params baked in. The caller can use
|
|
124
|
+
// it from a browser / curl / IoT device without holding the secret.
|
|
125
|
+
//
|
|
126
|
+
// Notes:
|
|
127
|
+
// • For presigned URLs, x-amz-content-sha256 is the literal string
|
|
128
|
+
// "UNSIGNED-PAYLOAD" (S3 special-cases this).
|
|
129
|
+
// • Only `host` is in SignedHeaders by default. If you need to enforce
|
|
130
|
+
// specific headers at request time, add them to `headers`.
|
|
131
|
+
// • Max expiry is 7 days (604800s) for SigV4.
|
|
132
|
+
export function presignUrl({
|
|
133
|
+
method = "GET", url, headers = {}, expiresIn = 900,
|
|
134
|
+
region, service = SERVICE,
|
|
135
|
+
accessKeyId, secretAccessKey, sessionToken = null,
|
|
136
|
+
nowMs,
|
|
137
|
+
}) {
|
|
138
|
+
if (expiresIn < 1 || expiresIn > 604800) {
|
|
139
|
+
throw new Error(`presignUrl: expiresIn must be 1..604800 (got ${expiresIn})`);
|
|
140
|
+
}
|
|
141
|
+
const u = new URL(url);
|
|
142
|
+
const now = new Date(nowMs ?? Date.now());
|
|
143
|
+
const amzDate = amzTimestamp(now);
|
|
144
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
145
|
+
const credentialScope = `${dateStamp}/${region}/${service}/${TERMINATOR}`;
|
|
146
|
+
|
|
147
|
+
// Sign at minimum `host` (every request has it). Extra headers can be
|
|
148
|
+
// forced via `headers`.
|
|
149
|
+
const signedHeaders = { ...headers, host: u.host };
|
|
150
|
+
const { canonical: canonHeaders, signed: signedHeaderList } = canonicalHeaders(signedHeaders);
|
|
151
|
+
|
|
152
|
+
// Add the X-Amz-* query parameters BEFORE building the canonical
|
|
153
|
+
// query string (they participate in the signature).
|
|
154
|
+
u.searchParams.set("X-Amz-Algorithm", ALGO);
|
|
155
|
+
u.searchParams.set("X-Amz-Credential", `${accessKeyId}/${credentialScope}`);
|
|
156
|
+
u.searchParams.set("X-Amz-Date", amzDate);
|
|
157
|
+
u.searchParams.set("X-Amz-Expires", String(expiresIn));
|
|
158
|
+
u.searchParams.set("X-Amz-SignedHeaders", signedHeaderList);
|
|
159
|
+
if (sessionToken) u.searchParams.set("X-Amz-Security-Token", sessionToken);
|
|
160
|
+
|
|
161
|
+
const canonicalRequest = [
|
|
162
|
+
method.toUpperCase(),
|
|
163
|
+
canonicalUri(u.pathname),
|
|
164
|
+
canonicalQuery(u.searchParams),
|
|
165
|
+
canonHeaders,
|
|
166
|
+
signedHeaderList,
|
|
167
|
+
"UNSIGNED-PAYLOAD",
|
|
168
|
+
].join("\n");
|
|
169
|
+
|
|
170
|
+
const stringToSign = [ALGO, amzDate, credentialScope, sha256Hex(canonicalRequest)].join("\n");
|
|
171
|
+
const kSigning = derivSigningKey(secretAccessKey, dateStamp, region, service);
|
|
172
|
+
const signature = hmac(kSigning, stringToSign).toString("hex");
|
|
173
|
+
|
|
174
|
+
u.searchParams.set("X-Amz-Signature", signature);
|
|
175
|
+
return u.toString();
|
|
176
|
+
}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aws-s3",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AWS S3 connector — buckets, files, folders, plus AWS-specific features (storage classes, server-side encryption, KMS, tags, requester pays, presigned URLs). For non-AWS S3-compatible providers (Wasabi, MinIO, R2, B2…) use the generic `s3` plugin. Mirrors n8n's AWS S3 node. Auth via a workspace `generic` config with accessKeyId + secretAccessKey + region (plus optional sessionToken / customEndpoint).",
|
|
5
|
+
"ui": { "category": "storage", "icon": "amazon-s3" },
|
|
6
|
+
"primaryOutput": "result",
|
|
7
|
+
|
|
8
|
+
"configRefs": [
|
|
9
|
+
{
|
|
10
|
+
"name": "aws-s3",
|
|
11
|
+
"type": "generic",
|
|
12
|
+
"required": true,
|
|
13
|
+
"description": "Generic config: { accessKeyId, secretAccessKey, region, sessionToken?, customEndpoint? }. The endpoint is derived from the region (s3.<region>.amazonaws.com) unless customEndpoint is set — useful for VPC interface endpoints, S3 Transfer Acceleration, AWS GovCloud, or AWS China regions."
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
|
|
17
|
+
"inputSchema": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": ["operation"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"operation": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Which AWS S3 call to run.",
|
|
24
|
+
"enum": [
|
|
25
|
+
"bucket.getAll",
|
|
26
|
+
"bucket.create",
|
|
27
|
+
"bucket.delete",
|
|
28
|
+
"bucket.search",
|
|
29
|
+
"bucket.location",
|
|
30
|
+
"file.getAll",
|
|
31
|
+
"file.head",
|
|
32
|
+
"file.upload",
|
|
33
|
+
"file.download",
|
|
34
|
+
"file.copy",
|
|
35
|
+
"file.delete",
|
|
36
|
+
"file.presignedUrl",
|
|
37
|
+
"folder.create",
|
|
38
|
+
"folder.getAll",
|
|
39
|
+
"folder.delete"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"config": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Workspace config name to read credentials from. Default: 'aws-s3'."
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"bucket": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Bucket name. Required for bucket.create / bucket.delete / bucket.search / bucket.location / file.* / folder.*."
|
|
51
|
+
},
|
|
52
|
+
"key": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Object key (path within the bucket). Required for file.{upload,download,copy,delete,head,presignedUrl}."
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
"prefix": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Key prefix filter. Used by bucket.search / file.getAll / folder.getAll / folder.delete."
|
|
60
|
+
},
|
|
61
|
+
"delimiter": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"default": "/",
|
|
64
|
+
"description": "Delimiter used to group keys into 'folders'. Default '/'. Used by folder.getAll."
|
|
65
|
+
},
|
|
66
|
+
"maxKeys": {
|
|
67
|
+
"type": "integer",
|
|
68
|
+
"minimum": 1,
|
|
69
|
+
"maximum": 1000,
|
|
70
|
+
"default": 1000,
|
|
71
|
+
"description": "Page size for list operations (S3 caps this at 1000)."
|
|
72
|
+
},
|
|
73
|
+
"continuationToken": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "Opaque pagination token from a previous list response."
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
"body": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "Object content for file.upload. Plain text by default; set `bodyEncoding: 'base64'` to upload binary data."
|
|
81
|
+
},
|
|
82
|
+
"bodyEncoding": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"enum": ["utf8", "base64"],
|
|
85
|
+
"default": "utf8",
|
|
86
|
+
"description": "How to interpret `body`. Use 'base64' to upload binary files."
|
|
87
|
+
},
|
|
88
|
+
"contentType": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Content-Type header for file.upload. Default: application/octet-stream (binary) or text/plain (utf8)."
|
|
91
|
+
},
|
|
92
|
+
"metadata": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"description": "User metadata for file.upload. Each key is sent as x-amz-meta-<key>."
|
|
95
|
+
},
|
|
96
|
+
"tags": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"description": "Object tags for file.upload. URL-encoded into the x-amz-tagging header (e.g. {env:'prod', team:'data'} → 'env=prod&team=data')."
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
"acl": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"enum": ["private", "public-read", "public-read-write", "authenticated-read", "aws-exec-read", "bucket-owner-read", "bucket-owner-full-control"],
|
|
104
|
+
"description": "Canned ACL. Used by file.upload / folder.create / bucket.create. Note: many AWS accounts have ACLs disabled via 'Object Ownership = Bucket owner enforced'; in that case the ACL header is rejected."
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
"storageClass": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"enum": ["STANDARD", "REDUCED_REDUNDANCY", "STANDARD_IA", "ONEZONE_IA", "INTELLIGENT_TIERING", "GLACIER", "DEEP_ARCHIVE", "GLACIER_IR"],
|
|
110
|
+
"description": "Storage class for file.upload. AWS S3-specific. GLACIER and DEEP_ARCHIVE need restore-before-read."
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
"serverSideEncryption": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"enum": ["AES256", "aws:kms", "aws:kms:dsse"],
|
|
116
|
+
"description": "Server-side encryption for file.upload. Set ssekmsKeyId when using aws:kms or aws:kms:dsse."
|
|
117
|
+
},
|
|
118
|
+
"ssekmsKeyId": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "KMS key ID or ARN. Required when serverSideEncryption is aws:kms / aws:kms:dsse and you want a non-default key."
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
"requesterPays": {
|
|
124
|
+
"type": "boolean",
|
|
125
|
+
"default": false,
|
|
126
|
+
"description": "Set x-amz-request-payer: requester. Required when reading from or writing to a bucket configured as Requester Pays."
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
"copySource": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Source for file.copy in `bucket/key` form (e.g. 'src-bucket/path/to/file.txt'). Destination is bucket+key."
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
"folderName": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"description": "Folder name for folder.create. Will be created as an empty object with key ending in '/'."
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
"responseEncoding": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"enum": ["utf8", "base64"],
|
|
142
|
+
"default": "base64",
|
|
143
|
+
"description": "How file.download returns the body. 'base64' is safe for binary. 'utf8' for text-only objects."
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
"presignedMethod": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"enum": ["GET", "PUT"],
|
|
149
|
+
"default": "GET",
|
|
150
|
+
"description": "HTTP method to sign for file.presignedUrl."
|
|
151
|
+
},
|
|
152
|
+
"presignedExpiresIn": {
|
|
153
|
+
"type": "integer",
|
|
154
|
+
"minimum": 1,
|
|
155
|
+
"maximum": 604800,
|
|
156
|
+
"default": 900,
|
|
157
|
+
"description": "Expiry in seconds for file.presignedUrl. Max 604800 (7 days)."
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
"region": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"description": "Override the region from the config for this call only."
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
"timeoutMs": {
|
|
166
|
+
"type": "integer",
|
|
167
|
+
"minimum": 1,
|
|
168
|
+
"maximum": 120000,
|
|
169
|
+
"default": 30000
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
"outputSchema": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"required": ["ok", "operation"],
|
|
177
|
+
"properties": {
|
|
178
|
+
"ok": { "type": "boolean" },
|
|
179
|
+
"operation": { "type": "string", "description": "Echo of the input operation that ran." },
|
|
180
|
+
"status": { "type": "integer", "description": "HTTP status from the underlying S3 call (omitted for file.presignedUrl)." },
|
|
181
|
+
"result": { "description": "Operation-specific payload. See README for shapes." },
|
|
182
|
+
"url": { "type": "string", "description": "Deep-link to the affected bucket/object on the AWS endpoint (when applicable)." }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@daisy-workflow/plugin-aws-s3",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Daisy external plugin — AWS S3 connector (AWS-specific: region-derived endpoints, storage classes, server-side encryption, KMS, tags, requester pays, presigned URLs).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@daisy-workflow/plugin-sdk": "^0.1.0"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT"
|
|
17
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# publish-docker.sh — build & push the aws-s3 plugin image to Docker Hub.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# ./publish-docker.sh # build + push :<version> and :latest, multi-arch
|
|
7
|
+
# IMAGE=foo/bar ./publish-docker.sh # override image name
|
|
8
|
+
# PLATFORMS=linux/amd64 ./publish-docker.sh # single-arch
|
|
9
|
+
# PUSH=0 ./publish-docker.sh # build only, don't push
|
|
10
|
+
# NO_LATEST=1 ./publish-docker.sh # skip the :latest tag
|
|
11
|
+
#
|
|
12
|
+
# Prereqs on your machine:
|
|
13
|
+
# - Docker Desktop (or dockerd) running
|
|
14
|
+
# - docker buildx available (comes with Docker Desktop)
|
|
15
|
+
# - You're logged in: docker login -u <dockerhub-user>
|
|
16
|
+
#
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
IMAGE="${IMAGE:-vivek13186/daisy-plugin-aws-s3}"
|
|
20
|
+
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
|
|
21
|
+
PUSH="${PUSH:-1}"
|
|
22
|
+
NO_LATEST="${NO_LATEST:-0}"
|
|
23
|
+
|
|
24
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
cd "$SCRIPT_DIR"
|
|
26
|
+
|
|
27
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
28
|
+
echo "ERROR: node is required to read version from package.json" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
32
|
+
if [[ -z "$VERSION" || "$VERSION" == "undefined" ]]; then
|
|
33
|
+
echo "ERROR: could not read version from package.json" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
38
|
+
echo "ERROR: docker CLI not found in PATH" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
if ! docker buildx version >/dev/null 2>&1; then
|
|
42
|
+
echo "ERROR: docker buildx is required (install Docker Desktop or the buildx plugin)" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
BUILDER="daisy-plugin-builder"
|
|
47
|
+
if ! docker buildx inspect "$BUILDER" >/dev/null 2>&1; then
|
|
48
|
+
echo ">> creating buildx builder: $BUILDER"
|
|
49
|
+
docker buildx create --name "$BUILDER" --driver docker-container --use >/dev/null
|
|
50
|
+
else
|
|
51
|
+
docker buildx use "$BUILDER" >/dev/null
|
|
52
|
+
fi
|
|
53
|
+
docker buildx inspect --bootstrap >/dev/null
|
|
54
|
+
|
|
55
|
+
TAG_LIST=("${IMAGE}:${VERSION}")
|
|
56
|
+
if [[ "$NO_LATEST" != "1" ]]; then
|
|
57
|
+
TAG_LIST+=("${IMAGE}:latest")
|
|
58
|
+
fi
|
|
59
|
+
TAG_FLAGS=()
|
|
60
|
+
for t in "${TAG_LIST[@]}"; do
|
|
61
|
+
TAG_FLAGS+=(--tag "$t")
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
PUSH_FLAG="--push"
|
|
65
|
+
if [[ "$PUSH" == "0" ]]; then
|
|
66
|
+
PUSH_FLAG="--load"
|
|
67
|
+
if [[ "$PLATFORMS" == *","* ]]; then
|
|
68
|
+
echo ">> PUSH=0 requested; forcing single platform linux/amd64 (--load can't load multi-arch)"
|
|
69
|
+
PLATFORMS="linux/amd64"
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
echo ">> image: $IMAGE"
|
|
74
|
+
echo ">> version: $VERSION"
|
|
75
|
+
echo ">> platforms: $PLATFORMS"
|
|
76
|
+
echo ">> tags: ${TAG_LIST[*]}"
|
|
77
|
+
echo ">> action: $([[ "$PUSH" == "1" ]] && echo push || echo build-only)"
|
|
78
|
+
|
|
79
|
+
if [[ "$PUSH" == "1" ]]; then
|
|
80
|
+
if ! docker info 2>/dev/null | grep -q "Username:"; then
|
|
81
|
+
echo ">> not logged in to Docker Hub — running: docker login"
|
|
82
|
+
docker login
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
docker buildx build \
|
|
87
|
+
--platform "$PLATFORMS" \
|
|
88
|
+
"${TAG_FLAGS[@]}" \
|
|
89
|
+
--file Dockerfile \
|
|
90
|
+
$PUSH_FLAG \
|
|
91
|
+
.
|
|
92
|
+
|
|
93
|
+
echo ">> done."
|
|
94
|
+
if [[ "$PUSH" == "1" ]]; then
|
|
95
|
+
echo ">> pulled with: docker pull ${IMAGE}:${VERSION}"
|
|
96
|
+
echo ">> hub page: https://hub.docker.com/r/${IMAGE}"
|
|
97
|
+
fi
|