mirror of
https://github.com/coder/registry.git
synced 2026-06-07 15:08:15 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c4cd86130 | |||
| 28fc956110 | |||
| 2701dc09af | |||
| 60611ed593 | |||
| 7df0cb25c5 | |||
| cbb39bda6f | |||
| 99bd4a4139 | |||
| c819ca7f83 | |||
| accf5a34ab | |||
| bb222f36a5 | |||
| 3677e93e36 | |||
| a3ba616aec | |||
| 24dc52fb17 | |||
| 678c3e631e |
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
uses: crate-ci/typos@v1.41.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 35 KiB |
+5
-47
@@ -1,47 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
|
||||
<path d="M0 0 C337.92 0 675.84 0 1024 0 C1024 337.92 1024 675.84 1024 1024 C686.08 1024 348.16 1024 0 1024 C0 686.08 0 348.16 0 0 Z " fill="#020708" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C40.22460379 0 80.4466421 0.00304941 120.67057991 0.15463066 C125.95372492 0.17442425 131.23687063 0.19398844 136.52001762 0.21324348 C147.77834068 0.25435249 159.03666181 0.29592484 170.29497623 0.33933926 C170.99953161 0.34205531 171.70408698 0.34477135 172.42999252 0.34756971 C173.13533008 0.3502892 173.84066764 0.35300869 174.56737906 0.35581058 C175.99454502 0.36131272 177.42171098 0.36681401 178.84887695 0.37231445 C179.55666117 0.37504276 180.26444539 0.37777106 180.99367761 0.38058204 C192.50410753 0.42485496 204.01454726 0.46550406 215.52499563 0.50465261 C227.56090905 0.54564378 239.59680781 0.58979401 251.63269895 0.63687855 C258.30394122 0.66290814 264.97518119 0.68769341 271.64644051 0.70907402 C277.91414179 0.72924498 284.18181589 0.75352717 290.4494915 0.78049088 C292.69376491 0.78952735 294.93804399 0.79730645 297.18232727 0.80341911 C322.46351662 0.87417207 347.71815265 1.5042775 373 2 C373 96.05 373 190.1 373 287 C249.91 287 126.82 287 0 287 C0 192.29 0 97.58 0 0 Z " fill="#999F9E" transform="translate(326,395)"/>
|
||||
<path d="M0 0 C37.29 0 74.58 0 113 0 C113 46.86 113 93.72 113 142 C75.71 142 38.42 142 0 142 C0 95.14 0 48.28 0 0 Z " fill="#181E23" transform="translate(550,445)"/>
|
||||
<path d="M0 0 C37.29 0 74.58 0 113 0 C113 46.53 113 93.06 113 141 C95.613125 141.0309375 95.613125 141.0309375 77.875 141.0625 C74.3054248 141.071604 70.73584961 141.08070801 67.05810547 141.09008789 C62.46899414 141.09460449 62.46899414 141.09460449 60.27615356 141.09544373 C58.84198693 141.09691347 57.40782093 141.10027703 55.97366333 141.10557556 C37.3007381 141.17069509 18.67741441 140.49151091 0 140 C0 93.8 0 47.6 0 0 Z " fill="#181E23" transform="translate(362,446)"/>
|
||||
<path d="M0 0 C0 14.52 0 29.04 0 44 C-118.14 44 -236.28 44 -358 44 C-359.71802382 40.56395235 -359.1196618 36.61561104 -359.09765625 32.8359375 C-359.0962413 31.92872955 -359.09482635 31.02152161 -359.09336853 30.08682251 C-359.08776096 27.18284845 -359.07520718 24.27895085 -359.0625 21.375 C-359.05748598 19.40885528 -359.0529229 17.44270935 -359.04882812 15.4765625 C-359.03778906 10.6510121 -359.02051935 5.82551903 -359 1 C-319.04215869 0.87185681 -279.08431526 0.74439427 -239.12646896 0.61781773 C-234.3862332 0.60280127 -229.64599745 0.58777929 -224.90576172 0.57275391 C-223.49049118 0.56826798 -223.49049118 0.56826798 -222.04662931 0.56369144 C-206.8611473 0.51554559 -191.67566674 0.46695532 -176.49018669 0.41819459 C-160.85514345 0.36800044 -145.22009869 0.3182983 -129.58505249 0.26903296 C-120.82560112 0.24141872 -112.06615054 0.21359008 -103.30670166 0.18519592 C-68.87102254 0.07367311 -34.43592726 -0.03058253 0 0 Z " fill="#FEFEFE" transform="translate(692,838)"/>
|
||||
<path d="M0 0 C117.15 0 234.3 0 355 0 C355 13.86 355 27.72 355 42 C237.85 42 120.7 42 0 42 C0 28.14 0 14.28 0 0 Z " fill="#FEFEFE" transform="translate(334,135)"/>
|
||||
<path d="M0 0 C11.88 0 23.76 0 36 0 C36 19.8 36 39.6 36 60 C91.44 60 146.88 60 204 60 C204 71.55 204 83.1 204 95 C168.85263573 95.02994071 133.70661685 94.95253395 98.55956407 94.80645666 C91.61749391 94.77764681 84.67541903 94.75005412 77.7333438 94.72249681 C65.36625264 94.67337442 52.99916523 94.62336685 40.63208008 94.57275391 C28.66105336 94.52376386 16.69002497 94.47522104 4.71899414 94.42724609 C3.97369616 94.4242591 3.22839818 94.4212721 2.46051542 94.41819459 C-1.2806363 94.4032035 -5.02178805 94.38822085 -8.76293981 94.37324238 C-39.50863188 94.25013489 -70.25431782 94.12555501 -101 94 C-101 93.67 -101 93.34 -101 93 C-134.66 92.505 -134.66 92.505 -169 92 C-169 81.44 -169 70.88 -169 60 C-113.23 60 -57.46 60 0 60 C0 40.2 0 20.4 0 0 Z " fill="#191F24" transform="translate(495,302)"/>
|
||||
<path d="M0 0 C62.7 0 125.4 0 190 0 C190 11.88 190 23.76 190 36 C127.3 36 64.6 36 0 36 C0 24.12 0 12.24 0 0 Z " fill="#474B51" transform="translate(418,786)"/>
|
||||
<path d="M0 0 C49.5 0 99 0 150 0 C150 14.52 150 29.04 150 44 C100.5 44 51 44 0 44 C0 29.48 0 14.96 0 0 Z " fill="#FDFEFE" transform="translate(200,239)"/>
|
||||
<path d="M0 0 C48.18 0 96.36 0 146 0 C146 14.52 146 29.04 146 44 C131.979151 44.02742613 117.95836252 44.05097148 103.9375 44.0625 C102.90375118 44.06335602 101.87000237 44.06421204 100.80492783 44.06509399 C67.19004349 44.08969196 33.60574878 43.83234053 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FEFEFE" transform="translate(677,239)"/>
|
||||
<path d="M0 0 C51.48 0 102.96 0 156 0 C156 13.53 156 27.06 156 41 C104.52 41 53.04 41 0 41 C0 27.47 0 13.94 0 0 Z " fill="#FEFEFE" transform="translate(259,188)"/>
|
||||
<path d="M0 0 C1.28729507 -0.00281982 2.57459015 -0.00563965 3.90089417 -0.00854492 C5.34078993 -0.00660032 6.78068557 -0.00455213 8.22058105 -0.00241089 C9.72647647 -0.00376487 11.23237154 -0.00554479 12.73826599 -0.00772095 C16.839391 -0.01229564 20.94049097 -0.01050558 25.04161644 -0.00734186 C29.32396896 -0.00481844 33.60631927 -0.00715575 37.88867188 -0.00872803 C45.08150621 -0.01054978 52.2743303 -0.00814327 59.46716309 -0.00338745 C67.79516627 0.00205518 76.12314497 0.00029253 84.45114756 -0.00521386 C91.58828139 -0.00974483 98.72540783 -0.01039561 105.86254263 -0.00777519 C110.13098648 -0.00621233 114.39941881 -0.00601889 118.66786194 -0.00931168 C122.67984227 -0.01219017 126.69179089 -0.01021511 130.70376778 -0.00441742 C132.18045054 -0.0030897 133.65713595 -0.00348413 135.13381767 -0.00568008 C137.1412128 -0.0083911 139.14861372 -0.0043972 141.15600586 0 C142.28205986 0.00037703 143.40811386 0.00075405 144.56829071 0.0011425 C147.07800293 0.12698364 147.07800293 0.12698364 148.07800293 1.12698364 C148.07800293 14.32698364 148.07800293 27.52698364 148.07800293 41.12698364 C96.92800293 41.12698364 45.77800293 41.12698364 -6.92199707 41.12698364 C-6.92199707 0.00231762 -6.92199707 0.00231762 0 0 Z " fill="#FEFEFE" transform="translate(616.9219970703125,187.87301635742188)"/>
|
||||
<path d="M0 0 C43.23 0 86.46 0 131 0 C131 15.84 131 31.68 131 48 C87.77 48 44.54 48 0 48 C0 32.16 0 16.32 0 0 Z " fill="#FEFEFE" transform="translate(128,353)"/>
|
||||
<path d="M0 0 C67.815 0.495 67.815 0.495 137 1 C137 15.19 137 29.38 137 44 C136.34 44 135.68 44 135 44 C135 44.66 135 45.32 135 46 C90.45 46 45.9 46 0 46 C0 30.82 0 15.64 0 0 Z " fill="#FDFEFE" transform="translate(156,294)"/>
|
||||
<path d="M0 0 C45.21 0 90.42 0 137 0 C137 14.19 137 28.38 137 43 C136.34 43 135.68 43 135 43 C135 43.66 135 44.32 135 45 C90.45 45 45.9 45 0 45 C0 30.15 0 15.3 0 0 Z " fill="#FBFCFC" transform="translate(732,295)"/>
|
||||
<path d="M0 0 C43.23 0 86.46 0 131 0 C131 15.18 131 30.36 131 46 C98.474264 46.72107728 65.97062256 47.09604795 33.4375 47.0625 C32.60141457 47.06164398 31.76532913 47.06078796 30.90390778 47.05990601 C20.60257228 47.04857042 10.30132061 47.02553343 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FEFEFE" transform="translate(765,354)"/>
|
||||
<path d="M0 0 C26.73 0 53.46 0 81 0 C82.19877676 50.94801223 82.19877676 50.94801223 82 74 C77.05167364 74.80762419 72.35065215 75.12200785 67.32055664 75.11352539 C66.57629897 75.11364593 65.8320413 75.11376646 65.06523037 75.11389065 C62.68017418 75.11315027 60.29519828 75.10556237 57.91015625 75.09765625 C56.42801959 75.09618411 54.9458824 75.09516508 53.46374512 75.09460449 C47.99665543 75.08938309 42.52957691 75.07542183 37.0625 75.0625 C24.831875 75.041875 12.60125 75.02125 0 75 C0 50.25 0 25.5 0 0 Z " fill="#FB4740" transform="translate(473,226)"/>
|
||||
<path d="M0 0 C36.96 0 73.92 0 112 0 C112.6261198 6.88731776 113.15084094 13.48218949 113.1328125 20.3515625 C113.13376923 21.17707611 113.13472595 22.00258972 113.13571167 22.8531189 C113.1363846 24.57216469 113.13459319 26.29121317 113.13037109 28.01025391 C113.12494037 30.65402778 113.13038111 33.29763707 113.13671875 35.94140625 C113.13605916 37.61979204 113.13478047 39.29817773 113.1328125 40.9765625 C113.13483673 41.76809723 113.13686096 42.55963196 113.13894653 43.37515259 C113.11499765 48.88500235 113.11499765 48.88500235 112 50 C110.54518505 50.0956161 109.08574514 50.12188351 107.62779236 50.12025452 C106.68403244 50.12162918 105.74027252 50.12300385 104.76791382 50.12442017 C103.72422638 50.12082489 102.68053894 50.11722961 101.60522461 50.11352539 C100.51267868 50.11367142 99.42013275 50.11381744 98.29447937 50.1139679 C94.66374767 50.11326822 91.03306654 50.10547329 87.40234375 50.09765625 C84.89270557 50.09579226 82.38306702 50.09436827 79.87342834 50.09336853 C73.93065407 50.08993348 67.98789616 50.08204273 62.04513031 50.07201904 C53.43224996 50.0578058 44.81936343 50.05254739 36.2064743 50.04621124 C24.13763437 50.03649856 12.06884226 50.01734306 0 50 C0 33.5 0 17 0 0 Z " fill="#FDFDFD" transform="translate(117,474)"/>
|
||||
<path d="M0 0 C36.63 0 73.26 0 111 0 C111 48 111 48 110 50 C73.7 50 37.4 50 0 50 C0 33.5 0 17 0 0 Z " fill="#FEFEFE" transform="translate(795,474)"/>
|
||||
<path d="M0 0 C54.12 0 108.24 0 164 0 C164 10.89 164 21.78 164 33 C109.88 33 55.76 33 0 33 C0 22.11 0 11.22 0 0 Z " fill="#181E23" transform="translate(431,622)"/>
|
||||
<path d="M0 0 C14.58571527 -0.0225479 29.17141907 -0.04091327 43.75714779 -0.05181217 C50.52910421 -0.05697491 57.30104905 -0.06401718 64.07299805 -0.07543945 C70.60224426 -0.08644714 77.13148294 -0.09227625 83.66073799 -0.09487724 C86.15793612 -0.09673199 88.65513355 -0.10035354 91.15232658 -0.10573006 C94.63664069 -0.11293684 98.12090431 -0.1139911 101.60522461 -0.11352539 C102.64891205 -0.11712067 103.69259949 -0.12071594 104.76791382 -0.12442017 C105.71167374 -0.1230455 106.65543365 -0.12167084 107.62779236 -0.12025452 C108.45279708 -0.12117631 109.2778018 -0.1220981 110.12780666 -0.12304783 C112 0 112 0 113 1 C113 16.18 113 31.36 113 47 C75.71 47 38.42 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FCFCFC" transform="translate(117,414)"/>
|
||||
<path d="M0 0 C0 15.84 0 31.68 0 48 C-36.96 48 -73.92 48 -112 48 C-112 32.49 -112 16.98 -112 1 C-92.15950898 0.75043408 -92.15950898 0.75043408 -83.5078125 0.64453125 C-77.67218771 0.57308961 -71.83656856 0.50132395 -66.00097656 0.42724609 C-43.99861261 0.14838814 -22.0044946 -0.06103882 0 0 Z " fill="#FAFAFA" transform="translate(906,537)"/>
|
||||
<path d="M0 0 C36.63 0 73.26 0 111 0 C111 15.51 111 31.02 111 47 C74.37 47 37.74 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FBFCFC" transform="translate(118,538)"/>
|
||||
<path d="M0 0 C36.3 0 72.6 0 110 0 C110 15.18 110 30.36 110 46 C86.98227311 46.96572558 64.03031552 47.12476904 40.99664307 47.06866455 C36.24190736 47.05823048 31.48716586 47.05382501 26.73242188 47.04882812 C17.48826669 47.03826689 8.24413623 47.02131845 -1 47 C-1.02529825 40.62793828 -1.04284496 34.25588946 -1.05493164 27.88378906 C-1.05996891 25.71483883 -1.06679891 23.545892 -1.07543945 21.37695312 C-1.08753151 18.26431437 -1.09323428 15.1517204 -1.09765625 12.0390625 C-1.10281754 11.065065 -1.10797882 10.0910675 -1.11329651 9.08755493 C-1.11337204 8.18677216 -1.11344757 7.28598938 -1.11352539 6.35791016 C-1.115746 5.56293121 -1.11796661 4.76795227 -1.12025452 3.94888306 C-1 2 -1 2 0 0 Z " fill="#FBFCFB" transform="translate(795,414)"/>
|
||||
<path d="M0 0 C34.98 0 69.96 0 106 0 C106 15.84 106 31.68 106 48 C71.02 48 36.04 48 0 48 C0 32.16 0 16.32 0 0 Z " fill="#FBFCFC" transform="translate(154,658)"/>
|
||||
<path d="M0 0 C34.32 0 68.64 0 104 0 C104 15.84 104 31.68 104 48 C69.35 48 34.7 48 -1 48 C-1 31.99652815 -0.6951464 15.9883671 0 0 Z " fill="#FBFBFB" transform="translate(765,658)"/>
|
||||
<path d="M0 0 C10.56252046 -0.02735617 21.1249617 -0.05092969 31.6875 -0.0625 C32.46348038 -0.06335602 33.23946075 -0.06421204 34.03895569 -0.06509399 C57.71503621 -0.08818645 81.33439347 0.16962784 105 1 C105 16.18 105 31.36 105 47 C70.35 47 35.7 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FCFDFD" transform="translate(125,598)"/>
|
||||
<path d="M0 0 C34.32 0 68.64 0 104 0 C104 15.51 104 31.02 104 47 C69.68 47 35.36 47 0 47 C0 31.49 0 15.98 0 0 Z " fill="#FBFCFC" transform="translate(794,598)"/>
|
||||
<path d="M0 0 C37.62 0 75.24 0 114 0 C114 12.54 114 25.08 114 38 C76.38 38 38.76 38 0 38 C0 25.46 0 12.92 0 0 Z " fill="#474B51" transform="translate(456,716)"/>
|
||||
<path d="M0 0 C30.69 0 61.38 0 93 0 C93 15.18 93 30.36 93 46 C62.31 46 31.62 46 0 46 C0 30.82 0 15.64 0 0 Z " fill="#FCFDFD" transform="translate(732,719)"/>
|
||||
<path d="M0 0 C30.69 0 61.38 0 93 0 C93 14.85 93 29.7 93 45 C62.31 45 31.62 45 0 45 C0 30.15 0 15.3 0 0 Z " fill="#FEFEFE" transform="translate(200,720)"/>
|
||||
<path d="M0 0 C11.22 0 22.44 0 34 0 C34.6943434 30.12954393 35.09747148 60.23833134 35.0625 90.375 C35.06121597 91.48948643 35.06121597 91.48948643 35.05990601 92.62648773 C35.04860423 101.7510267 35.02558276 110.87548153 35 120 C23.45 120 11.9 120 0 120 C0 80.4 0 40.8 0 0 Z " fill="#474B50" transform="translate(258,470)"/>
|
||||
<path d="M0 0 C30.36 0 60.72 0 92 0 C92 14.19 92 28.38 92 43 C61.64 43 31.28 43 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FCFDFD" transform="translate(259,781)"/>
|
||||
<path d="M0 0 C30.03 0 60.06 0 91 0 C91 14.19 91 28.38 91 43 C60.97 43 30.94 43 0 43 C0 28.81 0 14.62 0 0 Z " fill="#FCFDFD" transform="translate(674,781)"/>
|
||||
<path d="M0 0 C16.5 0 33 0 50 0 C50 25.41 50 50.82 50 77 C33.5 77 17 77 0 77 C0 51.59 0 26.18 0 0 Z " fill="#2CCDD4" transform="translate(582,478)"/>
|
||||
<path d="M0 0 C10.56 0 21.12 0 32 0 C32 39.27 32 78.54 32 119 C21.44 119 10.88 119 0 119 C0 79.73 0 40.46 0 0 Z " fill="#474B50" transform="translate(732,471)"/>
|
||||
<path d="M0 0 C16.17 0 32.34 0 49 0 C49 25.41 49 50.82 49 77 C32.83 77 16.66 77 0 77 C0 51.59 0 26.18 0 0 Z " fill="#2DCCD2" transform="translate(394,478)"/>
|
||||
<path d="M0 0 C15.84 0 31.68 0 48 0 C48 0.33 48 0.66 48 1 C32.49 1 16.98 1 1 1 C1 25.75 1 50.5 1 76 C28.06 75.67 55.12 75.34 83 75 C82.67 65.76 82.34 56.52 82 47 C81.93702338 38.74754429 81.90172306 30.50164687 81.9375 22.25 C81.94254243 20.16145945 81.94710032 18.07291767 81.95117188 15.984375 C81.96192066 10.98955155 81.97902162 5.99479052 82 1 C82.33 1 82.66 1 83 1 C83.57831229 19.95510799 84.11107346 38.91589618 84.23217773 57.88024902 C84.24403757 59.45720133 84.26110851 61.03412324 84.28344727 62.61096191 C84.31307026 64.8085447 84.32331211 67.00536665 84.328125 69.203125 C84.3374707 70.44739258 84.34681641 71.69166016 84.35644531 72.97363281 C84 76 84 76 82.73034668 77.68478394 C80.56608158 79.32981626 79.63802574 79.28920572 76.95800781 79.06982422 C75.71830231 78.98657394 75.71830231 78.98657394 74.45355225 78.90164185 C73.54591125 78.82839691 72.63827026 78.75515198 71.703125 78.6796875 C62.53186675 78.09888659 53.42327768 77.92843278 44.234375 78.0078125 C42.339151 78.0199881 42.339151 78.0199881 40.40563965 78.03240967 C35.17784805 78.06788344 29.95020985 78.1100683 24.72265625 78.17138672 C20.85162217 78.21602536 16.98061225 78.23702797 13.109375 78.2578125 C11.32536285 78.28574387 11.32536285 78.28574387 9.50531006 78.3142395 C7.87024506 78.31942093 7.87024506 78.31942093 6.20214844 78.32470703 C4.76279938 78.33922409 4.76279938 78.33922409 3.29437256 78.35403442 C2.15865814 78.17878738 2.15865814 78.17878738 1 78 C-1.11766624 74.82350064 -1.24885987 74.11349575 -1.23046875 70.48828125 C-1.22917969 69.52510986 -1.22789062 68.56193848 -1.2265625 67.56958008 C-1.21367187 66.49474365 -1.20078125 65.41990723 -1.1875 64.3125 C-1.18105469 63.18158936 -1.17460937 62.05067871 -1.16796875 60.88549805 C-1.00069226 40.5888483 -0.4546929 20.29067045 0 0 Z " fill="#4C0F0C" transform="translate(472,225)"/>
|
||||
<path d="M0 0 C8.45159814 -0.02350035 16.90318192 -0.04110214 25.35480499 -0.05181217 C29.28212158 -0.05696064 33.20941531 -0.06390567 37.13671875 -0.07543945 C53.56520732 -0.12238185 69.94755695 -0.05605073 86.36132812 0.74389648 C96.40041688 1.22706882 106.45208983 1.36454744 116.5 1.5625 C118.60677558 1.60589009 120.71354645 1.64950917 122.8203125 1.69335938 C127.88015843 1.79819282 132.94005639 1.8999973 138 2 C138 2.33 138 2.66 138 3 C92.79 3 47.58 3 1 3 C1 5.64 1 8.28 1 11 C0.67 11 0.34 11 0 11 C0 7.37 0 3.74 0 0 Z " fill="#6F7576" transform="translate(326,395)"/>
|
||||
<path d="M0 0 C0.33 0 0.66 0 1 0 C1 7.26 1 14.52 1 22 C37.3 22 73.6 22 111 22 C111.495 22.99 111.495 22.99 112 24 C74.71 24 37.42 24 -1 24 C-0.67 16.08 -0.34 8.16 0 0 Z " fill="#0E1418" transform="translate(551,563)"/>
|
||||
<path d="M0 0 C0.33 0 0.66 0 1 0 C1.57831229 18.95510799 2.11107346 37.91589618 2.23217773 56.88024902 C2.24403757 58.45720133 2.26110851 60.03412324 2.28344727 61.61096191 C2.31307026 63.8085447 2.32331211 66.00536665 2.328125 68.203125 C2.3374707 69.44739258 2.34681641 70.69166016 2.35644531 71.97363281 C2 75 2 75 0.79858398 76.92016602 C-1.65515594 78.39334257 -3.26106965 78.2550765 -6.10546875 78.07421875 C-7.08837891 78.01943359 -8.07128906 77.96464844 -9.08398438 77.90820312 C-10.62022461 77.79895508 -10.62022461 77.79895508 -12.1875 77.6875 C-13.22326172 77.62626953 -14.25902344 77.56503906 -15.32617188 77.50195312 C-17.88531957 77.34864013 -20.44267045 77.1807893 -23 77 C-23.33 76.34 -23.66 75.68 -24 75 C-11.625 74.505 -11.625 74.505 1 74 C0.835 70.32875 0.67 66.6575 0.5 62.875 C-0.33621628 41.9290255 -0.08802477 20.95802133 0 0 Z " fill="#390D0B" transform="translate(554,226)"/>
|
||||
<path d="M0 0 C0.66 0 1.32 0 2 0 C2 15.18 2 30.36 2 46 C0.68 45.67 -0.64 45.34 -2 45 C-1.88269497 38.75188831 -1.75785931 32.50396027 -1.62768555 26.25610352 C-1.58426577 24.12885993 -1.54259607 22.0015799 -1.50268555 19.87426758 C-1.44515293 16.82361849 -1.38141033 13.77315411 -1.31640625 10.72265625 C-1.29969376 9.76537125 -1.28298126 8.80808624 -1.26576233 7.8217926 C-1.24581711 6.94095505 -1.22587189 6.06011749 -1.20532227 5.15258789 C-1.18977798 4.37315323 -1.1742337 3.59371857 -1.15821838 2.79066467 C-1 1 -1 1 0 0 Z " fill="#D1D3D3" transform="translate(228,599)"/>
|
||||
<path d="M0 0 C35.64 0.33 71.28 0.66 108 1 C108 1.33 108 1.66 108 2 C54.54 2.495 54.54 2.495 0 3 C0 2.01 0 1.02 0 0 Z " fill="#F5F7F7" transform="translate(117,521)"/>
|
||||
<path d="M0 0 C6.93 0 13.86 0 21 0 C21 0.99 21 1.98 21 3 C14.07 3 7.14 3 0 3 C0 2.01 0 1.02 0 0 Z " fill="#272A2A" transform="translate(158,292)"/>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="135" height="62" viewBox="0 0 135 62" fill="none">
|
||||
<path d="M3.168 48V22.272H9.648L9.888 28.464L9.216 28.176C9.568 26.8 10.096 25.632 10.8 24.672C11.536 23.712 12.416 22.976 13.44 22.464C14.464 21.952 15.584 21.696 16.8 21.696C18.944 21.696 20.672 22.32 21.984 23.568C23.328 24.816 24.192 26.496 24.576 28.608L23.664 28.656C23.952 27.152 24.448 25.888 25.152 24.864C25.888 23.808 26.784 23.024 27.84 22.512C28.896 21.968 30.08 21.696 31.392 21.696C33.184 21.696 34.72 22.064 36 22.8C37.28 23.536 38.272 24.64 38.976 26.112C39.68 27.552 40.032 29.328 40.032 31.44V48H32.832V33.456C32.832 31.44 32.528 29.936 31.92 28.944C31.312 27.92 30.32 27.408 28.944 27.408C28.08 27.408 27.344 27.648 26.736 28.128C26.128 28.608 25.648 29.312 25.296 30.24C24.976 31.136 24.816 32.24 24.816 33.552V48H18.336V33.552C18.336 31.568 18.048 30.048 17.472 28.992C16.896 27.936 15.904 27.408 14.496 27.408C13.632 27.408 12.88 27.648 12.24 28.128C11.632 28.608 11.168 29.312 10.848 30.24C10.528 31.168 10.368 32.272 10.368 33.552V48H3.168ZM54.2254 48.576C51.5694 48.576 49.4894 47.728 47.9854 46.032C46.5134 44.304 45.7774 41.904 45.7774 38.832V22.272H52.9774V37.152C52.9774 39.136 53.2814 40.592 53.8894 41.52C54.4974 42.416 55.4574 42.864 56.7694 42.864C58.2414 42.864 59.3774 42.368 60.1774 41.376C61.0094 40.352 61.4254 38.832 61.4254 36.816V22.272H68.6254V48H62.0494L61.8574 40.608L62.7694 40.8C62.3854 43.36 61.4734 45.296 60.0334 46.608C58.5934 47.92 56.6574 48.576 54.2254 48.576ZM72.8486 48L82.0166 34.944L73.0886 22.272H80.7206L86.2406 30.528L91.5686 22.272H99.3926L90.5126 34.992L99.6326 48H92.0006L86.3366 39.264L80.6246 48H72.8486Z" fill="white"/>
|
||||
<rect x="109" y="13" width="26" height="35" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>Scaleway icon</title><path d="M16.61 11.11v5.72a1.77 1.77 0 0 1-1.54 1.69h-4a1.43 1.43 0 0 1-1.31-1.22 1.09 1.09 0 0 1 0-.18 1.37 1.37 0 0 1 1.37-1.36h1.74a1 1 0 0 0 1-1v-3.62a1.4 1.4 0 0 1 1.18-1.39h.17a1.37 1.37 0 0 1 1.39 1.36zm-6.46 1.74V9.26a1 1 0 0 1 1-1H13a1.37 1.37 0 0 0 1.37-1.37 1 1 0 0 0 0-.17 1.45 1.45 0 0 0-1.41-1.2H9a1.81 1.81 0 0 0-1.58 1.66v5.7a1.37 1.37 0 0 0 1.37 1.37H9a1.4 1.4 0 0 0 1.15-1.4zm12-4.29V20A4.53 4.53 0 0 1 18 24h-7.58a8.57 8.57 0 0 1-8.56-8.57V4.54A4.54 4.54 0 0 1 6.4 0h7.18a8.56 8.56 0 0 1 8.56 8.56zm-2.74 0a5.83 5.83 0 0 0-5.82-5.82H6.4a1.79 1.79 0 0 0-1.8 1.8v10.89a5.83 5.83 0 0 0 5.82 5.8h7.44a1.79 1.79 0 0 0 1.54-1.48z"/></svg>
|
||||
|
After Width: | Height: | Size: 913 B |
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"type_meta": {
|
||||
"cx22": { "cores": 2, "memory_gb": 4, "disk_gb": 40 },
|
||||
"cx32": { "cores": 4, "memory_gb": 8, "disk_gb": 80 },
|
||||
"cx42": { "cores": 8, "memory_gb": 16, "disk_gb": 160 },
|
||||
"cx52": { "cores": 16, "memory_gb": 32, "disk_gb": 320 },
|
||||
"cpx11": { "cores": 2, "memory_gb": 2, "disk_gb": 40 },
|
||||
"cpx21": { "cores": 3, "memory_gb": 4, "disk_gb": 80 },
|
||||
"cpx31": { "cores": 4, "memory_gb": 8, "disk_gb": 160 },
|
||||
"cpx41": { "cores": 8, "memory_gb": 16, "disk_gb": 240 },
|
||||
"cpx51": { "cores": 16, "memory_gb": 32, "disk_gb": 360 },
|
||||
"ccx13": { "cores": 2, "memory_gb": 8, "disk_gb": 80 },
|
||||
"ccx23": { "cores": 4, "memory_gb": 16, "disk_gb": 160 },
|
||||
"ccx33": { "cores": 8, "memory_gb": 32, "disk_gb": 240 },
|
||||
"ccx43": { "cores": 16, "memory_gb": 64, "disk_gb": 360 },
|
||||
"ccx53": { "cores": 32, "memory_gb": 128, "disk_gb": 600 },
|
||||
"ccx63": { "cores": 48, "memory_gb": 192, "disk_gb": 960 }
|
||||
},
|
||||
"availability": {
|
||||
"fsn1": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"ash": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hel1": ["cx22", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hil": ["cpx11", "cpx21", "cpx31", "cpx41", "ccx13", "ccx23", "ccx33"],
|
||||
"nbg1": ["cx22", "cx32", "cx42", "cx52", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"sin": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"]
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ terraform {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
http = {
|
||||
source = "hashicorp/http"
|
||||
version = "~> 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +21,24 @@ provider "hcloud" {
|
||||
token = var.hcloud_token
|
||||
}
|
||||
|
||||
data "http" "hcloud_locations" {
|
||||
url = "https://api.hetzner.cloud/v1/locations"
|
||||
|
||||
request_headers = {
|
||||
Authorization = "Bearer ${var.hcloud_token}"
|
||||
Accept = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
data "http" "hcloud_server_types" {
|
||||
url = "https://api.hetzner.cloud/v1/server_types"
|
||||
|
||||
request_headers = {
|
||||
Authorization = "Bearer ${var.hcloud_token}"
|
||||
Accept = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
# Available locations: https://docs.hetzner.com/cloud/general/locations/
|
||||
data "coder_parameter" "hcloud_location" {
|
||||
name = "hcloud_location"
|
||||
@@ -24,29 +46,18 @@ data "coder_parameter" "hcloud_location" {
|
||||
description = "Select the Hetzner Cloud location for your workspace."
|
||||
type = "string"
|
||||
default = "fsn1"
|
||||
option {
|
||||
name = "DE Falkenstein"
|
||||
value = "fsn1"
|
||||
}
|
||||
option {
|
||||
name = "US Ashburn, VA"
|
||||
value = "ash"
|
||||
}
|
||||
option {
|
||||
name = "US Hillsboro, OR"
|
||||
value = "hil"
|
||||
}
|
||||
option {
|
||||
name = "SG Singapore"
|
||||
value = "sin"
|
||||
}
|
||||
option {
|
||||
name = "DE Nuremberg"
|
||||
value = "nbg1"
|
||||
}
|
||||
option {
|
||||
name = "FI Helsinki"
|
||||
value = "hel1"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.hcloud_locations
|
||||
content {
|
||||
name = format(
|
||||
"%s (%s, %s)",
|
||||
upper(option.value.name),
|
||||
option.value.city,
|
||||
option.value.country
|
||||
)
|
||||
value = option.value.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,17 +120,47 @@ resource "hcloud_volume_attachment" "home_volume_attachment" {
|
||||
locals {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
|
||||
# Data source: local JSON file under the module directory
|
||||
# Check API for latest server types & availability: https://docs.hetzner.cloud/reference/cloud#server-types
|
||||
hcloud_server_types_data = jsondecode(file("${path.module}/hetzner_server_types.json"))
|
||||
hcloud_server_type_meta = local.hcloud_server_types_data.type_meta
|
||||
hcloud_server_types_by_location = local.hcloud_server_types_data.availability
|
||||
# --------------------
|
||||
# Locations
|
||||
# --------------------
|
||||
hcloud_locations = [
|
||||
for loc in jsondecode(data.http.hcloud_locations.response_body).locations : {
|
||||
name = loc.name
|
||||
city = loc.city
|
||||
country = loc.country
|
||||
}
|
||||
]
|
||||
|
||||
# --------------------
|
||||
# Server Types
|
||||
# --------------------
|
||||
hcloud_server_types = {
|
||||
for st in jsondecode(data.http.hcloud_server_types.response_body).server_types :
|
||||
st.name => {
|
||||
cores = st.cores
|
||||
memory_gb = st.memory
|
||||
disk_gb = st.disk
|
||||
locations = [for l in st.locations : l.name]
|
||||
deprecated = st.deprecated
|
||||
}
|
||||
if st.deprecated == false
|
||||
}
|
||||
|
||||
hcloud_server_type_options_for_selected_location = [
|
||||
for type_name in lookup(local.hcloud_server_types_by_location, data.coder_parameter.hcloud_location.value, []) : {
|
||||
name = format("%s (%d vCPU, %dGB RAM, %dGB)", upper(type_name), local.hcloud_server_type_meta[type_name].cores, local.hcloud_server_type_meta[type_name].memory_gb, local.hcloud_server_type_meta[type_name].disk_gb)
|
||||
value = type_name
|
||||
for name, meta in local.hcloud_server_types : {
|
||||
name = format(
|
||||
"%s (%d vCPU, %dGB RAM, %dGB)",
|
||||
upper(name),
|
||||
meta.cores,
|
||||
meta.memory_gb,
|
||||
meta.disk_gb
|
||||
)
|
||||
value = name
|
||||
}
|
||||
if contains(
|
||||
meta.locations,
|
||||
data.coder_parameter.hcloud_location.value
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -180,4 +221,4 @@ module "code-server" {
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ up a default or custom tmux configuration with session save/restore capabilities
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -39,7 +39,7 @@ module "tmux" {
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.example.id
|
||||
tmux_config = "" # Optional: custom tmux.conf content
|
||||
save_interval = 1 # Optional: save interval in minutes
|
||||
@@ -78,7 +78,7 @@ This module can provision multiple tmux sessions, each as a separate app in the
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = var.agent_id
|
||||
sessions = ["default", "dev", "anomaly"]
|
||||
tmux_config = <<-EOT
|
||||
|
||||
@@ -55,7 +55,7 @@ resource "coder_script" "tmux" {
|
||||
display_name = "tmux"
|
||||
icon = "/icon/terminal.svg"
|
||||
script = templatefile("${path.module}/scripts/run.sh", {
|
||||
TMUX_CONFIG = var.tmux_config
|
||||
TMUX_CONFIG = base64encode(var.tmux_config)
|
||||
SAVE_INTERVAL = var.save_interval
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
@@ -4,7 +4,7 @@ BOLD='\033[0;1m'
|
||||
|
||||
# Convert templated variables to shell variables
|
||||
SAVE_INTERVAL="${SAVE_INTERVAL}"
|
||||
TMUX_CONFIG="${TMUX_CONFIG}"
|
||||
TMUX_CONFIG=$(echo -n "${TMUX_CONFIG}" | base64 -d)
|
||||
|
||||
# Function to install tmux
|
||||
install_tmux() {
|
||||
@@ -73,7 +73,7 @@ setup_tmux_config() {
|
||||
mkdir -p "$config_dir"
|
||||
|
||||
if [ -n "$TMUX_CONFIG" ]; then
|
||||
printf "$TMUX_CONFIG" > "$config_file"
|
||||
printf "%s" "$TMUX_CONFIG" > "$config_file"
|
||||
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
|
||||
else
|
||||
cat > "$config_file" << EOF
|
||||
|
||||
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.8"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.8"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
enable_boundary = true
|
||||
@@ -72,7 +72,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.8"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -108,7 +108,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.8"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
install_claude_code = true
|
||||
@@ -130,7 +130,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.8"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -203,7 +203,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.8"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -260,7 +260,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.8"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
@@ -48,6 +48,13 @@ function install_boundary() {
|
||||
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
|
||||
# Install boundary by compiling from source
|
||||
echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)"
|
||||
|
||||
echo "Removing existing boundary directory to allow re-running the script safely"
|
||||
if [ -d boundary ]; then
|
||||
rm -rf boundary
|
||||
fi
|
||||
|
||||
echo "Clone boundary repository"
|
||||
git clone https://github.com/coder/boundary.git
|
||||
cd boundary
|
||||
git checkout "$ARG_BOUNDARY_VERSION"
|
||||
|
||||
@@ -14,7 +14,7 @@ A file browser for your workspace.
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -41,7 +41,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
database_path = ".config/filebrowser.db"
|
||||
}
|
||||
@@ -53,7 +53,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
subdomain = false
|
||||
|
||||
@@ -28,7 +28,7 @@ if [[ ! -f "${DB_PATH}" ]]; then
|
||||
filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
|
||||
fi
|
||||
|
||||
filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
|
||||
filebrowser config set --baseURL=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
|
||||
|
||||
printf "👷 Starting filebrowser in background... \n\n"
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { type Server, serve } from "bun";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { serve } from "bun";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
setDefaultTimeout,
|
||||
} from "bun:test";
|
||||
import {
|
||||
createJSONResponse,
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
@@ -11,77 +19,48 @@ import {
|
||||
writeCoder,
|
||||
} from "~test";
|
||||
|
||||
describe("github-upload-public-key", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("creates new key if one does not exist", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
await writeCoder(id, "echo foo");
|
||||
|
||||
const url = server.url.toString().slice(0, -1);
|
||||
const exec = await execContainer(id, [
|
||||
"env",
|
||||
`CODER_ACCESS_URL=${url}`,
|
||||
`GITHUB_API_URL=${url}`,
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key has been added to GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
// we need to increase timeout to pull the container
|
||||
}, 15000);
|
||||
|
||||
it("does nothing if one already exists", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
// use keyword to make server return a existing key
|
||||
await writeCoder(id, "echo findkey");
|
||||
|
||||
const url = server.url.toString().slice(0, -1);
|
||||
const exec = await execContainer(id, [
|
||||
"env",
|
||||
`CODER_ACCESS_URL=${url}`,
|
||||
`GITHUB_API_URL=${url}`,
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key is already on GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
});
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
afterEach(async () => {
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const setupContainer = async (
|
||||
image = "lorello/alpine-bash",
|
||||
vars: Record<string, string> = {},
|
||||
) => {
|
||||
const server = await setupServer();
|
||||
const server = setupServer();
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
|
||||
registerCleanup(async () => {
|
||||
server.stop();
|
||||
});
|
||||
registerCleanup(async () => {
|
||||
await removeContainer(id);
|
||||
});
|
||||
|
||||
return { id, instance, server };
|
||||
};
|
||||
|
||||
const setupServer = async (): Promise<Server> => {
|
||||
let url: URL;
|
||||
const fakeSlackHost = serve({
|
||||
const setupServer = () => {
|
||||
const fakeGithubHost = serve({
|
||||
fetch: (req) => {
|
||||
url = new URL(req.url);
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/api/v2/users/me/gitsshkey") {
|
||||
return createJSONResponse({
|
||||
public_key: "exists",
|
||||
@@ -128,5 +107,60 @@ const setupServer = async (): Promise<Server> => {
|
||||
port: 0,
|
||||
});
|
||||
|
||||
return fakeSlackHost;
|
||||
return fakeGithubHost;
|
||||
};
|
||||
|
||||
setDefaultTimeout(30 * 1000);
|
||||
|
||||
describe("github-upload-public-key", () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("creates new key if one does not exist", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
await writeCoder(id, "echo foo");
|
||||
|
||||
const url = server.url.toString().slice(0, -1);
|
||||
const exec = await execContainer(id, [
|
||||
"env",
|
||||
`CODER_ACCESS_URL=${url}`,
|
||||
`GITHUB_API_URL=${url}`,
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key has been added to GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("does nothing if one already exists", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
// use keyword to make server return a existing key
|
||||
await writeCoder(id, "echo findkey");
|
||||
|
||||
const url = server.url.toString().slice(0, -1);
|
||||
const exec = await execContainer(id, [
|
||||
"env",
|
||||
`CODER_ACCESS_URL=${url}`,
|
||||
`GITHUB_API_URL=${url}`,
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key is already on GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,9 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
# tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional
|
||||
}
|
||||
```
|
||||
|
||||
@@ -40,7 +39,7 @@ When `default` contains IDE codes, those IDEs are created directly without user
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
|
||||
@@ -53,7 +52,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
# Show parameter with limited options
|
||||
@@ -67,7 +66,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
@@ -82,7 +81,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -109,7 +108,7 @@ module "jetbrains" {
|
||||
module "jetbrains_pycharm" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -129,11 +128,11 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons:
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
|
||||
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -170,13 +169,6 @@ resource "coder_metadata" "container_info" {
|
||||
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config`
|
||||
- `major_version` and `channel` control which API endpoint is queried (when API access is available)
|
||||
|
||||
### Tooltip
|
||||
|
||||
- **`tooltip`**: Optional markdown text displayed when hovering over IDE app buttons
|
||||
- If not specified, no tooltip is shown
|
||||
- Supports markdown formatting for rich text (bold, italic, links, etc.)
|
||||
- All IDE apps created by this module will show the same tooltip text
|
||||
|
||||
## Supported IDEs
|
||||
|
||||
All JetBrains IDEs with remote development capabilities:
|
||||
|
||||
@@ -2,15 +2,15 @@ variables {
|
||||
# Default IDE config, mirrored from main.tf for test assertions.
|
||||
# If main.tf defaults change, update this map to match.
|
||||
expected_ide_config = {
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,16 +187,16 @@ run "tooltip_when_provided" {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
|
||||
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."])
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
|
||||
error_message = "Expected coder_app tooltip to be set when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "tooltip_null_when_not_provided" {
|
||||
run "tooltip_default_when_not_provided" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
@@ -206,8 +206,41 @@ run "tooltip_null_when_not_provided" {
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == null])
|
||||
error_message = "Expected coder_app tooltip to be null when not provided"
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
|
||||
error_message = "Expected coder_app tooltip to be the default JetBrains Toolbox message when not provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "channel_eap" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
channel = "eap"
|
||||
major_version = "latest"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.ide_metadata["GO"].json_data.type == "eap"
|
||||
error_message = "Expected the API to return a release of type 'eap', but got '${output.ide_metadata["GO"].json_data.type}'"
|
||||
}
|
||||
}
|
||||
|
||||
run "specific_major_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
major_version = "2025.3"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.ide_metadata["GO"].json_data.majorVersion == "2025.3"
|
||||
error_message = "Expected the API to return a release for major version '2025.3', but got '${output.ide_metadata["GO"].json_data.majorVersion}'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,3 +327,27 @@ run "output_multiple_ides" {
|
||||
error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'"
|
||||
}
|
||||
}
|
||||
run "validate_output_schema" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = alltrue([
|
||||
for key, meta in output.ide_metadata : (
|
||||
can(meta.icon) &&
|
||||
can(meta.name) &&
|
||||
can(meta.identifier) &&
|
||||
can(meta.key) &&
|
||||
can(meta.build) &&
|
||||
# json_data can be null, but the key must exist
|
||||
can(meta.json_data)
|
||||
)
|
||||
])
|
||||
error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test."
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ variable "coder_parameter_order" {
|
||||
variable "tooltip" {
|
||||
type = string
|
||||
description = "Markdown text that is displayed when hovering over workspace apps."
|
||||
default = null
|
||||
default = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
|
||||
variable "major_version" {
|
||||
@@ -70,8 +70,8 @@ variable "major_version" {
|
||||
description = "The major version of the IDE. i.e. 2025.1"
|
||||
default = "latest"
|
||||
validation {
|
||||
condition = can(regex("^[0-9]{4}\\.[0-2]{1}$", var.major_version)) || var.major_version == "latest"
|
||||
error_message = "The major_version must be a valid version number. i.e. 2025.1 or latest"
|
||||
condition = can(regex("^[0-9]{4}\\.[1-3]$", var.major_version)) || var.major_version == "latest"
|
||||
error_message = "The major_version must be a valid version number (e.g., 2025.1) or 'latest'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ variable "download_base_link" {
|
||||
|
||||
data "http" "jetbrains_ide_versions" {
|
||||
for_each = length(var.default) == 0 ? var.options : var.default
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}"
|
||||
}
|
||||
|
||||
variable "ide_config" {
|
||||
@@ -138,9 +138,9 @@ variable "ide_config" {
|
||||
- build: The build number of the IDE.
|
||||
Example:
|
||||
{
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
}
|
||||
EOT
|
||||
type = map(object({
|
||||
@@ -149,15 +149,15 @@ variable "ide_config" {
|
||||
build = string
|
||||
}))
|
||||
default = {
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
|
||||
}
|
||||
validation {
|
||||
condition = length(var.ide_config) > 0
|
||||
@@ -182,6 +182,20 @@ locals {
|
||||
)
|
||||
}
|
||||
|
||||
# Filter the parsed response for the requested major version if not "latest"
|
||||
filtered_releases = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => [
|
||||
for r in try(local.parsed_responses[code][keys(local.parsed_responses[code])[0]], []) :
|
||||
r if var.major_version == "latest" || r.majorVersion == var.major_version
|
||||
]
|
||||
}
|
||||
|
||||
# Select the latest release for the requested major version (first item in the filtered list)
|
||||
selected_releases = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code =>
|
||||
length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null
|
||||
}
|
||||
|
||||
# Dynamically generate IDE configurations based on options with fallback to ide_config
|
||||
options_metadata = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => {
|
||||
@@ -191,13 +205,10 @@ locals {
|
||||
key = code
|
||||
|
||||
# Use API build number if available, otherwise fall back to ide_config build number
|
||||
build = length(keys(local.parsed_responses[code])) > 0 ? (
|
||||
local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0].build
|
||||
) : var.ide_config[code].build
|
||||
build = local.selected_releases[code] != null ? local.selected_releases[code].build : var.ide_config[code].build
|
||||
|
||||
# Store API data for potential future use (only if API is available)
|
||||
json_data = length(keys(local.parsed_responses[code])) > 0 ? local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0] : null
|
||||
response_key = length(keys(local.parsed_responses[code])) > 0 ? keys(local.parsed_responses[code])[0] : null
|
||||
# Store API data for potential future use
|
||||
json_data = local.selected_releases[code]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install and run [mux](https://github.com/coder/mux) in a Coder wor
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -37,7 +37,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -48,7 +48,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.main.id
|
||||
# Default is "latest"; set to a specific version to pin
|
||||
install_version = "0.4.0"
|
||||
@@ -61,7 +61,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.main.id
|
||||
port = 8080
|
||||
}
|
||||
@@ -75,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.main.id
|
||||
use_cached = true
|
||||
}
|
||||
@@ -89,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.main.id
|
||||
install = false
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
fi
|
||||
# sed-based fallback
|
||||
if [ -z "$TARBALL_URL" ]; then
|
||||
TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"tarball\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
|
||||
TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"tarball":"\([^"]*\)".*/\1/p' | head -n1)"
|
||||
fi
|
||||
# Fallback: resolve version then construct tarball URL
|
||||
if [ -z "$TARBALL_URL" ]; then
|
||||
@@ -106,10 +106,10 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_JSON" | node -e 'try{const fs=require("fs");const data=JSON.parse(fs.readFileSync(0,"utf8"));if(data&&data.version){console.log(data.version);}}catch(e){}')"
|
||||
fi
|
||||
if [ -z "$RESOLVED_VERSION" ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -n1)"
|
||||
fi
|
||||
if [ -z "$RESOLVED_VERSION" ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '\"version\":\"[^\"]*\"' | head -n1 | cut -d '\"' -f4)"
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '"version":"[^"]*"' | head -n1 | cut -d '"' -f4)"
|
||||
fi
|
||||
if [ -n "$RESOLVED_VERSION" ]; then
|
||||
VERSION_TO_USE="$RESOLVED_VERSION"
|
||||
@@ -141,9 +141,9 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
fi
|
||||
if [ -z "$BIN_PATH" ]; then
|
||||
# sed fallbacks (handle both string and object forms)
|
||||
BIN_PATH=$(sed -n 's/.*\"bin\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' "$TMP_DIR/package/package.json" | head -n1)
|
||||
BIN_PATH=$(sed -n 's/.*"bin"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP_DIR/package/package.json" | head -n1)
|
||||
if [ -z "$BIN_PATH" ]; then
|
||||
BIN_PATH=$(sed -n '/\"bin\"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*\"mux\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)
|
||||
BIN_PATH=$(sed -n '/"bin"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*"mux"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
fi
|
||||
fi
|
||||
if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then
|
||||
|
||||
@@ -13,7 +13,7 @@ Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
@@ -34,7 +34,7 @@ If you have a Vault token, you can provide it to automatically configure authent
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token # Optional
|
||||
@@ -50,7 +50,7 @@ Install the Vault CLI without any authentication:
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
@@ -61,7 +61,7 @@ module "vault_cli" {
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_cli_version = "1.15.0"
|
||||
@@ -73,7 +73,7 @@ module "vault_cli" {
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
install_dir = "/home/coder/bin"
|
||||
@@ -87,7 +87,7 @@ For Vault Enterprise users who need to specify a namespace:
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token
|
||||
@@ -102,7 +102,7 @@ Install the Vault Enterprise binary. This is required if using SAML authenticati
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
enterprise = true
|
||||
|
||||
@@ -7,40 +7,34 @@ INSTALL_DIR=${INSTALL_DIR}
|
||||
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
|
||||
ENTERPRISE=${ENTERPRISE}
|
||||
|
||||
# Fetch URL content. If dest is provided, write to file; otherwise output to stdout.
|
||||
# Usage: fetch <url> [dest]
|
||||
# Fetch URL content to stdout
|
||||
fetch() {
|
||||
url="$1"
|
||||
dest="$${2:-}"
|
||||
|
||||
# Detect HTTP client on first run
|
||||
if [ -z "$${HTTP_CLIENT:-}" ]; then
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="curl"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="wget"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="busybox"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$${dest}" ]; then
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" -o "$${dest}" ;;
|
||||
wget) wget -O "$${dest}" "$${url}" ;;
|
||||
busybox) busybox wget -O "$${dest}" "$${url}" ;;
|
||||
esac
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -sSL --fail "$${url}"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -qO- "$${url}"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox wget -qO- "$${url}"
|
||||
else
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" ;;
|
||||
wget) wget -qO- "$${url}" ;;
|
||||
busybox) busybox wget -qO- "$${url}" ;;
|
||||
esac
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Download URL to a file
|
||||
fetch_to_file() {
|
||||
dest="$1"
|
||||
url="$2"
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -sSL --fail "$${url}" -o "$${dest}"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -O "$${dest}" "$${url}"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox wget -O "$${dest}" "$${url}"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -141,7 +135,7 @@ install() {
|
||||
cd "$${TEMP_DIR}" || return 1
|
||||
|
||||
printf "Downloading from %s\n" "$${DOWNLOAD_URL}"
|
||||
if ! fetch "$${DOWNLOAD_URL}" vault.zip; then
|
||||
if ! fetch_to_file vault.zip "$${DOWNLOAD_URL}"; then
|
||||
printf "Failed to download Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,15 @@
|
||||
---
|
||||
display_name: "Mossy Lion"
|
||||
bio: "Tinkerer, exploring European cloud providers"
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "mossylion"
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Mossy Lion
|
||||
|
||||
Exploring European cloud providers. Usually find me outdoors but if not, somewhere deep in Kubernetes and infra
|
||||
|
||||
## Templates
|
||||
|
||||
- **scaleway-instance**: Scaleway workspace instance with persistent home directory
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
display_name: "Scaleway Instance"
|
||||
description: "A workspace spun up on a Scaleway Instance"
|
||||
icon: "../../../../.icons/scaleway.svg"
|
||||
verified: false
|
||||
tags: ["scaleway", "vm", "linux"]
|
||||
---
|
||||
|
||||
# Scaleway Instance Template
|
||||
|
||||
This template provisions Coder workspaces on [Scaleway](https://www.scaleway.com/) cloud instances with full customization options for regions, instance types, operating systems, and storage configurations.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-region support**: Choose from France (Paris), Netherlands (Amsterdam), or Poland (Warsaw)
|
||||
- **Flexible instance sizing**: Wide range of instance types from development to high-performance computing
|
||||
- **Multiple OS options**: Debian 12/13, Ubuntu 24.04, and Fedora 41
|
||||
- **Customizable storage**: Adjustable disk size with configurable IOPS
|
||||
- **IPv4 and IPv6 networking**: Dual-stack IP configuration for enhanced connectivity
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Scaleway Account Setup
|
||||
|
||||
1. Create a [Scaleway account](https://console.scaleway.com/)
|
||||
2. Create a new project or use an existing one
|
||||
3. Generate API credentials:
|
||||
- Go to **IAM** > **API Keys** in the Scaleway Console
|
||||
- Create a new API key
|
||||
- Note down the **Access Key** and **Secret Key**
|
||||
- Copy your **Project ID** from the project settings
|
||||
- Give permissions for **BlockStorageFullAccess**, **ProjectReadOnly**, **InstancesFullAccess** as a starting point
|
||||
|
||||
## Architecture
|
||||
|
||||
This template creates the following resources for each workspace:
|
||||
|
||||
### Persistent Resources
|
||||
|
||||
- **Block Volume**: Mounted as user's home directory (preserves all data, configs, and projects)
|
||||
|
||||
### Ephemeral Resources (destroyed when workspace stops)
|
||||
|
||||
- **Scaleway Instance**: Virtual machine created fresh on each workspace start
|
||||
- **IPv4 Address**: Routed IPv4 address assigned dynamically
|
||||
- **IPv6 Address**: Routed IPv6 address assigned dynamically
|
||||
- **Cloud-init Configuration**: Automated setup of the Coder agent and persistent storage mounting
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Region Selection
|
||||
|
||||
Choose from three available regions:
|
||||
|
||||
- **France - Paris (fr-par)**: Default, lowest latency for European users
|
||||
- **Netherlands - Amsterdam (nl-ams)**: Alternative European location
|
||||
- **Poland - Warsaw (pl-waw)**: Eastern European option
|
||||
|
||||
### Instance Types
|
||||
|
||||
The template supports a comprehensive range of Scaleway instance types:
|
||||
|
||||
#### Development Instances
|
||||
|
||||
- **STARDUST1-S**: 1 CPU, 1GB RAM - Basic development
|
||||
- **DEV1-S/M/L/XL**: 2-4 CPUs, 2-12GB RAM - Standard development
|
||||
|
||||
#### Production Instances
|
||||
|
||||
- **ENT1 Series**: 2-96 CPUs, 8-384GB RAM - Enterprise workloads
|
||||
- **GP1 Series**: 4-48 CPUs, 16-256GB RAM - General purpose
|
||||
- **PRO2 Series**: 2-32 CPUs, 8-128GB RAM - Professional workloads
|
||||
|
||||
#### Specialized Instances
|
||||
|
||||
- **L4 Series**: GPU-enabled instances for AI/ML workloads
|
||||
- **COPARM1 Series**: ARM64 architecture for specific use cases
|
||||
|
||||
### Operating System Options
|
||||
|
||||
- **Debian 13 (Trixie)**: Latest Debian release
|
||||
- **Debian 12 (Bookworm)**: Stable Debian LTS
|
||||
- **Ubuntu 24.04 (Noble)**: Latest Ubuntu LTS
|
||||
- **Fedora 41**: Cutting-edge features and packages
|
||||
|
||||
### Storage Configuration
|
||||
|
||||
- **Home Directory Size**: 10-500GB adjustable via slider (your entire home directory)
|
||||
- **IOPS**: 5,000 or 15,000 IOPS options for performance tuning
|
||||
|
||||
## Template Components
|
||||
|
||||
### Included Tools
|
||||
|
||||
- **VS Code Server**: Browser-based IDE with full extension support
|
||||
- **System Monitoring**: CPU, RAM, and disk usage metrics
|
||||
- **Dotfiles Support**: Automatic dotfiles synchronization on workspace start
|
||||
- **Custom Environment Variables**: Pre-configured welcome message
|
||||
|
||||
### Cloud-init Setup
|
||||
|
||||
The template uses cloud-init for:
|
||||
|
||||
- Automatic Coder agent installation and configuration
|
||||
- User account setup with proper permissions
|
||||
- Persistent home directory mounting (automatic disk partitioning and filesystem creation)
|
||||
- Development tools initialization
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Workspace
|
||||
|
||||
1. **Select Template**: Choose "Scaleway Instance" from your Coder templates
|
||||
2. **Configure Region**: Pick your preferred Scaleway region
|
||||
3. **Choose Instance**: Select instance type based on your performance needs
|
||||
4. **Select OS**: Pick your preferred operating system
|
||||
5. **Set Home Directory Size**: Adjust storage size (10-500GB) for your persistent home directory
|
||||
6. **Create**: Launch your workspace
|
||||
|
||||
### Managing Costs
|
||||
|
||||
- **VM instances are destroyed** when workspace stops (zero compute costs when not in use)
|
||||
- **IP addresses are released** when workspace stops (no static IP charges)
|
||||
- **Home directory persists** on dedicated block volume (small storage cost only)
|
||||
- **Fresh OS** on each workspace start with persistent user data
|
||||
- Choose appropriate instance sizes for your workload requirements
|
||||
|
||||
## Customization
|
||||
|
||||
### Extending the Template
|
||||
|
||||
You can customize this template by:
|
||||
|
||||
1. **Adding Software**: Modify cloud-init scripts to install additional tools
|
||||
2. **Custom Modules**: Include additional Coder modules from the registry
|
||||
3. **Network Configuration**: Adjust security groups or network settings
|
||||
4. **Startup Scripts**: Add custom initialization logic
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Updating Instance Types
|
||||
|
||||
To update the available instance types, regenerate the `scaleway-config.json` file:
|
||||
|
||||
```bash
|
||||
scw instance server-type list -o json | jq 'map({name, cpu, gpu, ram, arch})' > scaleway-config.json.json
|
||||
```
|
||||
|
||||
This pulls the latest instance types from Scaleway and formats them for use in the template.
|
||||
|
||||
## References
|
||||
|
||||
- [Scaleway Documentation](https://www.scaleway.com/en/docs/)
|
||||
- [Scaleway Instance Types](https://www.scaleway.com/en/pricing/#instances)
|
||||
- [Coder Templates Documentation](https://coder.com/docs/templates)
|
||||
- [Terraform Scaleway Provider](https://registry.terraform.io/providers/scaleway/scaleway/latest/docs)
|
||||
@@ -0,0 +1,35 @@
|
||||
#cloud-config
|
||||
cloud_final_modules:
|
||||
- [scripts-user, always]
|
||||
hostname: ${hostname}
|
||||
users:
|
||||
- name: ${linux_user}
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
shell: /bin/bash
|
||||
|
||||
# Setup persistent storage disk
|
||||
disk_setup:
|
||||
/dev/sdb:
|
||||
table_type: gpt
|
||||
layout: true
|
||||
overwrite: false
|
||||
|
||||
fs_setup:
|
||||
- label: persistent-home
|
||||
filesystem: ext4
|
||||
device: /dev/sdb1
|
||||
partition: auto
|
||||
|
||||
mounts:
|
||||
- ["/dev/sdb1", "/home/${linux_user}", "ext4", "defaults", "0", "2"]
|
||||
|
||||
# Fix ownership after mounting
|
||||
runcmd:
|
||||
- chown -R ${linux_user}:${linux_user} /home/${linux_user}
|
||||
- chmod 755 /home/${linux_user}
|
||||
|
||||
# Automatically grow the partition
|
||||
growpart:
|
||||
mode: auto
|
||||
devices: ['/']
|
||||
ignore_growroot_disabled: false
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
sudo -u '${linux_user}' sh -c 'export CODER_AGENT_TOKEN="${coder_agent_token}"; ${init_script}'
|
||||
@@ -0,0 +1,337 @@
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 2"
|
||||
}
|
||||
scaleway = {
|
||||
source = "scaleway/scaleway"
|
||||
version = "~> 2"
|
||||
}
|
||||
cloudinit = {
|
||||
source = "hashicorp/cloudinit"
|
||||
version = "~> 2"
|
||||
}
|
||||
}
|
||||
required_version = ">= 1.0"
|
||||
}
|
||||
|
||||
provider "scaleway" {
|
||||
access_key = var.access_key
|
||||
secret_key = var.secret_key
|
||||
region = data.coder_parameter.region.value
|
||||
}
|
||||
|
||||
locals {
|
||||
hostname = lower(data.coder_workspace.me.name)
|
||||
linux_user = "coder"
|
||||
}
|
||||
|
||||
data "cloudinit_config" "user_data" {
|
||||
gzip = false
|
||||
base64_encode = false
|
||||
|
||||
boundary = "//"
|
||||
|
||||
part {
|
||||
filename = "cloud-config.yaml"
|
||||
content_type = "text/cloud-config"
|
||||
|
||||
content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", {
|
||||
hostname = local.hostname
|
||||
linux_user = local.linux_user
|
||||
})
|
||||
}
|
||||
|
||||
part {
|
||||
filename = "userdata.sh"
|
||||
content_type = "text/x-shellscript"
|
||||
|
||||
content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", {
|
||||
linux_user = local.linux_user
|
||||
init_script = coder_agent.main.init_script
|
||||
coder_agent_token = coder_agent.main.token
|
||||
})
|
||||
}
|
||||
}
|
||||
data "coder_provisioner" "me" {}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = local.selected_arch
|
||||
os = data.coder_provisioner.me.os
|
||||
auth = "token"
|
||||
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
|
||||
# Install additional tools or run commands at workspace startup
|
||||
# Uncomment and customize as needed:
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y build-essential
|
||||
EOT
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
metadata {
|
||||
display_name = "Disk Usage"
|
||||
key = "1_disk_usage"
|
||||
script = "coder stat disk --path /home/${local.linux_user}"
|
||||
interval = 600
|
||||
timeout = 30
|
||||
}
|
||||
}
|
||||
|
||||
module "code-server" {
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
folder = "/home/${local.linux_user}"
|
||||
}
|
||||
|
||||
# Runs a script at workspace start/stop or on a cron schedule
|
||||
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script
|
||||
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
resource "coder_metadata" "workspace_info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = scaleway_instance_server.workspace[0].id
|
||||
|
||||
item {
|
||||
key = "region"
|
||||
value = data.coder_parameter.region.value
|
||||
}
|
||||
item {
|
||||
key = "instance type"
|
||||
value = scaleway_instance_server.workspace[0].type
|
||||
}
|
||||
item {
|
||||
key = "image"
|
||||
value = data.coder_parameter.base_image.value
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_metadata" "volume_info" {
|
||||
resource_id = scaleway_block_volume.persistent_storage.id
|
||||
|
||||
item {
|
||||
key = "size"
|
||||
value = "${scaleway_block_volume.persistent_storage.size_in_gb} GiB"
|
||||
}
|
||||
item {
|
||||
key = "iops"
|
||||
value = scaleway_block_volume.persistent_storage.iops
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "region" {
|
||||
name = "Scaleway Region"
|
||||
description = "Region to deploy server into"
|
||||
type = "string"
|
||||
default = "fr-par"
|
||||
option {
|
||||
name = "France - Paris (fr-par)"
|
||||
value = "fr-par"
|
||||
icon = "/emojis/1f1eb-1f1f7.png"
|
||||
}
|
||||
option {
|
||||
name = "Netherlands - Amsterdam (nl-ams)"
|
||||
value = "nl-ams"
|
||||
icon = "/emojis/1f1f3-1f1f1.png"
|
||||
}
|
||||
option {
|
||||
name = "Poland - Warsaw (pl-waw)"
|
||||
value = "pl-waw"
|
||||
icon = "/emojis/1f1f5-1f1f1.png"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "base_image" {
|
||||
name = "Image"
|
||||
description = "Which base image would you like to use?"
|
||||
type = "string"
|
||||
form_type = "radio"
|
||||
default = "debian_trixie"
|
||||
|
||||
option {
|
||||
name = "Debian 13 (Trixie)"
|
||||
value = "debian_trixie"
|
||||
icon = "/icon/debian.svg"
|
||||
}
|
||||
|
||||
option {
|
||||
name = "Debian 12 (Bookworm)"
|
||||
value = "debian_bookworm"
|
||||
icon = "/icon/debian.svg"
|
||||
}
|
||||
|
||||
option {
|
||||
name = "Ubuntu 24.04 (Noble)"
|
||||
value = "ubuntu_noble"
|
||||
icon = "/icon/ubuntu.svg"
|
||||
}
|
||||
|
||||
option {
|
||||
name = "Fedora 41"
|
||||
value = "fedora_41"
|
||||
icon = "/icon/fedora.svg"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "root_volume_size" {
|
||||
name = "Root Volume Size"
|
||||
description = "Size of the OS/boot disk in GB"
|
||||
type = "number"
|
||||
form_type = "slider"
|
||||
default = "20"
|
||||
order = 7
|
||||
validation {
|
||||
min = 10
|
||||
max = 1000
|
||||
monotonic = "increasing"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "disk_size" {
|
||||
name = "Persistent Storage Size"
|
||||
description = "Size of the additional persistent storage volume in GB"
|
||||
type = "number"
|
||||
form_type = "slider"
|
||||
default = "10"
|
||||
order = 8
|
||||
validation {
|
||||
min = 10
|
||||
max = 500
|
||||
monotonic = "increasing"
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
scaleway_config_raw = jsondecode(file("${path.module}/scaleway-config.json"))
|
||||
|
||||
scaleway_instance_options = {
|
||||
for instance in local.scaleway_config_raw :
|
||||
instance.name => {
|
||||
name = "${instance.name} (${instance.cpu} CPU, ${instance.gpu} GPU, ${floor(instance.ram / 1073741824)} GB RAM)"
|
||||
value = instance.name
|
||||
}
|
||||
}
|
||||
|
||||
instance_arch_map = {
|
||||
for instance in local.scaleway_config_raw :
|
||||
instance.name => instance.arch
|
||||
}
|
||||
|
||||
# Convert Scaleway arch format to Coder arch format
|
||||
selected_arch = local.instance_arch_map[data.coder_parameter.instance_size.value] == "x86_64" ? "amd64" : local.instance_arch_map[data.coder_parameter.instance_size.value]
|
||||
}
|
||||
|
||||
data "coder_parameter" "instance_size" {
|
||||
name = "instance_size"
|
||||
display_name = "Instance Size"
|
||||
description = "Which Instance Size should be used?"
|
||||
default = "DEV1-M"
|
||||
type = "string"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = false
|
||||
form_type = "dropdown"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.scaleway_instance_options
|
||||
content {
|
||||
name = option.value.name
|
||||
value = option.value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "volume_iops" {
|
||||
name = "Volume IOPS"
|
||||
description = "IOPS to provision for disk"
|
||||
type = "number"
|
||||
default = 5000
|
||||
option {
|
||||
name = "5000"
|
||||
value = 5000
|
||||
}
|
||||
option {
|
||||
name = "15000"
|
||||
value = 15000
|
||||
}
|
||||
}
|
||||
|
||||
resource "scaleway_instance_server" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
type = data.coder_parameter.instance_size.value
|
||||
image = data.coder_parameter.base_image.value
|
||||
ip_ids = [scaleway_instance_ip.server_ip[0].id, scaleway_instance_ip.v4_server_ip[0].id]
|
||||
project_id = var.project_id
|
||||
user_data = {
|
||||
cloud-init = data.cloudinit_config.user_data.rendered
|
||||
}
|
||||
additional_volume_ids = [scaleway_block_volume.persistent_storage.id]
|
||||
|
||||
root_volume {
|
||||
size_in_gb = data.coder_parameter.root_volume_size.value
|
||||
}
|
||||
}
|
||||
|
||||
resource "scaleway_block_volume" "persistent_storage" {
|
||||
iops = data.coder_parameter.volume_iops.value
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
size_in_gb = data.coder_parameter.disk_size.value
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
|
||||
resource "scaleway_instance_ip" "server_ip" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
type = "routed_ipv6"
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
resource "scaleway_instance_ip" "v4_server_ip" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
type = "routed_ipv4"
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
variable "project_id" {
|
||||
type = string
|
||||
description = "ID of the project to deploy into"
|
||||
}
|
||||
|
||||
variable "access_key" {
|
||||
type = string
|
||||
description = "Access key to use to deploy"
|
||||
}
|
||||
|
||||
variable "secret_key" {
|
||||
type = string
|
||||
description = "Secret key to use to deploy"
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
[
|
||||
{
|
||||
"name": "COPARM1-2C-8G",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "COPARM1-4C-16G",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "COPARM1-8C-32G",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "COPARM1-16C-64G",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "COPARM1-32C-128G",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "DEV1-S",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 2147483648,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "DEV1-M",
|
||||
"cpu": 3,
|
||||
"gpu": 0,
|
||||
"ram": 4294967296,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "DEV1-L",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "DEV1-XL",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 12884901888,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-XXS",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-XS",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-S",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-M",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-L",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-XL",
|
||||
"cpu": 64,
|
||||
"gpu": 0,
|
||||
"ram": 274877906944,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-2XL",
|
||||
"cpu": 96,
|
||||
"gpu": 0,
|
||||
"ram": 412316860416,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-XS",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-S",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-M",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-L",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-XL",
|
||||
"cpu": 48,
|
||||
"gpu": 0,
|
||||
"ram": 274877906944,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "L4-1-24G",
|
||||
"cpu": 8,
|
||||
"gpu": 1,
|
||||
"ram": 51539607552,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "L4-2-24G",
|
||||
"cpu": 16,
|
||||
"gpu": 2,
|
||||
"ram": 103079215104,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "L4-4-24G",
|
||||
"cpu": 32,
|
||||
"gpu": 4,
|
||||
"ram": 206158430208,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "L4-8-24G",
|
||||
"cpu": 64,
|
||||
"gpu": 8,
|
||||
"ram": 412316860416,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PLAY2-PICO",
|
||||
"cpu": 1,
|
||||
"gpu": 0,
|
||||
"ram": 2147483648,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PLAY2-NANO",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 4294967296,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PLAY2-MICRO",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-2C-4G",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 4294967296,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-2C-8G",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-2C-16G",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-4C-8G",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-4C-16G",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-2C-8G-WIN",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-4C-32G",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-8C-16G",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HN-3",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 4294967296,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-8C-32G",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-4C-16G-WIN",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-8C-64G",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-16C-32G",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HN-5",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-16C-64G",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-8C-32G-WIN",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HN-10",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-16C-128G",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-32C-64G",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-32C-128G",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-48C-96G",
|
||||
"cpu": 48,
|
||||
"gpu": 0,
|
||||
"ram": 103079215104,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-16C-64G-WIN",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-32C-256G",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 274877906944,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-64C-128G",
|
||||
"cpu": 64,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-48C-192G",
|
||||
"cpu": 48,
|
||||
"gpu": 0,
|
||||
"ram": 206158430208,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-64C-256G",
|
||||
"cpu": 64,
|
||||
"gpu": 0,
|
||||
"ram": 274877906944,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-48C-384G",
|
||||
"cpu": 48,
|
||||
"gpu": 0,
|
||||
"ram": 412316860416,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-32C-128G-WIN",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-64C-512G",
|
||||
"cpu": 64,
|
||||
"gpu": 0,
|
||||
"ram": 549755813888,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-XXS",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-XS",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-S",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-M",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-L",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "RENDER-S",
|
||||
"cpu": 10,
|
||||
"gpu": 1,
|
||||
"ram": 45097156608,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "STARDUST1-S",
|
||||
"cpu": 1,
|
||||
"gpu": 0,
|
||||
"ram": 1073741824,
|
||||
"arch": "x86_64"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
# Local and OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
*.tmp
|
||||
*.swp
|
||||
*.bak
|
||||
|
||||
# Terraform
|
||||
.terraform/
|
||||
.terraform.lock.hcl
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
||||
crash.log
|
||||
|
||||
# Node / Bun / Python / other tool artifacts
|
||||
node_modules/
|
||||
bun.lockb
|
||||
package-lock.json
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cloud credentials and keys
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.json
|
||||
*.env
|
||||
.envrc
|
||||
aws-credentials
|
||||
gcp.json
|
||||
azure-creds.json
|
||||
|
||||
# Archives
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
|
||||
# Workspace artifacts
|
||||
workspace/
|
||||
output/
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,14 @@
|
||||
---
|
||||
display_name: "Noah Boyers"
|
||||
bio: "Cloud & DevOps engineer with an MBA, building scalable multi-cloud infrastructure."
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "noahboyers"
|
||||
linkedin: "https://www.linkedin.com/in/nboyers"
|
||||
website: "https://nobosoftware.com"
|
||||
support_email: "hello@nobosoftware.com"
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Noah Boyers
|
||||
|
||||
Cloud and DevOps engineer focused on scalable, secure, and automated infrastructure across AWS, Azure, and GCP.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
display_name: Cloud DevOps Workspace
|
||||
description: A multi-cloud DevOps workspace that runs on Amazon EKS and provides authenticated access to AWS, Azure, and GCP.
|
||||
icon: ../../../../.icons/cloud-devops.svg
|
||||
verified: false
|
||||
tags: [devops, kubernetes, aws, eks, multi-cloud, terraform, cdk, pulumi]
|
||||
---
|
||||
|
||||
# Cloud DevOps Workspace
|
||||
|
||||
A secure, company-standard DevOps environment for platform and cloud engineers.
|
||||
|
||||
This template deploys workspaces **into an existing Amazon EKS cluster** and provides developers with tools and credentials to work with **AWS, Azure, and GCP** from inside their workspace.
|
||||
|
||||
Supports multiple Infrastructure-as-Code frameworks — **Terraform**, **AWS CDK**, and **Pulumi** — for flexible, multi-cloud development.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Cloud Ready** — authenticate to AWS, Azure, or GCP from a single workspace
|
||||
- **Runs on EKS** — leverages existing Kubernetes infrastructure for scaling and security
|
||||
- **IaC Tools Included** — Terraform, Terragrunt, CDK, Pulumi, tfsec, and more
|
||||
- **Secure Isolation** — each workspace runs in its own Kubernetes namespace
|
||||
- **Configurable Auth** — supports IRSA (AWS), Federated Identity (Azure), and WIF (GCP)
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------------------------------------------------------- | --------------------------------------------------------------- | ------ | ----------- |
|
||||
| `host_cluster_name` | EKS cluster name where workspaces are deployed | string | — |
|
||||
| `iac_tool` | Infrastructure-as-Code framework (`terraform`, `cdk`, `pulumi`) | string | `terraform` |
|
||||
| `enable_aws` | Enable AWS authentication and tools | bool | `true` |
|
||||
| `enable_azure` | Enable Azure authentication and tools | bool | `false` |
|
||||
| `enable_gcp` | Enable GCP authentication and tools | bool | `false` |
|
||||
| `aws_access_key_id` / `aws_secret_access_key` | AWS credentials (optional) | string | `""` |
|
||||
| `azure_client_id` / `azure_client_secret` / `azure_tenant_id` | Azure credentials (optional) | string | `""` |
|
||||
| `gcp_service_account` | GCP Service Account JSON (optional) | string | `""` |
|
||||
|
||||
## Runtime Architecture
|
||||
|
||||
| Layer | Platform | Purpose |
|
||||
| ----------------------- | ------------------ | ------------------------------------------------------------ |
|
||||
| **Infrastructure** | Amazon EKS | Where Coder deploys and runs the workspaces |
|
||||
| **Workspace Container** | Ubuntu-based image | Developer environment (Terraform, CDK, Pulumi, CLIs) |
|
||||
| **Cloud Access** | AWS / Azure / GCP | Target environments for deploying infrastructure or services |
|
||||
|
||||
## Required Permissions and Setup Steps
|
||||
|
||||
This template **runs on EKS** but allows developers inside the workspace to authenticate with **AWS, Azure, or GCP** using their own credentials or service identities.
|
||||
|
||||
### Coder & Infrastructure (Admin Setup)
|
||||
|
||||
Your Coder deployment must have:
|
||||
|
||||
- Network access to an **existing EKS cluster**
|
||||
- The Coder Helm chart installed and healthy
|
||||
- Terraform configured with access to the EKS API
|
||||
|
||||
#### Minimum AWS IAM Permissions
|
||||
|
||||
For the identity running the template (Coder service account, Terraform runner, or user):
|
||||
|
||||
```json
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"eks:DescribeCluster",
|
||||
"eks:ListClusters",
|
||||
"sts:GetCallerIdentity",
|
||||
"sts:AssumeRole"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 0.23"
|
||||
}
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.23"
|
||||
}
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Coder workspace context ---
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
# --- EKS connection ---
|
||||
data "aws_eks_cluster" "eks" {
|
||||
name = trimspace(var.host_cluster_name)
|
||||
}
|
||||
|
||||
|
||||
data "aws_eks_cluster_auth" "eks" {
|
||||
name = trimspace(var.host_cluster_name)
|
||||
}
|
||||
|
||||
provider "kubernetes" {
|
||||
host = data.aws_eks_cluster.eks.endpoint
|
||||
cluster_ca_certificate = base64decode(data.aws_eks_cluster.eks.certificate_authority[0].data)
|
||||
token = data.aws_eks_cluster_auth.eks.token
|
||||
}
|
||||
|
||||
# --- Namespace per workspace ---
|
||||
resource "kubernetes_namespace" "workspace" {
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
|
||||
labels = {
|
||||
"coder.workspace" = data.coder_workspace.me.name
|
||||
"coder.owner" = data.coder_workspace_owner.me.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- ServiceAccount (IRSA optional) ---
|
||||
resource "kubernetes_service_account" "workspace" {
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
|
||||
namespace = kubernetes_namespace.workspace.metadata[0].name
|
||||
|
||||
annotations = var.enable_aws && var.aws_role_arn != "" ? {
|
||||
"eks.amazonaws.com/role-arn" = var.aws_role_arn
|
||||
} : {}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Coder Agent definition ---
|
||||
resource "coder_agent" "main" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
|
||||
startup_script = file("${path.module}/scripts/setup-workspace.sh")
|
||||
|
||||
env = {
|
||||
# IaC tool & cloud toggles
|
||||
IAC_TOOL = var.iac_tool
|
||||
ENABLE_AWS = tostring(var.enable_aws)
|
||||
ENABLE_AZURE = tostring(var.enable_azure)
|
||||
ENABLE_GCP = tostring(var.enable_gcp)
|
||||
|
||||
# Developer credentials
|
||||
AWS_ACCESS_KEY_ID = var.aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY = var.aws_secret_access_key
|
||||
AZURE_CLIENT_ID = var.azure_client_id
|
||||
AZURE_TENANT_ID = var.azure_tenant_id
|
||||
AZURE_CLIENT_SECRET = var.azure_client_secret
|
||||
GCP_SERVICE_ACCOUNT = var.gcp_service_account
|
||||
}
|
||||
}
|
||||
|
||||
# --- Kubernetes Pod (runs workspace container) ---
|
||||
resource "kubernetes_pod" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
|
||||
namespace = kubernetes_namespace.workspace.metadata[0].name
|
||||
labels = {
|
||||
"app" = "coder-workspace"
|
||||
"coder.owner" = data.coder_workspace_owner.me.name
|
||||
"coder.agent" = "true"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
service_account_name = kubernetes_service_account.workspace.metadata[0].name
|
||||
|
||||
container {
|
||||
name = "workspace"
|
||||
image = "codercom/enterprise-base:ubuntu"
|
||||
command = ["/bin/bash", "-c", coder_agent.main.init_script]
|
||||
|
||||
env {
|
||||
name = "CODER_AGENT_TOKEN"
|
||||
value = coder_agent.main.token
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = { cpu = "500m", memory = "1Gi" }
|
||||
limits = { cpu = "2", memory = "4Gi" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [coder_agent.main]
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env bash
|
||||
# cloud-auth.sh — Multi-cloud auth helpers (source this file, don't execute)
|
||||
# Supports:
|
||||
# - AWS: access keys or IRSA (via pod SA)
|
||||
# - Azure: federated token or client secret
|
||||
# - GCP: service account JSON or Workload Identity Federation (KSA -> SA)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# -------- util --------
|
||||
_has() { command -v "$1" > /dev/null 2>&1; }
|
||||
_docker_ok() { _has docker && [[ -S /var/run/docker.sock ]]; }
|
||||
|
||||
cloud-auth-help() {
|
||||
cat << 'EOHELP'
|
||||
Multi-Cloud Authentication Helper — source this file:
|
||||
|
||||
source ~/workspace/cloud-auth.sh
|
||||
|
||||
Environment variables (read if set):
|
||||
|
||||
# Common toggles (optional)
|
||||
ENABLE_AWS=true|false
|
||||
ENABLE_AZURE=true|false
|
||||
ENABLE_GCP=true|false
|
||||
|
||||
# AWS
|
||||
AWS_REGION=us-west-2
|
||||
AWS_ACCESS_KEY_ID=...
|
||||
AWS_SECRET_ACCESS_KEY=...
|
||||
AWS_SESSION_TOKEN=... # optional (STS); if unset, IRSA/IMDS is used
|
||||
|
||||
# Azure
|
||||
AZURE_CLIENT_ID=...
|
||||
AZURE_TENANT_ID=...
|
||||
AZURE_CLIENT_SECRET=... # OR:
|
||||
AZURE_FEDERATED_TOKEN_FILE=/var/run/secrets/azure/tokens/azure-identity-token
|
||||
|
||||
# GCP
|
||||
GCP_PROJECT_ID=...
|
||||
# Option A (Service Account JSON):
|
||||
GCP_SERVICE_ACCOUNT='{ ... }'
|
||||
# Option B (Workload Identity Federation):
|
||||
GCP_WORKLOAD_IDENTITY_PROVIDER=projects/..../locations/global/workloadIdentityPools/.../providers/...
|
||||
# (uses KSA token at /var/run/secrets/kubernetes.io/serviceaccount/token)
|
||||
|
||||
Functions:
|
||||
|
||||
# AWS
|
||||
aws-login # ensures creds (keys or IRSA), sets region config if provided
|
||||
aws-check # prints caller identity
|
||||
aws-ecr-login # docker login to ECR (if docker socket present)
|
||||
|
||||
# Azure
|
||||
azure-login # SP login via federated token OR client secret
|
||||
azure-check # prints account info
|
||||
azure-acr-login # docker login to ACR (requires AZURE_ACR_NAME)
|
||||
|
||||
# GCP
|
||||
gcp-login # SA JSON or WIF
|
||||
gcp-check # prints active gcloud account & project
|
||||
gcp-gar-login # docker auth to GAR (requires GCP_REGION & PROJECT)
|
||||
|
||||
# Convenience
|
||||
multicloud-login # calls the per-cloud logins if toggles are true
|
||||
multicloud-check # calls the per-cloud checks
|
||||
EOHELP
|
||||
}
|
||||
|
||||
# -------- AWS --------
|
||||
aws-login() {
|
||||
[[ "${ENABLE_AWS:-true}" == "true" ]] || {
|
||||
echo "AWS disabled"
|
||||
return 0
|
||||
}
|
||||
if ! _has aws; then
|
||||
echo "aws CLI not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If access keys are present, write standard files; otherwise rely on IRSA/IMDS
|
||||
if [[ -n "${AWS_ACCESS_KEY_ID:-}" ]]; then
|
||||
mkdir -p "${HOME}/.aws"
|
||||
{
|
||||
echo "[default]"
|
||||
echo "aws_access_key_id=${AWS_ACCESS_KEY_ID}"
|
||||
echo "aws_secret_access_key=${AWS_SECRET_ACCESS_KEY:-}"
|
||||
[[ -n "${AWS_SESSION_TOKEN:-}" ]] && echo "aws_session_token=${AWS_SESSION_TOKEN}"
|
||||
} > "${HOME}/.aws/credentials"
|
||||
if [[ -n "${AWS_REGION:-}" ]]; then
|
||||
{
|
||||
echo "[default]"
|
||||
echo "region=${AWS_REGION}"
|
||||
} > "${HOME}/.aws/config"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate
|
||||
if ! aws sts get-caller-identity > /dev/null 2>&1; then
|
||||
echo "❌ AWS auth failed (neither valid keys nor IRSA available)"
|
||||
return 1
|
||||
fi
|
||||
echo "✅ AWS auth OK"
|
||||
}
|
||||
|
||||
aws-check() {
|
||||
_has aws || {
|
||||
echo "aws CLI not found"
|
||||
return 1
|
||||
}
|
||||
aws sts get-caller-identity
|
||||
}
|
||||
|
||||
aws-ecr-login() {
|
||||
_has aws || {
|
||||
echo "aws CLI not found"
|
||||
return 1
|
||||
}
|
||||
_docker_ok || {
|
||||
echo "ℹ️ docker socket not available; skipping ECR login"
|
||||
return 0
|
||||
}
|
||||
: "${AWS_REGION:=us-east-1}"
|
||||
aws-login > /dev/null || return 1
|
||||
AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
|
||||
aws ecr get-login-password --region "${AWS_REGION}" \
|
||||
| docker login --username AWS --password-stdin \
|
||||
"${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
echo "✅ ECR login OK → ${ECR_REGISTRY}"
|
||||
}
|
||||
|
||||
# -------- Azure --------
|
||||
azure-login() {
|
||||
[[ "${ENABLE_AZURE:-false}" == "true" ]] || {
|
||||
echo "Azure disabled"
|
||||
return 0
|
||||
}
|
||||
_has az || {
|
||||
echo "az CLI not found"
|
||||
return 1
|
||||
}
|
||||
[[ -n "${AZURE_CLIENT_ID:-}" && -n "${AZURE_TENANT_ID:-}" ]] || {
|
||||
echo "❌ Set AZURE_CLIENT_ID and AZURE_TENANT_ID"
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "${AZURE_FEDERATED_TOKEN_FILE:-}" && -f "${AZURE_FEDERATED_TOKEN_FILE}" ]]; then
|
||||
az login --service-principal \
|
||||
--username "${AZURE_CLIENT_ID}" \
|
||||
--tenant "${AZURE_TENANT_ID}" \
|
||||
--federated-token "$(cat "${AZURE_FEDERATED_TOKEN_FILE}")" \
|
||||
--allow-no-subscriptions
|
||||
elif [[ -n "${AZURE_CLIENT_SECRET:-}" ]]; then
|
||||
az login --service-principal \
|
||||
-u "${AZURE_CLIENT_ID}" -p "${AZURE_CLIENT_SECRET}" \
|
||||
--tenant "${AZURE_TENANT_ID}"
|
||||
else
|
||||
echo "❌ Provide AZURE_FEDERATED_TOKEN_FILE or AZURE_CLIENT_SECRET"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "✅ Azure auth OK"
|
||||
}
|
||||
|
||||
azure-check() {
|
||||
_has az || {
|
||||
echo "az CLI not found"
|
||||
return 1
|
||||
}
|
||||
az account show
|
||||
}
|
||||
|
||||
azure-acr-login() {
|
||||
_has az || {
|
||||
echo "az CLI not found"
|
||||
return 1
|
||||
}
|
||||
_docker_ok || {
|
||||
echo "ℹ️ docker socket not available; skipping ACR login"
|
||||
return 0
|
||||
}
|
||||
[[ -n "${AZURE_ACR_NAME:-}" ]] || {
|
||||
echo "❌ Set AZURE_ACR_NAME"
|
||||
return 1
|
||||
}
|
||||
az account show > /dev/null 2>&1 || azure-login
|
||||
az acr login --name "${AZURE_ACR_NAME}"
|
||||
export ACR_REGISTRY="${AZURE_ACR_NAME}.azurecr.io"
|
||||
echo "✅ ACR login OK → ${ACR_REGISTRY}"
|
||||
}
|
||||
|
||||
# -------- GCP --------
|
||||
gcp-login() {
|
||||
[[ "${ENABLE_GCP:-false}" == "true" ]] || {
|
||||
echo "GCP disabled"
|
||||
return 0
|
||||
}
|
||||
_has gcloud || {
|
||||
echo "gcloud not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "${GCP_SERVICE_ACCOUNT:-}" ]]; then
|
||||
# Service Account JSON path
|
||||
echo "${GCP_SERVICE_ACCOUNT}" > /tmp/gcp.json || {
|
||||
echo "❌ Failed to write GCP credentials"
|
||||
return 1
|
||||
}
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json || {
|
||||
echo "❌ Failed to set GCP credentials path"
|
||||
return 1
|
||||
}
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp.json --quiet || {
|
||||
echo "❌ GCP service account auth failed"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
# Workload Identity Federation using KSA token + WIP provider
|
||||
[[ -n "${GCP_WORKLOAD_IDENTITY_PROVIDER:-}" && -n "${GCP_PROJECT_ID:-}" ]] || {
|
||||
echo "❌ Provide GCP_SERVICE_ACCOUNT JSON or set GCP_WORKLOAD_IDENTITY_PROVIDER & GCP_PROJECT_ID"
|
||||
return 1
|
||||
}
|
||||
[[ -f "/var/run/secrets/kubernetes.io/serviceaccount/token" ]] || {
|
||||
echo "❌ KSA token not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
TMP="/tmp/gcp-wif-$$.json"
|
||||
cat > "${TMP}" << 'EOF'
|
||||
{
|
||||
"type": "external_account",
|
||||
"audience": "//iam.googleapis.com/${GCP_WORKLOAD_IDENTITY_PROVIDER}",
|
||||
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
|
||||
"token_url": "https://sts.googleapis.com/v1/token",
|
||||
"credential_source": {
|
||||
"file": "/var/run/secrets/kubernetes.io/serviceaccount/token",
|
||||
"format": { "type": "text" }
|
||||
}
|
||||
}
|
||||
EOF
|
||||
[[ $? -eq 0 ]] || {
|
||||
echo "❌ Failed to write GCP WIF config"
|
||||
return 1
|
||||
}
|
||||
export GOOGLE_APPLICATION_CREDENTIALS="${TMP}" || {
|
||||
echo "❌ Failed to set GCP credentials path"
|
||||
return 1
|
||||
}
|
||||
gcloud auth login --cred-file="${GOOGLE_APPLICATION_CREDENTIALS}" --quiet || {
|
||||
echo "❌ GCP WIF auth failed"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [[ -n "${GCP_PROJECT_ID:-}" ]]; then
|
||||
gcloud config set project "${GCP_PROJECT_ID}" --quiet
|
||||
fi
|
||||
echo "✅ GCP auth OK"
|
||||
}
|
||||
|
||||
gcp-check() {
|
||||
_has gcloud || {
|
||||
echo "gcloud not found"
|
||||
return 1
|
||||
}
|
||||
gcloud auth list
|
||||
gcloud config get-value project || true
|
||||
}
|
||||
|
||||
gcp-gar-login() {
|
||||
_docker_ok || {
|
||||
echo "ℹ️ docker socket not available; skipping GAR login"
|
||||
return 0
|
||||
}
|
||||
: "${GCP_REGION:=us-central1}"
|
||||
[[ -n "${GCP_PROJECT_ID:-}" ]] || {
|
||||
echo "❌ Set GCP_PROJECT_ID"
|
||||
return 1
|
||||
}
|
||||
gcloud auth list --filter=status:ACTIVE --format="value(account)" > /dev/null || gcp-login
|
||||
gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet
|
||||
export GAR_REGISTRY="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}"
|
||||
echo "✅ GAR configured → ${GAR_REGISTRY}"
|
||||
}
|
||||
|
||||
# -------- Convenience --------
|
||||
multicloud-login() {
|
||||
if [[ "${ENABLE_AWS:-true}" == "true" ]]; then
|
||||
aws-login
|
||||
fi
|
||||
if [[ "${ENABLE_AZURE:-false}" == "true" ]]; then
|
||||
azure-login
|
||||
fi
|
||||
if [[ "${ENABLE_GCP:-false}" == "true" ]]; then
|
||||
gcp-login
|
||||
fi
|
||||
echo "✨ Multi-cloud login complete"
|
||||
}
|
||||
|
||||
multicloud-check() {
|
||||
if [[ "${ENABLE_AWS:-true}" == "true" ]]; then
|
||||
echo "AWS:"
|
||||
aws-check
|
||||
echo
|
||||
fi
|
||||
if [[ "${ENABLE_AZURE:-false}" == "true" ]]; then
|
||||
echo "Azure:"
|
||||
azure-check
|
||||
echo
|
||||
fi
|
||||
if [[ "${ENABLE_GCP:-false}" == "true" ]]; then
|
||||
echo "GCP:"
|
||||
gcp-check
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
echo "✨ cloud-auth loaded. Run 'cloud-auth-help' for usage."
|
||||
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =========================
|
||||
# Helpers & safe defaults
|
||||
# =========================
|
||||
log() { printf '%s %s\n' "👉" "$*"; }
|
||||
ok() { printf '%s %s\n' "✅" "$*"; }
|
||||
skip() { printf '%s %s\n' "⏭️" "$*"; }
|
||||
warn() { printf '%s %s\n' "⚠️" "$*"; }
|
||||
|
||||
# Detect CPU arch (amd64/arm64)
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64 | amd64) echo amd64 ;;
|
||||
aarch64 | arm64) echo arm64 ;;
|
||||
*) echo amd64 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Map to Docker static tarball arch names
|
||||
docker_tar_arch() {
|
||||
case "$(arch)" in
|
||||
amd64) echo x86_64 ;;
|
||||
arm64) echo aarch64 ;;
|
||||
*) echo x86_64 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
SAFE_TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$SAFE_TMP"' EXIT
|
||||
|
||||
safe_dl() { # url dest
|
||||
curl -fL --retry 5 --retry-delay 2 --connect-timeout 10 -o "$2" "$1" || {
|
||||
echo "Failed to download $1"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
docker_ok() {
|
||||
command -v docker > /dev/null 2>&1 && [[ -S /var/run/docker.sock ]]
|
||||
}
|
||||
|
||||
# Ensure user bin dir
|
||||
mkdir -p "$HOME/.local/bin" "$HOME/workspace/app"
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Inputs (with sane defaults)
|
||||
IAC_TOOL="${IAC_TOOL:-terraform}"
|
||||
TERRAFORM_VERSION="${TERRAFORM_VERSION:-1.6.0}"
|
||||
|
||||
ENABLE_AWS="${ENABLE_AWS:-true}"
|
||||
ENABLE_AZURE="${ENABLE_AZURE:-false}"
|
||||
ENABLE_GCP="${ENABLE_GCP:-false}"
|
||||
|
||||
AWS_REGION="${AWS_REGION:-}"
|
||||
AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}"
|
||||
AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}"
|
||||
AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}"
|
||||
|
||||
AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-}"
|
||||
AZURE_TENANT_ID="${AZURE_TENANT_ID:-}"
|
||||
AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET:-}"
|
||||
AZURE_FEDERATED_TOKEN_FILE="${AZURE_FEDERATED_TOKEN_FILE:-}"
|
||||
|
||||
GCP_PROJECT_ID="${GCP_PROJECT_ID:-}"
|
||||
GCP_SERVICE_ACCOUNT="${GCP_SERVICE_ACCOUNT:-}" # full JSON if not using WIF
|
||||
|
||||
REPO_URL="${REPO_URL:-${repo_url:-}}"
|
||||
DEFAULT_BRANCH="${DEFAULT_BRANCH:-${default_branch:-main}}"
|
||||
WORKDIR="${WORKDIR:-$HOME/workspace/app}"
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN:-${GIT_TOKEN:-}}"
|
||||
|
||||
GIT_AUTHOR_NAME="${GIT_AUTHOR_NAME:-}"
|
||||
GIT_AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-}"
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Multi-Cloud DevOps Workspace Setup (no sudo) ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo
|
||||
|
||||
# ==========================================================
|
||||
# Write multi-cloud helper functions to ~/workspace/cloud-auth.sh
|
||||
# ==========================================================
|
||||
cat > "${HOME}/workspace/cloud-auth.sh" << 'EOAUTHSCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
aws-ecr-login() {
|
||||
: "${AWS_REGION:=us-east-1}"
|
||||
if ! command -v aws >/dev/null 2>&1; then echo "aws CLI not found"; return 1; fi
|
||||
if ! aws sts get-caller-identity &>/dev/null; then
|
||||
echo "❌ AWS creds not available (IRSA or keys)"; return 1; fi
|
||||
AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
|
||||
if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then
|
||||
aws ecr get-login-password --region "${AWS_REGION}" | \
|
||||
docker login --username AWS --password-stdin \
|
||||
"${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
echo "✅ ECR login OK → ${ECR_REGISTRY}"
|
||||
else
|
||||
echo "ℹ️ docker socket not available; skipping docker login"
|
||||
fi
|
||||
}
|
||||
|
||||
aws-check() { aws sts get-caller-identity && echo "✓ AWS creds valid"; }
|
||||
|
||||
azure-login() {
|
||||
if ! command -v az >/dev/null 2>&1; then echo "az CLI not found"; return 1; fi
|
||||
if [[ -n "${AZURE_FEDERATED_TOKEN_FILE:-}" && -f "${AZURE_FEDERATED_TOKEN_FILE}" ]]; then
|
||||
az login --service-principal --username "${AZURE_CLIENT_ID}" \
|
||||
--tenant "${AZURE_TENANT_ID}" \
|
||||
--federated-token "$(cat "${AZURE_FEDERATED_TOKEN_FILE}")" \
|
||||
--allow-no-subscriptions
|
||||
elif [[ -n "${AZURE_CLIENT_SECRET:-}" ]]; then
|
||||
az login --service-principal -u "${AZURE_CLIENT_ID}" -p "${AZURE_CLIENT_SECRET}" --tenant "${AZURE_TENANT_ID}"
|
||||
else
|
||||
echo "❌ Provide AZURE_FEDERATED_TOKEN_FILE or AZURE_CLIENT_SECRET"; return 1
|
||||
fi
|
||||
echo "✅ Azure auth OK"; az account show
|
||||
}
|
||||
|
||||
azure-acr-login() {
|
||||
[[ -n "${AZURE_ACR_NAME:-}" ]] || { echo "Set AZURE_ACR_NAME"; return 1; }
|
||||
az account show &>/dev/null || azure-login
|
||||
if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then
|
||||
az acr login --name "${AZURE_ACR_NAME}"
|
||||
export ACR_REGISTRY="${AZURE_ACR_NAME}.azurecr.io"
|
||||
echo "✅ ACR login OK → ${ACR_REGISTRY}"
|
||||
else
|
||||
echo "ℹ️ docker socket not available; skipping docker login"
|
||||
fi
|
||||
}
|
||||
|
||||
azure-check() { az account show && echo "✓ Azure creds valid" || { echo "❌ Not logged in"; return 1; }; }
|
||||
|
||||
gcp-login() {
|
||||
if ! command -v gcloud >/dev/null 2>&1; then echo "gcloud not found"; return 1; fi
|
||||
if [[ -n "${GCP_SERVICE_ACCOUNT:-}" ]]; then
|
||||
# SA JSON auth
|
||||
echo "${GCP_SERVICE_ACCOUNT}" > /tmp/gcp.json || { echo "❌ Failed to write GCP credentials"; return 1; }
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp.json --quiet || { echo "❌ GCP auth failed"; return 1; }
|
||||
else
|
||||
echo "❌ Provide GCP_SERVICE_ACCOUNT JSON (WIF path not configured here)"; return 1
|
||||
fi
|
||||
[[ -n "${GCP_PROJECT_ID:-}" ]] && gcloud config set project "${GCP_PROJECT_ID}" --quiet || true
|
||||
echo "✅ GCP auth OK"; gcloud auth list
|
||||
}
|
||||
|
||||
gcp-gar-login() {
|
||||
: "${GCP_REGION:=us-central1}"
|
||||
[[ -n "${GCP_PROJECT_ID:-}" ]] || { echo "Set GCP_PROJECT_ID"; return 1; }
|
||||
gcloud auth list --filter=status:ACTIVE --format="value(account)" &>/dev/null || gcp-login
|
||||
if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then
|
||||
gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet
|
||||
export GAR_REGISTRY="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}"
|
||||
echo "✅ GAR configured → ${GAR_REGISTRY}"
|
||||
else
|
||||
echo "ℹ️ docker socket not available; skipping docker login"
|
||||
fi
|
||||
}
|
||||
|
||||
gcp-check() { gcloud auth list --filter=status:ACTIVE --format="value(account)" >/dev/null && echo "✓ GCP creds valid" || { echo "❌ Not logged in"; return 1; }; }
|
||||
|
||||
multicloud-login() {
|
||||
[[ "${ENABLE_AWS:-false}" == "true" ]] && command -v aws >/dev/null && aws-ecr-login || true
|
||||
[[ "${ENABLE_AZURE:-false}" == "true" ]] && command -v az >/dev/null && azure-login || true
|
||||
[[ "${ENABLE_GCP:-false}" == "true" ]] && command -v gcloud >/dev/null && gcp-login || true
|
||||
echo "✨ Multi-cloud login complete"
|
||||
}
|
||||
|
||||
multicloud-check() {
|
||||
[[ "${ENABLE_AWS:-false}" == "true" ]] && command -v aws >/dev/null && { echo "AWS:"; aws-check; echo; } || true
|
||||
[[ "${ENABLE_AZURE:-false}" == "true" ]] && command -v az >/dev/null && { echo "Azure:"; azure-check; echo; } || true
|
||||
[[ "${ENABLE_GCP:-false}" == "true" ]] && command -v gcloud >/dev/null && { echo "GCP:"; gcp-check; echo; } || true
|
||||
}
|
||||
|
||||
cloud-auth-help() {
|
||||
cat <<'EOHELP'
|
||||
Multi-Cloud Authentication Helper
|
||||
|
||||
Functions:
|
||||
AWS: aws-ecr-login, aws-check
|
||||
Azure: azure-login, azure-acr-login, azure-check
|
||||
GCP: gcp-login, gcp-gar-login, gcp-check
|
||||
Multi: multicloud-login, multicloud-check, cloud-auth-help
|
||||
EOHELP
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "✨ Multi-cloud auth helpers loaded. Run 'cloud-auth-help' for help."
|
||||
EOAUTHSCRIPT
|
||||
chmod +x "${HOME}/workspace/cloud-auth.sh"
|
||||
ok "Created ${HOME}/workspace/cloud-auth.sh"
|
||||
echo
|
||||
|
||||
# =========================
|
||||
# IaC tooling
|
||||
# =========================
|
||||
log "Installing IaC tooling (${IAC_TOOL})"
|
||||
case "$IAC_TOOL" in
|
||||
terraform)
|
||||
if ! command -v terraform > /dev/null 2>&1; then
|
||||
safe_dl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_$(arch).zip" "$SAFE_TMP/tf.zip"
|
||||
unzip -q "$SAFE_TMP/tf.zip" -d "$HOME/.local/bin"
|
||||
ok "Terraform ${TERRAFORM_VERSION} installed"
|
||||
else
|
||||
ok "Terraform already installed ($(terraform version | head -1))"
|
||||
fi
|
||||
;;
|
||||
cdk)
|
||||
if ! command -v npm > /dev/null 2>&1; then
|
||||
log "npm not found; installing Node via nvm"
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
# shellcheck disable=SC1090
|
||||
. "$NVM_DIR/nvm.sh"
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
# persist for future shells
|
||||
grep -q 'NVM_DIR' "$HOME/.bashrc" 2> /dev/null || {
|
||||
echo 'export NVM_DIR="$HOME/.nvm"' >> "$HOME/.bashrc"
|
||||
echo '. "$NVM_DIR/nvm.sh"' >> "$HOME/.bashrc"
|
||||
}
|
||||
fi
|
||||
npm install -g aws-cdk > /dev/null
|
||||
ok "AWS CDK installed ($(cdk --version))"
|
||||
;;
|
||||
pulumi)
|
||||
if ! command -v pulumi > /dev/null 2>&1; then
|
||||
curl -fsSL https://get.pulumi.com | sh
|
||||
export PATH="$PATH:$HOME/.pulumi/bin"
|
||||
ok "Pulumi installed ($(pulumi version))"
|
||||
else
|
||||
ok "Pulumi already installed ($(pulumi version))"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
warn "Unknown IAC_TOOL=${IAC_TOOL}; skipping IaC install"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Extras: Terragrunt, tflint, tfsec, terraform-docs, pre-commit
|
||||
if ! command -v terragrunt > /dev/null 2>&1; then
|
||||
TG_VER="0.54.0"
|
||||
safe_dl "https://github.com/gruntwork-io/terragrunt/releases/download/v${TG_VER}/terragrunt_linux_$(arch)" "$HOME/.local/bin/terragrunt"
|
||||
chmod +x "$HOME/.local/bin/terragrunt"
|
||||
ok "Terragrunt v${TG_VER} installed"
|
||||
fi
|
||||
|
||||
if ! command -v tflint > /dev/null 2>&1; then
|
||||
# official installer handles arch
|
||||
curl -fsSL https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
|
||||
mv -f /tmp/tflint "$HOME/.local/bin/" 2> /dev/null || true
|
||||
ok "tflint installed"
|
||||
fi
|
||||
|
||||
if ! command -v tfsec > /dev/null 2>&1; then
|
||||
TFSEC_VER="1.28.1"
|
||||
safe_dl "https://github.com/aquasecurity/tfsec/releases/download/v${TFSEC_VER}/tfsec-linux-$(arch)" "$HOME/.local/bin/tfsec"
|
||||
chmod +x "$HOME/.local/bin/tfsec"
|
||||
ok "tfsec v${TFSEC_VER} installed"
|
||||
fi
|
||||
|
||||
if ! command -v terraform-docs > /dev/null 2>&1; then
|
||||
TFD_VER="0.17.0"
|
||||
safe_dl "https://github.com/terraform-docs/terraform-docs/releases/download/v${TFD_VER}/terraform-docs-v${TFD_VER}-linux-$(arch).tar.gz" "$SAFE_TMP/terraform-docs.tgz"
|
||||
tar -xzf "$SAFE_TMP/terraform-docs.tgz" -C "$SAFE_TMP"
|
||||
install -m 0755 "$SAFE_TMP/terraform-docs" "$HOME/.local/bin/terraform-docs"
|
||||
ok "terraform-docs v${TFD_VER} installed"
|
||||
fi
|
||||
|
||||
if ! command -v pre-commit > /dev/null 2>&1; then
|
||||
if command -v pip3 > /dev/null 2>&1; then
|
||||
pip3 install --user --quiet pre-commit
|
||||
ok "pre-commit installed"
|
||||
elif command -v python3 > /dev/null 2>&1; then
|
||||
python3 -m pip install --user --quiet pre-commit
|
||||
ok "pre-commit installed"
|
||||
else
|
||||
warn "Python3/pip3 not found; skipping pre-commit"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Cloud CLIs (user-space)
|
||||
# =========================
|
||||
echo
|
||||
log "Installing Cloud CLIs (user-space)"
|
||||
|
||||
# AWS CLI v2
|
||||
if [[ "${ENABLE_AWS}" == "true" ]] && ! command -v aws > /dev/null 2>&1; then
|
||||
safe_dl "https://awscli.amazonaws.com/awscli-exe-linux-$(arch).zip" "$SAFE_TMP/awscliv2.zip" \
|
||||
|| safe_dl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" "$SAFE_TMP/awscliv2.zip"
|
||||
unzip -q "$SAFE_TMP/awscliv2.zip" -d "$SAFE_TMP"
|
||||
"$SAFE_TMP/aws/install" -i "$HOME/.local/aws-cli" -b "$HOME/.local/bin" > /dev/null
|
||||
ok "AWS CLI installed"
|
||||
fi
|
||||
|
||||
# Azure CLI
|
||||
if [[ "${ENABLE_AZURE}" == "true" ]] && ! command -v az > /dev/null 2>&1; then
|
||||
if command -v pip3 > /dev/null 2>&1; then
|
||||
pip3 install --user --quiet azure-cli && ok "Azure CLI installed"
|
||||
elif command -v python3 > /dev/null 2>&1; then
|
||||
python3 -m pip install --user --quiet azure-cli && ok "Azure CLI installed"
|
||||
else
|
||||
warn "Python/pip not found; cannot install Azure CLI"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Google Cloud SDK
|
||||
if [[ "${ENABLE_GCP}" == "true" ]] && ! command -v gcloud > /dev/null 2>&1; then
|
||||
GSDK_ARCH="$([[ "$(arch)" == amd64 ]] && echo x86_64 || echo arm)"
|
||||
safe_dl "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-${GSDK_ARCH}.tar.gz" "$SAFE_TMP/gcloud.tgz"
|
||||
tar -xzf "$SAFE_TMP/gcloud.tgz" -C "$HOME"
|
||||
mv "$HOME/google-cloud-sdk" "$HOME/.local/google-cloud-sdk"
|
||||
ln -sf "$HOME/.local/google-cloud-sdk/bin/"{gcloud,gsutil,bq} "$HOME/.local/bin/" || true
|
||||
"$HOME/.local/google-cloud-sdk/install.sh" --quiet --rc-path /dev/null --path-update=false || true
|
||||
ok "Google Cloud SDK installed"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Container & K8s tools
|
||||
# =========================
|
||||
echo
|
||||
log "Installing container & Kubernetes tools"
|
||||
|
||||
# Docker CLI (client only)
|
||||
if ! command -v docker > /dev/null 2>&1; then
|
||||
DOCKER_VER="25.0.5"
|
||||
safe_dl "https://download.docker.com/linux/static/stable/$(docker_tar_arch)/docker-${DOCKER_VER}.tgz" "$SAFE_TMP/docker.tgz"
|
||||
tar -xzf "$SAFE_TMP/docker.tgz" -C "$SAFE_TMP"
|
||||
install -m 0755 "$SAFE_TMP/docker/docker" "$HOME/.local/bin/docker"
|
||||
ok "Docker client installed"
|
||||
fi
|
||||
|
||||
# kubectl
|
||||
if ! command -v kubectl > /dev/null 2>&1; then
|
||||
KREL="$(curl -fsSL https://dl.k8s.io/release/stable.txt)"
|
||||
safe_dl "https://dl.k8s.io/release/${KREL}/bin/linux/$(arch)/kubectl" "$SAFE_TMP/kubectl"
|
||||
install -m 0755 "$SAFE_TMP/kubectl" "$HOME/.local/bin/kubectl"
|
||||
ok "kubectl ${KREL} installed"
|
||||
fi
|
||||
|
||||
# Helm
|
||||
if ! command -v helm > /dev/null 2>&1; then
|
||||
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | USE_SUDO=false HELM_INSTALL_DIR="$HOME/.local/bin" bash
|
||||
ok "Helm installed"
|
||||
fi
|
||||
|
||||
# jq / yq
|
||||
if ! command -v jq > /dev/null 2>&1; then
|
||||
safe_dl "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-$(arch)" "$HOME/.local/bin/jq"
|
||||
chmod +x "$HOME/.local/bin/jq"
|
||||
ok "jq installed"
|
||||
fi
|
||||
|
||||
if ! command -v yq > /dev/null 2>&1; then
|
||||
safe_dl "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_$(arch)" "$HOME/.local/bin/yq"
|
||||
chmod +x "$HOME/.local/bin/yq"
|
||||
ok "yq installed"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Cloud runtime auth (optional)
|
||||
# =========================
|
||||
echo
|
||||
log "Configuring runtime cloud auth (if provided)"
|
||||
|
||||
# AWS keys (override IRSA if present)
|
||||
if [[ "${ENABLE_AWS}" == "true" ]] && [[ -n "$AWS_ACCESS_KEY_ID" ]]; then
|
||||
mkdir -p "$HOME/.aws"
|
||||
{
|
||||
echo "[default]"
|
||||
echo "aws_access_key_id=${AWS_ACCESS_KEY_ID}"
|
||||
echo "aws_secret_access_key=${AWS_SECRET_ACCESS_KEY:-}"
|
||||
[[ -n "$AWS_SESSION_TOKEN" ]] && echo "aws_session_token=${AWS_SESSION_TOKEN}"
|
||||
} > "$HOME/.aws/credentials" || { warn "Failed to write AWS credentials"; }
|
||||
if [[ -n "$AWS_REGION" ]]; then
|
||||
{
|
||||
echo "[default]"
|
||||
echo "region=${AWS_REGION}"
|
||||
} > "$HOME/.aws/config"
|
||||
fi
|
||||
ok "AWS runtime creds configured${AWS_REGION:+ (region ${AWS_REGION})}"
|
||||
else
|
||||
skip "AWS runtime creds not set"
|
||||
fi
|
||||
|
||||
# Azure SP (client secret path; federated handled by helper)
|
||||
if [[ "${ENABLE_AZURE}" == "true" ]] && [[ -n "$AZURE_CLIENT_ID" && -n "$AZURE_TENANT_ID" ]]; then
|
||||
if command -v az > /dev/null 2>&1; then
|
||||
if [[ -n "$AZURE_FEDERATED_TOKEN_FILE" && -f "$AZURE_FEDERATED_TOKEN_FILE" ]]; then
|
||||
az login --service-principal --username "$AZURE_CLIENT_ID" \
|
||||
--tenant "$AZURE_TENANT_ID" \
|
||||
--federated-token "$(cat "$AZURE_FEDERATED_TOKEN_FILE")" \
|
||||
--allow-no-subscriptions > /dev/null
|
||||
ok "Azure federated login complete"
|
||||
elif [[ -n "$AZURE_CLIENT_SECRET" ]]; then
|
||||
az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" > /dev/null
|
||||
ok "Azure SP login complete"
|
||||
else
|
||||
skip "Azure creds not provided (need federated token file or client secret)"
|
||||
fi
|
||||
else
|
||||
warn "Azure CLI not found; skipping login"
|
||||
fi
|
||||
else
|
||||
skip "Azure runtime auth not configured"
|
||||
fi
|
||||
|
||||
# GCP SA JSON
|
||||
if [[ "${ENABLE_GCP}" == "true" ]] && [[ -n "$GCP_SERVICE_ACCOUNT" ]]; then
|
||||
if command -v gcloud > /dev/null 2>&1; then
|
||||
echo "$GCP_SERVICE_ACCOUNT" > /tmp/gcp.json || { warn "Failed to write GCP credentials"; }
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp.json > /dev/null || { warn "GCP auth failed"; }
|
||||
[[ -n "$GCP_PROJECT_ID" ]] && gcloud config set project "$GCP_PROJECT_ID" --quiet || true
|
||||
ok "GCP SA auth complete"
|
||||
else
|
||||
warn "gcloud not found; skipping GCP auth"
|
||||
fi
|
||||
else
|
||||
skip "GCP runtime auth not configured"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Git identity & bootstrap
|
||||
# =========================
|
||||
echo
|
||||
log "Preparing workspace directory"
|
||||
|
||||
# Git identity
|
||||
if [[ -n "$GIT_AUTHOR_NAME" ]]; then
|
||||
git config --global user.name "$GIT_AUTHOR_NAME"
|
||||
fi
|
||||
if [[ -n "$GIT_AUTHOR_EMAIL" ]]; then
|
||||
git config --global user.email "$GIT_AUTHOR_EMAIL"
|
||||
fi
|
||||
|
||||
mkdir -p "$WORKDIR"
|
||||
|
||||
# Clone or init
|
||||
if [[ -n "$REPO_URL" ]]; then
|
||||
URL="$REPO_URL"
|
||||
if [[ -n "$GITHUB_TOKEN" && "$URL" =~ ^https://github.com/ ]]; then
|
||||
URL="${URL/https:\/\//https:\/\/${GITHUB_TOKEN}@}" || { warn "Failed to modify URL"; }
|
||||
warn "Using GITHUB_TOKEN for private repo clone"
|
||||
fi
|
||||
if [[ ! -d "$WORKDIR/.git" ]]; then
|
||||
log "Cloning ${REPO_URL} into ${WORKDIR}"
|
||||
git clone "$URL" "$WORKDIR" || { warn "Failed to clone repository"; }
|
||||
pushd "$WORKDIR" > /dev/null
|
||||
git checkout "$DEFAULT_BRANCH" || git checkout -b "$DEFAULT_BRANCH"
|
||||
popd > /dev/null
|
||||
ok "Repository ready @ ${DEFAULT_BRANCH}"
|
||||
else
|
||||
ok "Repo already present at ${WORKDIR}"
|
||||
fi
|
||||
else
|
||||
if [[ ! -d "$WORKDIR/.git" ]]; then
|
||||
log "Initializing empty repository in ${WORKDIR}"
|
||||
git init -q "$WORKDIR"
|
||||
pushd "$WORKDIR" > /dev/null
|
||||
git checkout -b "$DEFAULT_BRANCH" > /dev/null 2>&1 || true
|
||||
popd > /dev/null
|
||||
fi
|
||||
ok "Workspace ready at ${WORKDIR}"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Company Terraform skeleton
|
||||
# =========================
|
||||
echo
|
||||
log "Creating company Terraform skeleton (optional)"
|
||||
mkdir -p "$WORKDIR/terraform"/{environments/{dev,staging,prod},modules,policies,shared}
|
||||
cat > "$WORKDIR/terraform/README.md" << 'EOREADME'
|
||||
# Company Terraform Project
|
||||
- `environments/` contains per-env stacks.
|
||||
- `modules/` reusable infra modules.
|
||||
- `policies/` sentinel/policy-as-code.
|
||||
- `shared/` backend, providers, etc.
|
||||
EOREADME
|
||||
ok "Skeleton present at $WORKDIR/terraform"
|
||||
|
||||
# =========================
|
||||
# PATH persistence tip
|
||||
# =========================
|
||||
if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc" 2> /dev/null; then
|
||||
echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$HOME/.bashrc"
|
||||
fi
|
||||
|
||||
echo
|
||||
ok "Workspace ready!"
|
||||
echo " • IaC tool: ${IAC_TOOL}"
|
||||
echo " • AWS enabled: ${ENABLE_AWS}"
|
||||
echo " • Azure enabled: ${ENABLE_AZURE}"
|
||||
echo " • GCP enabled: ${ENABLE_GCP}"
|
||||
[[ -d "$WORKDIR/.git" ]] && echo " • Repo: ${REPO_URL:-<none>} @ ${DEFAULT_BRANCH}"
|
||||
echo " • Auth helpers: source ~/workspace/cloud-auth.sh"
|
||||
@@ -0,0 +1,120 @@
|
||||
# --- Host cluster (where the workspace runs) ---
|
||||
variable "host_cluster_name" {
|
||||
description = "EKS cluster name"
|
||||
type = string
|
||||
|
||||
validation {
|
||||
condition = can(regex("^[0-9A-Za-z][0-9A-Za-z_-]*$", trimspace(var.host_cluster_name)))
|
||||
error_message = "Cluster name must match ^[0-9A-Za-z][0-9A-Za-z_-]*$ (no leading space)."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# --- Admin: IaC tool & toggles ---
|
||||
variable "iac_tool" {
|
||||
description = "Infrastructure as Code tool"
|
||||
type = string
|
||||
default = "terraform"
|
||||
validation {
|
||||
condition = contains(["terraform", "cdk", "pulumi"], var.iac_tool)
|
||||
error_message = "Must be one of: terraform, cdk, pulumi"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
variable "enable_aws" {
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "enable_azure" {
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "enable_gcp" {
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
# --- AWS ---
|
||||
variable "aws_region" {
|
||||
type = string
|
||||
default = "us-west-2"
|
||||
}
|
||||
|
||||
variable "aws_role_arn" {
|
||||
type = string
|
||||
default = "" # IRSA optional
|
||||
}
|
||||
|
||||
variable "aws_access_key_id" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "aws_secret_access_key" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "aws_session_token" {
|
||||
description = "Optional STS session token"
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "repo_url" {
|
||||
description = "Git repository to clone into the workspace (optional)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "default_branch" {
|
||||
description = "Default branch name to use (if repo is empty or for initial checkout)"
|
||||
type = string
|
||||
default = "main"
|
||||
}
|
||||
|
||||
|
||||
# --- Azure ---
|
||||
variable "azure_subscription_id" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "azure_tenant_id" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "azure_client_id" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "azure_client_secret" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# --- GCP ---
|
||||
variable "gcp_project_id" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "gcp_service_account" {
|
||||
description = "Service Account JSON (paste full JSON) — leave empty if using WIF"
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user