diff --git a/deno.lock b/deno.lock index 0d758df1b..7a2b9b466 100644 --- a/deno.lock +++ b/deno.lock @@ -2,13 +2,100 @@ "version": "3", "packages": { "specifiers": { - "npm:pg@8.8.0": "npm:pg@8.8.0" + "npm:@types/node": "npm:@types/node@18.16.19", + "npm:@types/pg@^8.6.5": "npm:@types/pg@8.10.9", + "npm:mustache": "npm:mustache@4.2.0", + "npm:pg@8.8.0": "npm:pg@8.8.0", + "npm:ts-json-schema-generator": "npm:ts-json-schema-generator@1.2.0" }, "npm": { + "@types/json-schema@7.0.12": { + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/pg@8.10.9": { + "integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==", + "dependencies": { + "@types/node": "@types/node@18.16.19", + "pg-protocol": "pg-protocol@1.6.0", + "pg-types": "pg-types@4.0.1" + } + }, + "balanced-match@1.0.2": { + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dependencies": {} + }, + "brace-expansion@2.0.1": { + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "balanced-match@1.0.2" + } + }, "buffer-writer@2.0.0": { "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", "dependencies": {} }, + "commander@9.5.0": { + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dependencies": {} + }, + "fs.realpath@1.0.0": { + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dependencies": {} + }, + "glob@8.1.0": { + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "fs.realpath@1.0.0", + "inflight": "inflight@1.0.6", + "inherits": "inherits@2.0.4", + "minimatch": "minimatch@5.1.6", + "once": "once@1.4.0" + } + }, + "inflight@1.0.6": { + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "once@1.4.0", + "wrappy": "wrappy@1.0.2" + } + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dependencies": {} + }, + "json5@2.2.3": { + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dependencies": {} + }, + "minimatch@5.1.6": { + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "brace-expansion@2.0.1" + } + }, + "mustache@4.2.0": { + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dependencies": {} + }, + "normalize-path@3.0.0": { + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dependencies": {} + }, + "obuf@1.1.2": { + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dependencies": {} + }, + "once@1.4.0": { + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "wrappy@1.0.2" + } + }, "packet-reader@1.0.0": { "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", "dependencies": {} @@ -21,6 +108,10 @@ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", "dependencies": {} }, + "pg-numeric@1.0.2": { + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dependencies": {} + }, "pg-pool@3.6.1_pg@8.8.0": { "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", "dependencies": { @@ -41,6 +132,18 @@ "postgres-interval": "postgres-interval@1.2.0" } }, + "pg-types@4.0.1": { + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dependencies": { + "pg-int8": "pg-int8@1.0.1", + "pg-numeric": "pg-numeric@1.0.2", + "postgres-array": "postgres-array@3.0.2", + "postgres-bytea": "postgres-bytea@3.0.0", + "postgres-date": "postgres-date@2.0.1", + "postgres-interval": "postgres-interval@3.0.0", + "postgres-range": "postgres-range@1.1.3" + } + }, "pg@8.8.0": { "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==", "dependencies": { @@ -63,31 +166,105 @@ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "dependencies": {} }, + "postgres-array@3.0.2": { + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dependencies": {} + }, "postgres-bytea@1.0.0": { "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", "dependencies": {} }, + "postgres-bytea@3.0.0": { + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dependencies": { + "obuf": "obuf@1.1.2" + } + }, "postgres-date@1.0.7": { "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "dependencies": {} }, + "postgres-date@2.0.1": { + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dependencies": {} + }, "postgres-interval@1.2.0": { "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "dependencies": { "xtend": "xtend@4.0.2" } }, + "postgres-interval@3.0.0": { + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dependencies": {} + }, + "postgres-range@1.1.3": { + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dependencies": {} + }, + "safe-stable-stringify@2.4.3": { + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dependencies": {} + }, "split2@4.2.0": { "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "dependencies": {} }, + "ts-json-schema-generator@1.2.0": { + "integrity": "sha512-tUMeO3ZvA12d3HHh7T/AK8W5hmUhDRNtqWRHSMN3ZRbUFt+UmV0oX8k1RK4SA+a+BKNHpmW2v06MS49e8Fi3Yg==", + "dependencies": { + "@types/json-schema": "@types/json-schema@7.0.12", + "commander": "commander@9.5.0", + "glob": "glob@8.1.0", + "json5": "json5@2.2.3", + "normalize-path": "normalize-path@3.0.0", + "safe-stable-stringify": "safe-stable-stringify@2.4.3", + "typescript": "typescript@4.9.5" + } + }, + "typescript@4.9.5": { + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dependencies": {} + }, + "wrappy@1.0.2": { + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dependencies": {} + }, "xtend@4.0.2": { "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dependencies": {} } } }, + "redirects": { + "https://esm.sh/v124/@types/escodegen@latest/index.d.ts": "https://esm.sh/v124/@types/escodegen@0.0.9/index.d.ts", + "https://esm.sh/v124/@types/estraverse@^5/index.d.ts": "https://esm.sh/v124/@types/estraverse@5.1.4/index.d.ts", + "https://esm.sh/v124/@types/events@~1.1/index.d.ts": "https://esm.sh/v124/@types/events@1.1.0/index.d.ts", + "https://esm.sh/v124/@types/js-yaml@^4/index.d.ts": "https://esm.sh/v124/@types/js-yaml@4.0.5/index.d.ts", + "https://esm.sh/v124/@types/slice-ansi@~5.0/index.d.ts": "https://esm.sh/v124/@types/slice-ansi@5.0.0/index.d.ts", + "https://esm.sh/v124/@types/tar@^6/index.d.ts": "https://esm.sh/v124/@types/tar@6.1.6/index.d.ts" + }, "remote": { + "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", + "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d", + "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", + "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", + "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37", + "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f", + "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d", + "https://deno.land/std@0.140.0/hash/sha256.ts": "803846c7a5a8a5a97f31defeb37d72f519086c880837129934f5d6f72102a8e8", + "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", + "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", + "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", + "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44", + "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757", + "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21", "https://deno.land/std@0.153.0/_deno_unstable.ts": "4ddb8672d49d58b5bbc4a5a7a2f1b3bce4fd06aa4c8b8476728334391667de7b", "https://deno.land/std@0.153.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", "https://deno.land/std@0.153.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", @@ -363,11 +540,58 @@ "https://deno.land/std@0.177.0/node/internal_binding/uv.ts": "eb0048e30af4db407fb3f95563e30d70efd6187051c033713b0a5b768593a3a3", "https://deno.land/std@0.177.0/node/stream.ts": "09e348302af40dcc7dc58aa5e40fdff868d11d8d6b0cfb85cbb9c75b9fe450c7", "https://deno.land/std@0.177.0/node/string_decoder.ts": "1a17e3572037c512cc5fc4b29076613e90f225474362d18da908cb7e5ccb7e88", + "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.181.0/bytes/bytes_list.ts": "b4cbdfd2c263a13e8a904b12d082f6177ea97d9297274a4be134e989450dfa6a", + "https://deno.land/std@0.181.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2", + "https://deno.land/std@0.181.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://deno.land/std@0.181.0/bytes/ends_with.ts": "4228811ebc71615d27f065c54b5e815ec1972538772b0f413c0efe05245b472e", + "https://deno.land/std@0.181.0/bytes/equals.ts": "b87494ce5442dc786db46f91378100028c402f83a14a2f7bbff6bda7810aefe3", + "https://deno.land/std@0.181.0/bytes/includes_needle.ts": "76a8163126fb2f8bf86fd7f22192c3bb04bf6a20b987a095127c2ca08adf3ba6", + "https://deno.land/std@0.181.0/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24", + "https://deno.land/std@0.181.0/bytes/last_index_of_needle.ts": "7181072883cb4908c6ce8f7a5bb1d96787eef2c2ab3aa94fe4268ab326a53cbf", + "https://deno.land/std@0.181.0/bytes/mod.ts": "e869bba1e7a2e3a9cc6c2d55471888429a544e70a840c087672e656e7ba21815", + "https://deno.land/std@0.181.0/bytes/repeat.ts": "6f5e490d8d72bcbf8d84a6bb04690b9b3eb5822c5a11687bca73a2318a842294", + "https://deno.land/std@0.181.0/bytes/starts_with.ts": "3e607a70c9c09f5140b7a7f17a695221abcc7244d20af3eb47ccbb63f5885135", + "https://deno.land/std@0.181.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", + "https://deno.land/std@0.181.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", + "https://deno.land/std@0.181.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a", + "https://deno.land/std@0.181.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32", + "https://deno.land/std@0.181.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.181.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.181.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.181.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.181.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.181.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c", + "https://deno.land/std@0.181.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.181.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.181.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/std@0.181.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.181.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.181.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f", + "https://deno.land/std@0.182.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.182.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.182.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.182.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", + "https://deno.land/std@0.182.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", + "https://deno.land/std@0.182.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a", + "https://deno.land/std@0.182.0/fs/walk.ts": "920be35a7376db6c0b5b1caf1486fb962925e38c9825f90367f8f26b5e5d0897", + "https://deno.land/std@0.182.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.182.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.182.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.182.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.182.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.182.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c", + "https://deno.land/std@0.182.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.182.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.182.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", "https://deno.land/std@0.192.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", "https://deno.land/std@0.192.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", "https://deno.land/std@0.192.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", "https://deno.land/std@0.192.0/collections/_utils.ts": "5114abc026ddef71207a79609b984614e66a63a4bda17d819d56b0e72c51527e", "https://deno.land/std@0.192.0/collections/deep_merge.ts": "5a8ed29030f4471a5272785c57c3455fa79697b9a8f306013a8feae12bafc99a", + "https://deno.land/std@0.192.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", "https://deno.land/std@0.192.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978", "https://deno.land/std@0.192.0/fs/copy.ts": "14214efd94fc3aa6db1e4af2b4b9578e50f7362b7f3725d5a14ad259a5df26c8", "https://deno.land/std@0.192.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", @@ -382,28 +606,20 @@ "https://deno.land/std@0.192.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", "https://deno.land/std@0.192.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", "https://deno.land/std@0.192.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", - "https://deno.land/std@0.195.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", - "https://deno.land/std@0.195.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", - "https://deno.land/std@0.195.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.192.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.192.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.192.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", + "https://deno.land/std@0.192.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f", + "https://deno.land/std@0.192.0/testing/bdd.ts": "59f7f7503066d66a12e50ace81bfffae5b735b6be1208f5684b630ae6b4de1d0", + "https://deno.land/std@0.192.0/testing/mock.ts": "220ed9b8151cb2cac141043d4cfea7c47673fab5d18d1c1f0943297c8afb5d13", "https://deno.land/std@0.195.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d", - "https://deno.land/std@0.195.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978", - "https://deno.land/std@0.195.0/fs/copy.ts": "b4f7fe87190d7b310c88a2d9ff845210c0a2b7b0a094ec509747359023beb7d6", - "https://deno.land/std@0.195.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", - "https://deno.land/std@0.195.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.195.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", - "https://deno.land/std@0.195.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", - "https://deno.land/std@0.195.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", - "https://deno.land/std@0.195.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", - "https://deno.land/std@0.195.0/path/mod.ts": "f065032a7189404fdac3ad1a1551a9ac84751d2f25c431e101787846c86c79ef", - "https://deno.land/std@0.195.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", - "https://deno.land/std@0.195.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", - "https://deno.land/std@0.195.0/path/win32.ts": "4fca292f8d116fd6d62f243b8a61bd3d6835a9f0ede762ba5c01afe7c3c0aa12", "https://deno.land/std@0.50.0/path/_constants.ts": "f6c332625f21d49d5a69414ba0956ac784dbf4b26a278041308e4914ba1c7e2e", "https://deno.land/std@0.50.0/path/_util.ts": "b678a7ecbac6b04c1166832ae54e1024c0431dd2b340b013c46eb2956ab24d4c", "https://deno.land/std@0.50.0/path/interface.ts": "89f6e68b0e3bba1401a740c8d688290957de028ed86f95eafe76fe93790ae450", "https://deno.land/std@0.50.0/path/posix.ts": "b742fe902d5d6821c39c02319eb32fc5a92b4d4424b533c47f1a50610afbf381", "https://deno.land/x/cliffy@v0.25.7/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004", "https://deno.land/x/cliffy@v0.25.7/ansi/ansi_escapes.ts": "885f61f343223f27b8ec69cc138a54bea30542924eacd0f290cd84edcf691387", + "https://deno.land/x/cliffy@v0.25.7/ansi/chain.ts": "31fb9fcbf72fed9f3eb9b9487270d2042ccd46a612d07dd5271b1a80ae2140a0", "https://deno.land/x/cliffy@v0.25.7/ansi/colors.ts": "5f71993af5bd1aa0a795b15f41692d556d7c89584a601fed75997df844b832c9", "https://deno.land/x/cliffy@v0.25.7/ansi/cursor_position.ts": "d537491e31d9c254b208277448eff92ff7f55978c4928dea363df92c0df0813f", "https://deno.land/x/cliffy@v0.25.7/ansi/deps.ts": "0f35cb7e91868ce81561f6a77426ea8bc55dc15e13f84c7352f211023af79053", @@ -453,6 +669,7 @@ "https://deno.land/x/cliffy@v0.25.7/flags/types/string.ts": "e89b6a5ce322f65a894edecdc48b44956ec246a1d881f03e97bbda90dd8638c5", "https://deno.land/x/cliffy@v0.25.7/keycode/key_code.ts": "c4ab0ffd102c2534962b765ded6d8d254631821bf568143d9352c1cdcf7a24be", "https://deno.land/x/cliffy@v0.25.7/keycode/key_codes.ts": "917f0a2da0dbace08cf29bcfdaaa2257da9fe7e705fff8867d86ed69dfb08cfe", + "https://deno.land/x/cliffy@v0.25.7/keycode/mod.ts": "292d2f295316c6e0da6955042a7b31ab2968ff09f2300541d00f05ed6c2aa2d4", "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_input.ts": "737cff2de02c8ce35250f5dd79c67b5fc176423191a2abd1f471a90dd725659e", "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_list.ts": "79b301bf09eb19f0d070d897f613f78d4e9f93100d7e9a26349ef0bfaa7408d2", "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_prompt.ts": "8630ce89a66d83e695922df41721cada52900b515385d86def597dea35971bb2", @@ -478,8 +695,45 @@ "https://deno.land/x/cliffy@v0.25.7/table/row.ts": "5f519ba7488d2ef76cbbf50527f10f7957bfd668ce5b9169abbc44ec88302645", "https://deno.land/x/cliffy@v0.25.7/table/table.ts": "ec204c9d08bb3ff1939c5ac7412a4c9ed7d00925d4fc92aff9bfe07bd269258d", "https://deno.land/x/cliffy@v0.25.7/table/utils.ts": "187bb7dcbcfb16199a5d906113f584740901dfca1007400cba0df7dcd341bc29", + "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5", + "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff", + "https://deno.land/x/deno_cache@0.4.1/auth_tokens.ts": "5fee7e9155e78cedf3f6ff3efacffdb76ac1a76c86978658d9066d4fb0f7326e", + "https://deno.land/x/deno_cache@0.4.1/cache.ts": "51f72f4299411193d780faac8c09d4e8cbee951f541121ef75fcc0e94e64c195", + "https://deno.land/x/deno_cache@0.4.1/deno_dir.ts": "f2a9044ce8c7fe1109004cda6be96bf98b08f478ce77e7a07f866eff1bdd933f", + "https://deno.land/x/deno_cache@0.4.1/deps.ts": "8974097d6c17e65d9a82d39377ae8af7d94d74c25c0cbb5855d2920e063f2343", + "https://deno.land/x/deno_cache@0.4.1/dirs.ts": "d2fa473ef490a74f2dcb5abb4b9ab92a48d2b5b6320875df2dee64851fa64aa9", + "https://deno.land/x/deno_cache@0.4.1/disk_cache.ts": "1f3f5232cba4c56412d93bdb324c624e95d5dd179d0578d2121e3ccdf55539f9", + "https://deno.land/x/deno_cache@0.4.1/file_fetcher.ts": "07a6c5f8fd94bf50a116278cc6012b4921c70d2251d98ce1c9f3c352135c39f7", + "https://deno.land/x/deno_cache@0.4.1/http_cache.ts": "f632e0d6ec4a5d61ae3987737a72caf5fcdb93670d21032ddb78df41131360cd", + "https://deno.land/x/deno_cache@0.4.1/mod.ts": "ef1cda9235a93b89cb175fe648372fc0f785add2a43aa29126567a05e3e36195", + "https://deno.land/x/deno_cache@0.4.1/util.ts": "8cb686526f4be5205b92c819ca2ce82220aa0a8dd3613ef0913f6dc269dbbcfe", "https://deno.land/x/dir@1.5.1/home_dir/mod.ts": "53777e4fb2586ae02ce94adf2c99f2edb2dcf6e72d07ac289b71593cfacbdbbf", + "https://deno.land/x/dnt@0.36.0/lib/compiler.ts": "209ad2e1b294f93f87ec02ade9a0821f942d2e524104552d0aa8ff87021050a5", + "https://deno.land/x/dnt@0.36.0/lib/compiler_transforms.ts": "cbb1fd5948f5ced1aa5c5aed9e45134e2357ce1e7220924c1d7bded30dcd0dd0", + "https://deno.land/x/dnt@0.36.0/lib/mod.deps.ts": "30367fc68bcd2acf3b7020cf5cdd26f817f7ac9ac35c4bfb6c4551475f91bc3e", + "https://deno.land/x/dnt@0.36.0/lib/npm_ignore.ts": "b430caa1905b65ae89b119d84857b3ccc3cb783a53fc083d1970e442f791721d", + "https://deno.land/x/dnt@0.36.0/lib/package_json.ts": "61f35b06e374ed39ca776d29d67df4be7ee809d0bca29a8239687556c6d027c2", + "https://deno.land/x/dnt@0.36.0/lib/pkg/dnt_wasm.generated.js": "1f21e46f32209d8746fd5c7d3bc8f25c2cbebe1d9da4ef083eca1b621eb72598", + "https://deno.land/x/dnt@0.36.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "a6b95adc943a68d513fe8ed9ec7d260ac466b7a4bced4e942f733e494bb9f1be", + "https://deno.land/x/dnt@0.36.0/lib/shims.ts": "df1bd4d9a196dca4b2d512b1564fff64ac6c945189a273d706391f87f210d7e6", + "https://deno.land/x/dnt@0.36.0/lib/test_runner/get_test_runner_code.ts": "4dc7a73a13b027341c0688df2b29a4ef102f287c126f134c33f69f0339b46968", + "https://deno.land/x/dnt@0.36.0/lib/test_runner/test_runner.ts": "4d0da0500ec427d5f390d9a8d42fb882fbeccc92c92d66b6f2e758606dbd40e6", + "https://deno.land/x/dnt@0.36.0/lib/transform.deps.ts": "e42f2bdef46d098453bdba19261a67cf90b583f5d868f7fe83113c1380d9b85c", + "https://deno.land/x/dnt@0.36.0/lib/types.ts": "b8e228b2fac44c2ae902fbb73b1689f6ab889915bd66486c8a85c0c24255f5fb", + "https://deno.land/x/dnt@0.36.0/lib/utils.ts": "878b7ac7003a10c16e6061aa49dbef9b42bd43174853ebffc9b67ea47eeb11d8", + "https://deno.land/x/dnt@0.36.0/mod.ts": "670f1820f2115e6b6aa4f79999bc796e30cc0d0b45096b84a4e1db9f62b82984", + "https://deno.land/x/dnt@0.36.0/transform.ts": "1b127c5f22699c8ab2545b98aeca38c4e5c21405b0f5342ea17e9c46280ed277", + "https://deno.land/x/mock_file@v1.1.2/mod.ts": "57b111ba84b5611c09ed82ef300dd063eb278ef68bd286d5149e5b018eb8948d", + "https://deno.land/x/mock_file@v1.1.2/src/memory_file.ts": "24817e782c819cdfb48b9459dc8ad568e8fb2cd2934cd5b775688147e6dc4cbd", + "https://deno.land/x/mock_file@v1.1.2/src/polyfill.ts": "f10f05ec2493cb1e029b30cd211a21803b7683d811813d519696aa2939529b35", + "https://deno.land/x/ts_morph@18.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9", + "https://deno.land/x/ts_morph@18.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06", + "https://deno.land/x/ts_morph@18.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d", + "https://deno.land/x/ts_morph@18.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed", + "https://deno.land/x/ts_morph@18.0.0/common/ts_morph_common.js": "845671ca951073400ce142f8acefa2d39ea9a51e29ca80928642f3f8cf2b7700", + "https://deno.land/x/ts_morph@18.0.0/common/typescript.js": "d5c598b6a2db2202d0428fca5fd79fc9a301a71880831a805d778797d2413c59", "https://esm.sh/ansi-escapes@6.2.0": "508d1d1d8f44b52c45e9c701cd5a78c56d7764a14d005d5328f0cff72b0291b4", + "https://esm.sh/mustache@4.2.0": "da4f1d45c3fec1e78a516e6ce01ab4582ffc5b868ace8a244cc640fc25282c60", "https://esm.sh/v124/*acorn-loose@8.4.0": "5255c5344625c52612c421cb99b66d401af8772342a5de8bac38d1d78d0f94b4", "https://esm.sh/v124/*log-update@5.0.1": "49199324effae77a660f21ea75937847315fdb5f67a86881496717314bb95b2a", "https://esm.sh/v124/@aws-crypto/crc32@3.0.0/denonext/crc32.mjs": "28103a74b87c49846c0f2851adc7d6ca8f8d677f079819cd786d8f9b0af5e08e", @@ -627,6 +881,7 @@ "https://esm.sh/v124/ajv@8.11.0/denonext/dist/vocabularies/validation/required.js": "945988a3e33e21322f240fa5304a3dbbb4a80b2cf8c56032bfd27df9294c9793", "https://esm.sh/v124/ajv@8.11.0/denonext/dist/vocabularies/validation/uniqueItems.js": "77bf5a6f1bdd56129e0a217865e9d2beb2fd6623a6b288cc4b5af93c49cad69b", "https://esm.sh/v124/ajv@8.11.0/dist/2019.js": "646df403ae1a0d94045d530fe3b7ca65e4bd4a1163e6041d34bc9d321bb7334f", + "https://esm.sh/v124/ajv@8.11.0/dist/core": "7af19c4e1b07edc97c97d9a4e1b2859154d626fc9d10b618c8fda3b178ce26c0", "https://esm.sh/v124/ajv@8.12.0": "7621f26ca63271c26d3c0d3812be6a826809bb56036db0bf3ffb3e82bb7482d1", "https://esm.sh/v124/ajv@8.12.0/denonext/ajv.mjs": "dd2ce45ec01492d9d6bf91bc6bf0e2608fc8977d70cc4bbc0d2d7c1453d514b9", "https://esm.sh/v124/ajv@8.12.0/denonext/dist/2019.js": "26aae2b10cb7a43fe6527beae125ade89406357ddb58019e63d3414ca7c6a3f2", @@ -635,7 +890,10 @@ "https://esm.sh/v124/ansi-styles@6.2.1/denonext/ansi-styles.mjs": "d40ccc0e5be3eced4c723f48dbdb5249efa00b7f8561a90999b7487db3ee5c09", "https://esm.sh/v124/async@3.2.4/denonext/forEach.js": "c265bae8dbe1a8b9bbc26bf012f89f0c37fbb5864df1232af585596f13896ace", "https://esm.sh/v124/async@3.2.4/denonext/series.js": "a555746f7a024becc7b63f37ed3cf00aaa32302855e0c07cdfc0b3aa9abee3c2", + "https://esm.sh/v124/base64-js@1.5.1/denonext/base64-js.mjs": "fc961905a7e7a9a1c753f006903a610306d299569a0b3af9c48318a28ac48b84", "https://esm.sh/v124/bowser@2.11.0/denonext/bowser.mjs": "9876a6f4cb94547829dc82a448b1b7a02b590a819aeb0a29f7e6416fca3a446a", + "https://esm.sh/v124/buffer@4.9.2": "97db8c9b70e6b09f4a09440a62cb9a8a577c1a28787ece11dcaaed918d790840", + "https://esm.sh/v124/buffer@4.9.2/denonext/buffer.mjs": "b41fd0012d60aea2ec448dddefa54c90b4d9dd2a3a1bdb224679c8861a9cb28f", "https://esm.sh/v124/chownr@2.0.0/denonext/chownr.mjs": "230f6e23902418045f5057ff9a7cee6a7a28a21655eeb98f5671395d32839a1a", "https://esm.sh/v124/cli-cursor@4.0.0": "2ed1740a92df113d48f30ac950b174042c2f904556219b3165f04d4e50392cac", "https://esm.sh/v124/cli-cursor@4.0.0/denonext/cli-cursor.mjs": "0904486398e7456058b337418d8af5338924f07682b96f1b3016a01b94009ecc", @@ -648,6 +906,8 @@ "https://esm.sh/v124/estraverse@5.3.0": "a172212d2f4a87f930430cb7e0462cb507891b9f390e17025533f572d895092c", "https://esm.sh/v124/estraverse@5.3.0/denonext/estraverse.mjs": "3e3574b932e1b7c1476f467e6103559d7b978df412e50b6b97299679d4556004", "https://esm.sh/v124/esutils@2.0.3/denonext/esutils.mjs": "56b9038ae71de0a374c86435a347793030afc96deb151466a5009cb5384a85d0", + "https://esm.sh/v124/events@1.1.1": "aa42c257bbfdbd6e635628110bc65ef7d66d7110ffcdcdf894e3afc722e0c87b", + "https://esm.sh/v124/events@1.1.1/denonext/events.mjs": "cafdffd6a39d45b8282efe4b29a0b902ce76e3fd426840fb54eb3e6cec208fab", "https://esm.sh/v124/fast-deep-equal@3.1.3/denonext/fast-deep-equal.mjs": "6313b3e05436550e1c0aeb2a282206b9b8d9213b4c6f247964dd7bb4835fb9e5", "https://esm.sh/v124/fast-xml-parser@4.2.5/denonext/fast-xml-parser.mjs": "0132711d650c1ba44721b4bcf6e0636cd0e0f81894694e8e5b36242434eceb9c", "https://esm.sh/v124/fecha@4.2.3/denonext/fecha.mjs": "d87efb3f6dd068229dcaaf6ce19072720634695668c49da74e6d7289fc67ee48", @@ -655,9 +915,11 @@ "https://esm.sh/v124/fs-minipass@2.1.0/denonext/fs-minipass.mjs": "e3ee6d0df9871f490d05b070b47cd6495f8f33ae4eed90167242cd9819c82cca", "https://esm.sh/v124/hcl2-json-parser@1.0.1": "c50574c485772ae59ed44a3b8f679b85e6ff61788b76f9849e3da1065c249823", "https://esm.sh/v124/hcl2-json-parser@1.0.1/denonext/hcl2-json-parser.mjs": "a51581a1181c31284bfb1ff0907f79a34b248e3fcb1b333c986b8273781fb39f", + "https://esm.sh/v124/ieee754@1.2.1/denonext/ieee754.mjs": "9ec2806065f50afcd4cf3f3f2f38d93e777a92a5954dda00d219f57d4b24832f", "https://esm.sh/v124/inherits@2.0.4/denonext/inherits.mjs": "8095f3d6aea060c904fb24ae50f2882779c0acbe5d56814514c8b5153f3b4b3b", "https://esm.sh/v124/is-fullwidth-code-point@4.0.0/denonext/is-fullwidth-code-point.mjs": "e9801323732e385027651be9a815a58a6f340484316c9f6ed27a0eec6c48c161", "https://esm.sh/v124/is-stream@2.0.1/denonext/is-stream.mjs": "456bc690361378c52fac24a7f362044f036942b1298e812799a67882d6c1febf", + "https://esm.sh/v124/isarray@1.0.0/denonext/isarray.mjs": "6368a41cf02c83843453ac571deb4c393c14e6f5e1d9ca6bbe43a4623f3856c8", "https://esm.sh/v124/isexe@2.0.0/denonext/isexe.mjs": "733c6d04662ec8e40a169a321fcdfaaafb5fce27a3113408c09f17becbd1acd8", "https://esm.sh/v124/js-yaml@4.1.0": "546cd3718747e8cc7d431a079df570c6c62417b338387f6e0a059b4c102d3847", "https://esm.sh/v124/js-yaml@4.1.0/denonext/js-yaml.mjs": "b4e4f1b1cadcc873d4079f242cdc811b1a36fdd0eb01b9b54ca2ae6e2ff2a92f", @@ -674,6 +936,8 @@ "https://esm.sh/v124/ms@2.1.3/denonext/ms.mjs": "0f06597e493998793b8f52232868976128fca326eec3bcd56f10e66e6efd1839", "https://esm.sh/v124/one-time@1.0.0/denonext/one-time.mjs": "b3a4b3d5de194c6f2bf7a9556607a622f9ac27b08a35ac6e87d9f9e7580afcc4", "https://esm.sh/v124/onetime@5.1.2/denonext/onetime.mjs": "dcdb53bfa68b0d43d301c1fd4bb131150a6e58ba843db9b61e6dcd282c9434da", + "https://esm.sh/v124/querystring@0.2.0": "c426159c0d0a6c8732e9e3e976230edb77cb7b225ae7f5b4fc1c131563bf9537", + "https://esm.sh/v124/querystring@0.2.0/denonext/querystring.mjs": "0cc5ad58cddf88b56ccd9bbb47ecdb8707f037c9a25ad47cf38a7fdc4189ae1b", "https://esm.sh/v124/readable-stream@3.6.2/denonext/lib/_stream_writable.js": "fcd380d7da84d0708692263c1e46b1a673d7ab64da9f654b73abfad0219a7560", "https://esm.sh/v124/readable-stream@3.6.2/denonext/readable-stream.mjs": "061037c060d88267d4cbbd57688f0bf2ed6cceda918e17d407f83248a6aad672", "https://esm.sh/v124/restore-cursor@4.0.0/denonext/restore-cursor.mjs": "9c2be16e5018dcfeed7503897c1113d9427016b1b907013ee36cb1aa27173fb9", @@ -700,6 +964,8 @@ "https://esm.sh/v124/unique-names-generator@4.7.1": "6d61270c703d91c6fb27bd9b68a06f241659c3f01df03f76f4e8b540acf45c44", "https://esm.sh/v124/unique-names-generator@4.7.1/denonext/unique-names-generator.mjs": "b40a40ff6a8533a2f35c6a4b6883a5e5737d75287f2d635a863176247bcb7f19", "https://esm.sh/v124/uri-js@4.4.1/denonext/uri-js.mjs": "4b46d46cd678979debedbf2d05fa39b50ec17656e3960a9ec3418e7763aa7c34", + "https://esm.sh/v124/url@0.10.3": "07386e574238e0e64451b25c1d699146b6bace90f58e1e751dae337d003fe44c", + "https://esm.sh/v124/url@0.10.3/denonext/url.mjs": "ba58c05c534164ab58a98e7e9f9ba67400750a2d71031658c4580d7112ac4144", "https://esm.sh/v124/util-deprecate@1.0.2/denonext/util-deprecate.mjs": "e7f4e3a1ec5eb3f2e04dbfaf90e4874f535ca298a7c91512f1d083e1ba765c37", "https://esm.sh/v124/uuid@8.3.2/denonext/uuid.mjs": "c0da38266b24ac79d62b02290f17caba7e75d078f6771bbe8fb61bdef60f837c", "https://esm.sh/v124/which@3.0.1": "f27ee01cac772029a95d37fad83158ebe6a6cc4222ee9396f5f54bfe4903da6c", @@ -711,6 +977,7 @@ "https://esm.sh/v124/wrap-ansi@8.1.0": "da576a84619b45bf324bfb582b74f3c38b23deb5abb204d3b3aca329f6cd970d", "https://esm.sh/v124/wrap-ansi@8.1.0/denonext/wrap-ansi.mjs": "1804d60795c6034323e4b8dc92425aa9ab28385b87bb700772e16cb92bb0d8f8", "https://esm.sh/v124/yallist@4.0.0/denonext/yallist.mjs": "61f180d807dda50bac17028eda05d5722a3fecef6e98a9064e2353ea6864fd82", + "https://esm.sh/v128/mustache@4.2.0/denonext/mustache.mjs": "8c046aa4755b9beb2f203e01fdab7d36e4c75dd1bc22021be44ac31723ead9f0", "https://esm.sh/v129/ansi-escapes@6.2.0/denonext/ansi-escapes.mjs": "e82f350a4e3ef3113cd99b6d99513de1821bb9ae50bb4ed1dbbd6b9789af560d" } } diff --git a/examples/datacenters/local/datacenter.arc b/examples/datacenters/local/datacenter.arc index 4f730ffd6..dd3036f89 100644 --- a/examples/datacenters/local/datacenter.arc +++ b/examples/datacenters/local/datacenter.arc @@ -19,6 +19,7 @@ module "traefik" { name = "${datacenter.name}-gateway" image = "traefik:v2.10" command = [ + "--accesslog=true", "--providers.docker=true", "--api.insecure=true", "--api.dashboard=true" @@ -68,6 +69,57 @@ environment { } } + module "staticWebServer" { + when = contains(environment.nodes.*.type, "bucket") + build = "./deployment" + + volume { + host_path = "/var/run/docker.sock" + mount_path = "/var/run/docker.sock" + } + + volume { + host_path = "${var.secretsDir}/${environment.name}/buckets/" + mount_path = "/usr/share/nginx/html" + } + + environment = { + DOCKER_HOST = "unix:///var/run/docker.sock" + } + + inputs = { + name = "${environment.name}-static-web-server" + image = "nginx" + } + } + + module "localstack" { + // when = contains(environment.nodes.*.type, "bucket") + build = "./deployment" + + volume { + host_path = "/var/run/docker.sock" + mount_path = "/var/run/docker.sock" + } + + environment = { + DOCKER_HOST = "unix:///var/run/docker.sock" + } + + inputs = { + name = "localstack" + image = "localstack/localstack" + ports = [{ + internal = 4566 + external = 4566 + }] + environment = { + DOCKER_HOST = "unix:///var/run/docker.sock" + GATEWAY_LISTEN = "0.0.0.0:4566" + } + } + } + database { when = node.inputs.databaseType == "postgres" @@ -203,4 +255,111 @@ environment { image = module.build.image } } + + volume { + module "volume" { + build = "./volume" + + environment = { + DOCKER_HOST = "unix:///var/run/docker.sock" + } + + volume { + host_path = "/var/run/docker.sock" + mount_path = "/var/run/docker.sock" + } + + inputs = { + name = "${node.component}-${node.name}" + } + } + + outputs = { + id = module.volume.id + } + } + + task { + module "task" { + build = "./deployment" + + environment = { + DOCKER_HOST = "unix:///var/run/docker.sock" + } + + volume { + host_path = "/var/run/docker.sock" + mount_path = "/var/run/docker.sock" + } + + inputs = "${merge(node.inputs, { + volume_mounts = merge(node.inputs.volume_mounts, [{ + host_path = "/var/run/docker.sock", + mount_path = "/var/run/docker.sock" + }]) + })}" + } + } + + // bucket { + // module "dynamicBucket" { + // when = node.inputs.deploy + // build = "./deployment" + + // environment = { + // DOCKER_HOST = "unix:///var/run/docker.sock" + // } + + // volume { + // host_path = "/var/run/docker.sock" + // mount_path = "/var/run/docker.sock" + // } + + // # This volume is shared with the nginx webserver + // volume { + // host_path = "${var.secretsDir}/${environment.name}/buckets/" + // mount_path = "/data" + // } + + // inputs = merge(node.inputs.deploy, { + // volume_mounts = [{ + // host_path = "/data" + // mount_path = node.inputs.deploy.publish + // }] + // }) + // } + + // module "staticBucket" { + // when = node.inputs.directory + // build = "./deployment" + + // environment = { + // DOCKER_HOST = "unix:///var/run/docker.sock" + // } + + // volume { + // host_path = "/var/run/docker.sock" + // mount_path = "/var/run/docker.sock" + // } + + // # This volume is shared with the nginx webserver + // volume { + // host_path = "${var.secretsDir}/${environment.name}/buckets/" + // mount_path = "/data" + // } + + // inputs = { + // image = "alpine" + // command = [ + // "sh", + // "-c", + // "cp -r ${node.inputs.directory} /data" + // ] + // volume_mounts = [{ + // host_path = "/data" + // mount_path = "/data" + // }] + // } + // } + // } } \ No newline at end of file diff --git a/examples/datacenters/local/deployment/index.ts b/examples/datacenters/local/deployment/index.ts index 3cf7ef1b6..97e5aeab9 100644 --- a/examples/datacenters/local/deployment/index.ts +++ b/examples/datacenters/local/deployment/index.ts @@ -6,7 +6,7 @@ const config = new pulumi.Config(); export const name = config.require('name'); type Config = { - name?: string; + name: string; image: string; command?: string[]; entrypoint?: string[]; @@ -90,23 +90,26 @@ for (const key in inputServices) { const inputIngresses = config.getObject('ingresses') || []; for (const key in inputIngresses) { const value = inputIngresses[key]; + const routerKey = value.subdomain.replace(/\./g, '-').replace(/\*/g, 'star'); if (value.protocol === 'http') { labels.push({ - label: `traefik.http.routers.${value.subdomain}.rule`, - value: `Host(\`${value.host}\`) && PathPrefix(\`${value.path || '/'}\`)`, + label: `traefik.http.routers.${routerKey}.rule`, + value: value.host.includes('*') + ? `HostRegexp(\`${value.host.replace('*', '{subdomain:[a-z_-]+}')}\`) && PathPrefix(\`${value.path || '/'}\`)` + : `Host(\`${value.host}\`) && PathPrefix(\`${value.path || '/'}\`)`, }, { - label: `traefik.http.routers.${value.subdomain}.service`, + label: `traefik.http.routers.${routerKey}.service`, value: value.service, }); } else { labels.push({ - label: `traefik.tcp.routers.${value.subdomain}.rule`, + label: `traefik.tcp.routers.${routerKey}.rule`, value: `HostSNI(\`${value.host}\`)` }, { - label: `traefik.tcp.routers.${value.subdomain}.service`, + label: `traefik.tcp.routers.${routerKey}.service`, value: value.service, }, { - label: `traefik.tcp.routers.${value.subdomain}.tls.passthrough`, + label: `traefik.tcp.routers.${routerKey}.tls.passthrough`, value: 'true', }); } diff --git a/examples/datacenters/local/network/index.ts b/examples/datacenters/local/network/index.ts index 68f34f037..94ea98baa 100644 --- a/examples/datacenters/local/network/index.ts +++ b/examples/datacenters/local/network/index.ts @@ -3,22 +3,6 @@ import * as pulumi from "@pulumi/pulumi"; let config = new pulumi.Config(); -type Config = { - name?: string; - image: string; - command?: string[]; - labels?: Record; - services?: Record; - ports?: { - internal: number; - external?: number; - }[]; -}; - const network = new docker.Network('network', { name: config.get('name'), }); diff --git a/src/@resources/bucket/README.md b/src/@resources/bucket/README.md new file mode 100644 index 000000000..94d562ae3 --- /dev/null +++ b/src/@resources/bucket/README.md @@ -0,0 +1 @@ +# The `bucket` resource diff --git a/src/@resources/bucket/inputs.schema.json b/src/@resources/bucket/inputs.schema.json new file mode 100644 index 000000000..be9991879 --- /dev/null +++ b/src/@resources/bucket/inputs.schema.json @@ -0,0 +1,10 @@ +{ + "$ref": "#/definitions/BucketInputs", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "BucketInputs": { + "additionalProperties": {}, + "type": "object" + } + } +} \ No newline at end of file diff --git a/src/@resources/bucket/inputs.ts b/src/@resources/bucket/inputs.ts new file mode 100644 index 000000000..5fa0374b3 --- /dev/null +++ b/src/@resources/bucket/inputs.ts @@ -0,0 +1,3 @@ +export type BucketInputs = Record; + +export default BucketInputs; diff --git a/src/@resources/bucket/outputs.schema.json b/src/@resources/bucket/outputs.schema.json new file mode 100644 index 000000000..66d466a18 --- /dev/null +++ b/src/@resources/bucket/outputs.schema.json @@ -0,0 +1,46 @@ +{ + "$ref": "#/definitions/BucketOutputs", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "BucketOutputs": { + "additionalProperties": false, + "properties": { + "access_key_id": { + "description": "Access key ID used to authenticate with the bucket", + "type": "string" + }, + "endpoint": { + "description": "Endpoint that hosts the bucket", + "examples": [ + "https://nyc3.digitaloceanspaces.com", + "https://bucket.s3.region.amazonaws.com" + ], + "type": "string" + }, + "id": { + "description": "Unique ID of the bucket that was created", + "examples": [ + "abc123" + ], + "type": "string" + }, + "region": { + "description": "Region the bucket was created in", + "type": "string" + }, + "secret_access_key": { + "description": "Secret access key used to authenticate with the bucket", + "type": "string" + } + }, + "required": [ + "id", + "endpoint", + "region", + "access_key_id", + "secret_access_key" + ], + "type": "object" + } + } +} \ No newline at end of file diff --git a/src/@resources/bucket/outputs.ts b/src/@resources/bucket/outputs.ts new file mode 100644 index 000000000..e95275859 --- /dev/null +++ b/src/@resources/bucket/outputs.ts @@ -0,0 +1,32 @@ +export type BucketOutputs = { + /** + * Unique ID of the bucket that was created + * @example "abc123" + */ + id: string; + + /** + * Endpoint that hosts the bucket + * + * @example "https://nyc3.digitaloceanspaces.com" + * @example "https://bucket.s3.region.amazonaws.com" + */ + endpoint: string; + + /** + * Region the bucket was created in + */ + region: string; + + /** + * Access key ID used to authenticate with the bucket + */ + access_key_id: string; + + /** + * Secret access key used to authenticate with the bucket + */ + secret_access_key: string; +}; + +export default BucketOutputs; diff --git a/src/@resources/ingress/inputs.schema.json b/src/@resources/ingress/inputs.schema.json index 7bb1a907c..b46e9eeb7 100644 --- a/src/@resources/ingress/inputs.schema.json +++ b/src/@resources/ingress/inputs.schema.json @@ -3,116 +3,183 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "IngressRuleInputs": { - "additionalProperties": false, - "properties": { - "headers": { - "additionalProperties": { - "type": "string" - }, - "description": "Headers to include in responses", - "examples": [ - { - "X-Frame-Options": "DENY" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Headers to include in responses", + "examples": [ + { + "X-Frame-Options": "DENY" + } + ], + "type": "object" + }, + "internal": { + "default": false, + "description": "Whether or not this should be fulfilled by an internal load balancer (e.g. no public IP)", + "type": "boolean" + }, + "password": { + "description": "Basic auth password", + "examples": [ + "password" + ], + "type": "string" + }, + "path": { + "default": "/", + "description": "The path the ingress rule listens on", + "type": "string" + }, + "protocol": { + "default": "http", + "description": "The protocol the ingress rule listens for traffic on", + "type": "string" + }, + "service": { + "additionalProperties": false, + "description": "The configuration details of the target service", + "properties": { + "host": { + "description": "The hostname the service is listening on", + "examples": [ + "my-service" + ], + "type": "string" + }, + "name": { + "description": "Name of the service the ingress points to", + "examples": [ + "my-service" + ], + "type": "string" + }, + "port": { + "description": "The port the service deployment is listening on", + "examples": [ + 80 + ], + "type": "string" + }, + "protocol": { + "description": "The protocol the service is listening on", + "examples": [ + "http" + ], + "type": "string" + } + }, + "required": [ + "name", + "host", + "port", + "protocol" + ], + "type": "object" + }, + "subdomain": { + "description": "The subdomain the ingress rule listens on", + "examples": [ + "api" + ], + "type": "string" + }, + "username": { + "description": "Basic auth username", + "examples": [ + "admin" + ], + "type": "string" } + }, + "required": [ + "internal", + "path", + "protocol", + "service" ], "type": "object" }, - "internal": { - "default": false, - "description": "Whether or not this should be fulfilled by an internal load balancer (e.g. no public IP)", - "type": "boolean" - }, - "password": { - "description": "Basic auth password", - "examples": [ - "password" - ], - "type": "string" - }, - "path": { - "default": "/", - "description": "The path the ingress rule listens on", - "type": "string" - }, - "port": { - "description": "Port that the ingress rule listens for traffic on", - "examples": [ - 80 - ], - "type": [ - "string", - "number" - ] - }, - "protocol": { - "default": "http", - "description": "The protocol the ingress rule listens for traffic on", - "type": "string" - }, - "service": { + { "additionalProperties": false, - "description": "The configuration details of the target service", "properties": { - "host": { - "description": "The hostname the service is listening on", + "bucket": { + "additionalProperties": false, + "description": "Configuration details for a target bucket to route requests to", + "properties": { + "id": { + "description": "Unique ID of the bucket the ingress rule should route to", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Headers to include in responses", "examples": [ - "my-service" + { + "X-Frame-Options": "DENY" + } ], - "type": "string" + "type": "object" + }, + "internal": { + "default": false, + "description": "Whether or not this should be fulfilled by an internal load balancer (e.g. no public IP)", + "type": "boolean" }, - "name": { - "description": "Name of the service the ingress points to", + "password": { + "description": "Basic auth password", "examples": [ - "my-service" + "password" ], "type": "string" }, - "port": { - "description": "The port the service deployment is listening on", + "path": { + "default": "/", + "description": "The path the ingress rule listens on", + "type": "string" + }, + "protocol": { + "default": "http", + "description": "The protocol the ingress rule listens for traffic on", + "type": "string" + }, + "subdomain": { + "description": "The subdomain the ingress rule listens on", "examples": [ - 80 + "api" ], "type": "string" }, - "protocol": { - "description": "The protocol the service is listening on", + "username": { + "description": "Basic auth username", "examples": [ - "http" + "admin" ], "type": "string" } }, "required": [ - "name", - "host", - "port", + "bucket", + "internal", + "path", "protocol" ], "type": "object" - }, - "subdomain": { - "description": "The subdomain the ingress rule listens on", - "examples": [ - "api" - ], - "type": "string" - }, - "username": { - "description": "Basic auth username", - "examples": [ - "admin" - ], - "type": "string" } - }, - "required": [ - "port", - "service", - "protocol", - "path", - "internal" - ], - "type": "object" + ] } } } \ No newline at end of file diff --git a/src/@resources/ingress/inputs.ts b/src/@resources/ingress/inputs.ts index a8d106674..92bee5c3a 100644 --- a/src/@resources/ingress/inputs.ts +++ b/src/@resources/ingress/inputs.ts @@ -1,10 +1,4 @@ -export type IngressRuleInputs = { - /** - * Port that the ingress rule listens for traffic on - * @example 80 - */ - port: string | number; - +type ServiceIngressInputs = { /** * The configuration details of the target service */ @@ -33,7 +27,21 @@ export type IngressRuleInputs = { */ protocol: string; }; +}; + +type BucketIngressInputs = { + /** + * Configuration details for a target bucket to route requests to + */ + bucket: { + /** + * Unique ID of the bucket the ingress rule should route to + */ + id: string; + }; +}; +export type IngressRuleInputs = (ServiceIngressInputs | BucketIngressInputs) & { /** * The protocol the ingress rule listens for traffic on * @default http diff --git a/src/@resources/task/inputs.schema.json b/src/@resources/task/inputs.schema.json new file mode 100644 index 000000000..78aa6fc9f --- /dev/null +++ b/src/@resources/task/inputs.schema.json @@ -0,0 +1,141 @@ +{ + "$ref": "#/definitions/TaskInputs", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "TaskInputs": { + "additionalProperties": false, + "properties": { + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Command to execute in the container", + "examples": [ + [ + "node", + "index.js" + ] + ] + }, + "cpu": { + "description": "Number of CPUs to allocate to the container", + "minimum": 0.1, + "type": "number" + }, + "entrypoint": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "default": [ + "" + ], + "description": "Entrypoint of the container" + }, + "environment": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "not": {} + } + ] + }, + "description": "Environment variables to pass to the container", + "examples": [ + { + "NODE_ENV": "production" + } + ], + "type": "object" + }, + "image": { + "description": "Image the container runs from", + "examples": [ + "registry.architect.io/my-image:latest" + ], + "type": "string" + }, + "memory": { + "description": "Amount of memory to allocate to the container", + "examples": [ + "512Mi", + "1Gi" + ], + "type": "string" + }, + "platform": { + "description": "Target platform the deployment will run on", + "examples": [ + "linux/amd64" + ], + "type": "string" + }, + "volume_mounts": { + "description": "A set of volumes to mount to the container", + "items": { + "additionalProperties": false, + "properties": { + "mount_path": { + "description": "Path in the container to mount the volume to", + "examples": [ + "/var/lib/my-volume" + ], + "type": "string" + }, + "readonly": { + "default": false, + "description": "Whether or not the volume should be mounted as read-only", + "type": "boolean" + }, + "volume": { + "description": "Name of the volume to mount", + "examples": [ + "my-volume" + ], + "type": "string" + } + }, + "required": [ + "volume", + "mount_path", + "readonly" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "image" + ], + "type": "object" + } + } +} \ No newline at end of file diff --git a/src/@resources/task/inputs.ts b/src/@resources/task/inputs.ts new file mode 100644 index 000000000..405b82573 --- /dev/null +++ b/src/@resources/task/inputs.ts @@ -0,0 +1,79 @@ +export type TaskInputs = { + /** + * Image the container runs from + * + * @example "registry.architect.io/my-image:latest" + */ + image: string; + + /** + * Command to execute in the container + * + * @example ["node", "index.js"] + */ + command?: string | string[]; + + /** + * Entrypoint of the container + * + * @default [""] + */ + entrypoint?: string | string[]; + + /** + * Target platform the deployment will run on + * + * @example "linux/amd64" + */ + platform?: string; + + /** + * Environment variables to pass to the container + * + * @example + * { + * "NODE_ENV": "production" + * } + */ + environment?: Record; + + /** + * Number of CPUs to allocate to the container + * @minimum 0.1 + */ + cpu?: number; + + /** + * Amount of memory to allocate to the container + * + * @example "512Mi" + * @example "1Gi" + */ + memory?: string; + + /** + * A set of volumes to mount to the container + */ + volume_mounts?: Array<{ + /** + * Name of the volume to mount + * @example "my-volume" + */ + volume: string; + + /** + * Path in the container to mount the volume to + * + * @example "/var/lib/my-volume" + */ + mount_path: string; + + /** + * Whether or not the volume should be mounted as read-only + * @default false + */ + readonly: boolean; + }>; +}; + +export default TaskInputs; diff --git a/src/@resources/task/outputs.schema.json b/src/@resources/task/outputs.schema.json new file mode 100644 index 000000000..1f45b2852 --- /dev/null +++ b/src/@resources/task/outputs.schema.json @@ -0,0 +1,10 @@ +{ + "$ref": "#/definitions/TaskOutputs", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "TaskOutputs": { + "additionalProperties": {}, + "type": "object" + } + } +} \ No newline at end of file diff --git a/src/@resources/task/outputs.ts b/src/@resources/task/outputs.ts new file mode 100644 index 000000000..f4b444a93 --- /dev/null +++ b/src/@resources/task/outputs.ts @@ -0,0 +1,3 @@ +export type TaskOutputs = Record; + +export default TaskOutputs; diff --git a/src/@resources/types.ts b/src/@resources/types.ts index 94ea31d62..e7b84527a 100644 --- a/src/@resources/types.ts +++ b/src/@resources/types.ts @@ -1,3 +1,5 @@ +import type bucketInputs from './bucket/inputs.ts'; +import type bucketOutputs from './bucket/outputs.ts'; import type cronjobInputs from './cronjob/inputs.ts'; import type cronjobOutputs from './cronjob/outputs.ts'; import type databaseInputs from './database/inputs.ts'; @@ -14,10 +16,13 @@ import type secretInputs from './secret/inputs.ts'; import type secretOutputs from './secret/outputs.ts'; import type serviceInputs from './service/inputs.ts'; import type serviceOutputs from './service/outputs.ts'; +import type taskInputs from './task/inputs.ts'; +import type taskOutputs from './task/outputs.ts'; import type volumeInputs from './volume/inputs.ts'; import type volumeOutputs from './volume/outputs.ts'; export type ResourceType = + | 'bucket' | 'cronjob' | 'database' | 'databaseUser' @@ -26,9 +31,11 @@ export type ResourceType = | 'ingress' | 'secret' | 'service' + | 'task' | 'volume'; export const ResourceTypeList: ResourceType[] = [ + 'bucket', 'cronjob', 'database', 'databaseUser', @@ -37,10 +44,12 @@ export const ResourceTypeList: ResourceType[] = [ 'ingress', 'secret', 'service', + 'task', 'volume', ]; export type ResourceInputs = { + 'bucket': bucketInputs; 'cronjob': cronjobInputs; 'database': databaseInputs; 'databaseUser': databaseUserInputs; @@ -49,10 +58,12 @@ export type ResourceInputs = { 'ingress': ingressInputs; 'secret': secretInputs; 'service': serviceInputs; + 'task': taskInputs; 'volume': volumeInputs; }; export type ResourceOutputs = { + 'bucket': bucketOutputs; 'cronjob': cronjobOutputs; 'database': databaseOutputs; 'databaseUser': databaseUserOutputs; @@ -61,6 +72,7 @@ export type ResourceOutputs = { 'ingress': ingressOutputs; 'secret': secretOutputs; 'service': serviceOutputs; + 'task': taskOutputs; 'volume': volumeOutputs; }; diff --git a/src/@resources/volume/inputs.schema.json b/src/@resources/volume/inputs.schema.json index da0dea9ef..c750f9224 100644 --- a/src/@resources/volume/inputs.schema.json +++ b/src/@resources/volume/inputs.schema.json @@ -11,18 +11,8 @@ "/Users/batman/my-volume" ], "type": "string" - }, - "name": { - "description": "Name to give to the volume resource", - "examples": [ - "my-volume" - ], - "type": "string" } }, - "required": [ - "name" - ], "type": "object" } } diff --git a/src/@resources/volume/inputs.ts b/src/@resources/volume/inputs.ts index cfdd66c8d..5e984a71e 100644 --- a/src/@resources/volume/inputs.ts +++ b/src/@resources/volume/inputs.ts @@ -1,10 +1,4 @@ export type VolumeInputs = { - /** - * Name to give to the volume resource - * @example "my-volume" - */ - name: string; - /** * Path on the host machine to mount the volume to * @example "/Users/batman/my-volume" diff --git a/src/@resources/volume/outputs.schema.json b/src/@resources/volume/outputs.schema.json index 78c98434e..b6cb43fd4 100644 --- a/src/@resources/volume/outputs.schema.json +++ b/src/@resources/volume/outputs.schema.json @@ -3,7 +3,19 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "VolumeOutputs": { - "additionalProperties": {}, + "additionalProperties": false, + "properties": { + "id": { + "description": "The unique ID of the volume", + "examples": [ + "my-volume" + ], + "type": "string" + } + }, + "required": [ + "id" + ], "type": "object" } } diff --git a/src/@resources/volume/outputs.ts b/src/@resources/volume/outputs.ts index abaa05786..90c94d810 100644 --- a/src/@resources/volume/outputs.ts +++ b/src/@resources/volume/outputs.ts @@ -1,3 +1,10 @@ -export type VolumeOutputs = Record; +export type VolumeOutputs = { + /** + * The unique ID of the volume + * + * @example "my-volume" + */ + id: string; +}; export default VolumeOutputs; diff --git a/src/commands/apply/datacenter.ts b/src/commands/apply/datacenter.ts index b12c7f2d6..e2355490c 100644 --- a/src/commands/apply/datacenter.ts +++ b/src/commands/apply/datacenter.ts @@ -113,15 +113,20 @@ async function apply_datacenter_action(options: ApplyDatacenterOptions, name: st if (datacenterEnvironments.length > 0) { for (const environmentRecord of datacenterEnvironments) { console.log(`Updating environment ${environmentRecord.name}`); - await applyEnvironment({ + const { success } = await applyEnvironment({ command_helper, name: environmentRecord.name, logger, autoApprove: true, targetEnvironment: environmentRecord.config, }); + + if (success) { + console.log(`Environment ${environmentRecord.name} updated successfully`); + } else { + console.error(`%cEnvironment ${environmentRecord.name} update failed`, 'color: red'); + } } - console.log('Environments updated successfully'); command_helper.infraRenderer.doneRenderingGraph(); } }).catch(async (err) => { diff --git a/src/commands/apply/environment.ts b/src/commands/apply/environment.ts index 46093f5c0..7a32b2ab0 100644 --- a/src/commands/apply/environment.ts +++ b/src/commands/apply/environment.ts @@ -31,7 +31,7 @@ export async function applyEnvironmentAction(options: ApplyEnvironmentOptions, n }); } - return applyEnvironment({ + const { success, update } = await applyEnvironment({ command_helper, logger, name, @@ -39,6 +39,12 @@ export async function applyEnvironmentAction(options: ApplyEnvironmentOptions, n datacenter: options.datacenter, targetEnvironment: await parseEnvironment(config_path || {}), }); + + if (!success) { + console.log(`Environment ${update ? 'update' : 'creation'} failed`); + } else { + console.log(`Environment ${name} ${update ? 'updated' : 'created'} successfully`); + } } export default ApplyEnvironmentCommand; diff --git a/src/commands/apply/utils.ts b/src/commands/apply/utils.ts index 252f8b794..317de7cad 100644 --- a/src/commands/apply/utils.ts +++ b/src/commands/apply/utils.ts @@ -44,70 +44,58 @@ export const promptForDatacenter = async (command_helper: CommandHelper, name?: return selected; }; -export const applyEnvironment = async (options: ApplyEnvironmentOptions) => { - const environmentRecord = await options.command_helper.environmentStore.get(options.name); - const notHasDatacenter = !options.datacenter && !environmentRecord; - - let datacenterRecord: DatacenterRecord | undefined; - if (notHasDatacenter) { - datacenterRecord = await ArcctlConfig.getDefaultDatacenter(options.command_helper); +export const applyEnvironment = async ( + options: ApplyEnvironmentOptions, +): Promise<{ success: boolean; update: boolean }> => { + const existingEnvironmentRecord = await options.command_helper.environmentStore.get(options.name); + let datacenterRecord = options.datacenter + ? await options.command_helper.datacenterStore.get(options.datacenter) + : existingEnvironmentRecord + ? await options.command_helper.datacenterStore.get(existingEnvironmentRecord.datacenter) + : await ArcctlConfig.getDefaultDatacenter(options.command_helper); + + if (!datacenterRecord) { + datacenterRecord = await promptForDatacenter(options.command_helper); } - const targetDatacenterName = !datacenterRecord - ? (await promptForDatacenter(options.command_helper, options.datacenter)).name - : datacenterRecord.name || environmentRecord?.datacenter; - - const targetDatacenter = targetDatacenterName - ? await options.command_helper.datacenterStore.get(targetDatacenterName) - : undefined; - if (!targetDatacenter) { - console.error(`Couldn't find a datacenter named ${targetDatacenterName}`); - Deno.exit(1); + if (!datacenterRecord) { + throw new Error(`No valid datacenter provided`); } const targetEnvironment = options.targetEnvironment || await parseEnvironment({}); - - const environmentGraph = await targetEnvironment.getGraph( + const targetAppGraph = await targetEnvironment.getGraph( options.name, options.command_helper.componentStore, options.debug, ); - const targetGraph = targetDatacenter.config.getGraph(environmentGraph, { + const targetInfraGraph = datacenterRecord.config.getGraph(targetAppGraph, { environmentName: options.name, - datacenterName: targetDatacenter.name, + datacenterName: datacenterRecord.name, }); - targetGraph.validate(); + targetInfraGraph.validate(); - const startingDatacenter = (await options.command_helper.datacenterStore.get(targetDatacenterName!))!; - startingDatacenter.config.getGraph(environmentGraph, { - environmentName: options.name, - datacenterName: targetDatacenter.name, - }); - - const startingGraph = environmentRecord ? environmentRecord.priorState : targetDatacenter.priorState; - - const infraGraph = await InfraGraph.plan({ - before: startingGraph, - after: targetGraph, + const plannedChanges = await InfraGraph.plan({ + before: existingEnvironmentRecord ? existingEnvironmentRecord.priorState : datacenterRecord.priorState, + after: targetInfraGraph, context: PlanContext.Environment, }); - infraGraph.validate(); - await options.command_helper.infraRenderer.confirmGraph(infraGraph, options.autoApprove); + plannedChanges.validate(); + await options.command_helper.infraRenderer.confirmGraph(plannedChanges, options.autoApprove); let interval: number | undefined = undefined; if (!options.logger) { interval = setInterval(() => { - options.command_helper.infraRenderer.renderGraph(infraGraph, { clear: true }); + options.command_helper.infraRenderer.renderGraph(plannedChanges, { clear: true }); }, 1000 / cliSpinners.dots.frames.length); } const success = await options.command_helper.environmentUtils.applyEnvironment( options.name, - startingDatacenter, - targetEnvironment!, - infraGraph, + datacenterRecord, + targetEnvironment, + plannedChanges, { logger: options.logger, }, @@ -116,12 +104,11 @@ export const applyEnvironment = async (options: ApplyEnvironmentOptions) => { if (interval) { clearInterval(interval); } - options.command_helper.infraRenderer.renderGraph(infraGraph, { clear: !options.logger, disableSpinner: true }); + options.command_helper.infraRenderer.renderGraph(plannedChanges, { clear: !options.logger, disableSpinner: true }); options.command_helper.infraRenderer.doneRenderingGraph(); - if (!success) { - console.log(`Environment ${environmentRecord ? 'update' : 'creation'} failed`); - } else { - console.log(`Environment ${options.name} ${environmentRecord ? 'updated' : 'created'} successfully`); - } + return { + success, + update: !!existingEnvironmentRecord, + }; }; diff --git a/src/commands/build/component.ts b/src/commands/build/component.ts index a64bdc460..f30a2e45f 100644 --- a/src/commands/build/component.ts +++ b/src/commands/build/component.ts @@ -1,4 +1,3 @@ -import * as mod from 'https://deno.land/std@0.195.0/fs/copy.ts'; import * as path from 'std/path/mod.ts'; import { Component, parseComponent } from '../../components/index.ts'; import { verifyDocker } from '../../docker/helper.ts'; @@ -107,36 +106,6 @@ const ComponentBuildCommand = BaseCommand() build_args.push(build_context); await getDigest(build_args, options.verbose); return imageRepository.toString(); - }, async (build_options) => { - imageRepository.tag = imageRepository.tag + '-' + build_options.deployment_name + '-volumes-' + - build_options.volume_name; - console.log( - 'Building image for volume', - build_options.deployment_name + '.volumes.' + build_options.volume_name, - ); - - // Create the directory for the new volume container - const tmpDir = await Deno.makeTempDir(); - await mod.copy(build_options.host_path, path.join(tmpDir, 'contents')); - Deno.writeTextFileSync( - path.join(tmpDir, 'Dockerfile'), - ` - FROM alpine:latest - WORKDIR /app - COPY ./contents . - CMD ["sh", "-c", "cp -r ./* $TARGET_DIR"] - `, - ); - - // Publish the volume container - const buildArgs = ['build', '--push', '--tag', imageRepository.toString()]; - if (options.verbose) { - buildArgs.push('--quiet'); - } - buildArgs.push(tmpDir); - - await getDigest(buildArgs, options.verbose); - return imageRepository.toString(); }); // Tag and push the component itself @@ -175,33 +144,6 @@ const ComponentBuildCommand = BaseCommand() buildArgs.push(path.join(Deno.cwd(), context, build_options.context)); } - return getDigest(buildArgs, options.verbose); - }, async (build_options) => { - console.log( - 'Building image for volume', - build_options.deployment_name + '.volumes.' + build_options.volume_name, - ); - - // Create the directory for the new volume container - const tmpDir = await Deno.makeTempDir(); - await mod.copy(build_options.host_path, path.join(tmpDir, 'contents')); - Deno.writeTextFileSync( - path.join(tmpDir, 'Dockerfile'), - ` - FROM alpine:latest - WORKDIR /app - COPY ./contents . - CMD ["sh", "-c", "cp -r ./* $TARGET_DIR"] - `, - ); - - // Publish the volume container - const buildArgs = ['build']; - if (options.verbose) { - buildArgs.push('--quiet'); - } - buildArgs.push(tmpDir); - return getDigest(buildArgs, options.verbose); }); @@ -215,13 +157,6 @@ const ComponentBuildCommand = BaseCommand() await exec('docker', { args: ['tag', sourceRef, targetRef] }); console.log(`Deployment Tagged: ${targetRef}`); return targetRef; - }, async (digest: string, deploymentName: string, volumeName: string) => { - const imageRepository = new ImageRepository(tag); - const targetRef = imageRepository.toString() + '-deployments-' + deploymentName + '-volumes-' + volumeName; - - await exec('docker', { args: ['tag', digest, targetRef] }); - console.log(`Volume Tagged: ${targetRef}`); - return targetRef; }); const component_digest = await command_helper.componentStore.add(component); diff --git a/src/commands/tag.ts b/src/commands/tag.ts index d518d6155..eef8b115a 100644 --- a/src/commands/tag.ts +++ b/src/commands/tag.ts @@ -18,7 +18,7 @@ async function tag_action(options: GlobalOptions, source: string, target: string component.tag(async (sourceRef: string, targetName: string) => { const imageRepository = new ImageRepository(target); - const targetRef = imageRepository.toString() + '-deployments-' + targetName; + const targetRef = imageRepository.toString() + '-' + targetName; await exec('docker', { args: ['tag', sourceRef, targetRef] }); console.log(`Deployment Tagged: ${targetRef}`); return targetRef; diff --git a/src/components/__tests__/version-helper.ts b/src/components/__tests__/version-helper.ts index 7ee0f1c59..39fa76b2f 100644 --- a/src/components/__tests__/version-helper.ts +++ b/src/components/__tests__/version-helper.ts @@ -274,7 +274,6 @@ export const testIngressGeneration = ( component: 'component', inputs: { password: `\${{ component/service/${options.service_name}.password }}`, - port: `\${{ component/service/${options.service_name}.port }}`, protocol: `\${{ component/service/${options.service_name}.protocol }}`, service: { name: `\${{ component/service/${options.service_name}.name }}`, diff --git a/src/components/component-schema.ts b/src/components/component-schema.ts index 4aa442ca0..315a2e9af 100644 --- a/src/components/component-schema.ts +++ b/src/components/component-schema.ts @@ -2759,6 +2759,184 @@ export default { { 'additionalProperties': false, 'properties': { + 'buckets': { + 'additionalProperties': { + 'anyOf': [ + { + 'additionalProperties': false, + 'properties': { + 'description': { + 'description': 'A human-readable description of the bucket', + 'examples': [ + 'CSV reporting dumps', + ], + 'type': 'string', + }, + 'directory': { + 'description': 'The directory containing the contents to upload to the bucket', + 'examples': [ + './reports', + ], + 'type': 'string', + }, + 'image': { + 'description': 'The built image containing the contents to upload inside the workdir', + 'examples': [ + 'registry.architect.io/my-org/my-image:latest', + ], + 'type': 'string', + }, + }, + 'type': 'object', + }, + { + 'additionalProperties': false, + 'properties': { + 'deploy': { + 'additionalProperties': false, + 'description': 'Settings outlining a step that should be run when the component gets deployed', + 'properties': { + 'command': { + 'anyOf': [ + { + 'type': 'string', + }, + { + 'items': { + 'type': 'string', + }, + 'type': 'array', + }, + ], + 'description': 'Command to use when the container is booted up', + 'examples': [ + [ + 'npm', + 'start', + ], + ], + }, + 'cpu': { + 'description': 'The amount of CPU to allocate to each instance of the deployment', + 'type': [ + 'number', + 'string', + ], + }, + 'entrypoint': { + 'anyOf': [ + { + 'type': 'string', + }, + { + 'items': { + 'type': 'string', + }, + 'type': 'array', + }, + ], + 'default': [ + '', + ], + 'description': 'The executable to run every time the container is booted up', + }, + 'environment': { + 'additionalProperties': { + 'type': 'string', + }, + 'description': 'Environment variables to pass to the service', + 'examples': [ + { + 'NODE_ENV': 'production', + }, + { + 'BACKEND_URL': '${{ ingresses.backend.url }}', + }, + ], + 'type': 'object', + }, + 'image': { + 'description': 'Docker image to use for the deployment', + 'examples': [ + '${{ builds.frontend.image }}', + 'my-registry.com/my-app:latest', + ], + 'type': 'string', + }, + 'memory': { + 'description': 'The amount of memory to allocate to each instance of the deployment', + 'type': 'string', + }, + 'platform': { + 'description': 'Set platform if server is multi-platform capable', + 'examples': [ + 'linux/amd64', + ], + 'type': 'string', + }, + 'publish': { + 'description': 'The path to the directory containing the contents to upload to the bucket', + 'examples': [ + './dist', + ], + 'type': 'string', + }, + 'volumes': { + 'additionalProperties': { + 'additionalProperties': false, + 'properties': { + 'host_path': { + 'description': 'Path on the host machine to sync with the volume', + 'examples': [ + '/Users/batman/app/src', + ], + 'type': 'string', + }, + 'image': { + 'description': 'OCI image containing the contents to seed the volume with', + 'type': 'string', + }, + 'mount_path': { + 'description': 'Path inside the container to mount the volume to', + 'examples': [ + '/app/src', + ], + 'type': 'string', + }, + }, + 'required': [ + 'mount_path', + ], + 'type': 'object', + }, + 'description': 'Volumes that should be created and attached to each replica', + 'type': 'object', + }, + }, + 'required': [ + 'image', + 'publish', + ], + 'type': 'object', + }, + 'description': { + 'description': 'A human-readable description of the bucket', + 'examples': [ + 'A static website', + ], + 'type': 'string', + }, + }, + 'required': [ + 'deploy', + ], + 'type': 'object', + }, + ], + }, + 'description': 'Buckets that can be used to store and serve files', + 'type': 'object', + }, 'builds': { 'additionalProperties': { 'additionalProperties': false, @@ -3740,38 +3918,63 @@ export default { }, 'ingresses': { 'additionalProperties': { - 'additionalProperties': false, - 'properties': { - 'headers': { - 'additionalProperties': { - 'type': 'string', - }, - 'description': 'Additional headers to include in responses', - 'examples': [ - { - 'Access-Control-Allow-Credentials': 'true', - 'Access-Control-Allow-Origin': '${{ variables.allowed_return_urls }}', + 'anyOf': [ + { + 'additionalProperties': false, + 'properties': { + 'headers': { + 'additionalProperties': { + 'type': 'string', + }, + 'description': 'Additional headers to include in responses', + 'examples': [ + { + 'Access-Control-Allow-Credentials': 'true', + 'Access-Control-Allow-Origin': '${{ variables.allowed_return_urls }}', + }, + ], + 'type': 'object', + }, + 'internal': { + 'default': false, + 'description': 'Whether or not the ingress rule should be attached to an internal gateway', + 'type': 'boolean', }, + 'service': { + 'description': 'Service the ingress rule forwards traffic to', + 'examples': [ + 'backend', + ], + 'type': 'string', + }, + }, + 'required': [ + 'service', ], 'type': 'object', }, - 'internal': { - 'default': false, - 'description': 'Whether or not the ingress rule should be attached to an internal gateway', - 'type': 'boolean', - }, - 'service': { - 'description': 'Service the ingress rule forwards traffic to', - 'examples': [ - 'backend', + { + 'additionalProperties': false, + 'properties': { + 'bucket': { + 'description': 'The static bucket to serve content from', + 'examples': [ + 'my-bucket', + ], + 'type': 'string', + }, + 'internal': { + 'default': false, + 'description': 'Whether or not the ingress rule should be attached to an internal gateway', + 'type': 'boolean', + }, + }, + 'required': [ + 'bucket', ], - 'type': 'string', + 'type': 'object', }, - }, - 'required': [ - 'service', ], - 'type': 'object', }, 'description': 'Claims for external (e.g. client) access to a service', 'type': 'object', diff --git a/src/components/component.schema.json b/src/components/component.schema.json index 79a349bb8..00de43bee 100644 --- a/src/components/component.schema.json +++ b/src/components/component.schema.json @@ -2729,6 +2729,184 @@ { "additionalProperties": false, "properties": { + "buckets": { + "additionalProperties": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "description": { + "description": "A human-readable description of the bucket", + "examples": [ + "CSV reporting dumps" + ], + "type": "string" + }, + "directory": { + "description": "The directory containing the contents to upload to the bucket", + "examples": [ + "./reports" + ], + "type": "string" + }, + "image": { + "description": "The built image containing the contents to upload inside the workdir", + "examples": [ + "registry.architect.io/my-org/my-image:latest" + ], + "type": "string" + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "deploy": { + "additionalProperties": false, + "description": "Settings outlining a step that should be run when the component gets deployed", + "properties": { + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Command to use when the container is booted up", + "examples": [ + [ + "npm", + "start" + ] + ] + }, + "cpu": { + "description": "The amount of CPU to allocate to each instance of the deployment", + "type": [ + "number", + "string" + ] + }, + "entrypoint": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "default": [ + "" + ], + "description": "The executable to run every time the container is booted up" + }, + "environment": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables to pass to the service", + "examples": [ + { + "NODE_ENV": "production" + }, + { + "BACKEND_URL": "${{ ingresses.backend.url }}" + } + ], + "type": "object" + }, + "image": { + "description": "Docker image to use for the deployment", + "examples": [ + "${{ builds.frontend.image }}", + "my-registry.com/my-app:latest" + ], + "type": "string" + }, + "memory": { + "description": "The amount of memory to allocate to each instance of the deployment", + "type": "string" + }, + "platform": { + "description": "Set platform if server is multi-platform capable", + "examples": [ + "linux/amd64" + ], + "type": "string" + }, + "publish": { + "description": "The path to the directory containing the contents to upload to the bucket", + "examples": [ + "./dist" + ], + "type": "string" + }, + "volumes": { + "additionalProperties": { + "additionalProperties": false, + "properties": { + "host_path": { + "description": "Path on the host machine to sync with the volume", + "examples": [ + "/Users/batman/app/src" + ], + "type": "string" + }, + "image": { + "description": "OCI image containing the contents to seed the volume with", + "type": "string" + }, + "mount_path": { + "description": "Path inside the container to mount the volume to", + "examples": [ + "/app/src" + ], + "type": "string" + } + }, + "required": [ + "mount_path" + ], + "type": "object" + }, + "description": "Volumes that should be created and attached to each replica", + "type": "object" + } + }, + "required": [ + "image", + "publish" + ], + "type": "object" + }, + "description": { + "description": "A human-readable description of the bucket", + "examples": [ + "A static website" + ], + "type": "string" + } + }, + "required": [ + "deploy" + ], + "type": "object" + } + ] + }, + "description": "Buckets that can be used to store and serve files", + "type": "object" + }, "builds": { "additionalProperties": { "additionalProperties": false, @@ -3695,38 +3873,63 @@ }, "ingresses": { "additionalProperties": { - "additionalProperties": false, - "properties": { - "headers": { - "additionalProperties": { - "type": "string" - }, - "description": "Additional headers to include in responses", - "examples": [ - { - "Access-Control-Allow-Credentials": "true", - "Access-Control-Allow-Origin": "${{ variables.allowed_return_urls }}" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Additional headers to include in responses", + "examples": [ + { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "${{ variables.allowed_return_urls }}" + } + ], + "type": "object" + }, + "internal": { + "default": false, + "description": "Whether or not the ingress rule should be attached to an internal gateway", + "type": "boolean" + }, + "service": { + "description": "Service the ingress rule forwards traffic to", + "examples": [ + "backend" + ], + "type": "string" } + }, + "required": [ + "service" ], "type": "object" }, - "internal": { - "default": false, - "description": "Whether or not the ingress rule should be attached to an internal gateway", - "type": "boolean" - }, - "service": { - "description": "Service the ingress rule forwards traffic to", - "examples": [ - "backend" + { + "additionalProperties": false, + "properties": { + "bucket": { + "description": "The static bucket to serve content from", + "examples": [ + "my-bucket" + ], + "type": "string" + }, + "internal": { + "default": false, + "description": "Whether or not the ingress rule should be attached to an internal gateway", + "type": "boolean" + } + }, + "required": [ + "bucket" ], - "type": "string" + "type": "object" } - }, - "required": [ - "service" - ], - "type": "object" + ] }, "description": "Claims for external (e.g. client) access to a service", "type": "object" diff --git a/src/components/component.ts b/src/components/component.ts index fe3a86abe..ce2a4f2eb 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -9,12 +9,6 @@ export type GraphContext = { }; }; -export type VolumeBuildFn = (options: { - deployment_name: string; - volume_name: string; - host_path: string; -}) => Promise; - export type DockerBuildFn = (options: { name: string; context: string; @@ -28,12 +22,6 @@ export type DockerTagFn = ( targetName: string, ) => Promise; -export type VolumeTagFn = ( - digest: string, - deploymentName: string, - volumeName: string, -) => Promise; - export type DockerPushFn = (image: string) => Promise; export type ComponentDependencies = Array<{ @@ -46,9 +34,9 @@ export abstract class Component { public abstract getGraph(context: GraphContext): AppGraph; - public abstract build(buildFn: DockerBuildFn, volumeFn: VolumeBuildFn): Promise; + public abstract build(buildFn: DockerBuildFn): Promise; - public abstract tag(tagFn: DockerTagFn, volumeTagFn: VolumeTagFn): Promise; + public abstract tag(tagFn: DockerTagFn): Promise; public abstract push(pushFn: DockerPushFn): Promise; } diff --git a/src/components/v1/__tests__/index.test.ts b/src/components/v1/__tests__/index.test.ts index 903c0e688..003407187 100644 --- a/src/components/v1/__tests__/index.test.ts +++ b/src/components/v1/__tests__/index.test.ts @@ -389,7 +389,6 @@ describe('Component Schema: v1', () => { port: `\${{ ${interface_node.getId()}.port }}`, protocol: `\${{ ${interface_node.getId()}.protocol }}`, }, - port: `\${{ ${interface_node.getId()}.port }}`, internal: false, path: '/', }, @@ -454,7 +453,6 @@ describe('Component Schema: v1', () => { port: `\${{ ${service_node.getId()}.port }}`, protocol: `\${{ ${service_node.getId()}.protocol }}`, }, - port: `\${{ ${service_node.getId()}.port }}`, internal: false, path: '/', }, diff --git a/src/components/v1/index.ts b/src/components/v1/index.ts index b69751221..56726bf24 100644 --- a/src/components/v1/index.ts +++ b/src/components/v1/index.ts @@ -1,3 +1,4 @@ +import * as path from 'std/path/mod.ts'; import { ResourceInputs } from '../../@resources/index.ts'; import { GraphEdge } from '../../graphs/edge.ts'; import { AppGraph, AppGraphNode } from '../../graphs/index.ts'; @@ -8,8 +9,6 @@ import { DockerPushFn, DockerTagFn, GraphContext, - VolumeBuildFn, - VolumeTagFn, } from '../component.ts'; import { ComponentSchema } from '../schema.ts'; import { DatabaseSchemaV1 } from './database-schema-v1.ts'; @@ -210,7 +209,6 @@ export default class ComponentV1 extends Component { type: 'volume', component: context.component.name, inputs: { - name: `${context.component.name}/${service_name}-${volume_name}`, ...(volume_config.host_path ? { hostPath: volume_config.host_path } : {}), }, }); @@ -298,7 +296,6 @@ export default class ComponentV1 extends Component { type: 'ingress', component: context.component.name, inputs: { - port: `\${{ ${service_node.getId()}.port }}`, ...(interface_config.ingress.subdomain ? { subdomain: interface_config.ingress.subdomain } : {}), ...(interface_config.ingress.path ? { path: interface_config.ingress.path } : {}), ...(interface_config.ingress.internal !== undefined @@ -327,6 +324,13 @@ export default class ComponentV1 extends Component { }), ); + if (!('service' in ingress_node.inputs)) { + // This should never be hit. Typescript can't seem to infer the conditional type from the above inputs + throw new Error( + `Ingress expects to target a service, but does not (${ingress_node.getId()})`, + ); + } + deployment_node.inputs.ingresses = deployment_node.inputs.ingresses || []; deployment_node.inputs.ingresses?.push({ service: ingress_node.inputs.service.name, @@ -448,7 +452,6 @@ export default class ComponentV1 extends Component { type: 'volume', component: context.component.name, inputs: { - name: `${context.component.name}/${task_name}-${volume_name}`, ...(volume_config.host_path ? { hostPath: volume_config.host_path } : {}), }, }); @@ -547,7 +550,6 @@ export default class ComponentV1 extends Component { port: `\${{ ${interface_node.getId()}.port }}`, protocol: `\${{ ${interface_node.getId()}.protocol }}`, }, - port: `\${{ ${interface_node.getId()}.port }}`, ...(interface_config.ingress.subdomain ? { subdomain: interface_config.ingress.subdomain } : {}), ...(interface_config.ingress.path ? { path: interface_config.ingress.path } : {}), ...(interface_config.ingress.internal !== undefined ? { internal: interface_config.ingress.internal } : {}), @@ -666,11 +668,11 @@ export default class ComponentV1 extends Component { return res; } - public async build(buildFn: DockerBuildFn, volumeBuildFn: VolumeBuildFn): Promise { + public async build(buildFn: DockerBuildFn): Promise { for (const [svcName, svcConfig] of Object.entries(this.services || {})) { if ('build' in svcConfig) { const digest = await buildFn({ - name: svcName, + name: `services-${svcName}`, context: svcConfig.build.context, dockerfile: svcConfig.build.dockerfile, args: svcConfig.build.args, @@ -684,10 +686,21 @@ export default class ComponentV1 extends Component { for (const [volumeName, volumeConfig] of Object.entries(svcConfig.volumes || {})) { if (volumeConfig.host_path) { - volumeConfig.image = await volumeBuildFn({ - host_path: volumeConfig.host_path, - volume_name: volumeName, - deployment_name: svcName, + const tmpDir = Deno.makeTempDirSync(); + const dockerfile = path.join(tmpDir, 'Dockerfile'); + Deno.writeTextFileSync( + dockerfile, + ` + FROM alpine:latest + COPY . . + CMD ["sh", "-c", "cp -r ./* $TARGET_DIR"] + `, + ); + + volumeConfig.image = await buildFn({ + context: volumeConfig.host_path, + dockerfile, + name: 'services-' + svcName + '-volumes-' + volumeName, }); } } @@ -696,7 +709,7 @@ export default class ComponentV1 extends Component { for (const [taskName, taskConfig] of Object.entries(this.tasks || {})) { if ('build' in taskConfig) { const digest = await buildFn({ - name: taskName, + name: `tasks-${taskName}`, context: taskConfig.build.context, dockerfile: taskConfig.build.dockerfile, args: taskConfig.build.args, @@ -710,10 +723,21 @@ export default class ComponentV1 extends Component { for (const [volumeName, volumeConfig] of Object.entries(taskConfig.volumes || {})) { if (volumeConfig.host_path) { - volumeConfig.image = await volumeBuildFn({ - host_path: volumeConfig.host_path, - volume_name: volumeName, - deployment_name: taskName, + const tmpDir = Deno.makeTempDirSync(); + const dockerfile = path.join(tmpDir, 'Dockerfile'); + Deno.writeTextFileSync( + dockerfile, + ` + FROM alpine:latest + COPY . . + CMD ["sh", "-c", "cp -r ./* $TARGET_DIR"] + `, + ); + + volumeConfig.image = await buildFn({ + context: volumeConfig.host_path, + dockerfile, + name: `tasks-${taskName}-volumes-${volumeName}`, }); } } @@ -722,29 +746,35 @@ export default class ComponentV1 extends Component { return this; } - public async tag(dockerTagFn: DockerTagFn, volumeTagFn: VolumeTagFn): Promise { + public async tag(dockerTagFn: DockerTagFn): Promise { for (const [svcName, svcConfig] of Object.entries(this.services || {})) { if ('image' in svcConfig) { - svcConfig.image = await dockerTagFn(svcConfig.image, svcName); + svcConfig.image = await dockerTagFn(svcConfig.image, `services-${svcName}`); this.services![svcName] = svcConfig; } for (const [volumeName, volumeConfig] of Object.entries(svcConfig.volumes || {})) { if (volumeConfig.image) { - svcConfig.volumes![volumeName].image = await volumeTagFn(volumeConfig.image, svcName, volumeName); + svcConfig.volumes![volumeName].image = await dockerTagFn( + volumeConfig.image, + `services-${svcName}-volumes-${volumeName}`, + ); } } } for (const [taskName, taskConfig] of Object.entries(this.tasks || {})) { if ('image' in taskConfig) { - taskConfig.image = await dockerTagFn(taskConfig.image, taskName); + taskConfig.image = await dockerTagFn(taskConfig.image, `tasks-${taskName}`); this.tasks![taskName] = taskConfig; } for (const [volumeName, volumeConfig] of Object.entries(taskConfig.volumes || {})) { if (volumeConfig.image) { - taskConfig.volumes![volumeName].image = await volumeTagFn(volumeConfig.image, taskName, volumeName); + taskConfig.volumes![volumeName].image = await dockerTagFn( + volumeConfig.image, + `tasks-${taskName}-volumes-${volumeName}`, + ); } } } diff --git a/src/components/v2/__tests__/index.test.ts b/src/components/v2/__tests__/index.test.ts index bd0842d3b..ada6eb3bb 100644 --- a/src/components/v2/__tests__/index.test.ts +++ b/src/components/v2/__tests__/index.test.ts @@ -217,7 +217,6 @@ describe('Component Schema: v2', () => { type: 'volume', component: 'component', inputs: { - name: 'component/main-src', hostPath: '/fake/source/src', }, }); @@ -363,7 +362,6 @@ describe('Component Schema: v2', () => { type: 'ingress', component: 'component', inputs: { - port: `\${{ ${svc_node.getId()}.port }}`, username: `\${{ ${svc_node.getId()}.username }}`, password: `\${{ ${svc_node.getId()}.password }}`, protocol: `\${{ ${svc_node.getId()}.protocol }}`, diff --git a/src/components/v2/bucket.ts b/src/components/v2/bucket.ts new file mode 100644 index 000000000..c5d5f156f --- /dev/null +++ b/src/components/v2/bucket.ts @@ -0,0 +1,41 @@ +import { ContainerSchemaV2 } from './container.ts'; + +export type BucketSchemaV2 = { + /** + * A human-readable description of the bucket + * + * @example "CSV reporting dumps" + */ + description?: string; + + /** + * The directory containing the contents to upload to the bucket + * @example "./reports" + */ + directory?: string; + + /** + * The built image containing the contents to upload inside the workdir + * @example "registry.architect.io/my-org/my-image:latest" + */ + image?: string; +} | { + /** + * A human-readable description of the bucket + * + * @example "A static website" + */ + description?: string; + + /** + * Settings outlining a step that should be run when the component gets deployed + */ + deploy: ContainerSchemaV2 & { + /** + * The path to the directory containing the contents to upload to the bucket + * + * @example "./dist" + */ + publish: string; + }; +}; diff --git a/src/components/v2/container.ts b/src/components/v2/container.ts new file mode 100644 index 000000000..a2b18d48d --- /dev/null +++ b/src/components/v2/container.ts @@ -0,0 +1,75 @@ +export type ContainerSchemaV2 = { + /** + * Docker image to use for the deployment + * + * @example "${{ builds.frontend.image }}" + * @example "my-registry.com/my-app:latest" + */ + image: string; + + /** + * Set platform if server is multi-platform capable + * + * @example "linux/amd64" + */ + platform?: string; + + /** + * Command to use when the container is booted up + * + * @example ["npm", "start"] + */ + command?: string | string[]; + + /** + * The executable to run every time the container is booted up + * + * @default [""] + */ + entrypoint?: string | string[]; + + /** + * Environment variables to pass to the service + * + * @example { "NODE_ENV": "production" } + * @example + * { + * "BACKEND_URL": "${{ ingresses.backend.url }}", + * } + */ + environment?: Record; + + /** + * The amount of CPU to allocate to each instance of the deployment + */ + cpu?: number | string; + + /** + * The amount of memory to allocate to each instance of the deployment + */ + memory?: string; + + /** + * Volumes that should be created and attached to each replica + */ + volumes?: Record; +}; diff --git a/src/components/v2/deployment.ts b/src/components/v2/deployment.ts index b3b477ecd..dd3f34b62 100644 --- a/src/components/v2/deployment.ts +++ b/src/components/v2/deployment.ts @@ -1,6 +1,7 @@ +import { ContainerSchemaV2 } from './container.ts'; import { ProbeSchema } from './probe.ts'; -export type DeploymentSchemaV2 = { +export type DeploymentSchemaV2 = ContainerSchemaV2 & { /** * Human readable description of the deployment * @@ -8,56 +9,6 @@ export type DeploymentSchemaV2 = { */ description?: string; - /** - * Docker image to use for the deployment - * - * @example "${{ builds.frontend.image }}" - * @example "my-registry.com/my-app:latest" - */ - image: string; - - /** - * Set platform if server is multi-platform capable - * - * @example "linux/amd64" - */ - platform?: string; - - /** - * Command to use when the container is booted up - * - * @example ["npm", "start"] - */ - command?: string | string[]; - - /** - * The executable to run every time the container is booted up - * - * @default [""] - */ - entrypoint?: string | string[]; - - /** - * Environment variables to pass to the service - * - * @example { "NODE_ENV": "production" } - * @example - * { - * "BACKEND_URL": "${{ ingresses.backend.url }}", - * } - */ - environment?: Record; - - /** - * The amount of CPU to allocate to each instance of the deployment - */ - cpu?: number | string; - - /** - * The amount of memory to allocate to each instance of the deployment - */ - memory?: string; - /** * The labels to apply to the deployment */ @@ -93,30 +44,6 @@ export type DeploymentSchemaV2 = { */ memory?: string; }; - - /** - * Volumes that should be created and attached to each replica - */ - volumes?: Record; }; export type DebuggableDeploymentSchemaV2 = DeploymentSchemaV2 & { diff --git a/src/components/v2/index.ts b/src/components/v2/index.ts index 314ff31c9..ee332bafc 100644 --- a/src/components/v2/index.ts +++ b/src/components/v2/index.ts @@ -9,10 +9,9 @@ import { DockerPushFn, DockerTagFn, GraphContext, - VolumeBuildFn, - VolumeTagFn, } from '../component.ts'; import { ComponentSchema } from '../schema.ts'; +import { BucketSchemaV2 } from './bucket.ts'; import { DebuggableBuildSchemaV2 } from './build.ts'; import { DependencySchemaV2 } from './dependency.ts'; import { DebuggableDeploymentSchemaV2 } from './deployment.ts'; @@ -189,6 +188,11 @@ export default class ComponentV2 extends Component { DebuggableDeploymentSchemaV2 >; + /** + * Buckets that can be used to store and serve files + */ + buckets?: Record; + /** * Services that can receive network traffic */ @@ -268,6 +272,20 @@ export default class ComponentV2 extends Component { * } */ headers?: Record; + } | { + /** + * The static bucket to serve content from + * + * @example "my-bucket" + */ + bucket: string; + + /** + * Whether or not the ingress rule should be attached to an internal gateway + * + * @default false + */ + internal?: boolean; } >; @@ -444,7 +462,6 @@ export default class ComponentV2 extends Component { type: 'volume', component: context.component.name, inputs: { - name: `${context.component.name}/${deployment_key}-${volumeKey}`, hostPath: host_path, }, }); @@ -584,6 +601,176 @@ export default class ComponentV2 extends Component { return graph; } + private addBucketsToGraph( + graph: AppGraph, + context: GraphContext, + ): AppGraph { + for (const [bucket_key, bucket_config] of Object.entries(this.buckets || {})) { + const bucket_node = new AppGraphNode({ + name: bucket_key, + type: 'bucket', + component: context.component.name, + inputs: {}, + }); + graph.insertNodes(bucket_node); + + // Check if we intend to seed the bucket + if ('deploy' in bucket_config || bucket_config.image || bucket_config.directory) { + const volume_node = new AppGraphNode({ + name: `${bucket_key}-bucket-contents`, + type: 'volume', + component: context.component.name, + inputs: {}, + }); + graph.insertNodes(volume_node); + + // TODO: Task that moves contents from volume to bucket + const data_migration_node = new AppGraphNode({ + name: `${bucket_key}-bucket-data-migration`, + type: 'task', + component: context.component.name, + inputs: { + image: 'architectio/s3cmd', + command: [ + 'sync', + '/data', + 's3://$BUCKET_ID', + ], + volume_mounts: [{ + volume: `\${{ ${volume_node.getId()}.id }}`, + mount_path: '/data', + readonly: true, + }], + environment: { + BUCKET_ID: `\${{ ${bucket_node.getId()}.id }}`, + ACCESS_KEY_ID: `\${{ ${bucket_node.getId()}.access_key_id }}`, + SECRET_ACCESS_KEY: `\${{ ${bucket_node.getId()}.secret_access_key }}`, + }, + }, + }); + graph.insertNodes(data_migration_node); + + // Handle bucket seeding + if ('deploy' in bucket_config) { + const deploy_hook_node = new AppGraphNode({ + name: `${bucket_key}-bucket-before-hook`, + type: 'task', + component: context.component.name, + inputs: { + image: bucket_config.deploy.image, + ...(bucket_config.deploy.platform ? { platform: bucket_config.deploy.platform } : {}), + ...(bucket_config.deploy.environment ? { environment: bucket_config.deploy.environment } : {}), + ...(bucket_config.deploy.command ? { command: bucket_config.deploy.command } : {}), + ...(bucket_config.deploy.entrypoint ? { entrypoint: bucket_config.deploy.entrypoint } : {}), + ...(bucket_config.deploy.cpu ? { cpu: Number(bucket_config.deploy.cpu) } : {}), + ...(bucket_config.deploy.memory ? { memory: bucket_config.deploy.memory } : {}), + volume_mounts: [{ + volume: `\${{ ${volume_node.getId()}.id }}`, + mount_path: bucket_config.deploy.publish, + readonly: false, + }], + }, + }); + graph.insertNodes(deploy_hook_node); + graph.insertEdges( + new GraphEdge({ + from: deploy_hook_node.getId(), + to: volume_node.getId(), + }), + // Ensures the data migration doesn't get run until the deploy hook is finished + new GraphEdge({ + from: data_migration_node.getId(), + to: deploy_hook_node.getId(), + }), + ); + } else if (bucket_config.image || bucket_config.directory) { + let build_contents_node: AppGraphNode<'dockerBuild'> | undefined; + + // Build the directory into an image containing its contents + if (bucket_config.directory && !bucket_config.image) { + const tmpDir = Deno.makeTempDirSync(); + const dockerfile = path.join(tmpDir, 'Dockerfile'); + Deno.writeTextFileSync( + dockerfile, + ` + FROM alpine:latest + COPY . . + CMD ["sh", "-c", "cp -r ./* $TARGET_DIR"] + `, + ); + + build_contents_node = new AppGraphNode({ + name: `${bucket_key}-bucket-before-hook`, + type: 'dockerBuild', + component: context.component.name, + inputs: { + context: bucket_config.directory, + dockerfile: dockerfile, + component_source: context.component.source, + }, + }); + graph.insertNodes(build_contents_node); + graph.insertEdges( + new GraphEdge({ + from: build_contents_node.getId(), + to: volume_node.getId(), + }), + // Ensures the data migration doesn't get run until the deploy hook is finished + new GraphEdge({ + from: data_migration_node.getId(), + to: build_contents_node.getId(), + }), + ); + } + + // If there are contents for the bucket, queue up a task to move said contents to a volume + if (bucket_config.image || build_contents_node) { + const move_data_to_volume_node = new AppGraphNode({ + name: `${bucket_key}-bucket-before-hook`, + type: 'task', + component: context.component.name, + inputs: { + image: (build_contents_node ? `\${{ ${build_contents_node.getId()}.image }}` : bucket_config.image)!, + command: ['sh', '-c', 'cp -r ./* $TARGET_DIR'], + volume_mounts: [{ + volume: `\${{ ${volume_node.getId()}.id }}`, + mount_path: '/data', + readonly: false, + }], + environment: { + TARGET_DIR: '/data', + }, + }, + }); + graph.insertNodes(move_data_to_volume_node); + graph.insertEdges( + new GraphEdge({ + from: move_data_to_volume_node.getId(), + to: volume_node.getId(), + }), + // Ensures the data migration doesn't get run until the deploy hook is finished + new GraphEdge({ + from: data_migration_node.getId(), + to: move_data_to_volume_node.getId(), + }), + ); + + if (build_contents_node) { + graph.insertEdges( + new GraphEdge({ + from: move_data_to_volume_node.getId(), + to: build_contents_node.getId(), + }), + ); + } + } + } + } + } + + return graph; + } + private addIngressesToGraph( graph: AppGraph, context: GraphContext, @@ -593,84 +780,127 @@ export default class ComponentV2 extends Component { this.ingresses || {}, ) ) { - const service_node = graph.nodes.find( - (n) => n.name === ingress_config.service && n.type === 'service', - ) as AppGraphNode<'service'> | undefined; - if (!service_node) { - throw new Error(`The service, ${ingress_config.service}, does not exist`); - } - - graph.insertNodes(service_node); + if ('service' in ingress_config) { + const service_node = graph.nodes.find( + (n) => n.name === ingress_config.service && n.type === 'service', + ) as AppGraphNode<'service'> | undefined; + if (!service_node) { + throw new Error(`The service, ${ingress_config.service}, does not exist`); + } - const ingress_node = new AppGraphNode({ - name: ingress_key, - type: 'ingress', - component: context.component.name, - inputs: { - port: `\${{ ${service_node.getId()}.port }}`, - service: { - name: `\${{ ${service_node.getId()}.name }}`, - host: `\${{ ${service_node.getId()}.host }}`, - port: `\${{ ${service_node.getId()}.port }}`, + const ingress_node = new AppGraphNode({ + name: ingress_key, + type: 'ingress', + component: context.component.name, + inputs: { + service: { + name: `\${{ ${service_node.getId()}.name }}`, + host: `\${{ ${service_node.getId()}.host }}`, + port: `\${{ ${service_node.getId()}.port }}`, + protocol: `\${{ ${service_node.getId()}.protocol }}`, + }, protocol: `\${{ ${service_node.getId()}.protocol }}`, + username: `\${{ ${service_node.getId()}.username }}`, + password: `\${{ ${service_node.getId()}.password }}`, + internal: ingress_config.internal ?? false, + path: '/', + ...(ingress_config.headers ? { headers: ingress_config.headers } : {}), }, - protocol: `\${{ ${service_node.getId()}.protocol }}`, - username: `\${{ ${service_node.getId()}.username }}`, - password: `\${{ ${service_node.getId()}.password }}`, - internal: false, - path: '/', - ...(ingress_config.internal !== undefined ? { internal: ingress_config.internal } : {}), - ...(ingress_config.headers ? { headers: ingress_config.headers } : {}), - }, - }); + }); - ingress_node.inputs = parseExpressionRefs( - graph, - this.normalizedDependencies, - context, - ingress_node.getId(), - ingress_node.inputs, - ); - graph.insertNodes(ingress_node); - graph.insertEdges( - new GraphEdge({ - from: ingress_node.getId(), - to: service_node.getId(), - }), - ); + ingress_node.inputs = parseExpressionRefs( + graph, + this.normalizedDependencies, + context, + ingress_node.getId(), + ingress_node.inputs, + ); + graph.insertNodes(ingress_node); + graph.insertEdges( + new GraphEdge({ + from: ingress_node.getId(), + to: service_node.getId(), + }), + ); + + if ('deployment' in service_node.inputs) { + const deployment_name = service_node.inputs.deployment; + const deployment_node = graph.nodes.find((n) => + n.type === 'deployment' && (n.inputs as AppGraphNode<'deployment'>).name === deployment_name + ) as + | AppGraphNode<'deployment'> + | undefined; + if (!deployment_node) { + throw new Error( + `No deployment named ${service_node.inputs.deployment}. Referenced by the service, ${service_node.name}`, + ); + } + + if (!('service' in ingress_node.inputs)) { + // This should never be hit. Typescript can't seem to infer the conditional type from the above inputs + throw new Error( + `Ingress expects to target a service, but does not (${ingress_node.getId()})`, + ); + } + + // Update deployment node with service references + deployment_node.inputs.ingresses = deployment_node.inputs.ingresses || []; + deployment_node.inputs.ingresses.push({ + service: ingress_node.inputs.service.name, + host: `\${{ ${ingress_node.getId()}.host }}`, + protocol: `\${{ ${ingress_node.getId()}.protocol }}`, + port: `\${{ ${ingress_node.getId()}.port }}`, + path: `\${{ ${ingress_node.getId()}.path }}`, + subdomain: `\${{ ${ingress_node.getId()}.subdomain }}`, + dns_zone: `\${{ ${ingress_node.getId()}.dns_zone }}`, + }); + graph.insertNodes(deployment_node); - if ('deployment' in service_node.inputs) { - const deployment_name = service_node.inputs.deployment; - const deployment_node = graph.nodes.find((n) => - n.type === 'deployment' && (n.inputs as AppGraphNode<'deployment'>).name === deployment_name - ) as - | AppGraphNode<'deployment'> - | undefined; - if (!deployment_node) { - throw new Error( - `No deployment named ${service_node.inputs.deployment}. Referenced by the service, ${service_node.name}`, + graph.insertEdges( + new GraphEdge({ + from: deployment_node.getId(), + to: ingress_node.getId(), + }), ); } + } else { + const bucket_node = graph.nodes.find( + (n) => n.name === ingress_config.bucket && n.type === 'bucket', + ) as AppGraphNode<'bucket'> | undefined; + if (!bucket_node) { + throw new Error(`The bucket, ${ingress_config.bucket}, does not exist`); + } - // Update deployment node with service references - deployment_node.inputs.ingresses = deployment_node.inputs.ingresses || []; - deployment_node.inputs.ingresses!.push({ - service: ingress_node.inputs.service.name, - host: `\${{ ${ingress_node.getId()}.host }}`, - protocol: `\${{ ${ingress_node.getId()}.protocol }}`, - port: `\${{ ${ingress_node.getId()}.port }}`, - path: `\${{ ${ingress_node.getId()}.path }}`, - subdomain: `\${{ ${ingress_node.getId()}.subdomain }}`, - dns_zone: `\${{ ${ingress_node.getId()}.dns_zone }}`, + const ingress_node = new AppGraphNode({ + name: ingress_key, + type: 'ingress', + component: context.component.name, + inputs: { + bucket: { + id: `\${{ ${bucket_node.getId()}.id }}`, + }, + protocol: 'http', + internal: ingress_config.internal ?? false, + path: '/', + }, }); - graph.insertNodes(deployment_node); + ingress_node.inputs = parseExpressionRefs( + graph, + this.normalizedDependencies, + context, + ingress_node.getId(), + ingress_node.inputs, + ); + graph.insertNodes(ingress_node); graph.insertEdges( new GraphEdge({ - from: deployment_node.getId(), - to: ingress_node.getId(), + from: ingress_node.getId(), + to: bucket_node.getId(), }), ); + + bucket_node.inputs; } } @@ -708,6 +938,7 @@ export default class ComponentV2 extends Component { let graph = new AppGraph(); graph = this.addVariablestoGraph(graph, context); graph = this.addBuildsToGraph(graph, context); + graph = this.addBucketsToGraph(graph, context); graph = this.addDatabasesToGraph(graph, context); graph = this.addDeploymentsToGraph(graph, context); graph = this.addServicesToGraph(graph, context); @@ -715,7 +946,7 @@ export default class ComponentV2 extends Component { return graph; } - public async build(buildFn: DockerBuildFn, volumeBuildFn: VolumeBuildFn): Promise { + public async build(buildFn: DockerBuildFn): Promise { for (const [buildName, buildConfig] of Object.entries(this.builds || {})) { const digest = await buildFn({ name: buildName, @@ -731,10 +962,21 @@ export default class ComponentV2 extends Component { for (const [deploymentName, deploymentConfig] of Object.entries(this.deployments || {})) { for (const [volumeName, volumeConfig] of Object.entries(deploymentConfig.volumes || {})) { if (volumeConfig.host_path) { - volumeConfig.image = await volumeBuildFn({ - host_path: volumeConfig.host_path, - volume_name: volumeName, - deployment_name: deploymentName, + const tmpDir = Deno.makeTempDirSync(); + const dockerfile = path.join(tmpDir, 'Dockerfile'); + Deno.writeTextFileSync( + dockerfile, + ` + FROM alpine:latest + COPY . . + CMD ["sh", "-c", "cp -r ./* $TARGET_DIR"] + `, + ); + + this.deployments![deploymentName].volumes![volumeName].image = await buildFn({ + context: volumeConfig.host_path, + dockerfile, + name: 'deployments-' + deploymentName + '-volumes-' + volumeName, }); } } @@ -742,10 +984,31 @@ export default class ComponentV2 extends Component { delete this.deployments?.[deploymentName].debug; } + for (const [bucket_key, bucket_config] of Object.entries(this.buckets || {})) { + if ('directory' in bucket_config && bucket_config.directory) { + const tmpDir = Deno.makeTempDirSync(); + const dockerfile = path.join(tmpDir, 'Dockerfile'); + Deno.writeTextFileSync( + dockerfile, + ` + FROM alpine:latest + COPY . . + CMD ["sh", "-c", "cp -r ./* $TARGET_DIR"] + `, + ); + + bucket_config.image = await buildFn({ + context: bucket_config.directory, + dockerfile, + name: 'buckets-' + bucket_key, + }); + } + } + return this; } - public async tag(tagFn: DockerTagFn, volumeTagFn: VolumeTagFn): Promise { + public async tag(tagFn: DockerTagFn): Promise { for (const [buildName, buildConfig] of Object.entries(this.builds || {})) { if (buildConfig.image) { const newTag = await tagFn(buildConfig.image, buildName); @@ -756,15 +1019,20 @@ export default class ComponentV2 extends Component { for (const [deploymentName, deploymentConfig] of Object.entries(this.deployments || {})) { for (const [volumeName, volumeConfig] of Object.entries(deploymentConfig.volumes || {})) { if (volumeConfig.image) { - deploymentConfig.volumes![volumeName].image = await volumeTagFn( + deploymentConfig.volumes![volumeName].image = await tagFn( volumeConfig.image, - deploymentName, - volumeName, + `deployments-${deploymentName}-volumes-${volumeName}`, ); } } } + for (const [bucket_key, bucket_config] of Object.entries(this.buckets || {})) { + if ('image' in bucket_config && bucket_config.image) { + bucket_config.image = await tagFn(bucket_config.image, `buckets-${bucket_key}`); + } + } + return this; } @@ -783,6 +1051,12 @@ export default class ComponentV2 extends Component { } } + for (const bucketConfig of Object.values(this.buckets || {})) { + if ('image' in bucketConfig && bucketConfig.image) { + await pushFn(bucketConfig.image); + } + } + return this; } } diff --git a/src/components/v2/schema.json b/src/components/v2/schema.json index 48158b4b9..433805ab0 100644 --- a/src/components/v2/schema.json +++ b/src/components/v2/schema.json @@ -5,6 +5,184 @@ "ComponentV2": { "additionalProperties": false, "properties": { + "buckets": { + "additionalProperties": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "description": { + "description": "A human-readable description of the bucket", + "examples": [ + "CSV reporting dumps" + ], + "type": "string" + }, + "directory": { + "description": "The directory containing the contents to upload to the bucket", + "examples": [ + "./reports" + ], + "type": "string" + }, + "image": { + "description": "The built image containing the contents to upload inside the workdir", + "examples": [ + "registry.architect.io/my-org/my-image:latest" + ], + "type": "string" + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "deploy": { + "additionalProperties": false, + "description": "Settings outlining a step that should be run when the component gets deployed", + "properties": { + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Command to use when the container is booted up", + "examples": [ + [ + "npm", + "start" + ] + ] + }, + "cpu": { + "description": "The amount of CPU to allocate to each instance of the deployment", + "type": [ + "number", + "string" + ] + }, + "entrypoint": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "default": [ + "" + ], + "description": "The executable to run every time the container is booted up" + }, + "environment": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables to pass to the service", + "examples": [ + { + "NODE_ENV": "production" + }, + { + "BACKEND_URL": "${{ ingresses.backend.url }}" + } + ], + "type": "object" + }, + "image": { + "description": "Docker image to use for the deployment", + "examples": [ + "${{ builds.frontend.image }}", + "my-registry.com/my-app:latest" + ], + "type": "string" + }, + "memory": { + "description": "The amount of memory to allocate to each instance of the deployment", + "type": "string" + }, + "platform": { + "description": "Set platform if server is multi-platform capable", + "examples": [ + "linux/amd64" + ], + "type": "string" + }, + "publish": { + "description": "The path to the directory containing the contents to upload to the bucket", + "examples": [ + "./dist" + ], + "type": "string" + }, + "volumes": { + "additionalProperties": { + "additionalProperties": false, + "properties": { + "host_path": { + "description": "Path on the host machine to sync with the volume", + "examples": [ + "/Users/batman/app/src" + ], + "type": "string" + }, + "image": { + "description": "OCI image containing the contents to seed the volume with", + "type": "string" + }, + "mount_path": { + "description": "Path inside the container to mount the volume to", + "examples": [ + "/app/src" + ], + "type": "string" + } + }, + "required": [ + "mount_path" + ], + "type": "object" + }, + "description": "Volumes that should be created and attached to each replica", + "type": "object" + } + }, + "required": [ + "image", + "publish" + ], + "type": "object" + }, + "description": { + "description": "A human-readable description of the bucket", + "examples": [ + "A static website" + ], + "type": "string" + } + }, + "required": [ + "deploy" + ], + "type": "object" + } + ] + }, + "description": "Buckets that can be used to store and serve files", + "type": "object" + }, "builds": { "additionalProperties": { "additionalProperties": false, @@ -971,38 +1149,63 @@ }, "ingresses": { "additionalProperties": { - "additionalProperties": false, - "properties": { - "headers": { - "additionalProperties": { - "type": "string" - }, - "description": "Additional headers to include in responses", - "examples": [ - { - "Access-Control-Allow-Credentials": "true", - "Access-Control-Allow-Origin": "${{ variables.allowed_return_urls }}" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Additional headers to include in responses", + "examples": [ + { + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Origin": "${{ variables.allowed_return_urls }}" + } + ], + "type": "object" + }, + "internal": { + "default": false, + "description": "Whether or not the ingress rule should be attached to an internal gateway", + "type": "boolean" + }, + "service": { + "description": "Service the ingress rule forwards traffic to", + "examples": [ + "backend" + ], + "type": "string" } + }, + "required": [ + "service" ], "type": "object" }, - "internal": { - "default": false, - "description": "Whether or not the ingress rule should be attached to an internal gateway", - "type": "boolean" - }, - "service": { - "description": "Service the ingress rule forwards traffic to", - "examples": [ - "backend" + { + "additionalProperties": false, + "properties": { + "bucket": { + "description": "The static bucket to serve content from", + "examples": [ + "my-bucket" + ], + "type": "string" + }, + "internal": { + "default": false, + "description": "Whether or not the ingress rule should be attached to an internal gateway", + "type": "boolean" + } + }, + "required": [ + "bucket" ], - "type": "string" + "type": "object" } - }, - "required": [ - "service" - ], - "type": "object" + ] }, "description": "Claims for external (e.g. client) access to a service", "type": "object" diff --git a/src/datacenters/datacenter-schema.ts b/src/datacenters/datacenter-schema.ts index a2a5f2749..21636e9e3 100644 --- a/src/datacenters/datacenter-schema.ts +++ b/src/datacenters/datacenter-schema.ts @@ -11,6 +11,192 @@ export default { 'items': { 'additionalProperties': false, 'properties': { + 'bucket': { + 'items': { + 'additionalProperties': false, + 'properties': { + 'module': { + 'additionalProperties': { + 'items': { + 'additionalProperties': false, + 'properties': { + 'build': { + 'description': 'The path to a module that will be built during the build step.', + 'examples': [ + './my-module', + ], + 'type': 'string', + }, + 'environment': { + 'additionalProperties': { + 'type': 'string', + }, + 'description': + 'Environment variables that should be provided to the container executing the module', + 'examples': [ + { + 'MY_ENV_VAR': 'my-value', + }, + ], + 'type': 'object', + }, + 'inputs': { + 'anyOf': [ + { + 'additionalProperties': {}, + 'type': 'object', + }, + { + 'type': 'string', + }, + ], + 'description': 'Input values for the module.', + 'examples': [ + { + 'image': 'nginx:latest', + 'port': 8080, + }, + ], + }, + 'plugin': { + 'default': 'pulumi', + 'description': 'The plugin used to build the module. Defaults to pulumi.', + 'enum': [ + 'pulumi', + 'opentofu', + ], + 'examples': [ + 'opentofu', + ], + 'type': 'string', + }, + 'source': { + 'description': 'The image source of the module.', + 'examples': [ + 'my-registry.com/my-image:latest', + ], + 'type': 'string', + }, + 'ttl': { + 'description': + 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], + 'type': 'string', + }, + 'volume': { + 'description': 'Volumes that should be mounted to the container executing the module', + 'items': { + 'additionalProperties': false, + 'properties': { + 'host_path': { + 'description': 'The path on the host machine to mount to the container', + 'examples': [ + '/Users/batman/my-volume', + ], + 'type': 'string', + }, + 'mount_path': { + 'description': 'The path in the container to mount the volume to', + 'examples': [ + '/app/my-volume', + ], + 'type': 'string', + }, + }, + 'required': [ + 'host_path', + 'mount_path', + ], + 'type': 'object', + }, + 'type': 'array', + }, + 'when': { + 'description': + 'A condition that restricts when the module should be created. Must resolve to a boolean.', + 'examples': [ + 'node.type == \'database\' && node.inputs.databaseType == \'postgres\'', + 'contains(environment.nodes.*.inputs.databaseType, \'postgres\')', + ], + 'type': 'string', + }, + }, + 'required': [ + 'inputs', + ], + 'type': 'object', + }, + 'type': 'array', + }, + 'description': 'Modules that will be created once per matching application resource', + 'type': 'object', + }, + 'outputs': { + 'additionalProperties': false, + 'description': 'A map of output values to be passed to upstream application resources', + 'examples': [ + { + 'host': '${module.database.host}', + 'id': '${module.database.id}', + 'password': '${module.database.password}', + 'port': '${module.database.port}', + 'username': '${module.database.username}', + }, + ], + 'properties': { + 'access_key_id': { + 'description': 'Access key ID used to authenticate with the bucket', + 'type': 'string', + }, + 'endpoint': { + 'description': 'Endpoint that hosts the bucket', + 'examples': [ + 'https://nyc3.digitaloceanspaces.com', + 'https://bucket.s3.region.amazonaws.com', + ], + 'type': 'string', + }, + 'id': { + 'description': 'Unique ID of the bucket that was created', + 'examples': [ + 'abc123', + ], + 'type': 'string', + }, + 'region': { + 'description': 'Region the bucket was created in', + 'type': 'string', + }, + 'secret_access_key': { + 'description': 'Secret access key used to authenticate with the bucket', + 'type': 'string', + }, + }, + 'required': [ + 'id', + 'endpoint', + 'region', + 'access_key_id', + 'secret_access_key', + ], + 'type': 'object', + }, + 'when': { + 'description': + 'A condition that restricts when the hook should be active. Must resolve to a boolean.', + 'examples': [ + 'node.type == \'database\' && node.inputs.databaseType == \'postgres\'', + 'contains(environment.nodes.*.inputs.databaseType, \'postgres\')', + ], + 'type': 'string', + }, + }, + 'type': 'object', + }, + 'type': 'array', + }, 'cronjob': { 'items': { 'additionalProperties': false, @@ -80,6 +266,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -227,6 +416,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -441,6 +633,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -655,6 +850,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -816,6 +1014,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -975,6 +1176,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -1195,6 +1399,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -1314,6 +1521,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -1473,6 +1683,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -1609,6 +1822,156 @@ export default { }, 'type': 'array', }, + 'task': { + 'items': { + 'additionalProperties': false, + 'properties': { + 'module': { + 'additionalProperties': { + 'items': { + 'additionalProperties': false, + 'properties': { + 'build': { + 'description': 'The path to a module that will be built during the build step.', + 'examples': [ + './my-module', + ], + 'type': 'string', + }, + 'environment': { + 'additionalProperties': { + 'type': 'string', + }, + 'description': + 'Environment variables that should be provided to the container executing the module', + 'examples': [ + { + 'MY_ENV_VAR': 'my-value', + }, + ], + 'type': 'object', + }, + 'inputs': { + 'anyOf': [ + { + 'additionalProperties': {}, + 'type': 'object', + }, + { + 'type': 'string', + }, + ], + 'description': 'Input values for the module.', + 'examples': [ + { + 'image': 'nginx:latest', + 'port': 8080, + }, + ], + }, + 'plugin': { + 'default': 'pulumi', + 'description': 'The plugin used to build the module. Defaults to pulumi.', + 'enum': [ + 'pulumi', + 'opentofu', + ], + 'examples': [ + 'opentofu', + ], + 'type': 'string', + }, + 'source': { + 'description': 'The image source of the module.', + 'examples': [ + 'my-registry.com/my-image:latest', + ], + 'type': 'string', + }, + 'ttl': { + 'description': + 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], + 'type': 'string', + }, + 'volume': { + 'description': 'Volumes that should be mounted to the container executing the module', + 'items': { + 'additionalProperties': false, + 'properties': { + 'host_path': { + 'description': 'The path on the host machine to mount to the container', + 'examples': [ + '/Users/batman/my-volume', + ], + 'type': 'string', + }, + 'mount_path': { + 'description': 'The path in the container to mount the volume to', + 'examples': [ + '/app/my-volume', + ], + 'type': 'string', + }, + }, + 'required': [ + 'host_path', + 'mount_path', + ], + 'type': 'object', + }, + 'type': 'array', + }, + 'when': { + 'description': + 'A condition that restricts when the module should be created. Must resolve to a boolean.', + 'examples': [ + 'node.type == \'database\' && node.inputs.databaseType == \'postgres\'', + 'contains(environment.nodes.*.inputs.databaseType, \'postgres\')', + ], + 'type': 'string', + }, + }, + 'required': [ + 'inputs', + ], + 'type': 'object', + }, + 'type': 'array', + }, + 'description': 'Modules that will be created once per matching application resource', + 'type': 'object', + }, + 'outputs': { + 'additionalProperties': false, + 'description': 'A map of output values to be passed to upstream application resources', + 'examples': [ + { + 'host': '${module.database.host}', + 'id': '${module.database.id}', + 'password': '${module.database.password}', + 'port': '${module.database.port}', + 'username': '${module.database.username}', + }, + ], + 'type': 'object', + }, + 'when': { + 'description': + 'A condition that restricts when the hook should be active. Must resolve to a boolean.', + 'examples': [ + 'node.type == \'database\' && node.inputs.databaseType == \'postgres\'', + 'contains(environment.nodes.*.inputs.databaseType, \'postgres\')', + ], + 'type': 'string', + }, + }, + 'type': 'object', + }, + 'type': 'array', + }, 'volume': { 'items': { 'additionalProperties': false, @@ -1678,6 +2041,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { @@ -1729,7 +2095,7 @@ export default { 'type': 'object', }, 'outputs': { - 'additionalProperties': {}, + 'additionalProperties': false, 'description': 'A map of output values to be passed to upstream application resources', 'examples': [ { @@ -1740,6 +2106,18 @@ export default { 'username': '${module.database.username}', }, ], + 'properties': { + 'id': { + 'description': 'The unique ID of the volume', + 'examples': [ + 'my-volume', + ], + 'type': 'string', + }, + }, + 'required': [ + 'id', + ], 'type': 'object', }, 'when': { @@ -1825,6 +2203,9 @@ export default { 'ttl': { 'description': 'The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.', + 'examples': [ + '24*60*60', + ], 'type': 'string', }, 'volume': { diff --git a/src/datacenters/datacenter.schema.json b/src/datacenters/datacenter.schema.json index e005fea98..642117932 100644 --- a/src/datacenters/datacenter.schema.json +++ b/src/datacenters/datacenter.schema.json @@ -10,6 +10,188 @@ "items": { "additionalProperties": false, "properties": { + "bucket": { + "items": { + "additionalProperties": false, + "properties": { + "module": { + "additionalProperties": { + "items": { + "additionalProperties": false, + "properties": { + "build": { + "description": "The path to a module that will be built during the build step.", + "examples": [ + "./my-module" + ], + "type": "string" + }, + "environment": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables that should be provided to the container executing the module", + "examples": [ + { + "MY_ENV_VAR": "my-value" + } + ], + "type": "object" + }, + "inputs": { + "anyOf": [ + { + "additionalProperties": {}, + "type": "object" + }, + { + "type": "string" + } + ], + "description": "Input values for the module.", + "examples": [ + { + "image": "nginx:latest", + "port": 8080 + } + ] + }, + "plugin": { + "default": "pulumi", + "description": "The plugin used to build the module. Defaults to pulumi.", + "enum": [ + "pulumi", + "opentofu" + ], + "examples": [ + "opentofu" + ], + "type": "string" + }, + "source": { + "description": "The image source of the module.", + "examples": [ + "my-registry.com/my-image:latest" + ], + "type": "string" + }, + "ttl": { + "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], + "type": "string" + }, + "volume": { + "description": "Volumes that should be mounted to the container executing the module", + "items": { + "additionalProperties": false, + "properties": { + "host_path": { + "description": "The path on the host machine to mount to the container", + "examples": [ + "/Users/batman/my-volume" + ], + "type": "string" + }, + "mount_path": { + "description": "The path in the container to mount the volume to", + "examples": [ + "/app/my-volume" + ], + "type": "string" + } + }, + "required": [ + "host_path", + "mount_path" + ], + "type": "object" + }, + "type": "array" + }, + "when": { + "description": "A condition that restricts when the module should be created. Must resolve to a boolean.", + "examples": [ + "node.type == 'database' && node.inputs.databaseType == 'postgres'", + "contains(environment.nodes.*.inputs.databaseType, 'postgres')" + ], + "type": "string" + } + }, + "required": [ + "inputs" + ], + "type": "object" + }, + "type": "array" + }, + "description": "Modules that will be created once per matching application resource", + "type": "object" + }, + "outputs": { + "additionalProperties": false, + "description": "A map of output values to be passed to upstream application resources", + "examples": [ + { + "host": "${module.database.host}", + "id": "${module.database.id}", + "password": "${module.database.password}", + "port": "${module.database.port}", + "username": "${module.database.username}" + } + ], + "properties": { + "access_key_id": { + "description": "Access key ID used to authenticate with the bucket", + "type": "string" + }, + "endpoint": { + "description": "Endpoint that hosts the bucket", + "examples": [ + "https://nyc3.digitaloceanspaces.com", + "https://bucket.s3.region.amazonaws.com" + ], + "type": "string" + }, + "id": { + "description": "Unique ID of the bucket that was created", + "examples": [ + "abc123" + ], + "type": "string" + }, + "region": { + "description": "Region the bucket was created in", + "type": "string" + }, + "secret_access_key": { + "description": "Secret access key used to authenticate with the bucket", + "type": "string" + } + }, + "required": [ + "id", + "endpoint", + "region", + "access_key_id", + "secret_access_key" + ], + "type": "object" + }, + "when": { + "description": "A condition that restricts when the hook should be active. Must resolve to a boolean.", + "examples": [ + "node.type == 'database' && node.inputs.databaseType == 'postgres'", + "contains(environment.nodes.*.inputs.databaseType, 'postgres')" + ], + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, "cronjob": { "items": { "additionalProperties": false, @@ -77,6 +259,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -220,6 +405,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -430,6 +618,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -640,6 +831,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -797,6 +991,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -952,6 +1149,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1168,6 +1368,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1284,6 +1487,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1439,6 +1645,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1573,6 +1782,152 @@ }, "type": "array" }, + "task": { + "items": { + "additionalProperties": false, + "properties": { + "module": { + "additionalProperties": { + "items": { + "additionalProperties": false, + "properties": { + "build": { + "description": "The path to a module that will be built during the build step.", + "examples": [ + "./my-module" + ], + "type": "string" + }, + "environment": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables that should be provided to the container executing the module", + "examples": [ + { + "MY_ENV_VAR": "my-value" + } + ], + "type": "object" + }, + "inputs": { + "anyOf": [ + { + "additionalProperties": {}, + "type": "object" + }, + { + "type": "string" + } + ], + "description": "Input values for the module.", + "examples": [ + { + "image": "nginx:latest", + "port": 8080 + } + ] + }, + "plugin": { + "default": "pulumi", + "description": "The plugin used to build the module. Defaults to pulumi.", + "enum": [ + "pulumi", + "opentofu" + ], + "examples": [ + "opentofu" + ], + "type": "string" + }, + "source": { + "description": "The image source of the module.", + "examples": [ + "my-registry.com/my-image:latest" + ], + "type": "string" + }, + "ttl": { + "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], + "type": "string" + }, + "volume": { + "description": "Volumes that should be mounted to the container executing the module", + "items": { + "additionalProperties": false, + "properties": { + "host_path": { + "description": "The path on the host machine to mount to the container", + "examples": [ + "/Users/batman/my-volume" + ], + "type": "string" + }, + "mount_path": { + "description": "The path in the container to mount the volume to", + "examples": [ + "/app/my-volume" + ], + "type": "string" + } + }, + "required": [ + "host_path", + "mount_path" + ], + "type": "object" + }, + "type": "array" + }, + "when": { + "description": "A condition that restricts when the module should be created. Must resolve to a boolean.", + "examples": [ + "node.type == 'database' && node.inputs.databaseType == 'postgres'", + "contains(environment.nodes.*.inputs.databaseType, 'postgres')" + ], + "type": "string" + } + }, + "required": [ + "inputs" + ], + "type": "object" + }, + "type": "array" + }, + "description": "Modules that will be created once per matching application resource", + "type": "object" + }, + "outputs": { + "additionalProperties": false, + "description": "A map of output values to be passed to upstream application resources", + "examples": [ + { + "host": "${module.database.host}", + "id": "${module.database.id}", + "password": "${module.database.password}", + "port": "${module.database.port}", + "username": "${module.database.username}" + } + ], + "type": "object" + }, + "when": { + "description": "A condition that restricts when the hook should be active. Must resolve to a boolean.", + "examples": [ + "node.type == 'database' && node.inputs.databaseType == 'postgres'", + "contains(environment.nodes.*.inputs.databaseType, 'postgres')" + ], + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, "volume": { "items": { "additionalProperties": false, @@ -1640,6 +1995,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1690,7 +2048,7 @@ "type": "object" }, "outputs": { - "additionalProperties": {}, + "additionalProperties": false, "description": "A map of output values to be passed to upstream application resources", "examples": [ { @@ -1701,6 +2059,18 @@ "username": "${module.database.username}" } ], + "properties": { + "id": { + "description": "The unique ID of the volume", + "examples": [ + "my-volume" + ], + "type": "string" + } + }, + "required": [ + "id" + ], "type": "object" }, "when": { @@ -1784,6 +2154,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { diff --git a/src/datacenters/v1/__tests__/datacenter.test.ts b/src/datacenters/v1/__tests__/datacenter.test.ts index 5b44230cd..4c4060f27 100644 --- a/src/datacenters/v1/__tests__/datacenter.test.ts +++ b/src/datacenters/v1/__tests__/datacenter.test.ts @@ -1081,7 +1081,6 @@ describe('DatacenterV1', () => { port: '8080', protocol: 'http', }, - port: 8080, protocol: 'http', path: '/', internal: false, @@ -1281,7 +1280,6 @@ describe('DatacenterV1', () => { port: `\${{ ${serviceNode.getId()}.port }}`, protocol: `\${{ ${serviceNode.getId()}.protocol }}`, }, - port: `\${{ ${serviceNode.getId()}.port }}`, protocol: `\${{ ${serviceNode.getId()}.protocol }}`, path: '/', internal: false, @@ -1379,7 +1377,6 @@ describe('DatacenterV1', () => { port: `port`, protocol: `protocol`, }, - port: 'port', protocol: `protocol`, path: '/', internal: false, @@ -1408,7 +1405,6 @@ describe('DatacenterV1', () => { dns_zone: 'architect.io', internal: false, path: '/', - port: 'port', protocol: 'protocol', service: { name: 'host', diff --git a/src/datacenters/v1/schema.json b/src/datacenters/v1/schema.json index ea7b2f6de..d8312fc2c 100644 --- a/src/datacenters/v1/schema.json +++ b/src/datacenters/v1/schema.json @@ -10,6 +10,188 @@ "items": { "additionalProperties": false, "properties": { + "bucket": { + "items": { + "additionalProperties": false, + "properties": { + "module": { + "additionalProperties": { + "items": { + "additionalProperties": false, + "properties": { + "build": { + "description": "The path to a module that will be built during the build step.", + "examples": [ + "./my-module" + ], + "type": "string" + }, + "environment": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables that should be provided to the container executing the module", + "examples": [ + { + "MY_ENV_VAR": "my-value" + } + ], + "type": "object" + }, + "inputs": { + "anyOf": [ + { + "additionalProperties": {}, + "type": "object" + }, + { + "type": "string" + } + ], + "description": "Input values for the module.", + "examples": [ + { + "image": "nginx:latest", + "port": 8080 + } + ] + }, + "plugin": { + "default": "pulumi", + "description": "The plugin used to build the module. Defaults to pulumi.", + "enum": [ + "pulumi", + "opentofu" + ], + "examples": [ + "opentofu" + ], + "type": "string" + }, + "source": { + "description": "The image source of the module.", + "examples": [ + "my-registry.com/my-image:latest" + ], + "type": "string" + }, + "ttl": { + "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], + "type": "string" + }, + "volume": { + "description": "Volumes that should be mounted to the container executing the module", + "items": { + "additionalProperties": false, + "properties": { + "host_path": { + "description": "The path on the host machine to mount to the container", + "examples": [ + "/Users/batman/my-volume" + ], + "type": "string" + }, + "mount_path": { + "description": "The path in the container to mount the volume to", + "examples": [ + "/app/my-volume" + ], + "type": "string" + } + }, + "required": [ + "host_path", + "mount_path" + ], + "type": "object" + }, + "type": "array" + }, + "when": { + "description": "A condition that restricts when the module should be created. Must resolve to a boolean.", + "examples": [ + "node.type == 'database' && node.inputs.databaseType == 'postgres'", + "contains(environment.nodes.*.inputs.databaseType, 'postgres')" + ], + "type": "string" + } + }, + "required": [ + "inputs" + ], + "type": "object" + }, + "type": "array" + }, + "description": "Modules that will be created once per matching application resource", + "type": "object" + }, + "outputs": { + "additionalProperties": false, + "description": "A map of output values to be passed to upstream application resources", + "examples": [ + { + "host": "${module.database.host}", + "id": "${module.database.id}", + "password": "${module.database.password}", + "port": "${module.database.port}", + "username": "${module.database.username}" + } + ], + "properties": { + "access_key_id": { + "description": "Access key ID used to authenticate with the bucket", + "type": "string" + }, + "endpoint": { + "description": "Endpoint that hosts the bucket", + "examples": [ + "https://nyc3.digitaloceanspaces.com", + "https://bucket.s3.region.amazonaws.com" + ], + "type": "string" + }, + "id": { + "description": "Unique ID of the bucket that was created", + "examples": [ + "abc123" + ], + "type": "string" + }, + "region": { + "description": "Region the bucket was created in", + "type": "string" + }, + "secret_access_key": { + "description": "Secret access key used to authenticate with the bucket", + "type": "string" + } + }, + "required": [ + "id", + "endpoint", + "region", + "access_key_id", + "secret_access_key" + ], + "type": "object" + }, + "when": { + "description": "A condition that restricts when the hook should be active. Must resolve to a boolean.", + "examples": [ + "node.type == 'database' && node.inputs.databaseType == 'postgres'", + "contains(environment.nodes.*.inputs.databaseType, 'postgres')" + ], + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, "cronjob": { "items": { "additionalProperties": false, @@ -77,6 +259,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -220,6 +405,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -430,6 +618,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -640,6 +831,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -797,6 +991,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -952,6 +1149,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1168,6 +1368,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1284,6 +1487,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1439,6 +1645,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1573,6 +1782,152 @@ }, "type": "array" }, + "task": { + "items": { + "additionalProperties": false, + "properties": { + "module": { + "additionalProperties": { + "items": { + "additionalProperties": false, + "properties": { + "build": { + "description": "The path to a module that will be built during the build step.", + "examples": [ + "./my-module" + ], + "type": "string" + }, + "environment": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables that should be provided to the container executing the module", + "examples": [ + { + "MY_ENV_VAR": "my-value" + } + ], + "type": "object" + }, + "inputs": { + "anyOf": [ + { + "additionalProperties": {}, + "type": "object" + }, + { + "type": "string" + } + ], + "description": "Input values for the module.", + "examples": [ + { + "image": "nginx:latest", + "port": 8080 + } + ] + }, + "plugin": { + "default": "pulumi", + "description": "The plugin used to build the module. Defaults to pulumi.", + "enum": [ + "pulumi", + "opentofu" + ], + "examples": [ + "opentofu" + ], + "type": "string" + }, + "source": { + "description": "The image source of the module.", + "examples": [ + "my-registry.com/my-image:latest" + ], + "type": "string" + }, + "ttl": { + "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], + "type": "string" + }, + "volume": { + "description": "Volumes that should be mounted to the container executing the module", + "items": { + "additionalProperties": false, + "properties": { + "host_path": { + "description": "The path on the host machine to mount to the container", + "examples": [ + "/Users/batman/my-volume" + ], + "type": "string" + }, + "mount_path": { + "description": "The path in the container to mount the volume to", + "examples": [ + "/app/my-volume" + ], + "type": "string" + } + }, + "required": [ + "host_path", + "mount_path" + ], + "type": "object" + }, + "type": "array" + }, + "when": { + "description": "A condition that restricts when the module should be created. Must resolve to a boolean.", + "examples": [ + "node.type == 'database' && node.inputs.databaseType == 'postgres'", + "contains(environment.nodes.*.inputs.databaseType, 'postgres')" + ], + "type": "string" + } + }, + "required": [ + "inputs" + ], + "type": "object" + }, + "type": "array" + }, + "description": "Modules that will be created once per matching application resource", + "type": "object" + }, + "outputs": { + "additionalProperties": false, + "description": "A map of output values to be passed to upstream application resources", + "examples": [ + { + "host": "${module.database.host}", + "id": "${module.database.id}", + "password": "${module.database.password}", + "port": "${module.database.port}", + "username": "${module.database.username}" + } + ], + "type": "object" + }, + "when": { + "description": "A condition that restricts when the hook should be active. Must resolve to a boolean.", + "examples": [ + "node.type == 'database' && node.inputs.databaseType == 'postgres'", + "contains(environment.nodes.*.inputs.databaseType, 'postgres')" + ], + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, "volume": { "items": { "additionalProperties": false, @@ -1640,6 +1995,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": { @@ -1690,7 +2048,7 @@ "type": "object" }, "outputs": { - "additionalProperties": {}, + "additionalProperties": false, "description": "A map of output values to be passed to upstream application resources", "examples": [ { @@ -1701,6 +2059,18 @@ "username": "${module.database.username}" } ], + "properties": { + "id": { + "description": "The unique ID of the volume", + "examples": [ + "my-volume" + ], + "type": "string" + } + }, + "required": [ + "id" + ], "type": "object" }, "when": { @@ -1784,6 +2154,9 @@ }, "ttl": { "description": "The Time to Live (in seconds) for a module. When the TTL for a module is expired, the next deploy will force an update of the module.", + "examples": [ + "24*60*60" + ], "type": "string" }, "volume": {