From 927aeba057f6c396ddad904f85f8adb7b880c686 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sat, 27 Mar 2021 13:48:11 -0400 Subject: [PATCH 01/40] start cli functionality --- bin/launch-coder | 4 + manual-cert/index.js | 0 manual-cert/values.yaml | 315 ++++++++++++++++ package-lock.json | 572 +++++++++++++++++++++++++++-- package.json | 19 +- shell-helpers/detectGoogleCloud.sh | 8 + src/cli.js | 90 +++++ 7 files changed, 979 insertions(+), 29 deletions(-) create mode 100755 bin/launch-coder create mode 100644 manual-cert/index.js create mode 100644 manual-cert/values.yaml create mode 100755 shell-helpers/detectGoogleCloud.sh create mode 100644 src/cli.js diff --git a/bin/launch-coder b/bin/launch-coder new file mode 100755 index 0000000..f059479 --- /dev/null +++ b/bin/launch-coder @@ -0,0 +1,4 @@ +#!/usr/bin/env node + +require = require("esm")(module /*, options*/); +require("../src/cli").cli(process.argv); diff --git a/manual-cert/index.js b/manual-cert/index.js new file mode 100644 index 0000000..e69de29 diff --git a/manual-cert/values.yaml b/manual-cert/values.yaml new file mode 100644 index 0000000..5b10090 --- /dev/null +++ b/manual-cert/values.yaml @@ -0,0 +1,315 @@ +# storageClassName -- Sets the storage class for all Coder services and user +# environments. By default the storageClassName is not specified and thus the +# default StorageClass is used. If storageClassName is not specified and a +# default StorageClass does not exist, then the deployment will fail. The +# storageClass MUST support the ReadWriteOnce access mode. +storageClassName: "" +# serviceType -- See the following for the different serviceType options and +# their use: +# https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types +serviceType: "ClusterIP" +# podSecurityPolicyName -- The name of the pod security policy to apply to all +# Coder services and user environments. The optional ingress has its own field +# for pod security policy as well. +podSecurityPolicyName: "" +# clusterDomainSuffix -- If you've set a custom default domain for your +# cluster, you may need to remove or change this DNS suffix for service +# resolution to work correctly. +clusterDomainSuffix: ".svc.cluster.local" +# contains configuration for the bundled ingress controller. +ingress: + # ingress.enable -- If set to true a Coder compatable ingress kind will be + # created. You can configure it with `ingress.annotations` below. + enable: true + # ingress.useDefault -- If set to true will deploy an nginx ingress that will + # allow you to access Coder from an external IP address, but if your + # kubernetes cluster is configured to provision external IP addresses. If you + # would like to bring your own ingress and hook Coder into that instead, set + # this value to false. + useDefault: true + # ingress.usePathWildcards -- Whether or not the ingress object should use + # path wildcards, i.e. ending with "/*". Some ingresses require this + # while others do not. You should check which path style your ingress + # requires. For ingress-nginx this should be set to false. + usePathWildcards: false + # ingress.host -- The hostname to use for accessing the platform. This can + # be left blank and the user can still access the platform from the external + # IP or a DNS name that resolves to the external IP address. + host: "" + # ingress.loadBalancerIP sets the external IP address of the ingress to the + # provided value. + loadBalancerIP: "" + # ingress.podSecurityPolicyName -- The name of the pod security policy the + # built in ingress controller should abide. It should be noted that the + # ingress controller requires the `NET_BIND_SERVICE` capability, privilege + # escalation, and access to privileged ports to successfully deploy. Ignored + # if `ingress.useDefault` is false. + podSecurityPolicyName: "" + # ingress.additionalAnnotations -- Deprecated. Please use `ingress.annotations`. + additionalAnnotations: [] + # ingress.annotations -- Additional annotations to be used when creating the + # ingress. These only apply to the Ingress Kubernetes kind. The annotations + # can be used to specify certificate issuers or other cloud provider specific + # integrations. + annotations: {} + # ingress.tls -- TLS options for the ingress. The hosts used for the tls + # configuration come from the ingress.host and the devurls.host variables. If + # those don't exist, then the TLS configuration will be ignored. + tls: + # ingress.tls.enable -- Enables the tls configuration. + enable: false + # ingress.tls.hostSecretName -- The secret to use for the ingress.host + # hostname. + hostSecretName: "" + # ingress.tls.devurlsHostSecretName -- The secret to use for the + # devurls.host hostname. + devurlsHostSecretName: "" + # ingress.service -- Options related to the ingress Kubernetes Service object. + service: + # ingress.service.annotations -- Additional annotations to add to the Service + # object. For example to make the ingress spawn an internal load balancer: + # annotations: + # cloud.google.com/load-balancer-type: "Internal" + annotations: {} +devurls: + # devurls.host -- Should be a wildcard hostname to allow matching against + # custom-created dev URLs. Leaving as an empty string results in devurls + # being disabled. Example: "*.devurls.coder.com". + host: "" +# Contains fields related to the Postgres backend. If providing your own +# instance, a minimum version of Postgres 11 is required with the contrib +# package installed. +postgres: + # postgres.useDefault -- Deploys an internal Postgres instance alongside the platform. + # It is not recommended to run the internal Postgres instance in production. + # If true, all other values are ignored. + useDefault: true + # postgres.host -- The host of the external postgres instance. + host: "" + # postgres.port -- The port of the external postgres instance. + port: "" + # postgres.user -- the user of the external postgres instance. + user: "" + # postgres.database -- The name of the database that coder will use. It must + # exist before Coder is installed. + database: "" + # postgres.passwordSecret -- The name of an existing secret in the current + # namespace with the password to the Postgres instance. The password must be + # contained in the secret field `password`. This should be set to an empty + # string if the database does not require a password to connect. + passwordSecret: "" + # postgres.sslMode -- Determines how the connection is made to the database. + # The acceptable values are: `disable`, `allow`, `prefer`, `require`, + # `verify-ca`, and `verify-full`. + sslMode: "require" +# imagePullPolicy -- Sets the policy for pulling a container image across all +# services. +imagePullPolicy: Always +# Contains configuration the REST API handling CRUD operations to +# the platform. +cemanager: + # cemanager.accessURL -- The cemanager access URL that the envproxy will use + # to communicate with the cemanager. This should be a full URL complete with + # protocol and no trailing slash. Uses internal cluster URL if not set. + # e.g. https://manager.coder.com + accessURL: "" + # cemanager.replicas -- The number of replicas to run of the manager. + replicas: 1 + # cemanager.image -- Injected during releases. + image: docker.io/coderenvs/coder-service:1.17.1 + # cemanager.resources -- Kubernetes resource request and limits for cemanager + # pods. + # To unset a value, set it to "". + # To unset all values, you can provide a values.yaml file which sets resources + # to nil. See values.yaml for an example. + # + # e.g: + # cemanager: + # # This will cause all values to be unset. + # resources: + # replica: 1 + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "250m" + memory: "512Mi" +# Contains the user interface of the platform. +dashboard: + # dashboard.replicas -- The number of replicas to run of the dashboard. + replicas: 1 + # dashboard.image -- Injected during releases. + image: docker.io/coderenvs/dashboard:1.17.1 + # dashboard.resources -- Kubernetes resource request and limits for dasboard + # pods. + # To unset a value, set it to "". + # To unset all values, you can provide a values.yaml file which sets resources + # to nil. See values.yaml for an example. + # + # e.g: + # dashboard: + # # This will cause all values to be unset. + # resources: + # replica: 1 + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "250m" + memory: "512Mi" +# envproxy contains configuration for the service handling long-lived +# connections to environments such as IDE or shell sessions. +envproxy: + # envproxy.accessURL -- The URL reported to cemanager. Must be accessible by + # cemanager and all users who can use this workspace provider. This should be + # a full URL, complete with protocol and trailing "/proxy" (no trailing + # slash). This is derived from the ingress.host or the access URL set during + # cemanager setup if not set. + # e.g. "https://proxy.coder.com/proxy" + accessURL: "" + # envproxy.clusterAddress -- The address of the K8s cluster, must be reachable + # from the cemanager. Defaults to + # "https://kubernetes.default.$clusterDomainSuffix:443" if not set. + clusterAddress: "" + # envproxy.replicas -- The number of replicas to run of the envproxy. + replicas: 1 + # envproxy.image -- Injected during releases. + image: docker.io/coderenvs/coder-service:1.17.1 + # envproxy.resources -- Kubernetes resource request and limits for envproxy + # pods. + # To unset a value, set it to "". + # To unset all values, you can provide a values.yaml file which sets resources + # to nil. See values.yaml for an example. + # + # e.g: + # envproxy: + # # This will cause all values to be unset. + # resources: + # replica: 1 + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "250m" + memory: "512Mi" + # envproxy.terminationGracePeriodSeconds -- Amount of seconds to wait before + # shutting down the environment proxy if there are still open connections. + # This is set very long intentionally so developers do not deal with + # disconnects during deployments. + terminationGracePeriodSeconds: 14400 +# timescale -- Contains configuration for the internal database. It is not +# recommended to run this service in production. See the `postgres` section for +# connecting to an external Postgres database. +timescale: + # timescale.image -- Injected during releases. + image: docker.io/coderenvs/timescale:1.17.1 + # timescale.resources -- Kubernetes resource request and limits for the + # timescale pod. + # To unset a value, set it to "". + # To unset all values, you can provide a values.yaml file which sets resources + # to nil. See values.yaml for an example. + # + # e.g: + # timescale: + # # This will cause all values to be unset. + # resources: + resources: + requests: + cpu: "250m" + memory: "1Gi" + # timescale.resources.requests.storage -- Specifies the size of the + # volume claim for persisting the database. + storage: "10Gi" + limits: + cpu: "250m" + memory: "1Gi" +envbox: + # envbox.image -- Injected during releases. + image: docker.io/coderenvs/envbox:1.17.1 +envbuilder: + # envbuilder.image -- Injected during releases. + image: docker.io/coderenvs/envbuilder:1.17.1 +# environments defines configuration that is applied to all +# user environments. +environments: + # environments.tolerations -- Tolerations are applied to all user + # environments. Each element is a regular pod toleration object. To set + # service tolerations see serviceTolerations. See values.yaml for an example. + # + # e.g. + # tolerations: + # - key: "my key" + # operator: "Exists" + # effect: "NoExecute" + # tolerationSeconds: 6000 + tolerations: [] + # environments.nodeSelector -- nodeSelector is applied to all user + # environments to specify eligible nodes for environments to run on. + # See: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector + # + # eg. + # nodeSelector: + # disktype: ssd + nodeSelector: {} +# certs -- Describes CAs that should be added to Coder services. These certs +# are NOT added to environments. +certs: + secret: + # certs.secret.name -- The name of the secret. + name: "" + # certs.secret.key -- The key in the secret pointing to the certificate + # bundle. + key: "" +# namespaceWhitelist -- DEPRECATED: A list of additional namespaces that +# environments may be deploy to. Use multiple workspace providers instead. +# Existing environments within these namespaces will continue to function, but +# new environments cannot be created within these namespaces. +# +# If you used this value before 1.17, you must keep it set to the same value +# until all environments within these namespaces are deleted, then you can set +# it back to []. +namespaceWhitelist: [] +ssh: + # ssh.enable -- Enables accessing environments via SSH. + enable: true +# serviceTolerations -- Tolerations are applied to all Coder managed services. +# Each element is a toleration object. To set user environment tolerations see +# environments.tolerations. See values.yaml for an example. +# +# e.g. +# serviceTolerations: +# - key: "my key" +# operator: "Exists" +# effect: "NoExecute" +# tolerationSeconds: 6000 +serviceTolerations: [] +# logging configures the logging format of Coder services. Specify one +# or more targets for logging output. If you have multiple output +# targets configured, coder will write output to multiple targets. +logging: + # logging.human -- Where to send logs that are formatted for readability by a + # human. Set to an empty string to disable. + human: /dev/stderr + # logging.stackdriver -- Where to send logs that are formatted for Google + # Stackdriver. Set to an empty string to disable. + stackdriver: "" + # logging.json -- Where to send logs that are formatted as JSON. Set to an + # empty string to disable. + json: "" + # logging.splunk -- Coder can send logs directly to Splunk, in + # addition to file-based output, if these values are configured. The + # channel is optional, and this logging is disabled if either the + # URL and Token are set to the empty string. + splunk: + # logging.splunk.url -- The Splunk HEC collector endpoint. + url: "" + # logging.splunk.token -- The Splunk HEC collector token. + token: "" + # logging.splunk_channel -- Optional. Specify the channel that you'd + # like to associate with all messages. + channel: "" +deploymentAnnotations: {} + diff --git a/package-lock.json b/package-lock.json index f72df11..12c9f14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,17 +47,30 @@ } } }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + } + } + }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -154,8 +167,7 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "chalk": { "version": "3.0.0", @@ -184,6 +196,11 @@ } } }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, "chokidar": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", @@ -212,6 +229,44 @@ "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", "dev": true }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -225,7 +280,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -233,8 +287,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "concat-map": { "version": "0.0.1", @@ -256,6 +309,16 @@ "xdg-basedir": "^4.0.0" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -271,6 +334,11 @@ "ms": "^2.1.1" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, "decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", @@ -304,8 +372,7 @@ "dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", - "dev": true + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" }, "duplexer3": { "version": "0.1.4", @@ -316,8 +383,7 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "end-of-stream": { "version": "1.4.4", @@ -328,12 +394,68 @@ "once": "^1.4.0" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", "dev": true }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, + "execa": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", + "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", + "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==" + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -343,6 +465,14 @@ "to-regex-range": "^5.0.1" } }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -350,6 +480,11 @@ "dev": true, "optional": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -420,6 +555,19 @@ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", "dev": true }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -444,6 +592,63 @@ "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", "dev": true }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -471,8 +676,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "is-glob": { "version": "4.0.1", @@ -517,6 +721,11 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -529,6 +738,11 @@ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", "dev": true }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", @@ -553,6 +767,20 @@ "package-json": "^6.3.0" } }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -576,6 +804,16 @@ } } }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -603,6 +841,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, "nodemon": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", @@ -642,6 +885,14 @@ "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -651,12 +902,46 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, "p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "dev": true }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, "package-json": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", @@ -677,6 +962,16 @@ } } }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", @@ -758,6 +1053,16 @@ "rc": "^1.2.8" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -767,6 +1072,33 @@ "lowercase-keys": "^1.0.0" } }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" + }, + "rxjs": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", + "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -790,17 +1122,33 @@ } } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -810,26 +1158,22 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, "requires": { "ansi-regex": "^5.0.0" } @@ -840,11 +1184,15 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "requires": { "ansi-regex": "^4.1.0" } }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -866,6 +1214,19 @@ "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", "dev": true }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", @@ -890,6 +1251,11 @@ "nopt": "~1.0.10" } }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -970,6 +1336,19 @@ "prepend-http": "^2.0.0" } }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, "widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -979,6 +1358,31 @@ "string-width": "^4.0.0" } }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1002,6 +1406,124 @@ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true + }, + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", + "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-interactive": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/yargs-interactive/-/yargs-interactive-3.0.0.tgz", + "integrity": "sha512-yJWGjwt/PkX8uJ6/u928RQtsTIWUbS8H9jYMvLXdrFmWmmTY781SXrZKgW/VF1H8ViOtYst4OeXX0UVTmjA1wg==", + "requires": { + "inquirer": "^7.0.0", + "yargs": "^14.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==" } } } diff --git a/package.json b/package.json index 82df067..79de89d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,14 @@ "name": "auto-coder", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "src/index.js", + "bin": { + "@bpmct/launch-coder": "bin/launch-coder", + "launch-coder": "bin/launch-coder" + }, + "publishConfig": { + "access": "restricted" + }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "nodemon index.js" @@ -10,10 +17,14 @@ "author": "", "license": "ISC", "devDependencies": { - "dotenv": "^8.2.0", "nodemon": "^2.0.7" }, "dependencies": { - "promisify-child-process": "^4.1.1" + "dotenv": "^8.2.0", + "esm": "^3.2.25", + "execa": "^5.0.0", + "promisify-child-process": "^4.1.1", + "yargs": "^16.2.0", + "yargs-interactive": "^3.0.0" } -} +} \ No newline at end of file diff --git a/shell-helpers/detectGoogleCloud.sh b/shell-helpers/detectGoogleCloud.sh new file mode 100755 index 0000000..9fe832d --- /dev/null +++ b/shell-helpers/detectGoogleCloud.sh @@ -0,0 +1,8 @@ +# thanks https://stackoverflow.com/a/38795846/10850488 + +GMETADATA_ADDR=`dig +short metadata.google.internal` +if [[ "${GMETADATA_ADDR}" == "" ]]; then + echo false +else + echo true +fi \ No newline at end of file diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..166365c --- /dev/null +++ b/src/cli.js @@ -0,0 +1,90 @@ +const yargs = require("yargs/yargs"); +import inquirer from "inquirer"; +const { hideBin } = require("yargs/helpers"); + +const execa = require("execa"); + +require("dotenv").config(); + +export async function cli(args) { + let argv = yargs(hideBin(args)) + .option("method", { + alias: "m", + type: "string", + description: "Method for deploying Coder (gcloud, general-k8s)", + }) + .option("domainType", { + alias: "d", + type: "string", + description: "Domain for the Coder Deployment (auto, custom)", + }) + .option("token", { + type: "string", + description: "API token for CloudFlare", + }) + .option("domainName", { + type: "string", + description: "[Manual-only] Your custom domain for Coder", + }) + .option("name", { + type: "string", + alias: "n", + description: "Name for Coder subdomain", + }).argv; + + // detect if we are on google cloud :) + + const checkGoogleCloud = await execa("/bin/sh", [ + // probably a silly way to do so, considering I can also ping in node + // oh well. hackathon + __dirname + "/../shell-helpers/detectGoogleCloud.sh", + ]); + if (!argv.method && checkGoogleCloud && checkGoogleCloud.stdout == "true") { + console.log( + "Auto-detected you are on Google Cloud, so we'll deploy there 🚀\nYou can manually change this by executing with --method" + ); + } else if (argv.method == undefined) { + console.log("YEP IT IS IN he", argv.method); + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "method", + message: "Where would you like to deploy Coder", + choices: [ + { + name: `Create a fresh Google Cloud cluster for me!`, + value: "gcloud", + }, + { + name: "Install Coder on my current cluster (sketchy)", + value: "k8s", + }, + ], + })), + }; + } + + // determine which type of domain to use + if (!argv.domainType) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "domainType", + message: "What type of domain would you like to use?", + choices: [ + { + name: `With a free domain from Coder (ex. [myname].${process.env.CLOUDFLARE_DOMAIN})`, + value: "auto", + }, + { + name: "With a domain name I own on Google CloudDNS", + value: "cloud-dns", + }, + ], + })), + }; + } + console.log("epic answer dood", argv); +} From 3a8560bb15a3e34df6546ed6eacfa87581173ee2 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sun, 28 Mar 2021 17:32:05 -0400 Subject: [PATCH 02/40] working cli and subprocesses --- package.json | 2 +- shell-helpers/googleCloudDefaultProject.sh | 1 + shell-helpers/googleCloudPrereqs.sh | 22 ++ shell-helpers/googleCloudProjects.sh | 1 + src/cli.js | 247 ++++++++++++++++++++- 5 files changed, 263 insertions(+), 10 deletions(-) create mode 100755 shell-helpers/googleCloudDefaultProject.sh create mode 100755 shell-helpers/googleCloudPrereqs.sh create mode 100755 shell-helpers/googleCloudProjects.sh diff --git a/package.json b/package.json index 79de89d..3400f7c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "nodemon index.js" + "dev": "nodemon bin/launch-coder" }, "author": "", "license": "ISC", diff --git a/shell-helpers/googleCloudDefaultProject.sh b/shell-helpers/googleCloudDefaultProject.sh new file mode 100755 index 0000000..20d33e4 --- /dev/null +++ b/shell-helpers/googleCloudDefaultProject.sh @@ -0,0 +1 @@ +gcloud config list --format 'value(core.project)' 2>/dev/null \ No newline at end of file diff --git a/shell-helpers/googleCloudPrereqs.sh b/shell-helpers/googleCloudPrereqs.sh new file mode 100755 index 0000000..bec8d37 --- /dev/null +++ b/shell-helpers/googleCloudPrereqs.sh @@ -0,0 +1,22 @@ +# Check prereqs. This can potentially search for specific version #s down the road. + +if ! [ -x "$(command -v git)" ]; then + echo 'Error: git is not installed.' >&2 + exit 1 +fi +# if ! [ -x "$(command -v terraform)" ]; then +# echo 'Error: terraform is not installed.' >&2 +# exit 1 +# fi +if ! [ -x "$(command -v gcloud)" ]; then + echo 'Error: gcloud is not installed.' >&2 + exit 1 +fi +if ! [ -x "$(command -v kubectl)" ]; then + echo 'Error: kubectl is not installed.' >&2 + exit 1 +fi +if ! [ -x "$(command -v helm)" ]; then + echo 'Error: helm is not installed.' >&2 + exit 1 +fi \ No newline at end of file diff --git a/shell-helpers/googleCloudProjects.sh b/shell-helpers/googleCloudProjects.sh new file mode 100755 index 0000000..2e68eca --- /dev/null +++ b/shell-helpers/googleCloudProjects.sh @@ -0,0 +1 @@ +gcloud projects list --format json \ No newline at end of file diff --git a/src/cli.js b/src/cli.js index 166365c..7e7fefc 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,11 +1,59 @@ const yargs = require("yargs/yargs"); import inquirer from "inquirer"; +import { exit } from "yargs"; const { hideBin } = require("yargs/helpers"); const execa = require("execa"); require("dotenv").config(); +const runHelperScript = async (filename, params) => { + try { + let run = await execa("/bin/sh", [ + __dirname + `/../shell-helpers/${filename}.sh`, + ]); + + if (run && run.stdout) { + return run.stdout; + } + } catch (err) { + throw err; + return; + } +}; + +const generateGoogleClusterCommand = (argv) => { + return `$ gcloud beta container --project "${argv.gcloudProjectId}" \\ + clusters create "${argv.gcloudClusterName}" \\ + --zone "${argv.gcloudClusterZone}" \\ + --no-enable-basic-auth \\ + --node-version "latest" \\ + --cluster-version "latest" \\ + --machine-type "${argv.gcloudClusterMachineType}" \\ + --image-type "UBUNTU" \\ + --disk-type "pd-standard" \\ + --disk-size "50" \\ + --metadata disable-legacy-endpoints=true \\ + --scopes "https://www.googleapis.com/auth/cloud-platform" \\ + --num-nodes "${argv.gcloudClusterMinNodes}" \\ + --enable-stackdriver-kubernetes \\ + --enable-ip-alias \\ + --network "projects/$PROJECT_ID/global/networks/default" \\ + --subnetwork "projects/$PROJECT_ID/regions/${ + argv.gcloudClusterZone + }/subnetworks/default" \\ + --default-max-pods-per-node "110" \\ + --addons HorizontalPodAutoscaling,HttpLoadBalancing \\ + --enable-autoupgrade \\ + --enable-autorepair \\${ + argv.gcloudClusterPreemtible ? "\n --preemtible \\" : "" + } + --enable-network-policy \\ + --enable-autoscaling \\ + --min-nodes "${argv.gcloudClusterMinNodes}" \\ + --max-nodes "${argv.gcloudClusterMaxNodes}"`; +}; + export async function cli(args) { let argv = yargs(hideBin(args)) .option("method", { @@ -30,30 +78,61 @@ export async function cli(args) { type: "string", alias: "n", description: "Name for Coder subdomain", + }) + .option("gcloud-project-id", { + type: "string", + }) + .option("gcloud-cluster-name", { + type: "string", + default: "coder", + }) + .option("gcloud-cluster-zone", { + type: "string", + default: "us-central1-a", + }) + .option("gcloud-cluster-machine-type", { + type: "string", + default: "e2-highmem-4", + }) + .option("gcloud-cluster-preemtible", { + type: "boolean", + default: true, + }) + .option("gcloud-cluster-autoscaling", { + type: "boolean", + default: true, + }) + .option("gcloud-cluster-min-nodes", { + type: "number", + default: 1, + }) + .option("gcloud-cluster-max-nodes", { + type: "number", + default: 3, + }) + // TODO: determine better naming for this: + .option("gcloud-skip-confirm-prompt", { + type: "boolean", }).argv; // detect if we are on google cloud :) - const checkGoogleCloud = await execa("/bin/sh", [ - // probably a silly way to do so, considering I can also ping in node - // oh well. hackathon - __dirname + "/../shell-helpers/detectGoogleCloud.sh", - ]); + const checkGoogleCloud = await runHelperScript("detectGoogleCloud"); + if (!argv.method && checkGoogleCloud && checkGoogleCloud.stdout == "true") { console.log( "Auto-detected you are on Google Cloud, so we'll deploy there 🚀\nYou can manually change this by executing with --method" ); } else if (argv.method == undefined) { - console.log("YEP IT IS IN he", argv.method); argv = { ...argv, ...(await inquirer.prompt({ type: "list", name: "method", - message: "Where would you like to deploy Coder", + message: "Where would you like to deploy Coder?", choices: [ { - name: `Create a fresh Google Cloud cluster for me!`, + name: `Create a fresh Google Cloud cluster for me and install Coder`, value: "gcloud", }, { @@ -65,6 +144,156 @@ export async function cli(args) { }; } + if (argv.method == "gcloud") { + // ensure gcloud-cli is installed and active + + // TODO: add better user education on what the prereqs are + console.log("Checking for prerequisites..."); + try { + await runHelperScript("googleCloudPrereqs"); + console.log("✅", "You seem to have all the dependencies installed!"); + } catch (err) { + console.log("❌", err.stderr); + return; + } + + if (!argv.gcloudProjectId) { + let defaultProject = false; + let projects = []; + + // try to get the default project + try { + const listOfProjects = await runHelperScript( + "googleCloudDefaultProject" + ); + + defaultProject = await runHelperScript("googleCloudDefaultProject"); + const projectsJson = await runHelperScript("googleCloudProjects"); + projects = JSON.parse(projectsJson).map((project) => { + return project.projectId; + }); + + // ensure we are actually fetching IDs + if (projects[0] == undefined) { + throw "could not read project ID"; + } + + console.log("📄 Got a list of your Google Cloud projects!\n"); + } catch (err) { + // reset projects list + projects = []; + + // TODO: ensure it is actually no biggie + console.log("Ran into an error fetching your projects... No biggie 🙂"); + } + + // show a select field if we found a list + if (projects.length) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "gcloudProjectId", + default: defaultProject, + message: `Google Cloud Project:`, + validate: (that) => { + // TODO: validate this project actually exists + return that != ""; + }, + choices: projects, + })), + }; + } else + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "input", + name: "gcloudProjectId", + default: undefined, + message: `Google Cloud Project:`, + validate: (that) => { + // TODO: validate this project actually exists + return that != ""; + }, + choices: [ + { + name: `Create a fresh Google Cloud cluster for me and install Coder`, + value: "gcloud", + }, + { + name: "Install Coder on my current cluster (sketchy)", + value: "k8s", + }, + ], + })), + }; + } + + let gCloudCommand = generateGoogleClusterCommand(argv); + + // TODO: impliment pricing calculations with Google API + let pricing_info = ""; + + if ( + argv.gcloudClusterZone == "us-central1-a" && + argv.gcloudClusterMachineType == "e2-highmem-4" && + argv.gcloudClusterMinNodes == "1" && + argv.gcloudClusterMaxNodes == "3" && + argv.gcloudClusterAutoscaling && + argv.gcloudClusterPreemtible + ) { + pricing_info = + "This cluster will cost you roughly $40-120/mo to run on Google Cloud depending on usage." + + "\n\nNote: this is just an estimate, we recommend researching yourself and monitoring billing:"; + } else { + pricing_info = + "You are not using default settings. Be sure to calculate the pricing info for your cluster"; + } + console.log( + "\n💻 Your command is:", + "\n------------\n", + + gCloudCommand, + "\n------------", + "\n\n💵 " + pricing_info + "\n", + "\t➡️ GKE Pricing: https://cloud.google.com/kubernetes-engine/pricing\n", + "\t➡️ Storage pricing: https://cloud.google.com/compute/disks-image-pricing\n", + "\t➡️ Machine pricing: https://cloud.google.com/compute/all-pricing\n\n", + "\t➡️ or use the Google Cloud Pricing Calculator: https://cloud.google.com/products/calculator\n", + "\n------------" + ); + + // TODO: impliment ability to edit cluster command in the cli (wohoo) + + if (!argv.gcloudSkipConfirmPrompt) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command?", + }); + + if (!runCommand.runIt) { + console.log( + `\n\nOk :) Feel free to modify the command as needed, run it yourself, then you can run "launch-coder --mode k8s" to install Coder on the cluster you manually created` + ); + return 0; + } + } + + const subprocess = execa("ping", ["google.com", "-c", "5"]); + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + console.log("WE KNOW THE PROCESS HAS COMPLETED"); + + // execa("echo", ["unicorns"]).stdout.pipe(process.stdout); + } else if (argv.method == "k8s") { + console.log("coming sooon moo"); + } else { + console.error("Error. Unknown method: " + argv.method); + return; + } + // determine which type of domain to use if (!argv.domainType) { argv = { @@ -86,5 +315,5 @@ export async function cli(args) { })), }; } - console.log("epic answer dood", argv); + console.log("\n\nat the end with a long argv:", Object.keys(argv).length); } From 6bc1a861171561128f44ea8e62632b705d290990 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sun, 28 Mar 2021 17:37:02 -0400 Subject: [PATCH 03/40] fix google cloud check --- src/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 7e7fefc..410218e 100644 --- a/src/cli.js +++ b/src/cli.js @@ -119,7 +119,7 @@ export async function cli(args) { const checkGoogleCloud = await runHelperScript("detectGoogleCloud"); - if (!argv.method && checkGoogleCloud && checkGoogleCloud.stdout == "true") { + if (!argv.method && checkGoogleCloud && checkGoogleCloud == "true") { console.log( "Auto-detected you are on Google Cloud, so we'll deploy there 🚀\nYou can manually change this by executing with --method" ); From a12e5b496d0b4fabab9bea0b359762e0184c347f Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sun, 28 Mar 2021 17:38:34 -0400 Subject: [PATCH 04/40] fix again whoopsies --- src/cli.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli.js b/src/cli.js index 410218e..ccba85c 100644 --- a/src/cli.js +++ b/src/cli.js @@ -120,6 +120,7 @@ export async function cli(args) { const checkGoogleCloud = await runHelperScript("detectGoogleCloud"); if (!argv.method && checkGoogleCloud && checkGoogleCloud == "true") { + argv.method == "gcloud"; console.log( "Auto-detected you are on Google Cloud, so we'll deploy there 🚀\nYou can manually change this by executing with --method" ); From 1a96eef83e863d64c931357e1944d0f69047a7e7 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sun, 28 Mar 2021 17:55:00 -0400 Subject: [PATCH 05/40] better cloud shell checks --- shell-helpers/detectCloudShell.sh | 7 +++++++ shell-helpers/detectGoogleCloud.sh | 8 -------- src/cli.js | 25 +++++++++++++++++-------- 3 files changed, 24 insertions(+), 16 deletions(-) create mode 100755 shell-helpers/detectCloudShell.sh delete mode 100755 shell-helpers/detectGoogleCloud.sh diff --git a/shell-helpers/detectCloudShell.sh b/shell-helpers/detectCloudShell.sh new file mode 100755 index 0000000..283f6e4 --- /dev/null +++ b/shell-helpers/detectCloudShell.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +if [[ $(hostname -s) == "cs-"* ]]; then + echo true +else + echo false +fi \ No newline at end of file diff --git a/shell-helpers/detectGoogleCloud.sh b/shell-helpers/detectGoogleCloud.sh deleted file mode 100755 index 9fe832d..0000000 --- a/shell-helpers/detectGoogleCloud.sh +++ /dev/null @@ -1,8 +0,0 @@ -# thanks https://stackoverflow.com/a/38795846/10850488 - -GMETADATA_ADDR=`dig +short metadata.google.internal` -if [[ "${GMETADATA_ADDR}" == "" ]]; then - echo false -else - echo true -fi \ No newline at end of file diff --git a/src/cli.js b/src/cli.js index ccba85c..2151382 100644 --- a/src/cli.js +++ b/src/cli.js @@ -117,14 +117,24 @@ export async function cli(args) { // detect if we are on google cloud :) - const checkGoogleCloud = await runHelperScript("detectGoogleCloud"); + const checkCloudShell = await runHelperScript("detectCloudShell"); - if (!argv.method && checkGoogleCloud && checkGoogleCloud == "true") { - argv.method == "gcloud"; - console.log( - "Auto-detected you are on Google Cloud, so we'll deploy there 🚀\nYou can manually change this by executing with --method" - ); - } else if (argv.method == undefined) { + if (!argv.method && checkCloudShell && checkCloudShell == "true") { + console.log("It looks like you are using Google Cloud Shell 🚀"); + + const gcloudCheck = await inquirer.prompt({ + type: "confirm", + default: true, + name: "confirm", + message: "Do you want to deploy a new Coder cluster?", + }); + + if (gcloudCheck.confirm) { + argv.method = "gcloud"; + } + } + + if (argv.method == undefined) { argv = { ...argv, ...(await inquirer.prompt({ @@ -149,7 +159,6 @@ export async function cli(args) { // ensure gcloud-cli is installed and active // TODO: add better user education on what the prereqs are - console.log("Checking for prerequisites..."); try { await runHelperScript("googleCloudPrereqs"); console.log("✅", "You seem to have all the dependencies installed!"); From 0e529b2daae22c05767ac8e2b82595b2e85d2670 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sun, 28 Mar 2021 21:19:26 -0400 Subject: [PATCH 06/40] create cluster and scripts for users --- .gitignore | 3 +- src/cli.js | 170 +++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 148 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 4dd2601..2850557 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules your_*.yaml -.env \ No newline at end of file +.env +out \ No newline at end of file diff --git a/src/cli.js b/src/cli.js index 2151382..53da3f3 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,10 +1,13 @@ const yargs = require("yargs/yargs"); import inquirer from "inquirer"; +import { domain } from "process"; import { exit } from "yargs"; const { hideBin } = require("yargs/helpers"); - const execa = require("execa"); +// reading and writing in out/ folder +const fs = require("fs"); + require("dotenv").config(); const runHelperScript = async (filename, params) => { @@ -23,7 +26,9 @@ const runHelperScript = async (filename, params) => { }; const generateGoogleClusterCommand = (argv) => { - return `$ gcloud beta container --project "${argv.gcloudProjectId}" \\ + // TODO: omit zone if it is intentionally left blank to support regional clusters + // note: this will involve modifying other gcloud commands that mention --zone + return `gcloud beta container --project "${argv.gcloudProjectId}" \\ clusters create "${argv.gcloudClusterName}" \\ --zone "${argv.gcloudClusterZone}" \\ --no-enable-basic-auth \\ @@ -38,20 +43,20 @@ const generateGoogleClusterCommand = (argv) => { --num-nodes "${argv.gcloudClusterMinNodes}" \\ --enable-stackdriver-kubernetes \\ --enable-ip-alias \\ - --network "projects/$PROJECT_ID/global/networks/default" \\ - --subnetwork "projects/$PROJECT_ID/regions/${ - argv.gcloudClusterZone - }/subnetworks/default" \\ + --network "projects/${argv.gcloudProjectId}/global/networks/default" \\ + --subnetwork "projects/${argv.gcloudProjectId}/regions/${ + argv.gcloudClusterRegion + }/subnetworks/default" \\ --default-max-pods-per-node "110" \\ --addons HorizontalPodAutoscaling,HttpLoadBalancing \\ --enable-autoupgrade \\ --enable-autorepair \\${ - argv.gcloudClusterPreemtible ? "\n --preemtible \\" : "" + argv.gcloudClusterPreemptible ? "\n --preemptible \\" : "" } --enable-network-policy \\ --enable-autoscaling \\ --min-nodes "${argv.gcloudClusterMinNodes}" \\ - --max-nodes "${argv.gcloudClusterMaxNodes}"`; + --max-nodes "${argv.gcloudClusterMaxNodes}"\n`; }; export async function cli(args) { @@ -61,6 +66,12 @@ export async function cli(args) { type: "string", description: "Method for deploying Coder (gcloud, general-k8s)", }) + .option("save-dir", { + alias: "f", + type: "string", + default: "~/.config/launch-coder", + description: "Path to save config files", + }) .option("domainType", { alias: "d", type: "string", @@ -86,6 +97,10 @@ export async function cli(args) { type: "string", default: "coder", }) + .option("gcloud-cluster-region", { + type: "string", + default: "us-central1", + }) .option("gcloud-cluster-zone", { type: "string", default: "us-central1-a", @@ -94,7 +109,7 @@ export async function cli(args) { type: "string", default: "e2-highmem-4", }) - .option("gcloud-cluster-preemtible", { + .option("gcloud-cluster-preemptible", { type: "boolean", default: true, }) @@ -110,8 +125,7 @@ export async function cli(args) { type: "number", default: 3, }) - // TODO: determine better naming for this: - .option("gcloud-skip-confirm-prompt", { + .option("skip-confirm-prompts", { type: "boolean", }).argv; @@ -147,7 +161,7 @@ export async function cli(args) { value: "gcloud", }, { - name: "Install Coder on my current cluster (sketchy)", + name: "Install Coder on my current cluster", value: "k8s", }, ], @@ -245,12 +259,13 @@ export async function cli(args) { let pricing_info = ""; if ( + argv.gcloudClusterRegion == "us-central1" && argv.gcloudClusterZone == "us-central1-a" && argv.gcloudClusterMachineType == "e2-highmem-4" && argv.gcloudClusterMinNodes == "1" && argv.gcloudClusterMaxNodes == "3" && argv.gcloudClusterAutoscaling && - argv.gcloudClusterPreemtible + argv.gcloudClusterPreemptible ) { pricing_info = "This cluster will cost you roughly $40-120/mo to run on Google Cloud depending on usage." + @@ -275,7 +290,7 @@ export async function cli(args) { // TODO: impliment ability to edit cluster command in the cli (wohoo) - if (!argv.gcloudSkipConfirmPrompt) { + if (!argv.skipConfirmPrompts) { const runCommand = await inquirer.prompt({ type: "confirm", default: true, @@ -291,17 +306,94 @@ export async function cli(args) { } } - const subprocess = execa("ping", ["google.com", "-c", "5"]); - subprocess.stdout.pipe(process.stdout); - const { stdout } = await subprocess; - console.log("WE KNOW THE PROCESS HAS COMPLETED"); + // TODO: create different folders for each session + console.log( + "💾 FYI: All of these scripts are being saved in: " + argv.saveDir + "\n" + ); - // execa("echo", ["unicorns"]).stdout.pipe(process.stdout); - } else if (argv.method == "k8s") { - console.log("coming sooon moo"); - } else { + // switch to the absolute path of the home directory if the user included ~/ + if (argv.saveDir.startsWith("~/")) { + const userHome = require("os").homedir(); + argv.saveDir = argv.saveDir.replace("~/", userHome + "/"); + } + + // create our out/ file to hold our creation script, among other things + await execa("mkdir", ["-p", argv.saveDir]).catch((err) => { + console.log(err); + }); + + // git init (or re-init so the user can easily source-control) + await execa("git", ["init", argv.saveDir]); + + // add our lovely script to the out folder + fs.writeFileSync( + argv.saveDir + "/createCluster.sh", + "#!/bin/sh\n" + gCloudCommand + ); + await fs.chmodSync(argv.saveDir + "/createCluster.sh", "755"); + + console.log("\n⏳ Creating your cluster. This will take a few minutes..."); + + try { + const subprocess = execa("/bin/sh", [argv.saveDir + "/createCluster.sh"]); + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + console.log("------------"); + console.log( + "✅", + `Cluster "${argv.gcloudClusterName}" has been created!` + ); + } catch (err) { + console.log("❌", "Process failed\n\n\n", err.stderr); + return; + } + + try { + await execa( + "gcloud", + `container clusters get-credentials ${argv.gcloudClusterName} --zone ${argv.gcloudClusterZone}`.split( + " " + ) + ); + console.log("✅", "Added to kube context"); + } catch (err) { + console.log("❌", "Unable to add to kube context:\n\n\n", err.stderr); + return; + } + + // So now we can move on to installing Coder! + } + + // if argv.method == "gcloud" at this point + // the script has succeeded in creating the cluster + // and switched context + if (argv.method != "k8s" && argv.method != "gcloud") { + // TODO: standardize these console.error("Error. Unknown method: " + argv.method); return; + } else if (argv.method == "k8s") { + // TODO: add checks to ensure the user has a cluster, + // and it has the necessary stuff for Coder + console.log( + "This script does not currently verify that you cluster is ready for Coder yet.\n\nWe recommend checking the docs before continuing:" + ); + console.log("\t➡️ https://coder.com/docs/setup/requirements"); + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you to proceed?", + }); + + if (!runCommand.runIt) { + console.log( + `\nExited. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` + ); + return 0; + } + } } // determine which type of domain to use @@ -314,16 +406,46 @@ export async function cli(args) { message: "What type of domain would you like to use?", choices: [ { - name: `With a free domain from Coder (ex. [myname].${process.env.CLOUDFLARE_DOMAIN})`, + name: `A free domain from Coder (ex. [myname].${process.env.CLOUDFLARE_DOMAIN})`, value: "auto", }, { - name: "With a domain name I own on Google CloudDNS", + name: "A domain name I own on Google CloudDNS", value: "cloud-dns", }, + { + name: "Do not set up a domain for now", + value: "none", + }, ], })), }; } + + // validate domainType + if (argv.domainType == "auto") { + console.log("AUTOMA"); + } else if (argv.domainType == "cloud-dns") { + console.log("Well, this is coming soon 💀"); + } else if (argv.domainType == "none") { + console.log( + "\nWarning: This means you can't use Coder with DevURLs, a primary way of accessing web services\ninside of a Coder Workspace:\n", + "\t📄 Docs: https://coder.com/docs/environments/devurls\n", + "\t🌎 Alternative: https://ngrok.com/docs (you can nstall this in your images)\n\n" + ); + + console.log( + "You can always add a domain later, and use a custom provider via our docs." + ); + + // TODO: add confirmations + } else { + // TODO: standardize these + console.error("Error. Unknown domainType: " + argv.domainType); + return; + } + + // install and access Coder + console.log("\n\nat the end with a long argv:", Object.keys(argv).length); } From 089d1ca85ec066a75980f01546ea4190b197636e Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sun, 28 Mar 2021 22:08:57 -0400 Subject: [PATCH 07/40] start auto cloudflare stuff --- index.js | 2 +- package-lock.json | 5 ++++ package.json | 3 ++- src/cli.js | 69 +++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 2991a93..bea1820 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ const { exec, spawn, fork, execFile } = require("promisify-child-process"); let inject = { user_domain: `newest.coding.pics`, - user_email: "victorialslocum@gmail.com", + // user_email: "victorialslocum@gmail.com", user_namespace: "script", cloudflare_email: process.env.CLOUDFLARE_EMAIL, cloudflare_api: process.env.CLOUDFLARE_TOKEN, diff --git a/package-lock.json b/package-lock.json index 12c9f14..0441619 100644 --- a/package-lock.json +++ b/package-lock.json @@ -743,6 +743,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", diff --git a/package.json b/package.json index 3400f7c..533abc6 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,9 @@ "dotenv": "^8.2.0", "esm": "^3.2.25", "execa": "^5.0.0", + "js-sha256": "^0.9.0", "promisify-child-process": "^4.1.1", "yargs": "^16.2.0", "yargs-interactive": "^3.0.0" } -} \ No newline at end of file +} diff --git a/src/cli.js b/src/cli.js index 53da3f3..c4a92b2 100644 --- a/src/cli.js +++ b/src/cli.js @@ -8,6 +8,10 @@ const execa = require("execa"); // reading and writing in out/ folder const fs = require("fs"); +// TODO change this +const cloudflareEmail = "me@bpmct.net"; +const cloudflareDomain = "coding.pics"; + require("dotenv").config(); const runHelperScript = async (filename, params) => { @@ -90,6 +94,11 @@ export async function cli(args) { alias: "n", description: "Name for Coder subdomain", }) + .option("namespace", { + type: "string", + default: "coder", + description: "Namespace for Coder", + }) .option("gcloud-project-id", { type: "string", }) @@ -206,9 +215,6 @@ export async function cli(args) { } catch (err) { // reset projects list projects = []; - - // TODO: ensure it is actually no biggie - console.log("Ran into an error fetching your projects... No biggie 🙂"); } // show a select field if we found a list @@ -227,7 +233,11 @@ export async function cli(args) { choices: projects, })), }; - } else + } else { + console.log( + "🤔 We couldn't determine if you have any Google Cloud Projects.\n", + "\t➡️ Create one here: https://console.cloud.google.com/projectcreate" + ); argv = { ...argv, ...(await inquirer.prompt({ @@ -251,10 +261,13 @@ export async function cli(args) { ], })), }; + } } let gCloudCommand = generateGoogleClusterCommand(argv); + // TODO: add info on what this cluster means + // TODO: impliment pricing calculations with Google API let pricing_info = ""; @@ -300,9 +313,9 @@ export async function cli(args) { if (!runCommand.runIt) { console.log( - `\n\nOk :) Feel free to modify the command as needed, run it yourself, then you can run "launch-coder --mode k8s" to install Coder on the cluster you manually created` + `\n\nOk :) Feel free to modify the command as needed, run it yourself, then you can run "launch-coder --method k8s" to install Coder on the cluster you manually created` ); - return 0; + return; } } @@ -332,12 +345,17 @@ export async function cli(args) { ); await fs.chmodSync(argv.saveDir + "/createCluster.sh", "755"); + // TODO: find a way to actually make live updates work + // or point the user to the URL to watch live. + // ex. https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/coder/details?project=kubernetes-cluster-302420 + // we have all the info console.log("\n⏳ Creating your cluster. This will take a few minutes..."); try { const subprocess = execa("/bin/sh", [argv.saveDir + "/createCluster.sh"]); subprocess.stdout.pipe(process.stdout); const { stdout } = await subprocess; + // TODO: consolidate the spacers console.log("------------"); console.log( "✅", @@ -375,9 +393,9 @@ export async function cli(args) { // TODO: add checks to ensure the user has a cluster, // and it has the necessary stuff for Coder console.log( - "This script does not currently verify that you cluster is ready for Coder yet.\n\nWe recommend checking the docs before continuing:" + "This script does not currently verify that your cluster is ready for Coder.\n\nWe recommend checking the docs before continuing:" ); - console.log("\t➡️ https://coder.com/docs/setup/requirements"); + console.log("\t➡️ https://coder.com/docs/setup/requirements\n"); if (!argv.skipConfirmPrompts) { const runCommand = await inquirer.prompt({ @@ -406,7 +424,7 @@ export async function cli(args) { message: "What type of domain would you like to use?", choices: [ { - name: `A free domain from Coder (ex. [myname].${process.env.CLOUDFLARE_DOMAIN})`, + name: `A free domain from Coder (ex. [myname].${cloudflareDomain})`, value: "auto", }, { @@ -420,13 +438,40 @@ export async function cli(args) { ], })), }; + } else { + console.log("------------"); } // validate domainType if (argv.domainType == "auto") { - console.log("AUTOMA"); + // check if we have the cloudflare token + if (!process.env.DOMAIN_TOKEN) { + console.log( + "\n🔒 At this time, you need a special token from a Coder rep to get a subdomain\n" + + "For more info, join our Slack Community: https://cdr.co/join-community" + ); + return; + } + + // sha256 validate the token + // used for verifying domain token + var sha256 = require("js-sha256"); + + // verify the token + // TODO: potentially do this server-side so that expired tokens + // don't get improperly verified on an old local version + if ( + sha256(process.env.DOMAIN_TOKEN) != + "7d3eb96148c592b64ddfb4f3038a329acc22ea94669780dfa9de85b768ed27b1" + ) { + console.log("\n❌ The domain token you supplied is not valid."); + return; + } + + // hello } else if (argv.domainType == "cloud-dns") { console.log("Well, this is coming soon 💀"); + return 0; } else if (argv.domainType == "none") { console.log( "\nWarning: This means you can't use Coder with DevURLs, a primary way of accessing web services\ninside of a Coder Workspace:\n", @@ -435,7 +480,7 @@ export async function cli(args) { ); console.log( - "You can always add a domain later, and use a custom provider via our docs." + "You can always add a domain later, and use a custom provider via our docs.\n" ); // TODO: add confirmations @@ -447,5 +492,7 @@ export async function cli(args) { // install and access Coder + // TODO: tell the user they can save this to a PRIVATE + // repo in GIT (maybe idk if that is bad practice) console.log("\n\nat the end with a long argv:", Object.keys(argv).length); } From 8bbea9e82e0c796d62a3f12aa1d8cc1b97bdff7e Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 00:59:46 -0400 Subject: [PATCH 08/40] full launch capabilities --- config-store/update-coder.sh | 20 ++ shell-helpers/getAdminPassword.sh | 3 + shell-helpers/googleCloudDefaultProject.sh | 1 + shell-helpers/googleCloudPrereqs.sh | 1 + shell-helpers/googleCloudProjects.sh | 1 + shell-helpers/installCertManager.sh | 8 + src/cli.js | 315 +++++++++++++++++++-- 7 files changed, 323 insertions(+), 26 deletions(-) create mode 100644 config-store/update-coder.sh create mode 100755 shell-helpers/getAdminPassword.sh create mode 100755 shell-helpers/installCertManager.sh diff --git a/config-store/update-coder.sh b/config-store/update-coder.sh new file mode 100644 index 0000000..99d2605 --- /dev/null +++ b/config-store/update-coder.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# allow namespace creation to fail, assume it already exists +kubectl create namespace INJECT_NAMESPACE || true + +# set context to the namespace +kubectl config set-context --current --namespace=INJECT_NAMESPACE + +# add out issuer +kubectl apply -f INJECT_SAVEDIR/issuer.yaml + +# add/update the coder helm chart +helm repo update +helm repo add coder https://helm.coder.com + +# install/upgrade coder (warning: this will ignore any manually-set values outside of INJECT_SAVEDIR/values.yaml) +helm upgrade --namespace INJECT_NAMESPACE --install --atomic --wait coder coder/coder --values INJECT_SAVEDIR/values.yaml + +# kind of lazy. sleep to make sure everything is ready for the CLI to continue +sleep 10 \ No newline at end of file diff --git a/shell-helpers/getAdminPassword.sh b/shell-helpers/getAdminPassword.sh new file mode 100755 index 0000000..3d6a6cf --- /dev/null +++ b/shell-helpers/getAdminPassword.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +kubectl logs -n coder -l coder.deployment=cemanager -c cemanager --tail=-1 | grep -A1 -B2 Password \ No newline at end of file diff --git a/shell-helpers/googleCloudDefaultProject.sh b/shell-helpers/googleCloudDefaultProject.sh index 20d33e4..afa3ca7 100755 --- a/shell-helpers/googleCloudDefaultProject.sh +++ b/shell-helpers/googleCloudDefaultProject.sh @@ -1 +1,2 @@ +#!/bin/sh gcloud config list --format 'value(core.project)' 2>/dev/null \ No newline at end of file diff --git a/shell-helpers/googleCloudPrereqs.sh b/shell-helpers/googleCloudPrereqs.sh index bec8d37..2b69302 100755 --- a/shell-helpers/googleCloudPrereqs.sh +++ b/shell-helpers/googleCloudPrereqs.sh @@ -1,3 +1,4 @@ +#!/bin/sh # Check prereqs. This can potentially search for specific version #s down the road. if ! [ -x "$(command -v git)" ]; then diff --git a/shell-helpers/googleCloudProjects.sh b/shell-helpers/googleCloudProjects.sh index 2e68eca..1b55787 100755 --- a/shell-helpers/googleCloudProjects.sh +++ b/shell-helpers/googleCloudProjects.sh @@ -1 +1,2 @@ +#!/bin/sh gcloud projects list --format json \ No newline at end of file diff --git a/shell-helpers/installCertManager.sh b/shell-helpers/installCertManager.sh new file mode 100755 index 0000000..adf1fd0 --- /dev/null +++ b/shell-helpers/installCertManager.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# Install cert-manager +kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.1/cert-manager.yaml >> /dev/null + +# Verify it is installed +sleep 10 +kubectl get pods -n cert-manager --field-selector=status.phase=Running --no-headers | wc -l \ No newline at end of file diff --git a/src/cli.js b/src/cli.js index c4a92b2..03e652a 100644 --- a/src/cli.js +++ b/src/cli.js @@ -5,12 +5,17 @@ import { exit } from "yargs"; const { hideBin } = require("yargs/helpers"); const execa = require("execa"); -// reading and writing in out/ folder -const fs = require("fs"); +// reading and writing in scripts for the user +const fs = require("fs").promises; -// TODO change this +// API calls +const axios = require("axios").default; + +// this data isn't exactly confidental, but is necessary for the program to run +// TODO: make this accessible via an external endpoint or something const cloudflareEmail = "me@bpmct.net"; const cloudflareDomain = "coding.pics"; +const cloudflareZone = "d8a2eda8c28877a96a209af791f739c8"; require("dotenv").config(); @@ -29,6 +34,25 @@ const runHelperScript = async (filename, params) => { } }; +const createProjectDir = async (saveDir) => { + // TODO: create different folders for each session + console.log( + "💾 FYI: Scripts & config are being saved in: " + + saveDir + + "\nfor future use\n" + ); + + // create our out/ file to hold our creation script, among other things + await execa("mkdir", ["-p", saveDir]).catch((err) => { + console.log(err); + }); + + // git init (or re-init so the user can easily source-control) + await execa("git", ["init", saveDir]); + + return true; +}; + const generateGoogleClusterCommand = (argv) => { // TODO: omit zone if it is intentionally left blank to support regional clusters // note: this will involve modifying other gcloud commands that mention --zone @@ -178,6 +202,12 @@ export async function cli(args) { }; } + // switch to the absolute path of the home directory if the user included ~/ + if (argv.saveDir.startsWith("~/")) { + const userHome = require("os").homedir(); + argv.saveDir = argv.saveDir.replace("~/", userHome + "/"); + } + if (argv.method == "gcloud") { // ensure gcloud-cli is installed and active @@ -319,31 +349,14 @@ export async function cli(args) { } } - // TODO: create different folders for each session - console.log( - "💾 FYI: All of these scripts are being saved in: " + argv.saveDir + "\n" - ); - - // switch to the absolute path of the home directory if the user included ~/ - if (argv.saveDir.startsWith("~/")) { - const userHome = require("os").homedir(); - argv.saveDir = argv.saveDir.replace("~/", userHome + "/"); - } - - // create our out/ file to hold our creation script, among other things - await execa("mkdir", ["-p", argv.saveDir]).catch((err) => { - console.log(err); - }); - - // git init (or re-init so the user can easily source-control) - await execa("git", ["init", argv.saveDir]); + await createProjectDir(argv.saveDir); // add our lovely script to the out folder - fs.writeFileSync( - argv.saveDir + "/createCluster.sh", + await fs.writeFile( + argv.saveDir + "/create-cluster.sh", "#!/bin/sh\n" + gCloudCommand ); - await fs.chmodSync(argv.saveDir + "/createCluster.sh", "755"); + await fs.chmod(argv.saveDir + "/create-cluster.sh", "755"); // TODO: find a way to actually make live updates work // or point the user to the URL to watch live. @@ -352,7 +365,9 @@ export async function cli(args) { console.log("\n⏳ Creating your cluster. This will take a few minutes..."); try { - const subprocess = execa("/bin/sh", [argv.saveDir + "/createCluster.sh"]); + const subprocess = execa("/bin/sh", [ + argv.saveDir + "/create-cluster.sh", + ]); subprocess.stdout.pipe(process.stdout); const { stdout } = await subprocess; // TODO: consolidate the spacers @@ -468,7 +483,255 @@ export async function cli(args) { return; } - // hello + const validateName = (name) => { + // TODO: possibly add error message here + var regex = new RegExp("^[a-zA-Z]+[a-zA-Z0-9\\-]*$"); + if (!regex.test(name)) { + console.log("❗ Please enter a valid name (ex. `acme-co`)"); + return false; + } + return true; + }; + + // determine which type of domain to use + if (!argv.name) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "input", + name: "name", + message: `Enter a name for your Coder deployment (____.${cloudflareDomain}):`, + validate: validateName, + })), + }; + } else { + validateName(argv.name); + } + const domainName = argv.name + "." + cloudflareDomain; + + // ensure this domain has not been used + try { + const domainSearch = await axios.request({ + method: "GET", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records?name=${encodeURIComponent( + domainName + )}`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + }); + if (domainSearch.data.result.length) { + console.log( + `\nError: The domain ${domainName} has been used before. Use another or contact us at https://cdr.co/join-community` + ); + return; + } + } catch (err) { + console.log(`Error connecting to CloudFlare:`, err); + return; + } + + // create dir for our files + // TODO: make this a bit smarter and only run if method == "k8s" as this is being done in gcloud + await createProjectDir(argv.saveDir); + + // get the base config + let issuer = await fs.readFile( + __dirname + "/../config-store/cloudflare-issuer.yaml", + "utf8" + ); + let helm = await fs.readFile( + __dirname + "/../config-store/helm-values.yaml", + "utf8" + ); + + // add our values to the sample file + // TODO: add validation to all these values + helm = helm.split("INJECT_USER_DOMAIN").join(domainName); + issuer = issuer.split("INJECT_USER_NAMESPACE").join(argv.namespace); + issuer = issuer + .split("INJECT_CLOUDFLARE_API") + .join(process.env.DOMAIN_TOKEN); + issuer = issuer.split("INJECT_USER_EMAIL").join(cloudflareEmail); + issuer = issuer.split("INJECT_USER_DOMAIN").join(domainName); + issuer = issuer.split("INJECT_CLOUDFLARE_EMAIL").join(cloudflareEmail); + + if (issuer.includes("INJECT_") || helm.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the files correctly. An error occured." + ); + return; + } + // write the issuer and helm config to a file with a trailing newline + try { + await fs.writeFile(argv.saveDir + "/issuer.yaml", issuer + "\n"); + await fs.writeFile(argv.saveDir + "/values.yaml", helm + "\n"); + } catch (err) { + console.log("❌ An error occured writing the config files", err); + } + + console.log( + "\n✅ Created the following config files:\n", + "\t📄 issuer.yaml: Configures a LetsEncrypt issuer for our domain\n", + "\t📄 values.yaml: Values for our Coder helm chart, telling it our URL and to point to the issuer\n" + ); + + // TODO: confirm cert-manager exists first + console.log( + "We need need to deploy cert-manager 1.0.1 to work with a domain. If you already have it installed, we can re-deploy harmlessly." + ); + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Deploy cert-manager on your cluster?", + }); + + if (!runCommand.runIt) { + console.log( + `\nCancelled the install. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` + ); + return 0; + } + } + + try { + console.log( + "\n⏳ Installing cert-manager. This will take a couple minutes..." + ); + // TODO: confirm this better. + const checkCertManager = await runHelperScript("installCertManager"); + // remove any weird spaces + const certManagerPods = checkCertManager.split(" ").join(""); + + if (certManagerPods >= 3) console.log("✅", "Installed cert-manager"); + else { + throw "could not detect pods running"; + } + } catch (err) { + console.log("❌", "An error occured installing cert-manager:", err); + return; + } + + let installScript = await fs.readFile( + __dirname + "/../config-store/update-coder.sh", + "utf8" + ); + // add our values to the sample file + // TODO: add validation to all these values + installScript = installScript + .split("INJECT_NAMESPACE") + .join(argv.namespace); + installScript = installScript.split("INJECT_SAVEDIR").join(argv.saveDir); + + // ensure we injected everything OK + if (installScript.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the install script correctly. An error occured." + ); + return; + } + + try { + await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); + await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); + } catch (err) { + console.log("❌ An error occured writing the install script", err); + } + + console.log( + "\n\n✅ Created an install/upgrade script that:\n", + "\t🌎 Deploys our issuer (issuer.yaml)\n", + "\t📊 Adds/updates the Coder helm chart\n", + `\t🚀 Installs/upgrades Coder with our values (values.yaml)\n\n` + + `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` + ); + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command and install Coder?", + }); + + if (!runCommand.runIt) { + console.log( + `\n\nOk :) Feel free to modify the command as needed and run it yourself.` + ); + return; + } + } + + const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); + console.log("------------"); + + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + // TODO: consolidate the spacers + console.log("------------"); + + console.log("\n⏳ Setting up the domain..."); + + // fetch our admin password now, but save it for later + const adminPassword = await runHelperScript("getAdminPassword"); + + const coderIP = await runHelperScript("getCoderIP").catch((err) => { + console.log( + "Error fetching the IP address for your Coder deployment. We can't set up the DNS records :(" + ); + return 1; + }); + + // set up DNS records to point the subdomain to the Coder IP + try { + // add record for root URL + await axios.request({ + method: "POST", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + data: { + type: "A", + name: argv.name, + content: coderIP, + ttl: 1, + proxied: false, + }, + }); + await axios.request({ + method: "POST", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + data: { + type: "A", + name: "*." + argv.name, + content: coderIP, + ttl: 1, + proxied: false, + }, + }); + } catch (err) { + console.log( + "\n\n", + "❌", + "Error setting up this subdomain... For help, contact us at https://cdr.co/join-community" + ); + } + + console.log("wowawiwa"); + + // create our script } else if (argv.domainType == "cloud-dns") { console.log("Well, this is coming soon 💀"); return 0; From eab6a387ab7c2f666c9d5a897a185de3cbd288cf Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 01:03:27 -0400 Subject: [PATCH 09/40] remove old stuff --- domain_config.yaml | 36 ------------------------------------ helm_config.yaml | 36 ------------------------------------ 2 files changed, 72 deletions(-) delete mode 100644 domain_config.yaml delete mode 100644 helm_config.yaml diff --git a/domain_config.yaml b/domain_config.yaml deleted file mode 100644 index ec76643..0000000 --- a/domain_config.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: cloudflare-api-key-secret - namespace: script -type: Opaque -stringData: - api-token: 7-Ai2uaoHWOGjZbERjMuBtgNMBKvO7Btqn4YxVF4 - ---- -apiVersion: cert-manager.io/v1alpha2 -kind: Issuer -metadata: - name: letsencrypt - namespace: script # Your Coder deployment namespace -spec: - acme: - email: victorialslocum@gmail.com - server: "https://acme-v02.api.letsencrypt.org/directory" - privateKeySecretRef: - name: letsencrypt-account-key - solvers: - - dns01: - cloudflare: - email: me@bpmct.net - apiTokenSecretRef: - name: cloudflare-api-token-secret - token: api-token - - # This section denotes which domains to use this issuer for. If you didn't - # limit which zones the API token had access to, you may wish to remove - # this section. - selector: - dnsZones: - # Only use this issuer for the domain example.com and its subdomains. - - 'victoria.coding.pics' diff --git a/helm_config.yaml b/helm_config.yaml deleted file mode 100644 index ec76643..0000000 --- a/helm_config.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: cloudflare-api-key-secret - namespace: script -type: Opaque -stringData: - api-token: 7-Ai2uaoHWOGjZbERjMuBtgNMBKvO7Btqn4YxVF4 - ---- -apiVersion: cert-manager.io/v1alpha2 -kind: Issuer -metadata: - name: letsencrypt - namespace: script # Your Coder deployment namespace -spec: - acme: - email: victorialslocum@gmail.com - server: "https://acme-v02.api.letsencrypt.org/directory" - privateKeySecretRef: - name: letsencrypt-account-key - solvers: - - dns01: - cloudflare: - email: me@bpmct.net - apiTokenSecretRef: - name: cloudflare-api-token-secret - token: api-token - - # This section denotes which domains to use this issuer for. If you didn't - # limit which zones the API token had access to, you may wish to remove - # this section. - selector: - dnsZones: - # Only use this issuer for the domain example.com and its subdomains. - - 'victoria.coding.pics' From d79d841ecc13b0f31e496d85b616e705136f79a3 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 01:04:18 -0400 Subject: [PATCH 10/40] remove more old stuff --- domains.md | 13 -- index.js | 43 ------ kubesail/update.sh | 1 - kubesail/values.yaml | 11 -- manual-cert/index.js | 0 manual-cert/values.yaml | 315 --------------------------------------- update-cloudflare-dns.js | 33 ---- 7 files changed, 416 deletions(-) delete mode 100644 domains.md delete mode 100644 index.js delete mode 100755 kubesail/update.sh delete mode 100644 kubesail/values.yaml delete mode 100644 manual-cert/index.js delete mode 100644 manual-cert/values.yaml delete mode 100644 update-cloudflare-dns.js diff --git a/domains.md b/domains.md deleted file mode 100644 index 504f2bf..0000000 --- a/domains.md +++ /dev/null @@ -1,13 +0,0 @@ -# coder one-click install & domain issuer - -friction points (and potential solutions): - -## create and configure kubernetes cluster - -- terraform -- gcp bash script - -## add domain to utilize Dev URLs - -- kubesail.io -- bens jank script diff --git a/index.js b/index.js deleted file mode 100644 index bea1820..0000000 --- a/index.js +++ /dev/null @@ -1,43 +0,0 @@ -require("dotenv").config(); -const axios = require("axios").default; -const fs = require("fs").promises; -const { exec, spawn, fork, execFile } = require("promisify-child-process"); - -let inject = { - user_domain: `newest.coding.pics`, - // user_email: "victorialslocum@gmail.com", - user_namespace: "script", - cloudflare_email: process.env.CLOUDFLARE_EMAIL, - cloudflare_api: process.env.CLOUDFLARE_TOKEN, -}; - -let generateCommands = async () => { - let issuer = await fs.readFile("config-store/cloudflare-issuer.yaml", "utf8"); - let helm = await fs.readFile("config-store/helm-values.yaml", "utf8"); - for (const [key, value] of Object.entries(inject)) { - // replaces INJECT_KEY with the proper value - issuer = issuer.split("INJECT_" + key.toUpperCase()).join(value); - helm = helm.split("INJECT_" + key.toUpperCase()).join(value); - } - - // write the issuer and helm config to a file with a trailing newline - await fs.writeFile("your_issuer.yaml", issuer + "\n"); - await fs.writeFile("your_values.yaml", helm + "\n"); - - console.log( - "\n\n[auto-coder] Fantastic. You can now deploy Coder with: 👇\n--------" - ); - console.log(`kubectl apply -f your_issuer.yaml`); - console.log( - `helm install coder coder/coder --namespace ${inject.user_namespace} --values your_values.yaml` - ); - const fetchIPData = await spawn(`./shell-helpers/getCoderIP.sh`, { - encoding: "utf8", - }).catch((err) => console.log("no IP yet...")); - - if (fetchIPData) { - console.log(fetchIPData.stdout); - } -}; - -generateCommands(); diff --git a/kubesail/update.sh b/kubesail/update.sh deleted file mode 100755 index 5cbfe46..0000000 --- a/kubesail/update.sh +++ /dev/null @@ -1 +0,0 @@ -helm upgrade --namespace coder --install --atomic --wait coder coder/coder --values values.yaml \ No newline at end of file diff --git a/kubesail/values.yaml b/kubesail/values.yaml deleted file mode 100644 index 7d283b6..0000000 --- a/kubesail/values.yaml +++ /dev/null @@ -1,11 +0,0 @@ -ingress: - useDefault: true - host: "coder.google-cloud.bpmct.ksdns.io" - tls: - enable: true - hostSecretName: coder-root-cert - devurlsHostSecretName: coder-devurls-cert - additionalAnnotations: - - "cert-manager.io/cluster-issuer: letsencrypt" -# devurls: -# host: "*.coder.google-cloud.bpmct.usw1.k8g8.com" diff --git a/manual-cert/index.js b/manual-cert/index.js deleted file mode 100644 index e69de29..0000000 diff --git a/manual-cert/values.yaml b/manual-cert/values.yaml deleted file mode 100644 index 5b10090..0000000 --- a/manual-cert/values.yaml +++ /dev/null @@ -1,315 +0,0 @@ -# storageClassName -- Sets the storage class for all Coder services and user -# environments. By default the storageClassName is not specified and thus the -# default StorageClass is used. If storageClassName is not specified and a -# default StorageClass does not exist, then the deployment will fail. The -# storageClass MUST support the ReadWriteOnce access mode. -storageClassName: "" -# serviceType -- See the following for the different serviceType options and -# their use: -# https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types -serviceType: "ClusterIP" -# podSecurityPolicyName -- The name of the pod security policy to apply to all -# Coder services and user environments. The optional ingress has its own field -# for pod security policy as well. -podSecurityPolicyName: "" -# clusterDomainSuffix -- If you've set a custom default domain for your -# cluster, you may need to remove or change this DNS suffix for service -# resolution to work correctly. -clusterDomainSuffix: ".svc.cluster.local" -# contains configuration for the bundled ingress controller. -ingress: - # ingress.enable -- If set to true a Coder compatable ingress kind will be - # created. You can configure it with `ingress.annotations` below. - enable: true - # ingress.useDefault -- If set to true will deploy an nginx ingress that will - # allow you to access Coder from an external IP address, but if your - # kubernetes cluster is configured to provision external IP addresses. If you - # would like to bring your own ingress and hook Coder into that instead, set - # this value to false. - useDefault: true - # ingress.usePathWildcards -- Whether or not the ingress object should use - # path wildcards, i.e. ending with "/*". Some ingresses require this - # while others do not. You should check which path style your ingress - # requires. For ingress-nginx this should be set to false. - usePathWildcards: false - # ingress.host -- The hostname to use for accessing the platform. This can - # be left blank and the user can still access the platform from the external - # IP or a DNS name that resolves to the external IP address. - host: "" - # ingress.loadBalancerIP sets the external IP address of the ingress to the - # provided value. - loadBalancerIP: "" - # ingress.podSecurityPolicyName -- The name of the pod security policy the - # built in ingress controller should abide. It should be noted that the - # ingress controller requires the `NET_BIND_SERVICE` capability, privilege - # escalation, and access to privileged ports to successfully deploy. Ignored - # if `ingress.useDefault` is false. - podSecurityPolicyName: "" - # ingress.additionalAnnotations -- Deprecated. Please use `ingress.annotations`. - additionalAnnotations: [] - # ingress.annotations -- Additional annotations to be used when creating the - # ingress. These only apply to the Ingress Kubernetes kind. The annotations - # can be used to specify certificate issuers or other cloud provider specific - # integrations. - annotations: {} - # ingress.tls -- TLS options for the ingress. The hosts used for the tls - # configuration come from the ingress.host and the devurls.host variables. If - # those don't exist, then the TLS configuration will be ignored. - tls: - # ingress.tls.enable -- Enables the tls configuration. - enable: false - # ingress.tls.hostSecretName -- The secret to use for the ingress.host - # hostname. - hostSecretName: "" - # ingress.tls.devurlsHostSecretName -- The secret to use for the - # devurls.host hostname. - devurlsHostSecretName: "" - # ingress.service -- Options related to the ingress Kubernetes Service object. - service: - # ingress.service.annotations -- Additional annotations to add to the Service - # object. For example to make the ingress spawn an internal load balancer: - # annotations: - # cloud.google.com/load-balancer-type: "Internal" - annotations: {} -devurls: - # devurls.host -- Should be a wildcard hostname to allow matching against - # custom-created dev URLs. Leaving as an empty string results in devurls - # being disabled. Example: "*.devurls.coder.com". - host: "" -# Contains fields related to the Postgres backend. If providing your own -# instance, a minimum version of Postgres 11 is required with the contrib -# package installed. -postgres: - # postgres.useDefault -- Deploys an internal Postgres instance alongside the platform. - # It is not recommended to run the internal Postgres instance in production. - # If true, all other values are ignored. - useDefault: true - # postgres.host -- The host of the external postgres instance. - host: "" - # postgres.port -- The port of the external postgres instance. - port: "" - # postgres.user -- the user of the external postgres instance. - user: "" - # postgres.database -- The name of the database that coder will use. It must - # exist before Coder is installed. - database: "" - # postgres.passwordSecret -- The name of an existing secret in the current - # namespace with the password to the Postgres instance. The password must be - # contained in the secret field `password`. This should be set to an empty - # string if the database does not require a password to connect. - passwordSecret: "" - # postgres.sslMode -- Determines how the connection is made to the database. - # The acceptable values are: `disable`, `allow`, `prefer`, `require`, - # `verify-ca`, and `verify-full`. - sslMode: "require" -# imagePullPolicy -- Sets the policy for pulling a container image across all -# services. -imagePullPolicy: Always -# Contains configuration the REST API handling CRUD operations to -# the platform. -cemanager: - # cemanager.accessURL -- The cemanager access URL that the envproxy will use - # to communicate with the cemanager. This should be a full URL complete with - # protocol and no trailing slash. Uses internal cluster URL if not set. - # e.g. https://manager.coder.com - accessURL: "" - # cemanager.replicas -- The number of replicas to run of the manager. - replicas: 1 - # cemanager.image -- Injected during releases. - image: docker.io/coderenvs/coder-service:1.17.1 - # cemanager.resources -- Kubernetes resource request and limits for cemanager - # pods. - # To unset a value, set it to "". - # To unset all values, you can provide a values.yaml file which sets resources - # to nil. See values.yaml for an example. - # - # e.g: - # cemanager: - # # This will cause all values to be unset. - # resources: - # replica: 1 - resources: - requests: - cpu: "250m" - memory: "512Mi" - limits: - cpu: "250m" - memory: "512Mi" -# Contains the user interface of the platform. -dashboard: - # dashboard.replicas -- The number of replicas to run of the dashboard. - replicas: 1 - # dashboard.image -- Injected during releases. - image: docker.io/coderenvs/dashboard:1.17.1 - # dashboard.resources -- Kubernetes resource request and limits for dasboard - # pods. - # To unset a value, set it to "". - # To unset all values, you can provide a values.yaml file which sets resources - # to nil. See values.yaml for an example. - # - # e.g: - # dashboard: - # # This will cause all values to be unset. - # resources: - # replica: 1 - resources: - requests: - cpu: "250m" - memory: "512Mi" - limits: - cpu: "250m" - memory: "512Mi" -# envproxy contains configuration for the service handling long-lived -# connections to environments such as IDE or shell sessions. -envproxy: - # envproxy.accessURL -- The URL reported to cemanager. Must be accessible by - # cemanager and all users who can use this workspace provider. This should be - # a full URL, complete with protocol and trailing "/proxy" (no trailing - # slash). This is derived from the ingress.host or the access URL set during - # cemanager setup if not set. - # e.g. "https://proxy.coder.com/proxy" - accessURL: "" - # envproxy.clusterAddress -- The address of the K8s cluster, must be reachable - # from the cemanager. Defaults to - # "https://kubernetes.default.$clusterDomainSuffix:443" if not set. - clusterAddress: "" - # envproxy.replicas -- The number of replicas to run of the envproxy. - replicas: 1 - # envproxy.image -- Injected during releases. - image: docker.io/coderenvs/coder-service:1.17.1 - # envproxy.resources -- Kubernetes resource request and limits for envproxy - # pods. - # To unset a value, set it to "". - # To unset all values, you can provide a values.yaml file which sets resources - # to nil. See values.yaml for an example. - # - # e.g: - # envproxy: - # # This will cause all values to be unset. - # resources: - # replica: 1 - resources: - requests: - cpu: "250m" - memory: "512Mi" - limits: - cpu: "250m" - memory: "512Mi" - # envproxy.terminationGracePeriodSeconds -- Amount of seconds to wait before - # shutting down the environment proxy if there are still open connections. - # This is set very long intentionally so developers do not deal with - # disconnects during deployments. - terminationGracePeriodSeconds: 14400 -# timescale -- Contains configuration for the internal database. It is not -# recommended to run this service in production. See the `postgres` section for -# connecting to an external Postgres database. -timescale: - # timescale.image -- Injected during releases. - image: docker.io/coderenvs/timescale:1.17.1 - # timescale.resources -- Kubernetes resource request and limits for the - # timescale pod. - # To unset a value, set it to "". - # To unset all values, you can provide a values.yaml file which sets resources - # to nil. See values.yaml for an example. - # - # e.g: - # timescale: - # # This will cause all values to be unset. - # resources: - resources: - requests: - cpu: "250m" - memory: "1Gi" - # timescale.resources.requests.storage -- Specifies the size of the - # volume claim for persisting the database. - storage: "10Gi" - limits: - cpu: "250m" - memory: "1Gi" -envbox: - # envbox.image -- Injected during releases. - image: docker.io/coderenvs/envbox:1.17.1 -envbuilder: - # envbuilder.image -- Injected during releases. - image: docker.io/coderenvs/envbuilder:1.17.1 -# environments defines configuration that is applied to all -# user environments. -environments: - # environments.tolerations -- Tolerations are applied to all user - # environments. Each element is a regular pod toleration object. To set - # service tolerations see serviceTolerations. See values.yaml for an example. - # - # e.g. - # tolerations: - # - key: "my key" - # operator: "Exists" - # effect: "NoExecute" - # tolerationSeconds: 6000 - tolerations: [] - # environments.nodeSelector -- nodeSelector is applied to all user - # environments to specify eligible nodes for environments to run on. - # See: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector - # - # eg. - # nodeSelector: - # disktype: ssd - nodeSelector: {} -# certs -- Describes CAs that should be added to Coder services. These certs -# are NOT added to environments. -certs: - secret: - # certs.secret.name -- The name of the secret. - name: "" - # certs.secret.key -- The key in the secret pointing to the certificate - # bundle. - key: "" -# namespaceWhitelist -- DEPRECATED: A list of additional namespaces that -# environments may be deploy to. Use multiple workspace providers instead. -# Existing environments within these namespaces will continue to function, but -# new environments cannot be created within these namespaces. -# -# If you used this value before 1.17, you must keep it set to the same value -# until all environments within these namespaces are deleted, then you can set -# it back to []. -namespaceWhitelist: [] -ssh: - # ssh.enable -- Enables accessing environments via SSH. - enable: true -# serviceTolerations -- Tolerations are applied to all Coder managed services. -# Each element is a toleration object. To set user environment tolerations see -# environments.tolerations. See values.yaml for an example. -# -# e.g. -# serviceTolerations: -# - key: "my key" -# operator: "Exists" -# effect: "NoExecute" -# tolerationSeconds: 6000 -serviceTolerations: [] -# logging configures the logging format of Coder services. Specify one -# or more targets for logging output. If you have multiple output -# targets configured, coder will write output to multiple targets. -logging: - # logging.human -- Where to send logs that are formatted for readability by a - # human. Set to an empty string to disable. - human: /dev/stderr - # logging.stackdriver -- Where to send logs that are formatted for Google - # Stackdriver. Set to an empty string to disable. - stackdriver: "" - # logging.json -- Where to send logs that are formatted as JSON. Set to an - # empty string to disable. - json: "" - # logging.splunk -- Coder can send logs directly to Splunk, in - # addition to file-based output, if these values are configured. The - # channel is optional, and this logging is disabled if either the - # URL and Token are set to the empty string. - splunk: - # logging.splunk.url -- The Splunk HEC collector endpoint. - url: "" - # logging.splunk.token -- The Splunk HEC collector token. - token: "" - # logging.splunk_channel -- Optional. Specify the channel that you'd - # like to associate with all messages. - channel: "" -deploymentAnnotations: {} - diff --git a/update-cloudflare-dns.js b/update-cloudflare-dns.js deleted file mode 100644 index 7cdc5aa..0000000 --- a/update-cloudflare-dns.js +++ /dev/null @@ -1,33 +0,0 @@ -require("dotenv").config(); -const axios = require("axios").default; - -let ip = "34.67.78.1"; - -let domain = "newest.coding.pics"; - -// d8a2eda8c28877a96a209af791f739c8 - -var options = { - method: "POST", - url: `https://api.cloudflare.com/client/v4/zones/${process.env.CLOUDFLARE_ZONE_ID}/dns_records`, - headers: { - Authorization: `Bearer ${process.env.CLOUDFLARE_TOKEN}`, - "Content-Type": "application/json", - }, - data: { - type: "A", - name: domain, - content: ip, - ttl: 1, - proxied: false, - }, -}; - -axios - .request(options) - .then(function (response) { - console.log(response.data.result); - }) - .catch(function (error) { - console.error(error); - }); From 16ae0131da23cac313f9f639d52c3488b0b04525 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 01:05:40 -0400 Subject: [PATCH 11/40] install axios --- package-lock.json | 13 +++++++++++++ package.json | 1 + 2 files changed, 14 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0441619..730973a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,6 +85,14 @@ "picomatch": "^2.0.4" } }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -473,6 +481,11 @@ "locate-path": "^3.0.0" } }, + "follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", diff --git a/package.json b/package.json index 533abc6..0dd5bb6 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "nodemon": "^2.0.7" }, "dependencies": { + "axios": "^0.21.1", "dotenv": "^8.2.0", "esm": "^3.2.25", "execa": "^5.0.0", From 31d5fc4eb7315c07fcf6449775bf18ee5f49753b Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 01:34:57 -0400 Subject: [PATCH 12/40] admin password stuff :) --- src/cli.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 03e652a..d4e42ea 100644 --- a/src/cli.js +++ b/src/cli.js @@ -729,7 +729,17 @@ export async function cli(args) { ); } - console.log("wowawiwa"); + console.log( + "\n\n🎉 Coder has been installed! Log in at https://" + domainName + ); + if (adminPassword == "") { + // TODO: auto reset it? + console.log( + "\nWe couldn't find your admin password. See the docs on how to reset it: https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" + ); + } else { + console.log(adminPassword); + } // create our script } else if (argv.domainType == "cloud-dns") { From a9a89cfdfbaa6f2ffa264e593a1db0101e22547d Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 01:59:57 -0400 Subject: [PATCH 13/40] potentially allow installing from IP --- config-store/update-coder-no-domain.sh | 17 +++++ config-store/update-coder.sh | 2 +- src/cli.js | 101 ++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 config-store/update-coder-no-domain.sh diff --git a/config-store/update-coder-no-domain.sh b/config-store/update-coder-no-domain.sh new file mode 100644 index 0000000..797b431 --- /dev/null +++ b/config-store/update-coder-no-domain.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# allow namespace creation to fail, assume it already exists +kubectl create namespace INJECT_NAMESPACE || true + +# set context to the namespace +kubectl config set-context --current --namespace=INJECT_NAMESPACE + +# add/update the coder helm chart +helm repo update +helm repo add coder https://helm.coder.com + +# install/upgrade coder (warning: this will ignore any manually-set values) +helm upgrade --namespace INJECT_NAMESPACE --install --atomic --wait coder coder/coder + +# kind of lazy. sleep to make sure everything is ready for the CLI to continue +sleep 10 \ No newline at end of file diff --git a/config-store/update-coder.sh b/config-store/update-coder.sh index 99d2605..45a9a28 100644 --- a/config-store/update-coder.sh +++ b/config-store/update-coder.sh @@ -6,7 +6,7 @@ kubectl create namespace INJECT_NAMESPACE || true # set context to the namespace kubectl config set-context --current --namespace=INJECT_NAMESPACE -# add out issuer +# add our issuer kubectl apply -f INJECT_SAVEDIR/issuer.yaml # add/update the coder helm chart diff --git a/src/cli.js b/src/cli.js index d4e42ea..9ff3de3 100644 --- a/src/cli.js +++ b/src/cli.js @@ -312,6 +312,7 @@ export async function cli(args) { ) { pricing_info = "This cluster will cost you roughly $40-120/mo to run on Google Cloud depending on usage." + + "\nQuestions about cluster size? Ask at https://cdr.co/join-community" + "\n\nNote: this is just an estimate, we recommend researching yourself and monitoring billing:"; } else { pricing_info = @@ -443,9 +444,13 @@ export async function cli(args) { value: "auto", }, { - name: "A domain name I own on Google CloudDNS", + name: "A domain name I own on Google CloudDNS (Coming soon)", value: "cloud-dns", }, + { + name: "A domain name I own on CloudFlare (Coming soon)", + value: "cloudflare", + }, { name: "Do not set up a domain for now", value: "none", @@ -679,7 +684,7 @@ export async function cli(args) { console.log("\n⏳ Setting up the domain..."); // fetch our admin password now, but save it for later - const adminPassword = await runHelperScript("getAdminPassword"); + const loginDetails = await runHelperScript("getAdminPassword"); const coderIP = await runHelperScript("getCoderIP").catch((err) => { console.log( @@ -732,30 +737,108 @@ export async function cli(args) { console.log( "\n\n🎉 Coder has been installed! Log in at https://" + domainName ); - if (adminPassword == "") { - // TODO: auto reset it? + if (loginDetails == "") { + // TODO: allow the user to reset from here console.log( - "\nWe couldn't find your admin password. See the docs on how to reset it: https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" + "\nWe couldn't find your admin password. See the docs on how to reset it: \n\t➡️ https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" ); } else { - console.log(adminPassword); + console.log(loginDetails); } // create our script - } else if (argv.domainType == "cloud-dns") { - console.log("Well, this is coming soon 💀"); + } else if ( + argv.domainType == "cloud-dns" || + argv.domainType == "cloudflare" + ) { + console.log( + "This is coming soon. For support doing this, join the community: https;//cdr.co/join-community" + ); return 0; } else if (argv.domainType == "none") { console.log( "\nWarning: This means you can't use Coder with DevURLs, a primary way of accessing web services\ninside of a Coder Workspace:\n", "\t📄 Docs: https://coder.com/docs/environments/devurls\n", - "\t🌎 Alternative: https://ngrok.com/docs (you can nstall this in your images)\n\n" + "\t🌎 Alternative: https://ngrok.com/docs (you can install this in your images)\n\n" ); console.log( "You can always add a domain later, and use a custom provider via our docs.\n" ); + // TODO: definitely fix me!! + // very sad repeated code :( + // i wanted 2 working options + let installScript = await fs.readFile( + __dirname + "/../config-store/update-no-domain.sh", + "utf8" + ); + + // ensure we injected everything OK + if (installScript.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the install script correctly. An error occured." + ); + return; + } + + try { + await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); + await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); + } catch (err) { + console.log("❌ An error occured writing the install script", err); + } + + console.log( + "\n\n✅ Created an install/upgrade script that:\n", + "\t📊 Adds/updates the Coder helm chart\n", + `\t🚀 Installs/upgrades Coder\n\n` + + `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` + ); + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command and install Coder?", + }); + + if (!runCommand.runIt) { + console.log( + `\n\nOk :) Feel free to modify the command as needed and run it yourself.` + ); + return; + } + } + + const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); + console.log("------------"); + + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + // TODO: consolidate the spacers + console.log("------------"); + + // fetch our admin password now + const loginDetails = await runHelperScript("getAdminPassword"); + + const coderIP = await runHelperScript("getCoderIP").catch((err) => { + console.log("Error fetching the IP address for your Coder deployment."); + return 1; + }); + + console.log("\n\n🎉 Coder has been installed! Log in at http://" + coderIP); + if (loginDetails == "") { + // TODO: auto reset it? + console.log( + "\nWe couldn't find your admin password. See the docs on how to reset it: https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" + ); + } else { + console.log(loginDetails); + } + // TODO: add confirmations } else { // TODO: standardize these From 677c2f1729eb41fe0214dc864eb11a2cf70ea91d Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:01:16 -0400 Subject: [PATCH 14/40] add readme --- src/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/README.md diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..fccdd5a --- /dev/null +++ b/src/README.md @@ -0,0 +1,9 @@ +# launch-coder + +Preferred environment: Google Cloud Shell + +How to use: + +```sh +launch-coder --help +``` From 47c3a6de5d443b30dde208a201b868123297ead2 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:01:27 -0400 Subject: [PATCH 15/40] format readme --- src/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/README.md b/src/README.md index fccdd5a..2ab6e77 100644 --- a/src/README.md +++ b/src/README.md @@ -2,7 +2,7 @@ Preferred environment: Google Cloud Shell -How to use: +## How to use ```sh launch-coder --help From c0b026ab6d061cfc8aa07db562b12654b05a5993 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:01:51 -0400 Subject: [PATCH 16/40] publicize --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0dd5bb6..f6fcf5f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "launch-coder": "bin/launch-coder" }, "publishConfig": { - "access": "restricted" + "access": "public" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", @@ -29,4 +29,4 @@ "yargs": "^16.2.0", "yargs-interactive": "^3.0.0" } -} +} \ No newline at end of file From dbc04369bfced7dabe35dd5dc99be293a0d111ed Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:18:14 -0400 Subject: [PATCH 17/40] add readme and package info --- package.json | 15 +++++++++++---- src/README.md | 31 ++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f6fcf5f..1e39156 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,8 @@ { - "name": "auto-coder", + "name": "@bpmct/launch-coder", "version": "1.0.0", - "description": "", "main": "src/index.js", "bin": { - "@bpmct/launch-coder": "bin/launch-coder", "launch-coder": "bin/launch-coder" }, "publishConfig": { @@ -28,5 +26,14 @@ "promisify-child-process": "^4.1.1", "yargs": "^16.2.0", "yargs-interactive": "^3.0.0" - } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/bpmct/auto-coder.git" + }, + "bugs": { + "url": "https://github.com/bpmct/auto-coder/issues" + }, + "homepage": "https://github.com/bpmct/auto-coder#readme", + "description": "" } \ No newline at end of file diff --git a/src/README.md b/src/README.md index 2ab6e77..0ca6b73 100644 --- a/src/README.md +++ b/src/README.md @@ -1,9 +1,38 @@ # launch-coder +⚠️: This was a hackathon project and is not recommended for production use or in sensative environments. + +--- + +Launch [Coder](https://coder.com) in a simple way. It can: + +- Create the recommended Google Cloud Cluster for you +- Install Coder with an automatic domain name ([yourname].coding.pics) + Preferred environment: Google Cloud Shell ## How to use +No need to install: + ```sh -launch-coder --help +# For a guided install: +npx @bpmct/launch-coder + +# See all commands: +npx @bpmct/launch-coder --help ``` + +## Install on your machine + +```sh + +npm i -g launch-coder + +launch-coder + +``` + +launch-coder will not install or provision anything without your permission :) + +Questions? Join Slack [https://cdr.co/join-community](https://cdr.co/join-community) From 29793602ad06b2eae3e306d618d9f811d64100ef Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:19:06 -0400 Subject: [PATCH 18/40] upgrade --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e39156..1ded9c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bpmct/launch-coder", - "version": "1.0.0", + "version": "1.0.1", "main": "src/index.js", "bin": { "launch-coder": "bin/launch-coder" From 226d11c204e39fc7f2d6fd145ee4f2c30fa9cfbd Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:19:53 -0400 Subject: [PATCH 19/40] bring readme to root level and re release --- src/README.md => README.md | 0 package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/README.md => README.md (100%) diff --git a/src/README.md b/README.md similarity index 100% rename from src/README.md rename to README.md diff --git a/package.json b/package.json index 1ded9c5..fb32705 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bpmct/launch-coder", - "version": "1.0.1", + "version": "1.0.2", "main": "src/index.js", "bin": { "launch-coder": "bin/launch-coder" From 090fc3fa5d79b0bcd6f60c907364f11c5ffddbdd Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:23:01 -0400 Subject: [PATCH 20/40] add ci --- .github/workflows/npm-publish.yml | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/npm-publish.yml diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..61c1838 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,33 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages + +name: Node.js Package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + - run: npm ci + - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} From 076d60aef709ca45ff148dace4a867b798fe65ee Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:24:00 -0400 Subject: [PATCH 21/40] improve formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ca6b73..ad63ca0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Launch [Coder](https://coder.com) in a simple way. It can: - Create the recommended Google Cloud Cluster for you -- Install Coder with an automatic domain name ([yourname].coding.pics) +- Install Coder with an automatic domain name: `[yourname].coding.pics` Preferred environment: Google Cloud Shell From e5047bf4e60ec8f75a9e652dedb1bcc8d7e3ffc3 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:25:58 -0400 Subject: [PATCH 22/40] do not test --- .github/workflows/npm-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 61c1838..b875164 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -6,6 +6,7 @@ name: Node.js Package on: release: types: [created] + workflow_dispatch: jobs: build: @@ -16,7 +17,6 @@ jobs: with: node-version: 12 - run: npm ci - - run: npm test publish-npm: needs: build From ea61cdebf3247c09254bac81260f8328e09910b9 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:33:25 -0400 Subject: [PATCH 23/40] fix bad filename --- src/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 9ff3de3..4c7baa4 100644 --- a/src/cli.js +++ b/src/cli.js @@ -770,7 +770,7 @@ export async function cli(args) { // very sad repeated code :( // i wanted 2 working options let installScript = await fs.readFile( - __dirname + "/../config-store/update-no-domain.sh", + __dirname + "/../config-store/update-coder-no-domain.sh", "utf8" ); From 7e933abfe301e9d779dd5aeb03b7ec6ec95da104 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 29 Mar 2021 06:40:45 +0000 Subject: [PATCH 24/40] fix ip-based install --- package-lock.json | 1978 ++++++++++++++++++++++++++++++++++++++++++++- src/cli.js | 1455 ++++++++++++++++----------------- 2 files changed, 2707 insertions(+), 726 deletions(-) diff --git a/package-lock.json b/package-lock.json index 730973a..fe6fa2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,1984 @@ { "name": "auto-coder", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "auto-coder", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^0.21.1", + "dotenv": "^8.2.0", + "esm": "^3.2.25", + "execa": "^5.0.0", + "js-sha256": "^0.9.0", + "promisify-child-process": "^4.1.1", + "yargs": "^16.2.0", + "yargs-interactive": "^3.0.0" + }, + "bin": { + "launch-coder": "bin/launch-coder" + }, + "devDependencies": { + "nodemon": "^2.0.7" + } + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "dev": true, + "dependencies": { + "string-width": "^3.0.0" + } + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "node_modules/chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", + "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", + "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "dev": true, + "dependencies": { + "ini": "1.3.7" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "dev": true + }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "dev": true, + "dependencies": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/nodemon": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", + "integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/promisify-child-process": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/promisify-child-process/-/promisify-child-process-4.1.1.tgz", + "integrity": "sha512-/sRjHZwoXf1rJ+8s4oWjYjGRVKNK1DUnqfRC1Zek18pl0cN6k3yJ1cCbqd0tWNe4h0Gr+SY4vR42N33+T82WkA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", + "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "dev": true, + "dependencies": { + "debug": "^2.2.0" + } + }, + "node_modules/undefsafe/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/undefsafe/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "dev": true, + "dependencies": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", + "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-interactive": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/yargs-interactive/-/yargs-interactive-3.0.0.tgz", + "integrity": "sha512-yJWGjwt/PkX8uJ6/u928RQtsTIWUbS8H9jYMvLXdrFmWmmTY781SXrZKgW/VF1H8ViOtYst4OeXX0UVTmjA1wg==", + "dependencies": { + "inquirer": "^7.0.0", + "yargs": "^14.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/yargs-interactive/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/yargs-interactive/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/yargs-interactive/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/yargs-interactive/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/yargs-interactive/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs-interactive/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs-interactive/node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "node_modules/yargs-interactive/node_modules/yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "dependencies": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "node_modules/yargs-interactive/node_modules/yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "engines": { + "node": ">=10" + } + } + }, "dependencies": { "@sindresorhus/is": { "version": "0.14.0", diff --git a/src/cli.js b/src/cli.js index 4c7baa4..ce34e57 100644 --- a/src/cli.js +++ b/src/cli.js @@ -20,43 +20,43 @@ const cloudflareZone = "d8a2eda8c28877a96a209af791f739c8"; require("dotenv").config(); const runHelperScript = async (filename, params) => { - try { - let run = await execa("/bin/sh", [ - __dirname + `/../shell-helpers/${filename}.sh`, - ]); + try { + let run = await execa("/bin/sh", [ + __dirname + `/../shell-helpers/${filename}.sh`, + ]); - if (run && run.stdout) { - return run.stdout; + if (run && run.stdout) { + return run.stdout; + } + } catch (err) { + throw err; + return; } - } catch (err) { - throw err; - return; - } }; const createProjectDir = async (saveDir) => { - // TODO: create different folders for each session - console.log( - "💾 FYI: Scripts & config are being saved in: " + - saveDir + - "\nfor future use\n" - ); - - // create our out/ file to hold our creation script, among other things - await execa("mkdir", ["-p", saveDir]).catch((err) => { - console.log(err); - }); - - // git init (or re-init so the user can easily source-control) - await execa("git", ["init", saveDir]); - - return true; + // TODO: create different folders for each session + console.log( + "💾 FYI: Scripts & config are being saved in: " + + saveDir + + "\nfor future use\n" + ); + + // create our out/ file to hold our creation script, among other things + await execa("mkdir", ["-p", saveDir]).catch((err) => { + console.log(err); + }); + + // git init (or re-init so the user can easily source-control) + await execa("git", ["init", saveDir]); + + return true; }; const generateGoogleClusterCommand = (argv) => { - // TODO: omit zone if it is intentionally left blank to support regional clusters - // note: this will involve modifying other gcloud commands that mention --zone - return `gcloud beta container --project "${argv.gcloudProjectId}" \\ + // TODO: omit zone if it is intentionally left blank to support regional clusters + // note: this will involve modifying other gcloud commands that mention --zone + return `gcloud beta container --project "${argv.gcloudProjectId}" \\ clusters create "${argv.gcloudClusterName}" \\ --zone "${argv.gcloudClusterZone}" \\ --no-enable-basic-auth \\ @@ -73,14 +73,14 @@ const generateGoogleClusterCommand = (argv) => { --enable-ip-alias \\ --network "projects/${argv.gcloudProjectId}/global/networks/default" \\ --subnetwork "projects/${argv.gcloudProjectId}/regions/${ - argv.gcloudClusterRegion - }/subnetworks/default" \\ + argv.gcloudClusterRegion + }/subnetworks/default" \\ --default-max-pods-per-node "110" \\ --addons HorizontalPodAutoscaling,HttpLoadBalancing \\ --enable-autoupgrade \\ --enable-autorepair \\${ - argv.gcloudClusterPreemptible ? "\n --preemptible \\" : "" - } + argv.gcloudClusterPreemptible ? "\n --preemptible \\" : "" + } --enable-network-policy \\ --enable-autoscaling \\ --min-nodes "${argv.gcloudClusterMinNodes}" \\ @@ -88,767 +88,772 @@ const generateGoogleClusterCommand = (argv) => { }; export async function cli(args) { - let argv = yargs(hideBin(args)) - .option("method", { - alias: "m", - type: "string", - description: "Method for deploying Coder (gcloud, general-k8s)", - }) - .option("save-dir", { - alias: "f", - type: "string", - default: "~/.config/launch-coder", - description: "Path to save config files", - }) - .option("domainType", { - alias: "d", - type: "string", - description: "Domain for the Coder Deployment (auto, custom)", - }) - .option("token", { - type: "string", - description: "API token for CloudFlare", - }) - .option("domainName", { - type: "string", - description: "[Manual-only] Your custom domain for Coder", - }) - .option("name", { - type: "string", - alias: "n", - description: "Name for Coder subdomain", - }) - .option("namespace", { - type: "string", - default: "coder", - description: "Namespace for Coder", - }) - .option("gcloud-project-id", { - type: "string", - }) - .option("gcloud-cluster-name", { - type: "string", - default: "coder", - }) - .option("gcloud-cluster-region", { - type: "string", - default: "us-central1", - }) - .option("gcloud-cluster-zone", { - type: "string", - default: "us-central1-a", - }) - .option("gcloud-cluster-machine-type", { - type: "string", - default: "e2-highmem-4", - }) - .option("gcloud-cluster-preemptible", { - type: "boolean", - default: true, - }) - .option("gcloud-cluster-autoscaling", { - type: "boolean", - default: true, - }) - .option("gcloud-cluster-min-nodes", { - type: "number", - default: 1, - }) - .option("gcloud-cluster-max-nodes", { - type: "number", - default: 3, - }) - .option("skip-confirm-prompts", { - type: "boolean", - }).argv; - - // detect if we are on google cloud :) - - const checkCloudShell = await runHelperScript("detectCloudShell"); - - if (!argv.method && checkCloudShell && checkCloudShell == "true") { - console.log("It looks like you are using Google Cloud Shell 🚀"); - - const gcloudCheck = await inquirer.prompt({ - type: "confirm", - default: true, - name: "confirm", - message: "Do you want to deploy a new Coder cluster?", - }); - - if (gcloudCheck.confirm) { - argv.method = "gcloud"; - } - } - - if (argv.method == undefined) { - argv = { - ...argv, - ...(await inquirer.prompt({ - type: "list", - name: "method", - message: "Where would you like to deploy Coder?", - choices: [ - { - name: `Create a fresh Google Cloud cluster for me and install Coder`, - value: "gcloud", - }, - { - name: "Install Coder on my current cluster", - value: "k8s", - }, - ], - })), - }; - } - - // switch to the absolute path of the home directory if the user included ~/ - if (argv.saveDir.startsWith("~/")) { - const userHome = require("os").homedir(); - argv.saveDir = argv.saveDir.replace("~/", userHome + "/"); - } - - if (argv.method == "gcloud") { - // ensure gcloud-cli is installed and active - - // TODO: add better user education on what the prereqs are - try { - await runHelperScript("googleCloudPrereqs"); - console.log("✅", "You seem to have all the dependencies installed!"); - } catch (err) { - console.log("❌", err.stderr); - return; - } - - if (!argv.gcloudProjectId) { - let defaultProject = false; - let projects = []; - - // try to get the default project - try { - const listOfProjects = await runHelperScript( - "googleCloudDefaultProject" - ); - - defaultProject = await runHelperScript("googleCloudDefaultProject"); - const projectsJson = await runHelperScript("googleCloudProjects"); - projects = JSON.parse(projectsJson).map((project) => { - return project.projectId; + let argv = yargs(hideBin(args)) + .option("method", { + alias: "m", + type: "string", + description: "Method for deploying Coder (gcloud, general-k8s)", + }) + .option("save-dir", { + alias: "f", + type: "string", + default: "~/.config/launch-coder", + description: "Path to save config files", + }) + .option("domainType", { + alias: "d", + type: "string", + description: "Domain for the Coder Deployment (auto, custom)", + }) + .option("token", { + type: "string", + description: "API token for CloudFlare", + }) + .option("domainName", { + type: "string", + description: "[Manual-only] Your custom domain for Coder", + }) + .option("name", { + type: "string", + alias: "n", + description: "Name for Coder subdomain", + }) + .option("namespace", { + type: "string", + default: "coder", + description: "Namespace for Coder", + }) + .option("gcloud-project-id", { + type: "string", + }) + .option("gcloud-cluster-name", { + type: "string", + default: "coder", + }) + .option("gcloud-cluster-region", { + type: "string", + default: "us-central1", + }) + .option("gcloud-cluster-zone", { + type: "string", + default: "us-central1-a", + }) + .option("gcloud-cluster-machine-type", { + type: "string", + default: "e2-highmem-4", + }) + .option("gcloud-cluster-preemptible", { + type: "boolean", + default: true, + }) + .option("gcloud-cluster-autoscaling", { + type: "boolean", + default: true, + }) + .option("gcloud-cluster-min-nodes", { + type: "number", + default: 1, + }) + .option("gcloud-cluster-max-nodes", { + type: "number", + default: 3, + }) + .option("skip-confirm-prompts", { + type: "boolean", + }).argv; + + // detect if we are on google cloud :) + + const checkCloudShell = await runHelperScript("detectCloudShell"); + + if (!argv.method && checkCloudShell && checkCloudShell == "true") { + console.log("It looks like you are using Google Cloud Shell 🚀"); + + const gcloudCheck = await inquirer.prompt({ + type: "confirm", + default: true, + name: "confirm", + message: "Do you want to deploy a new Coder cluster?", }); - // ensure we are actually fetching IDs - if (projects[0] == undefined) { - throw "could not read project ID"; + if (gcloudCheck.confirm) { + argv.method = "gcloud"; } + } - console.log("📄 Got a list of your Google Cloud projects!\n"); - } catch (err) { - // reset projects list - projects = []; - } - - // show a select field if we found a list - if (projects.length) { - argv = { - ...argv, - ...(await inquirer.prompt({ - type: "list", - name: "gcloudProjectId", - default: defaultProject, - message: `Google Cloud Project:`, - validate: (that) => { - // TODO: validate this project actually exists - return that != ""; - }, - choices: projects, - })), - }; - } else { - console.log( - "🤔 We couldn't determine if you have any Google Cloud Projects.\n", - "\t➡️ Create one here: https://console.cloud.google.com/projectcreate" - ); + if (argv.method == undefined) { argv = { - ...argv, - ...(await inquirer.prompt({ - type: "input", - name: "gcloudProjectId", - default: undefined, - message: `Google Cloud Project:`, - validate: (that) => { - // TODO: validate this project actually exists - return that != ""; - }, - choices: [ - { - name: `Create a fresh Google Cloud cluster for me and install Coder`, - value: "gcloud", - }, - { - name: "Install Coder on my current cluster (sketchy)", - value: "k8s", - }, - ], - })), + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "method", + message: "Where would you like to deploy Coder?", + choices: [ + { + name: `Create a fresh Google Cloud cluster for me and install Coder`, + value: "gcloud", + }, + { + name: "Install Coder on my current cluster", + value: "k8s", + }, + ], + })), }; - } } - let gCloudCommand = generateGoogleClusterCommand(argv); - - // TODO: add info on what this cluster means - - // TODO: impliment pricing calculations with Google API - let pricing_info = ""; - - if ( - argv.gcloudClusterRegion == "us-central1" && - argv.gcloudClusterZone == "us-central1-a" && - argv.gcloudClusterMachineType == "e2-highmem-4" && - argv.gcloudClusterMinNodes == "1" && - argv.gcloudClusterMaxNodes == "3" && - argv.gcloudClusterAutoscaling && - argv.gcloudClusterPreemptible - ) { - pricing_info = - "This cluster will cost you roughly $40-120/mo to run on Google Cloud depending on usage." + - "\nQuestions about cluster size? Ask at https://cdr.co/join-community" + - "\n\nNote: this is just an estimate, we recommend researching yourself and monitoring billing:"; - } else { - pricing_info = - "You are not using default settings. Be sure to calculate the pricing info for your cluster"; + // switch to the absolute path of the home directory if the user included ~/ + if (argv.saveDir.startsWith("~/")) { + const userHome = require("os").homedir(); + argv.saveDir = argv.saveDir.replace("~/", userHome + "/"); } - console.log( - "\n💻 Your command is:", - "\n------------\n", - - gCloudCommand, - "\n------------", - "\n\n💵 " + pricing_info + "\n", - "\t➡️ GKE Pricing: https://cloud.google.com/kubernetes-engine/pricing\n", - "\t➡️ Storage pricing: https://cloud.google.com/compute/disks-image-pricing\n", - "\t➡️ Machine pricing: https://cloud.google.com/compute/all-pricing\n\n", - "\t➡️ or use the Google Cloud Pricing Calculator: https://cloud.google.com/products/calculator\n", - "\n------------" - ); - // TODO: impliment ability to edit cluster command in the cli (wohoo) + if (argv.method == "gcloud") { + // ensure gcloud-cli is installed and active + + // TODO: add better user education on what the prereqs are + try { + await runHelperScript("googleCloudPrereqs"); + console.log("✅", "You seem to have all the dependencies installed!"); + } catch (err) { + console.log("❌", err.stderr); + return; + } - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Do you want to run this command?", - }); + if (!argv.gcloudProjectId) { + let defaultProject = false; + let projects = []; + + // try to get the default project + try { + const listOfProjects = await runHelperScript( + "googleCloudDefaultProject" + ); + + defaultProject = await runHelperScript("googleCloudDefaultProject"); + const projectsJson = await runHelperScript("googleCloudProjects"); + projects = JSON.parse(projectsJson).map((project) => { + return project.projectId; + }); + + // ensure we are actually fetching IDs + if (projects[0] == undefined) { + throw "could not read project ID"; + } + + console.log("📄 Got a list of your Google Cloud projects!\n"); + } catch (err) { + // reset projects list + projects = []; + } + + // show a select field if we found a list + if (projects.length) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "gcloudProjectId", + default: defaultProject, + message: `Google Cloud Project:`, + validate: (that) => { + // TODO: validate this project actually exists + return that != ""; + }, + choices: projects, + })), + }; + } else { + console.log( + "🤔 We couldn't determine if you have any Google Cloud Projects.\n", + "\t➡️ Create one here: https://console.cloud.google.com/projectcreate" + ); + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "input", + name: "gcloudProjectId", + default: undefined, + message: `Google Cloud Project:`, + validate: (that) => { + // TODO: validate this project actually exists + return that != ""; + }, + choices: [ + { + name: `Create a fresh Google Cloud cluster for me and install Coder`, + value: "gcloud", + }, + { + name: "Install Coder on my current cluster (sketchy)", + value: "k8s", + }, + ], + })), + }; + } + } - if (!runCommand.runIt) { + let gCloudCommand = generateGoogleClusterCommand(argv); + + // TODO: add info on what this cluster means + + // TODO: impliment pricing calculations with Google API + let pricing_info = ""; + + if ( + argv.gcloudClusterRegion == "us-central1" && + argv.gcloudClusterZone == "us-central1-a" && + argv.gcloudClusterMachineType == "e2-highmem-4" && + argv.gcloudClusterMinNodes == "1" && + argv.gcloudClusterMaxNodes == "3" && + argv.gcloudClusterAutoscaling && + argv.gcloudClusterPreemptible + ) { + pricing_info = + "This cluster will cost you roughly $40-120/mo to run on Google Cloud depending on usage." + + "\nQuestions about cluster size? Ask at https://cdr.co/join-community" + + "\n\nNote: this is just an estimate, we recommend researching yourself and monitoring billing:"; + } else { + pricing_info = + "You are not using default settings. Be sure to calculate the pricing info for your cluster"; + } console.log( - `\n\nOk :) Feel free to modify the command as needed, run it yourself, then you can run "launch-coder --method k8s" to install Coder on the cluster you manually created` + "\n💻 Your command is:", + "\n------------\n", + + gCloudCommand, + "\n------------", + "\n\n💵 " + pricing_info + "\n", + "\t➡️ GKE Pricing: https://cloud.google.com/kubernetes-engine/pricing\n", + "\t➡️ Storage pricing: https://cloud.google.com/compute/disks-image-pricing\n", + "\t➡️ Machine pricing: https://cloud.google.com/compute/all-pricing\n\n", + "\t➡️ or use the Google Cloud Pricing Calculator: https://cloud.google.com/products/calculator\n", + "\n------------" ); - return; - } - } - await createProjectDir(argv.saveDir); + // TODO: impliment ability to edit cluster command in the cli (wohoo) + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command?", + }); + + if (!runCommand.runIt) { + console.log( + `\n\nOk :) Feel free to modify the command as needed, run it yourself, then you can run "launch-coder --method k8s" to install Coder on the cluster you manually created` + ); + return; + } + } - // add our lovely script to the out folder - await fs.writeFile( - argv.saveDir + "/create-cluster.sh", - "#!/bin/sh\n" + gCloudCommand - ); - await fs.chmod(argv.saveDir + "/create-cluster.sh", "755"); + await createProjectDir(argv.saveDir); - // TODO: find a way to actually make live updates work - // or point the user to the URL to watch live. - // ex. https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/coder/details?project=kubernetes-cluster-302420 - // we have all the info - console.log("\n⏳ Creating your cluster. This will take a few minutes..."); + // add our lovely script to the out folder + await fs.writeFile( + argv.saveDir + "/create-cluster.sh", + "#!/bin/sh\n" + gCloudCommand + ); + await fs.chmod(argv.saveDir + "/create-cluster.sh", "755"); + + // TODO: find a way to actually make live updates work + // or point the user to the URL to watch live. + // ex. https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/coder/details?project=kubernetes-cluster-302420 + // we have all the info + console.log("\n⏳ Creating your cluster. This will take a few minutes..."); + + try { + const subprocess = execa("/bin/sh", [ + argv.saveDir + "/create-cluster.sh", + ]); + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + // TODO: consolidate the spacers + console.log("------------"); + console.log( + "✅", + `Cluster "${argv.gcloudClusterName}" has been created!` + ); + } catch (err) { + console.log("❌", "Process failed\n\n\n", err.stderr); + return; + } - try { - const subprocess = execa("/bin/sh", [ - argv.saveDir + "/create-cluster.sh", - ]); - subprocess.stdout.pipe(process.stdout); - const { stdout } = await subprocess; - // TODO: consolidate the spacers - console.log("------------"); - console.log( - "✅", - `Cluster "${argv.gcloudClusterName}" has been created!` - ); - } catch (err) { - console.log("❌", "Process failed\n\n\n", err.stderr); - return; - } + try { + await execa( + "gcloud", + `container clusters get-credentials ${argv.gcloudClusterName} --zone ${argv.gcloudClusterZone}`.split( + " " + ) + ); + console.log("✅", "Added to kube context"); + } catch (err) { + console.log("❌", "Unable to add to kube context:\n\n\n", err.stderr); + return; + } - try { - await execa( - "gcloud", - `container clusters get-credentials ${argv.gcloudClusterName} --zone ${argv.gcloudClusterZone}`.split( - " " - ) - ); - console.log("✅", "Added to kube context"); - } catch (err) { - console.log("❌", "Unable to add to kube context:\n\n\n", err.stderr); - return; + // So now we can move on to installing Coder! } - // So now we can move on to installing Coder! - } - - // if argv.method == "gcloud" at this point - // the script has succeeded in creating the cluster - // and switched context - if (argv.method != "k8s" && argv.method != "gcloud") { - // TODO: standardize these - console.error("Error. Unknown method: " + argv.method); - return; - } else if (argv.method == "k8s") { - // TODO: add checks to ensure the user has a cluster, - // and it has the necessary stuff for Coder - console.log( - "This script does not currently verify that your cluster is ready for Coder.\n\nWe recommend checking the docs before continuing:" - ); - console.log("\t➡️ https://coder.com/docs/setup/requirements\n"); - - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Do you to proceed?", - }); - - if (!runCommand.runIt) { + // if argv.method == "gcloud" at this point + // the script has succeeded in creating the cluster + // and switched context + if (argv.method != "k8s" && argv.method != "gcloud") { + // TODO: standardize these + console.error("Error. Unknown method: " + argv.method); + return; + } else if (argv.method == "k8s") { + // TODO: add checks to ensure the user has a cluster, + // and it has the necessary stuff for Coder console.log( - `\nExited. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` + "This script does not currently verify that your cluster is ready for Coder.\n\nWe recommend checking the docs before continuing:" ); - return 0; - } - } - } - - // determine which type of domain to use - if (!argv.domainType) { - argv = { - ...argv, - ...(await inquirer.prompt({ - type: "list", - name: "domainType", - message: "What type of domain would you like to use?", - choices: [ - { - name: `A free domain from Coder (ex. [myname].${cloudflareDomain})`, - value: "auto", - }, - { - name: "A domain name I own on Google CloudDNS (Coming soon)", - value: "cloud-dns", - }, - { - name: "A domain name I own on CloudFlare (Coming soon)", - value: "cloudflare", - }, - { - name: "Do not set up a domain for now", - value: "none", - }, - ], - })), - }; - } else { - console.log("------------"); - } - - // validate domainType - if (argv.domainType == "auto") { - // check if we have the cloudflare token - if (!process.env.DOMAIN_TOKEN) { - console.log( - "\n🔒 At this time, you need a special token from a Coder rep to get a subdomain\n" + - "For more info, join our Slack Community: https://cdr.co/join-community" - ); - return; - } - - // sha256 validate the token - // used for verifying domain token - var sha256 = require("js-sha256"); - - // verify the token - // TODO: potentially do this server-side so that expired tokens - // don't get improperly verified on an old local version - if ( - sha256(process.env.DOMAIN_TOKEN) != - "7d3eb96148c592b64ddfb4f3038a329acc22ea94669780dfa9de85b768ed27b1" - ) { - console.log("\n❌ The domain token you supplied is not valid."); - return; + console.log("\t➡️ https://coder.com/docs/setup/requirements\n"); + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you to proceed?", + }); + + if (!runCommand.runIt) { + console.log( + `\nExited. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` + ); + return 0; + } + } } - const validateName = (name) => { - // TODO: possibly add error message here - var regex = new RegExp("^[a-zA-Z]+[a-zA-Z0-9\\-]*$"); - if (!regex.test(name)) { - console.log("❗ Please enter a valid name (ex. `acme-co`)"); - return false; - } - return true; - }; - // determine which type of domain to use - if (!argv.name) { - argv = { - ...argv, - ...(await inquirer.prompt({ - type: "input", - name: "name", - message: `Enter a name for your Coder deployment (____.${cloudflareDomain}):`, - validate: validateName, - })), - }; + if (!argv.domainType) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "domainType", + message: "What type of domain would you like to use?", + choices: [ + { + name: `A free domain from Coder (ex. [myname].${cloudflareDomain})`, + value: "auto", + }, + { + name: "A domain name I own on Google CloudDNS (Coming soon)", + value: "cloud-dns", + }, + { + name: "A domain name I own on CloudFlare (Coming soon)", + value: "cloudflare", + }, + { + name: "Do not set up a domain for now", + value: "none", + }, + ], + })), + }; } else { - validateName(argv.name); + console.log("------------"); } - const domainName = argv.name + "." + cloudflareDomain; - // ensure this domain has not been used - try { - const domainSearch = await axios.request({ - method: "GET", - url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records?name=${encodeURIComponent( - domainName - )}`, - headers: { - Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, - "Content-Type": "application/json", - }, - }); - if (domainSearch.data.result.length) { - console.log( - `\nError: The domain ${domainName} has been used before. Use another or contact us at https://cdr.co/join-community` - ); - return; - } - } catch (err) { - console.log(`Error connecting to CloudFlare:`, err); - return; - } + // validate domainType + if (argv.domainType == "auto") { + // check if we have the cloudflare token + if (!process.env.DOMAIN_TOKEN) { + console.log( + "\n🔒 At this time, you need a special token from a Coder rep to get a subdomain\n" + + "For more info, join our Slack Community: https://cdr.co/join-community" + ); + return; + } - // create dir for our files - // TODO: make this a bit smarter and only run if method == "k8s" as this is being done in gcloud - await createProjectDir(argv.saveDir); + // sha256 validate the token + // used for verifying domain token + var sha256 = require("js-sha256"); + + // verify the token + // TODO: potentially do this server-side so that expired tokens + // don't get improperly verified on an old local version + if ( + sha256(process.env.DOMAIN_TOKEN) != + "7d3eb96148c592b64ddfb4f3038a329acc22ea94669780dfa9de85b768ed27b1" + ) { + console.log("\n❌ The domain token you supplied is not valid."); + return; + } - // get the base config - let issuer = await fs.readFile( - __dirname + "/../config-store/cloudflare-issuer.yaml", - "utf8" - ); - let helm = await fs.readFile( - __dirname + "/../config-store/helm-values.yaml", - "utf8" - ); + const validateName = (name) => { + // TODO: possibly add error message here + var regex = new RegExp("^[a-zA-Z]+[a-zA-Z0-9\\-]*$"); + if (!regex.test(name)) { + console.log("❗ Please enter a valid name (ex. `acme-co`)"); + return false; + } + return true; + }; - // add our values to the sample file - // TODO: add validation to all these values - helm = helm.split("INJECT_USER_DOMAIN").join(domainName); - issuer = issuer.split("INJECT_USER_NAMESPACE").join(argv.namespace); - issuer = issuer - .split("INJECT_CLOUDFLARE_API") - .join(process.env.DOMAIN_TOKEN); - issuer = issuer.split("INJECT_USER_EMAIL").join(cloudflareEmail); - issuer = issuer.split("INJECT_USER_DOMAIN").join(domainName); - issuer = issuer.split("INJECT_CLOUDFLARE_EMAIL").join(cloudflareEmail); - - if (issuer.includes("INJECT_") || helm.includes("INJECT_")) { - console.log( - "❌", - "Information was not injected into the files correctly. An error occured." - ); - return; - } - // write the issuer and helm config to a file with a trailing newline - try { - await fs.writeFile(argv.saveDir + "/issuer.yaml", issuer + "\n"); - await fs.writeFile(argv.saveDir + "/values.yaml", helm + "\n"); - } catch (err) { - console.log("❌ An error occured writing the config files", err); - } + // determine which type of domain to use + if (!argv.name) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "input", + name: "name", + message: `Enter a name for your Coder deployment (____.${cloudflareDomain}):`, + validate: validateName, + })), + }; + } else { + validateName(argv.name); + } + const domainName = argv.name + "." + cloudflareDomain; + + // ensure this domain has not been used + try { + const domainSearch = await axios.request({ + method: "GET", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records?name=${encodeURIComponent( + domainName + )}`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + }); + if (domainSearch.data.result.length) { + console.log( + `\nError: The domain ${domainName} has been used before. Use another or contact us at https://cdr.co/join-community` + ); + return; + } + } catch (err) { + console.log(`Error connecting to CloudFlare:`, err); + return; + } - console.log( - "\n✅ Created the following config files:\n", - "\t📄 issuer.yaml: Configures a LetsEncrypt issuer for our domain\n", - "\t📄 values.yaml: Values for our Coder helm chart, telling it our URL and to point to the issuer\n" - ); + // create dir for our files + // TODO: make this a bit smarter and only run if method == "k8s" as this is being done in gcloud + await createProjectDir(argv.saveDir); - // TODO: confirm cert-manager exists first - console.log( - "We need need to deploy cert-manager 1.0.1 to work with a domain. If you already have it installed, we can re-deploy harmlessly." - ); + // get the base config + let issuer = await fs.readFile( + __dirname + "/../config-store/cloudflare-issuer.yaml", + "utf8" + ); + let helm = await fs.readFile( + __dirname + "/../config-store/helm-values.yaml", + "utf8" + ); - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Deploy cert-manager on your cluster?", - }); + // add our values to the sample file + // TODO: add validation to all these values + helm = helm.split("INJECT_USER_DOMAIN").join(domainName); + issuer = issuer.split("INJECT_USER_NAMESPACE").join(argv.namespace); + issuer = issuer + .split("INJECT_CLOUDFLARE_API") + .join(process.env.DOMAIN_TOKEN); + issuer = issuer.split("INJECT_USER_EMAIL").join(cloudflareEmail); + issuer = issuer.split("INJECT_USER_DOMAIN").join(domainName); + issuer = issuer.split("INJECT_CLOUDFLARE_EMAIL").join(cloudflareEmail); + + if (issuer.includes("INJECT_") || helm.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the files correctly. An error occured." + ); + return; + } + // write the issuer and helm config to a file with a trailing newline + try { + await fs.writeFile(argv.saveDir + "/issuer.yaml", issuer + "\n"); + await fs.writeFile(argv.saveDir + "/values.yaml", helm + "\n"); + } catch (err) { + console.log("❌ An error occured writing the config files", err); + } - if (!runCommand.runIt) { console.log( - `\nCancelled the install. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` + "\n✅ Created the following config files:\n", + "\t📄 issuer.yaml: Configures a LetsEncrypt issuer for our domain\n", + "\t📄 values.yaml: Values for our Coder helm chart, telling it our URL and to point to the issuer\n" ); - return 0; - } - } - try { - console.log( - "\n⏳ Installing cert-manager. This will take a couple minutes..." - ); - // TODO: confirm this better. - const checkCertManager = await runHelperScript("installCertManager"); - // remove any weird spaces - const certManagerPods = checkCertManager.split(" ").join(""); - - if (certManagerPods >= 3) console.log("✅", "Installed cert-manager"); - else { - throw "could not detect pods running"; - } - } catch (err) { - console.log("❌", "An error occured installing cert-manager:", err); - return; - } + // TODO: confirm cert-manager exists first + console.log( + "We need need to deploy cert-manager 1.0.1 to work with a domain. If you already have it installed, we can re-deploy harmlessly." + ); - let installScript = await fs.readFile( - __dirname + "/../config-store/update-coder.sh", - "utf8" - ); - // add our values to the sample file - // TODO: add validation to all these values - installScript = installScript - .split("INJECT_NAMESPACE") - .join(argv.namespace); - installScript = installScript.split("INJECT_SAVEDIR").join(argv.saveDir); - - // ensure we injected everything OK - if (installScript.includes("INJECT_")) { - console.log( - "❌", - "Information was not injected into the install script correctly. An error occured." - ); - return; - } + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Deploy cert-manager on your cluster?", + }); + + if (!runCommand.runIt) { + console.log( + `\nCancelled the install. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` + ); + return 0; + } + } - try { - await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); - await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); - } catch (err) { - console.log("❌ An error occured writing the install script", err); - } + try { + console.log( + "\n⏳ Installing cert-manager. This will take a couple minutes..." + ); + // TODO: confirm this better. + const checkCertManager = await runHelperScript("installCertManager"); + // remove any weird spaces + const certManagerPods = checkCertManager.split(" ").join(""); + + if (certManagerPods >= 3) console.log("✅", "Installed cert-manager"); + else { + throw "could not detect pods running"; + } + } catch (err) { + console.log("❌", "An error occured installing cert-manager:", err); + return; + } - console.log( - "\n\n✅ Created an install/upgrade script that:\n", - "\t🌎 Deploys our issuer (issuer.yaml)\n", - "\t📊 Adds/updates the Coder helm chart\n", - `\t🚀 Installs/upgrades Coder with our values (values.yaml)\n\n` + - `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` - ); + let installScript = await fs.readFile( + __dirname + "/../config-store/update-coder.sh", + "utf8" + ); + // add our values to the sample file + // TODO: add validation to all these values + installScript = installScript + .split("INJECT_NAMESPACE") + .join(argv.namespace); + installScript = installScript.split("INJECT_SAVEDIR").join(argv.saveDir); + + // ensure we injected everything OK + if (installScript.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the install script correctly. An error occured." + ); + return; + } - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Do you want to run this command and install Coder?", - }); + try { + await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); + await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); + } catch (err) { + console.log("❌ An error occured writing the install script", err); + } - if (!runCommand.runIt) { console.log( - `\n\nOk :) Feel free to modify the command as needed and run it yourself.` + "\n\n✅ Created an install/upgrade script that:\n", + "\t🌎 Deploys our issuer (issuer.yaml)\n", + "\t📊 Adds/updates the Coder helm chart\n", + `\t🚀 Installs/upgrades Coder with our values (values.yaml)\n\n` + + `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` ); - return; - } - } - const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); - console.log("------------"); + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command and install Coder?", + }); + + if (!runCommand.runIt) { + console.log( + `\n\nOk :) Feel free to modify the command as needed and run it yourself.` + ); + return; + } + } - subprocess.stdout.pipe(process.stdout); - const { stdout } = await subprocess; - // TODO: consolidate the spacers - console.log("------------"); + const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); + console.log("------------"); - console.log("\n⏳ Setting up the domain..."); + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + // TODO: consolidate the spacers + console.log("------------"); - // fetch our admin password now, but save it for later - const loginDetails = await runHelperScript("getAdminPassword"); + console.log("\n⏳ Setting up the domain..."); - const coderIP = await runHelperScript("getCoderIP").catch((err) => { - console.log( - "Error fetching the IP address for your Coder deployment. We can't set up the DNS records :(" - ); - return 1; - }); - - // set up DNS records to point the subdomain to the Coder IP - try { - // add record for root URL - await axios.request({ - method: "POST", - url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, - headers: { - Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, - "Content-Type": "application/json", - }, - data: { - type: "A", - name: argv.name, - content: coderIP, - ttl: 1, - proxied: false, - }, - }); - await axios.request({ - method: "POST", - url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, - headers: { - Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, - "Content-Type": "application/json", - }, - data: { - type: "A", - name: "*." + argv.name, - content: coderIP, - ttl: 1, - proxied: false, - }, - }); - } catch (err) { - console.log( - "\n\n", - "❌", - "Error setting up this subdomain... For help, contact us at https://cdr.co/join-community" - ); - } + // fetch our admin password now, but save it for later + const loginDetails = await runHelperScript("getAdminPassword"); - console.log( - "\n\n🎉 Coder has been installed! Log in at https://" + domainName - ); - if (loginDetails == "") { - // TODO: allow the user to reset from here - console.log( - "\nWe couldn't find your admin password. See the docs on how to reset it: \n\t➡️ https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" - ); - } else { - console.log(loginDetails); - } + const coderIP = await runHelperScript("getCoderIP").catch((err) => { + console.log( + "Error fetching the IP address for your Coder deployment. We can't set up the DNS records :(" + ); + return 1; + }); - // create our script - } else if ( - argv.domainType == "cloud-dns" || - argv.domainType == "cloudflare" - ) { - console.log( - "This is coming soon. For support doing this, join the community: https;//cdr.co/join-community" - ); - return 0; - } else if (argv.domainType == "none") { - console.log( - "\nWarning: This means you can't use Coder with DevURLs, a primary way of accessing web services\ninside of a Coder Workspace:\n", - "\t📄 Docs: https://coder.com/docs/environments/devurls\n", - "\t🌎 Alternative: https://ngrok.com/docs (you can install this in your images)\n\n" - ); + // set up DNS records to point the subdomain to the Coder IP + try { + // add record for root URL + await axios.request({ + method: "POST", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + data: { + type: "A", + name: argv.name, + content: coderIP, + ttl: 1, + proxied: false, + }, + }); + await axios.request({ + method: "POST", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + data: { + type: "A", + name: "*." + argv.name, + content: coderIP, + ttl: 1, + proxied: false, + }, + }); + } catch (err) { + console.log( + "\n\n", + "❌", + "Error setting up this subdomain... For help, contact us at https://cdr.co/join-community" + ); + } - console.log( - "You can always add a domain later, and use a custom provider via our docs.\n" - ); + console.log( + "\n\n🎉 Coder has been installed! Log in at https://" + domainName + ); + if (loginDetails == "") { + // TODO: allow the user to reset from here + console.log( + "\nWe couldn't find your admin password. See the docs on how to reset it: \n\t➡️ https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" + ); + } else { + console.log(loginDetails); + } - // TODO: definitely fix me!! - // very sad repeated code :( - // i wanted 2 working options - let installScript = await fs.readFile( - __dirname + "/../config-store/update-coder-no-domain.sh", - "utf8" - ); + // create our script + } else if ( + argv.domainType == "cloud-dns" || + argv.domainType == "cloudflare" + ) { + console.log( + "This is coming soon. For support doing this, join the community: https;//cdr.co/join-community" + ); + return 0; + } else if (argv.domainType == "none") { + console.log( + "\nWarning: This means you can't use Coder with DevURLs, a primary way of accessing web services\ninside of a Coder Workspace:\n", + "\t📄 Docs: https://coder.com/docs/environments/devurls\n", + "\t🌎 Alternative: https://ngrok.com/docs (you can install this in your images)\n\n" + ); - // ensure we injected everything OK - if (installScript.includes("INJECT_")) { - console.log( - "❌", - "Information was not injected into the install script correctly. An error occured." - ); - return; - } + console.log( + "You can always add a domain later, and use a custom provider via our docs.\n" + ); - try { - await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); - await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); - } catch (err) { - console.log("❌ An error occured writing the install script", err); - } + // TODO: definitely fix me!! + // very sad repeated code :( + // i wanted 2 working options + let installScript = await fs.readFile( + __dirname + "/../config-store/update-coder-no-domain.sh", + "utf8" + ); - console.log( - "\n\n✅ Created an install/upgrade script that:\n", - "\t📊 Adds/updates the Coder helm chart\n", - `\t🚀 Installs/upgrades Coder\n\n` + - `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` - ); + // inject details + installScript = installScript + .split("INJECT_NAMESPACE") + .join(argv.namespace); + + // ensure we injected everything OK + if (installScript.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the install script correctly. An error occured." + ); + return; + } - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Do you want to run this command and install Coder?", - }); + try { + await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); + await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); + } catch (err) { + console.log("❌ An error occured writing the install script", err); + } - if (!runCommand.runIt) { console.log( - `\n\nOk :) Feel free to modify the command as needed and run it yourself.` + "\n\n✅ Created an install/upgrade script that:\n", + "\t📊 Adds/updates the Coder helm chart\n", + `\t🚀 Installs/upgrades Coder\n\n` + + `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` ); - return; - } - } - const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); - console.log("------------"); + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command and install Coder?", + }); + + if (!runCommand.runIt) { + console.log( + `\n\nOk :) Feel free to modify the command as needed and run it yourself.` + ); + return; + } + } - subprocess.stdout.pipe(process.stdout); - const { stdout } = await subprocess; - // TODO: consolidate the spacers - console.log("------------"); + const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); + console.log("------------"); - // fetch our admin password now - const loginDetails = await runHelperScript("getAdminPassword"); + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + // TODO: consolidate the spacers + console.log("------------"); - const coderIP = await runHelperScript("getCoderIP").catch((err) => { - console.log("Error fetching the IP address for your Coder deployment."); - return 1; - }); + // fetch our admin password now + const loginDetails = await runHelperScript("getAdminPassword"); + + const coderIP = await runHelperScript("getCoderIP").catch((err) => { + console.log("Error fetching the IP address for your Coder deployment."); + return 1; + }); + + console.log("\n\n🎉 Coder has been installed! Log in at http://" + coderIP); + if (loginDetails == "") { + // TODO: auto reset it? + console.log( + "\nWe couldn't find your admin password. See the docs on how to reset it: https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" + ); + } else { + console.log(loginDetails); + } - console.log("\n\n🎉 Coder has been installed! Log in at http://" + coderIP); - if (loginDetails == "") { - // TODO: auto reset it? - console.log( - "\nWe couldn't find your admin password. See the docs on how to reset it: https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" - ); + // TODO: add confirmations } else { - console.log(loginDetails); + // TODO: standardize these + console.error("Error. Unknown domainType: " + argv.domainType); + return; } - // TODO: add confirmations - } else { - // TODO: standardize these - console.error("Error. Unknown domainType: " + argv.domainType); - return; - } - - // install and access Coder + // install and access Coder - // TODO: tell the user they can save this to a PRIVATE - // repo in GIT (maybe idk if that is bad practice) - console.log("\n\nat the end with a long argv:", Object.keys(argv).length); + // TODO: tell the user they can save this to a PRIVATE + // repo in GIT (maybe idk if that is bad practice) + console.log("\n\nat the end with a long argv:", Object.keys(argv).length); } From fe757e76029445bb63a48e7a63a49c761c806ada Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:41:40 -0400 Subject: [PATCH 25/40] fix formatting from cloud shell dev --- src/cli.js | 1460 ++++++++++++++++++++++++++-------------------------- 1 file changed, 730 insertions(+), 730 deletions(-) diff --git a/src/cli.js b/src/cli.js index ce34e57..1c443d4 100644 --- a/src/cli.js +++ b/src/cli.js @@ -20,43 +20,43 @@ const cloudflareZone = "d8a2eda8c28877a96a209af791f739c8"; require("dotenv").config(); const runHelperScript = async (filename, params) => { - try { - let run = await execa("/bin/sh", [ - __dirname + `/../shell-helpers/${filename}.sh`, - ]); + try { + let run = await execa("/bin/sh", [ + __dirname + `/../shell-helpers/${filename}.sh`, + ]); - if (run && run.stdout) { - return run.stdout; - } - } catch (err) { - throw err; - return; + if (run && run.stdout) { + return run.stdout; } + } catch (err) { + throw err; + return; + } }; const createProjectDir = async (saveDir) => { - // TODO: create different folders for each session - console.log( - "💾 FYI: Scripts & config are being saved in: " + - saveDir + - "\nfor future use\n" - ); - - // create our out/ file to hold our creation script, among other things - await execa("mkdir", ["-p", saveDir]).catch((err) => { - console.log(err); - }); - - // git init (or re-init so the user can easily source-control) - await execa("git", ["init", saveDir]); - - return true; + // TODO: create different folders for each session + console.log( + "💾 FYI: Scripts & config are being saved in: " + + saveDir + + "\nfor future use\n" + ); + + // create our out/ file to hold our creation script, among other things + await execa("mkdir", ["-p", saveDir]).catch((err) => { + console.log(err); + }); + + // git init (or re-init so the user can easily source-control) + await execa("git", ["init", saveDir]); + + return true; }; const generateGoogleClusterCommand = (argv) => { - // TODO: omit zone if it is intentionally left blank to support regional clusters - // note: this will involve modifying other gcloud commands that mention --zone - return `gcloud beta container --project "${argv.gcloudProjectId}" \\ + // TODO: omit zone if it is intentionally left blank to support regional clusters + // note: this will involve modifying other gcloud commands that mention --zone + return `gcloud beta container --project "${argv.gcloudProjectId}" \\ clusters create "${argv.gcloudClusterName}" \\ --zone "${argv.gcloudClusterZone}" \\ --no-enable-basic-auth \\ @@ -73,14 +73,14 @@ const generateGoogleClusterCommand = (argv) => { --enable-ip-alias \\ --network "projects/${argv.gcloudProjectId}/global/networks/default" \\ --subnetwork "projects/${argv.gcloudProjectId}/regions/${ - argv.gcloudClusterRegion - }/subnetworks/default" \\ + argv.gcloudClusterRegion + }/subnetworks/default" \\ --default-max-pods-per-node "110" \\ --addons HorizontalPodAutoscaling,HttpLoadBalancing \\ --enable-autoupgrade \\ --enable-autorepair \\${ - argv.gcloudClusterPreemptible ? "\n --preemptible \\" : "" - } + argv.gcloudClusterPreemptible ? "\n --preemptible \\" : "" + } --enable-network-policy \\ --enable-autoscaling \\ --min-nodes "${argv.gcloudClusterMinNodes}" \\ @@ -88,772 +88,772 @@ const generateGoogleClusterCommand = (argv) => { }; export async function cli(args) { - let argv = yargs(hideBin(args)) - .option("method", { - alias: "m", - type: "string", - description: "Method for deploying Coder (gcloud, general-k8s)", - }) - .option("save-dir", { - alias: "f", - type: "string", - default: "~/.config/launch-coder", - description: "Path to save config files", - }) - .option("domainType", { - alias: "d", - type: "string", - description: "Domain for the Coder Deployment (auto, custom)", - }) - .option("token", { - type: "string", - description: "API token for CloudFlare", - }) - .option("domainName", { - type: "string", - description: "[Manual-only] Your custom domain for Coder", - }) - .option("name", { - type: "string", - alias: "n", - description: "Name for Coder subdomain", - }) - .option("namespace", { - type: "string", - default: "coder", - description: "Namespace for Coder", - }) - .option("gcloud-project-id", { - type: "string", - }) - .option("gcloud-cluster-name", { - type: "string", - default: "coder", - }) - .option("gcloud-cluster-region", { - type: "string", - default: "us-central1", - }) - .option("gcloud-cluster-zone", { - type: "string", - default: "us-central1-a", - }) - .option("gcloud-cluster-machine-type", { - type: "string", - default: "e2-highmem-4", - }) - .option("gcloud-cluster-preemptible", { - type: "boolean", - default: true, - }) - .option("gcloud-cluster-autoscaling", { - type: "boolean", - default: true, - }) - .option("gcloud-cluster-min-nodes", { - type: "number", - default: 1, - }) - .option("gcloud-cluster-max-nodes", { - type: "number", - default: 3, - }) - .option("skip-confirm-prompts", { - type: "boolean", - }).argv; - - // detect if we are on google cloud :) - - const checkCloudShell = await runHelperScript("detectCloudShell"); - - if (!argv.method && checkCloudShell && checkCloudShell == "true") { - console.log("It looks like you are using Google Cloud Shell 🚀"); - - const gcloudCheck = await inquirer.prompt({ - type: "confirm", - default: true, - name: "confirm", - message: "Do you want to deploy a new Coder cluster?", + let argv = yargs(hideBin(args)) + .option("method", { + alias: "m", + type: "string", + description: "Method for deploying Coder (gcloud, general-k8s)", + }) + .option("save-dir", { + alias: "f", + type: "string", + default: "~/.config/launch-coder", + description: "Path to save config files", + }) + .option("domainType", { + alias: "d", + type: "string", + description: "Domain for the Coder Deployment (auto, custom)", + }) + .option("token", { + type: "string", + description: "API token for CloudFlare", + }) + .option("domainName", { + type: "string", + description: "[Manual-only] Your custom domain for Coder", + }) + .option("name", { + type: "string", + alias: "n", + description: "Name for Coder subdomain", + }) + .option("namespace", { + type: "string", + default: "coder", + description: "Namespace for Coder", + }) + .option("gcloud-project-id", { + type: "string", + }) + .option("gcloud-cluster-name", { + type: "string", + default: "coder", + }) + .option("gcloud-cluster-region", { + type: "string", + default: "us-central1", + }) + .option("gcloud-cluster-zone", { + type: "string", + default: "us-central1-a", + }) + .option("gcloud-cluster-machine-type", { + type: "string", + default: "e2-highmem-4", + }) + .option("gcloud-cluster-preemptible", { + type: "boolean", + default: true, + }) + .option("gcloud-cluster-autoscaling", { + type: "boolean", + default: true, + }) + .option("gcloud-cluster-min-nodes", { + type: "number", + default: 1, + }) + .option("gcloud-cluster-max-nodes", { + type: "number", + default: 3, + }) + .option("skip-confirm-prompts", { + type: "boolean", + }).argv; + + // detect if we are on google cloud :) + + const checkCloudShell = await runHelperScript("detectCloudShell"); + + if (!argv.method && checkCloudShell && checkCloudShell == "true") { + console.log("It looks like you are using Google Cloud Shell 🚀"); + + const gcloudCheck = await inquirer.prompt({ + type: "confirm", + default: true, + name: "confirm", + message: "Do you want to deploy a new Coder cluster?", + }); + + if (gcloudCheck.confirm) { + argv.method = "gcloud"; + } + } + + if (argv.method == undefined) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "method", + message: "Where would you like to deploy Coder?", + choices: [ + { + name: `Create a fresh Google Cloud cluster for me and install Coder`, + value: "gcloud", + }, + { + name: "Install Coder on my current cluster", + value: "k8s", + }, + ], + })), + }; + } + + // switch to the absolute path of the home directory if the user included ~/ + if (argv.saveDir.startsWith("~/")) { + const userHome = require("os").homedir(); + argv.saveDir = argv.saveDir.replace("~/", userHome + "/"); + } + + if (argv.method == "gcloud") { + // ensure gcloud-cli is installed and active + + // TODO: add better user education on what the prereqs are + try { + await runHelperScript("googleCloudPrereqs"); + console.log("✅", "You seem to have all the dependencies installed!"); + } catch (err) { + console.log("❌", err.stderr); + return; + } + + if (!argv.gcloudProjectId) { + let defaultProject = false; + let projects = []; + + // try to get the default project + try { + const listOfProjects = await runHelperScript( + "googleCloudDefaultProject" + ); + + defaultProject = await runHelperScript("googleCloudDefaultProject"); + const projectsJson = await runHelperScript("googleCloudProjects"); + projects = JSON.parse(projectsJson).map((project) => { + return project.projectId; }); - if (gcloudCheck.confirm) { - argv.method = "gcloud"; + // ensure we are actually fetching IDs + if (projects[0] == undefined) { + throw "could not read project ID"; } - } - if (argv.method == undefined) { + console.log("📄 Got a list of your Google Cloud projects!\n"); + } catch (err) { + // reset projects list + projects = []; + } + + // show a select field if we found a list + if (projects.length) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "gcloudProjectId", + default: defaultProject, + message: `Google Cloud Project:`, + validate: (that) => { + // TODO: validate this project actually exists + return that != ""; + }, + choices: projects, + })), + }; + } else { + console.log( + "🤔 We couldn't determine if you have any Google Cloud Projects.\n", + "\t➡️ Create one here: https://console.cloud.google.com/projectcreate" + ); argv = { - ...argv, - ...(await inquirer.prompt({ - type: "list", - name: "method", - message: "Where would you like to deploy Coder?", - choices: [ - { - name: `Create a fresh Google Cloud cluster for me and install Coder`, - value: "gcloud", - }, - { - name: "Install Coder on my current cluster", - value: "k8s", - }, - ], - })), + ...argv, + ...(await inquirer.prompt({ + type: "input", + name: "gcloudProjectId", + default: undefined, + message: `Google Cloud Project:`, + validate: (that) => { + // TODO: validate this project actually exists + return that != ""; + }, + choices: [ + { + name: `Create a fresh Google Cloud cluster for me and install Coder`, + value: "gcloud", + }, + { + name: "Install Coder on my current cluster (sketchy)", + value: "k8s", + }, + ], + })), }; + } } - // switch to the absolute path of the home directory if the user included ~/ - if (argv.saveDir.startsWith("~/")) { - const userHome = require("os").homedir(); - argv.saveDir = argv.saveDir.replace("~/", userHome + "/"); - } + let gCloudCommand = generateGoogleClusterCommand(argv); - if (argv.method == "gcloud") { - // ensure gcloud-cli is installed and active + // TODO: add info on what this cluster means - // TODO: add better user education on what the prereqs are - try { - await runHelperScript("googleCloudPrereqs"); - console.log("✅", "You seem to have all the dependencies installed!"); - } catch (err) { - console.log("❌", err.stderr); - return; - } + // TODO: impliment pricing calculations with Google API + let pricing_info = ""; - if (!argv.gcloudProjectId) { - let defaultProject = false; - let projects = []; - - // try to get the default project - try { - const listOfProjects = await runHelperScript( - "googleCloudDefaultProject" - ); - - defaultProject = await runHelperScript("googleCloudDefaultProject"); - const projectsJson = await runHelperScript("googleCloudProjects"); - projects = JSON.parse(projectsJson).map((project) => { - return project.projectId; - }); - - // ensure we are actually fetching IDs - if (projects[0] == undefined) { - throw "could not read project ID"; - } - - console.log("📄 Got a list of your Google Cloud projects!\n"); - } catch (err) { - // reset projects list - projects = []; - } - - // show a select field if we found a list - if (projects.length) { - argv = { - ...argv, - ...(await inquirer.prompt({ - type: "list", - name: "gcloudProjectId", - default: defaultProject, - message: `Google Cloud Project:`, - validate: (that) => { - // TODO: validate this project actually exists - return that != ""; - }, - choices: projects, - })), - }; - } else { - console.log( - "🤔 We couldn't determine if you have any Google Cloud Projects.\n", - "\t➡️ Create one here: https://console.cloud.google.com/projectcreate" - ); - argv = { - ...argv, - ...(await inquirer.prompt({ - type: "input", - name: "gcloudProjectId", - default: undefined, - message: `Google Cloud Project:`, - validate: (that) => { - // TODO: validate this project actually exists - return that != ""; - }, - choices: [ - { - name: `Create a fresh Google Cloud cluster for me and install Coder`, - value: "gcloud", - }, - { - name: "Install Coder on my current cluster (sketchy)", - value: "k8s", - }, - ], - })), - }; - } - } + if ( + argv.gcloudClusterRegion == "us-central1" && + argv.gcloudClusterZone == "us-central1-a" && + argv.gcloudClusterMachineType == "e2-highmem-4" && + argv.gcloudClusterMinNodes == "1" && + argv.gcloudClusterMaxNodes == "3" && + argv.gcloudClusterAutoscaling && + argv.gcloudClusterPreemptible + ) { + pricing_info = + "This cluster will cost you roughly $40-120/mo to run on Google Cloud depending on usage." + + "\nQuestions about cluster size? Ask at https://cdr.co/join-community" + + "\n\nNote: this is just an estimate, we recommend researching yourself and monitoring billing:"; + } else { + pricing_info = + "You are not using default settings. Be sure to calculate the pricing info for your cluster"; + } + console.log( + "\n💻 Your command is:", + "\n------------\n", + + gCloudCommand, + "\n------------", + "\n\n💵 " + pricing_info + "\n", + "\t➡️ GKE Pricing: https://cloud.google.com/kubernetes-engine/pricing\n", + "\t➡️ Storage pricing: https://cloud.google.com/compute/disks-image-pricing\n", + "\t➡️ Machine pricing: https://cloud.google.com/compute/all-pricing\n\n", + "\t➡️ or use the Google Cloud Pricing Calculator: https://cloud.google.com/products/calculator\n", + "\n------------" + ); - let gCloudCommand = generateGoogleClusterCommand(argv); - - // TODO: add info on what this cluster means - - // TODO: impliment pricing calculations with Google API - let pricing_info = ""; - - if ( - argv.gcloudClusterRegion == "us-central1" && - argv.gcloudClusterZone == "us-central1-a" && - argv.gcloudClusterMachineType == "e2-highmem-4" && - argv.gcloudClusterMinNodes == "1" && - argv.gcloudClusterMaxNodes == "3" && - argv.gcloudClusterAutoscaling && - argv.gcloudClusterPreemptible - ) { - pricing_info = - "This cluster will cost you roughly $40-120/mo to run on Google Cloud depending on usage." + - "\nQuestions about cluster size? Ask at https://cdr.co/join-community" + - "\n\nNote: this is just an estimate, we recommend researching yourself and monitoring billing:"; - } else { - pricing_info = - "You are not using default settings. Be sure to calculate the pricing info for your cluster"; - } + // TODO: impliment ability to edit cluster command in the cli (wohoo) + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command?", + }); + + if (!runCommand.runIt) { console.log( - "\n💻 Your command is:", - "\n------------\n", - - gCloudCommand, - "\n------------", - "\n\n💵 " + pricing_info + "\n", - "\t➡️ GKE Pricing: https://cloud.google.com/kubernetes-engine/pricing\n", - "\t➡️ Storage pricing: https://cloud.google.com/compute/disks-image-pricing\n", - "\t➡️ Machine pricing: https://cloud.google.com/compute/all-pricing\n\n", - "\t➡️ or use the Google Cloud Pricing Calculator: https://cloud.google.com/products/calculator\n", - "\n------------" + `\n\nOk :) Feel free to modify the command as needed, run it yourself, then you can run "launch-coder --method k8s" to install Coder on the cluster you manually created` ); + return; + } + } - // TODO: impliment ability to edit cluster command in the cli (wohoo) - - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Do you want to run this command?", - }); - - if (!runCommand.runIt) { - console.log( - `\n\nOk :) Feel free to modify the command as needed, run it yourself, then you can run "launch-coder --method k8s" to install Coder on the cluster you manually created` - ); - return; - } - } + await createProjectDir(argv.saveDir); - await createProjectDir(argv.saveDir); + // add our lovely script to the out folder + await fs.writeFile( + argv.saveDir + "/create-cluster.sh", + "#!/bin/sh\n" + gCloudCommand + ); + await fs.chmod(argv.saveDir + "/create-cluster.sh", "755"); - // add our lovely script to the out folder - await fs.writeFile( - argv.saveDir + "/create-cluster.sh", - "#!/bin/sh\n" + gCloudCommand - ); - await fs.chmod(argv.saveDir + "/create-cluster.sh", "755"); - - // TODO: find a way to actually make live updates work - // or point the user to the URL to watch live. - // ex. https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/coder/details?project=kubernetes-cluster-302420 - // we have all the info - console.log("\n⏳ Creating your cluster. This will take a few minutes..."); - - try { - const subprocess = execa("/bin/sh", [ - argv.saveDir + "/create-cluster.sh", - ]); - subprocess.stdout.pipe(process.stdout); - const { stdout } = await subprocess; - // TODO: consolidate the spacers - console.log("------------"); - console.log( - "✅", - `Cluster "${argv.gcloudClusterName}" has been created!` - ); - } catch (err) { - console.log("❌", "Process failed\n\n\n", err.stderr); - return; - } + // TODO: find a way to actually make live updates work + // or point the user to the URL to watch live. + // ex. https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/coder/details?project=kubernetes-cluster-302420 + // we have all the info + console.log("\n⏳ Creating your cluster. This will take a few minutes..."); - try { - await execa( - "gcloud", - `container clusters get-credentials ${argv.gcloudClusterName} --zone ${argv.gcloudClusterZone}`.split( - " " - ) - ); - console.log("✅", "Added to kube context"); - } catch (err) { - console.log("❌", "Unable to add to kube context:\n\n\n", err.stderr); - return; - } + try { + const subprocess = execa("/bin/sh", [ + argv.saveDir + "/create-cluster.sh", + ]); + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + // TODO: consolidate the spacers + console.log("------------"); + console.log( + "✅", + `Cluster "${argv.gcloudClusterName}" has been created!` + ); + } catch (err) { + console.log("❌", "Process failed\n\n\n", err.stderr); + return; + } - // So now we can move on to installing Coder! + try { + await execa( + "gcloud", + `container clusters get-credentials ${argv.gcloudClusterName} --zone ${argv.gcloudClusterZone}`.split( + " " + ) + ); + console.log("✅", "Added to kube context"); + } catch (err) { + console.log("❌", "Unable to add to kube context:\n\n\n", err.stderr); + return; } - // if argv.method == "gcloud" at this point - // the script has succeeded in creating the cluster - // and switched context - if (argv.method != "k8s" && argv.method != "gcloud") { - // TODO: standardize these - console.error("Error. Unknown method: " + argv.method); - return; - } else if (argv.method == "k8s") { - // TODO: add checks to ensure the user has a cluster, - // and it has the necessary stuff for Coder + // So now we can move on to installing Coder! + } + + // if argv.method == "gcloud" at this point + // the script has succeeded in creating the cluster + // and switched context + if (argv.method != "k8s" && argv.method != "gcloud") { + // TODO: standardize these + console.error("Error. Unknown method: " + argv.method); + return; + } else if (argv.method == "k8s") { + // TODO: add checks to ensure the user has a cluster, + // and it has the necessary stuff for Coder + console.log( + "This script does not currently verify that your cluster is ready for Coder.\n\nWe recommend checking the docs before continuing:" + ); + console.log("\t➡️ https://coder.com/docs/setup/requirements\n"); + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you to proceed?", + }); + + if (!runCommand.runIt) { console.log( - "This script does not currently verify that your cluster is ready for Coder.\n\nWe recommend checking the docs before continuing:" + `\nExited. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` ); - console.log("\t➡️ https://coder.com/docs/setup/requirements\n"); - - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Do you to proceed?", - }); - - if (!runCommand.runIt) { - console.log( - `\nExited. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` - ); - return 0; - } - } + return 0; + } + } + } + + // determine which type of domain to use + if (!argv.domainType) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "list", + name: "domainType", + message: "What type of domain would you like to use?", + choices: [ + { + name: `A free domain from Coder (ex. [myname].${cloudflareDomain})`, + value: "auto", + }, + { + name: "A domain name I own on Google CloudDNS (Coming soon)", + value: "cloud-dns", + }, + { + name: "A domain name I own on CloudFlare (Coming soon)", + value: "cloudflare", + }, + { + name: "Do not set up a domain for now", + value: "none", + }, + ], + })), + }; + } else { + console.log("------------"); + } + + // validate domainType + if (argv.domainType == "auto") { + // check if we have the cloudflare token + if (!process.env.DOMAIN_TOKEN) { + console.log( + "\n🔒 At this time, you need a special token from a Coder rep to get a subdomain\n" + + "For more info, join our Slack Community: https://cdr.co/join-community" + ); + return; + } + + // sha256 validate the token + // used for verifying domain token + var sha256 = require("js-sha256"); + + // verify the token + // TODO: potentially do this server-side so that expired tokens + // don't get improperly verified on an old local version + if ( + sha256(process.env.DOMAIN_TOKEN) != + "7d3eb96148c592b64ddfb4f3038a329acc22ea94669780dfa9de85b768ed27b1" + ) { + console.log("\n❌ The domain token you supplied is not valid."); + return; } + const validateName = (name) => { + // TODO: possibly add error message here + var regex = new RegExp("^[a-zA-Z]+[a-zA-Z0-9\\-]*$"); + if (!regex.test(name)) { + console.log("❗ Please enter a valid name (ex. `acme-co`)"); + return false; + } + return true; + }; + // determine which type of domain to use - if (!argv.domainType) { - argv = { - ...argv, - ...(await inquirer.prompt({ - type: "list", - name: "domainType", - message: "What type of domain would you like to use?", - choices: [ - { - name: `A free domain from Coder (ex. [myname].${cloudflareDomain})`, - value: "auto", - }, - { - name: "A domain name I own on Google CloudDNS (Coming soon)", - value: "cloud-dns", - }, - { - name: "A domain name I own on CloudFlare (Coming soon)", - value: "cloudflare", - }, - { - name: "Do not set up a domain for now", - value: "none", - }, - ], - })), - }; + if (!argv.name) { + argv = { + ...argv, + ...(await inquirer.prompt({ + type: "input", + name: "name", + message: `Enter a name for your Coder deployment (____.${cloudflareDomain}):`, + validate: validateName, + })), + }; } else { - console.log("------------"); + validateName(argv.name); } + const domainName = argv.name + "." + cloudflareDomain; - // validate domainType - if (argv.domainType == "auto") { - // check if we have the cloudflare token - if (!process.env.DOMAIN_TOKEN) { - console.log( - "\n🔒 At this time, you need a special token from a Coder rep to get a subdomain\n" + - "For more info, join our Slack Community: https://cdr.co/join-community" - ); - return; - } + // ensure this domain has not been used + try { + const domainSearch = await axios.request({ + method: "GET", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records?name=${encodeURIComponent( + domainName + )}`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + }); + if (domainSearch.data.result.length) { + console.log( + `\nError: The domain ${domainName} has been used before. Use another or contact us at https://cdr.co/join-community` + ); + return; + } + } catch (err) { + console.log(`Error connecting to CloudFlare:`, err); + return; + } - // sha256 validate the token - // used for verifying domain token - var sha256 = require("js-sha256"); - - // verify the token - // TODO: potentially do this server-side so that expired tokens - // don't get improperly verified on an old local version - if ( - sha256(process.env.DOMAIN_TOKEN) != - "7d3eb96148c592b64ddfb4f3038a329acc22ea94669780dfa9de85b768ed27b1" - ) { - console.log("\n❌ The domain token you supplied is not valid."); - return; - } + // create dir for our files + // TODO: make this a bit smarter and only run if method == "k8s" as this is being done in gcloud + await createProjectDir(argv.saveDir); - const validateName = (name) => { - // TODO: possibly add error message here - var regex = new RegExp("^[a-zA-Z]+[a-zA-Z0-9\\-]*$"); - if (!regex.test(name)) { - console.log("❗ Please enter a valid name (ex. `acme-co`)"); - return false; - } - return true; - }; + // get the base config + let issuer = await fs.readFile( + __dirname + "/../config-store/cloudflare-issuer.yaml", + "utf8" + ); + let helm = await fs.readFile( + __dirname + "/../config-store/helm-values.yaml", + "utf8" + ); - // determine which type of domain to use - if (!argv.name) { - argv = { - ...argv, - ...(await inquirer.prompt({ - type: "input", - name: "name", - message: `Enter a name for your Coder deployment (____.${cloudflareDomain}):`, - validate: validateName, - })), - }; - } else { - validateName(argv.name); - } - const domainName = argv.name + "." + cloudflareDomain; - - // ensure this domain has not been used - try { - const domainSearch = await axios.request({ - method: "GET", - url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records?name=${encodeURIComponent( - domainName - )}`, - headers: { - Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, - "Content-Type": "application/json", - }, - }); - if (domainSearch.data.result.length) { - console.log( - `\nError: The domain ${domainName} has been used before. Use another or contact us at https://cdr.co/join-community` - ); - return; - } - } catch (err) { - console.log(`Error connecting to CloudFlare:`, err); - return; - } + // add our values to the sample file + // TODO: add validation to all these values + helm = helm.split("INJECT_USER_DOMAIN").join(domainName); + issuer = issuer.split("INJECT_USER_NAMESPACE").join(argv.namespace); + issuer = issuer + .split("INJECT_CLOUDFLARE_API") + .join(process.env.DOMAIN_TOKEN); + issuer = issuer.split("INJECT_USER_EMAIL").join(cloudflareEmail); + issuer = issuer.split("INJECT_USER_DOMAIN").join(domainName); + issuer = issuer.split("INJECT_CLOUDFLARE_EMAIL").join(cloudflareEmail); + + if (issuer.includes("INJECT_") || helm.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the files correctly. An error occured." + ); + return; + } + // write the issuer and helm config to a file with a trailing newline + try { + await fs.writeFile(argv.saveDir + "/issuer.yaml", issuer + "\n"); + await fs.writeFile(argv.saveDir + "/values.yaml", helm + "\n"); + } catch (err) { + console.log("❌ An error occured writing the config files", err); + } - // create dir for our files - // TODO: make this a bit smarter and only run if method == "k8s" as this is being done in gcloud - await createProjectDir(argv.saveDir); + console.log( + "\n✅ Created the following config files:\n", + "\t📄 issuer.yaml: Configures a LetsEncrypt issuer for our domain\n", + "\t📄 values.yaml: Values for our Coder helm chart, telling it our URL and to point to the issuer\n" + ); - // get the base config - let issuer = await fs.readFile( - __dirname + "/../config-store/cloudflare-issuer.yaml", - "utf8" - ); - let helm = await fs.readFile( - __dirname + "/../config-store/helm-values.yaml", - "utf8" - ); + // TODO: confirm cert-manager exists first + console.log( + "We need need to deploy cert-manager 1.0.1 to work with a domain. If you already have it installed, we can re-deploy harmlessly." + ); - // add our values to the sample file - // TODO: add validation to all these values - helm = helm.split("INJECT_USER_DOMAIN").join(domainName); - issuer = issuer.split("INJECT_USER_NAMESPACE").join(argv.namespace); - issuer = issuer - .split("INJECT_CLOUDFLARE_API") - .join(process.env.DOMAIN_TOKEN); - issuer = issuer.split("INJECT_USER_EMAIL").join(cloudflareEmail); - issuer = issuer.split("INJECT_USER_DOMAIN").join(domainName); - issuer = issuer.split("INJECT_CLOUDFLARE_EMAIL").join(cloudflareEmail); - - if (issuer.includes("INJECT_") || helm.includes("INJECT_")) { - console.log( - "❌", - "Information was not injected into the files correctly. An error occured." - ); - return; - } - // write the issuer and helm config to a file with a trailing newline - try { - await fs.writeFile(argv.saveDir + "/issuer.yaml", issuer + "\n"); - await fs.writeFile(argv.saveDir + "/values.yaml", helm + "\n"); - } catch (err) { - console.log("❌ An error occured writing the config files", err); - } + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Deploy cert-manager on your cluster?", + }); + if (!runCommand.runIt) { console.log( - "\n✅ Created the following config files:\n", - "\t📄 issuer.yaml: Configures a LetsEncrypt issuer for our domain\n", - "\t📄 values.yaml: Values for our Coder helm chart, telling it our URL and to point to the issuer\n" + `\nCancelled the install. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` ); + return 0; + } + } - // TODO: confirm cert-manager exists first - console.log( - "We need need to deploy cert-manager 1.0.1 to work with a domain. If you already have it installed, we can re-deploy harmlessly." - ); + try { + console.log( + "\n⏳ Installing cert-manager. This will take a couple minutes..." + ); + // TODO: confirm this better. + const checkCertManager = await runHelperScript("installCertManager"); + // remove any weird spaces + const certManagerPods = checkCertManager.split(" ").join(""); + + if (certManagerPods >= 3) console.log("✅", "Installed cert-manager"); + else { + throw "could not detect pods running"; + } + } catch (err) { + console.log("❌", "An error occured installing cert-manager:", err); + return; + } - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Deploy cert-manager on your cluster?", - }); - - if (!runCommand.runIt) { - console.log( - `\nCancelled the install. If you have any questions, feel free reach out on Slack:\n\t➡️ https://cdr.co/join-community\n` - ); - return 0; - } - } + let installScript = await fs.readFile( + __dirname + "/../config-store/update-coder.sh", + "utf8" + ); + // add our values to the sample file + // TODO: add validation to all these values + installScript = installScript + .split("INJECT_NAMESPACE") + .join(argv.namespace); + installScript = installScript.split("INJECT_SAVEDIR").join(argv.saveDir); + + // ensure we injected everything OK + if (installScript.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the install script correctly. An error occured." + ); + return; + } - try { - console.log( - "\n⏳ Installing cert-manager. This will take a couple minutes..." - ); - // TODO: confirm this better. - const checkCertManager = await runHelperScript("installCertManager"); - // remove any weird spaces - const certManagerPods = checkCertManager.split(" ").join(""); - - if (certManagerPods >= 3) console.log("✅", "Installed cert-manager"); - else { - throw "could not detect pods running"; - } - } catch (err) { - console.log("❌", "An error occured installing cert-manager:", err); - return; - } + try { + await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); + await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); + } catch (err) { + console.log("❌ An error occured writing the install script", err); + } - let installScript = await fs.readFile( - __dirname + "/../config-store/update-coder.sh", - "utf8" - ); - // add our values to the sample file - // TODO: add validation to all these values - installScript = installScript - .split("INJECT_NAMESPACE") - .join(argv.namespace); - installScript = installScript.split("INJECT_SAVEDIR").join(argv.saveDir); - - // ensure we injected everything OK - if (installScript.includes("INJECT_")) { - console.log( - "❌", - "Information was not injected into the install script correctly. An error occured." - ); - return; - } + console.log( + "\n\n✅ Created an install/upgrade script that:\n", + "\t🌎 Deploys our issuer (issuer.yaml)\n", + "\t📊 Adds/updates the Coder helm chart\n", + `\t🚀 Installs/upgrades Coder with our values (values.yaml)\n\n` + + `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` + ); - try { - await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); - await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); - } catch (err) { - console.log("❌ An error occured writing the install script", err); - } + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command and install Coder?", + }); + if (!runCommand.runIt) { console.log( - "\n\n✅ Created an install/upgrade script that:\n", - "\t🌎 Deploys our issuer (issuer.yaml)\n", - "\t📊 Adds/updates the Coder helm chart\n", - `\t🚀 Installs/upgrades Coder with our values (values.yaml)\n\n` + - `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` + `\n\nOk :) Feel free to modify the command as needed and run it yourself.` ); + return; + } + } - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Do you want to run this command and install Coder?", - }); - - if (!runCommand.runIt) { - console.log( - `\n\nOk :) Feel free to modify the command as needed and run it yourself.` - ); - return; - } - } + const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); + console.log("------------"); - const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); - console.log("------------"); + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + // TODO: consolidate the spacers + console.log("------------"); - subprocess.stdout.pipe(process.stdout); - const { stdout } = await subprocess; - // TODO: consolidate the spacers - console.log("------------"); + console.log("\n⏳ Setting up the domain..."); - console.log("\n⏳ Setting up the domain..."); + // fetch our admin password now, but save it for later + const loginDetails = await runHelperScript("getAdminPassword"); - // fetch our admin password now, but save it for later - const loginDetails = await runHelperScript("getAdminPassword"); + const coderIP = await runHelperScript("getCoderIP").catch((err) => { + console.log( + "Error fetching the IP address for your Coder deployment. We can't set up the DNS records :(" + ); + return 1; + }); - const coderIP = await runHelperScript("getCoderIP").catch((err) => { - console.log( - "Error fetching the IP address for your Coder deployment. We can't set up the DNS records :(" - ); - return 1; - }); + // set up DNS records to point the subdomain to the Coder IP + try { + // add record for root URL + await axios.request({ + method: "POST", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + data: { + type: "A", + name: argv.name, + content: coderIP, + ttl: 1, + proxied: false, + }, + }); + await axios.request({ + method: "POST", + url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, + headers: { + Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, + "Content-Type": "application/json", + }, + data: { + type: "A", + name: "*." + argv.name, + content: coderIP, + ttl: 1, + proxied: false, + }, + }); + } catch (err) { + console.log( + "\n\n", + "❌", + "Error setting up this subdomain... For help, contact us at https://cdr.co/join-community" + ); + } - // set up DNS records to point the subdomain to the Coder IP - try { - // add record for root URL - await axios.request({ - method: "POST", - url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, - headers: { - Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, - "Content-Type": "application/json", - }, - data: { - type: "A", - name: argv.name, - content: coderIP, - ttl: 1, - proxied: false, - }, - }); - await axios.request({ - method: "POST", - url: `https://api.cloudflare.com/client/v4/zones/${cloudflareZone}/dns_records`, - headers: { - Authorization: `Bearer ${process.env.DOMAIN_TOKEN}`, - "Content-Type": "application/json", - }, - data: { - type: "A", - name: "*." + argv.name, - content: coderIP, - ttl: 1, - proxied: false, - }, - }); - } catch (err) { - console.log( - "\n\n", - "❌", - "Error setting up this subdomain... For help, contact us at https://cdr.co/join-community" - ); - } + console.log( + "\n\n🎉 Coder has been installed! Log in at https://" + domainName + ); + if (loginDetails == "") { + // TODO: allow the user to reset from here + console.log( + "\nWe couldn't find your admin password. See the docs on how to reset it: \n\t➡️ https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" + ); + } else { + console.log(loginDetails); + } - console.log( - "\n\n🎉 Coder has been installed! Log in at https://" + domainName - ); - if (loginDetails == "") { - // TODO: allow the user to reset from here - console.log( - "\nWe couldn't find your admin password. See the docs on how to reset it: \n\t➡️ https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" - ); - } else { - console.log(loginDetails); - } + // create our script + } else if ( + argv.domainType == "cloud-dns" || + argv.domainType == "cloudflare" + ) { + console.log( + "This is coming soon. For support doing this, join the community: https;//cdr.co/join-community" + ); + return 0; + } else if (argv.domainType == "none") { + console.log( + "\nWarning: This means you can't use Coder with DevURLs, a primary way of accessing web services\ninside of a Coder Workspace:\n", + "\t📄 Docs: https://coder.com/docs/environments/devurls\n", + "\t🌎 Alternative: https://ngrok.com/docs (you can install this in your images)\n\n" + ); - // create our script - } else if ( - argv.domainType == "cloud-dns" || - argv.domainType == "cloudflare" - ) { - console.log( - "This is coming soon. For support doing this, join the community: https;//cdr.co/join-community" - ); - return 0; - } else if (argv.domainType == "none") { - console.log( - "\nWarning: This means you can't use Coder with DevURLs, a primary way of accessing web services\ninside of a Coder Workspace:\n", - "\t📄 Docs: https://coder.com/docs/environments/devurls\n", - "\t🌎 Alternative: https://ngrok.com/docs (you can install this in your images)\n\n" - ); + console.log( + "You can always add a domain later, and use a custom provider via our docs.\n" + ); - console.log( - "You can always add a domain later, and use a custom provider via our docs.\n" - ); + // TODO: definitely fix me!! + // very sad repeated code :( + // i wanted 2 working options + let installScript = await fs.readFile( + __dirname + "/../config-store/update-coder-no-domain.sh", + "utf8" + ); - // TODO: definitely fix me!! - // very sad repeated code :( - // i wanted 2 working options - let installScript = await fs.readFile( - __dirname + "/../config-store/update-coder-no-domain.sh", - "utf8" - ); + // inject details + installScript = installScript + .split("INJECT_NAMESPACE") + .join(argv.namespace); + + // ensure we injected everything OK + if (installScript.includes("INJECT_")) { + console.log( + "❌", + "Information was not injected into the install script correctly. An error occured." + ); + return; + } - // inject details - installScript = installScript - .split("INJECT_NAMESPACE") - .join(argv.namespace); - - // ensure we injected everything OK - if (installScript.includes("INJECT_")) { - console.log( - "❌", - "Information was not injected into the install script correctly. An error occured." - ); - return; - } + try { + await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); + await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); + } catch (err) { + console.log("❌ An error occured writing the install script", err); + } - try { - await fs.writeFile(argv.saveDir + "/update-coder.sh", installScript); - await fs.chmod(argv.saveDir + "/update-coder.sh", "755"); - } catch (err) { - console.log("❌ An error occured writing the install script", err); - } + console.log( + "\n\n✅ Created an install/upgrade script that:\n", + "\t📊 Adds/updates the Coder helm chart\n", + `\t🚀 Installs/upgrades Coder\n\n` + + `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` + ); + + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: "Do you want to run this command and install Coder?", + }); + if (!runCommand.runIt) { console.log( - "\n\n✅ Created an install/upgrade script that:\n", - "\t📊 Adds/updates the Coder helm chart\n", - `\t🚀 Installs/upgrades Coder\n\n` + - `💻 Preview it at: ${argv.saveDir}/update-coder.sh\n` + `\n\nOk :) Feel free to modify the command as needed and run it yourself.` ); + return; + } + } - if (!argv.skipConfirmPrompts) { - const runCommand = await inquirer.prompt({ - type: "confirm", - default: true, - name: "runIt", - message: "Do you want to run this command and install Coder?", - }); - - if (!runCommand.runIt) { - console.log( - `\n\nOk :) Feel free to modify the command as needed and run it yourself.` - ); - return; - } - } - - const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); - console.log("------------"); - - subprocess.stdout.pipe(process.stdout); - const { stdout } = await subprocess; - // TODO: consolidate the spacers - console.log("------------"); + const subprocess = execa("/bin/sh", [argv.saveDir + "/update-coder.sh"]); + console.log("------------"); - // fetch our admin password now - const loginDetails = await runHelperScript("getAdminPassword"); + subprocess.stdout.pipe(process.stdout); + const { stdout } = await subprocess; + // TODO: consolidate the spacers + console.log("------------"); - const coderIP = await runHelperScript("getCoderIP").catch((err) => { - console.log("Error fetching the IP address for your Coder deployment."); - return 1; - }); + // fetch our admin password now + const loginDetails = await runHelperScript("getAdminPassword"); - console.log("\n\n🎉 Coder has been installed! Log in at http://" + coderIP); - if (loginDetails == "") { - // TODO: auto reset it? - console.log( - "\nWe couldn't find your admin password. See the docs on how to reset it: https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" - ); - } else { - console.log(loginDetails); - } + const coderIP = await runHelperScript("getCoderIP").catch((err) => { + console.log("Error fetching the IP address for your Coder deployment."); + return 1; + }); - // TODO: add confirmations + console.log("\n\n🎉 Coder has been installed! Log in at http://" + coderIP); + if (loginDetails == "") { + // TODO: auto reset it? + console.log( + "\nWe couldn't find your admin password. See the docs on how to reset it: https://coder.com/docs/admin/access-control/password-reset#resetting-the-site-admin-password" + ); } else { - // TODO: standardize these - console.error("Error. Unknown domainType: " + argv.domainType); - return; + console.log(loginDetails); } - // install and access Coder + // TODO: add confirmations + } else { + // TODO: standardize these + console.error("Error. Unknown domainType: " + argv.domainType); + return; + } + + // install and access Coder - // TODO: tell the user they can save this to a PRIVATE - // repo in GIT (maybe idk if that is bad practice) - console.log("\n\nat the end with a long argv:", Object.keys(argv).length); + // TODO: tell the user they can save this to a PRIVATE + // repo in GIT (maybe idk if that is bad practice) + console.log("\n\nat the end with a long argv:", Object.keys(argv).length); } From eec5e1bd4d2182a49ad21d96c999b2f56b526ef8 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:42:27 -0400 Subject: [PATCH 26/40] more info about cloud shell --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad63ca0..b9db300 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Launch [Coder](https://coder.com) in a simple way. It can: - Create the recommended Google Cloud Cluster for you - Install Coder with an automatic domain name: `[yourname].coding.pics` -Preferred environment: Google Cloud Shell +Preferred environment: Google Cloud Shell. It just works. ## How to use From b35b8849a4bf6914e6525966e6159e2481638db7 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 02:46:59 -0400 Subject: [PATCH 27/40] bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb32705..28ecdba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bpmct/launch-coder", - "version": "1.0.2", + "version": "1.0.3", "main": "src/index.js", "bin": { "launch-coder": "bin/launch-coder" From f99084e1956c566d2fa2dca6e5201a7eb1907dc5 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 03:25:23 -0400 Subject: [PATCH 28/40] add troubleshooting --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index b9db300..87afe46 100644 --- a/README.md +++ b/README.md @@ -35,4 +35,14 @@ launch-coder launch-coder will not install or provision anything without your permission :) +## Troubleshooting + +On non-public Dev URLs: `An internal server error occurred`: + +- This is an error I get frequently with Dev URLs, GKE, and CloudFlare domains, and it always seems to go away. + - Re-create Dev URL + - Re create environment + - Wait patiently + - Last resort: Make Dev URL public + Questions? Join Slack [https://cdr.co/join-community](https://cdr.co/join-community) From 7147f95e729b9ebfc1f7ef9863ab45095f255c47 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 03:25:38 -0400 Subject: [PATCH 29/40] fix formatting --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 87afe46..7e97edd 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,6 @@ On non-public Dev URLs: `An internal server error occurred`: - Wait patiently - Last resort: Make Dev URL public +--- + Questions? Join Slack [https://cdr.co/join-community](https://cdr.co/join-community) From b475f6e386eca7201dd2599182bc25a0c65ebfae Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 03:31:44 -0400 Subject: [PATCH 30/40] remove silly debug ending --- src/cli.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 1c443d4..e3b0ff9 100644 --- a/src/cli.js +++ b/src/cli.js @@ -855,5 +855,4 @@ export async function cli(args) { // TODO: tell the user they can save this to a PRIVATE // repo in GIT (maybe idk if that is bad practice) - console.log("\n\nat the end with a long argv:", Object.keys(argv).length); } From 4ec2e7ab8e52c8f07e11fb7caac6e08dcebafd52 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 09:43:05 -0400 Subject: [PATCH 31/40] add brand new account info --- README.md | 13 ++++++++++++- package.json | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7e97edd..91e0683 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Preferred environment: Google Cloud Shell. It just works. No need to install: ```sh +# If you have never used GKE before: +gcloud services enable container.googleapis.com + # For a guided install: npx @bpmct/launch-coder @@ -37,7 +40,7 @@ launch-coder will not install or provision anything without your permission :) ## Troubleshooting -On non-public Dev URLs: `An internal server error occurred`: +On non-public Dev URLs: `An internal server error occurred`: - This is an error I get frequently with Dev URLs, GKE, and CloudFlare domains, and it always seems to go away. - Re-create Dev URL @@ -45,6 +48,14 @@ On non-public Dev URLs: `An internal server error occurred`: - Wait patiently - Last resort: Make Dev URL public +`Customer should enable service:container.googleapis.com before proceeding`: + +- This is for brand new acounts accounts, the script will handle this in the future. For now, enable by typing: + + ```sh + gcloud services enable container.googleapis.com + ``` + --- Questions? Join Slack [https://cdr.co/join-community](https://cdr.co/join-community) diff --git a/package.json b/package.json index 28ecdba..ceaefb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bpmct/launch-coder", - "version": "1.0.3", + "version": "1.0.4", "main": "src/index.js", "bin": { "launch-coder": "bin/launch-coder" From 325d44c9660fbd60f411d1233a7cb3f4b2a6f42b Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Mar 2021 19:01:57 -0400 Subject: [PATCH 32/40] update warning --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91e0683..80d3374 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # launch-coder -⚠️: This was a hackathon project and is not recommended for production use or in sensative environments. +⚠️: This was a hackathon project and is not recommended for production deployments or in sensative environments. --- From 82ee65bf40109988cbe4f54a4f6d8669f25a4ce6 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 31 Mar 2021 16:53:07 -0400 Subject: [PATCH 33/40] add vs code settings --- .vscode/extensions.json | 5 +++++ .vscode/settings.json | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..0f148de --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..89d1965 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" +} \ No newline at end of file From 2d391267034f7985a1dcd36d133d8a885b87b0b3 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 31 Mar 2021 16:53:15 -0400 Subject: [PATCH 34/40] enable k8s service --- shell-helpers/googleCloudK8sEnabled.sh | 13 ++++++++ src/cli.js | 44 +++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100755 shell-helpers/googleCloudK8sEnabled.sh diff --git a/shell-helpers/googleCloudK8sEnabled.sh b/shell-helpers/googleCloudK8sEnabled.sh new file mode 100755 index 0000000..10b9957 --- /dev/null +++ b/shell-helpers/googleCloudK8sEnabled.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# thanks https://gist.github.com/pydevops/cffbd3c694d599c6ca18342d3625af97#0212-enable-service + + +SERVICE="container.googleapis.com" +if [[ $(gcloud services list --format="value(serviceConfig.name)" \ + --filter="serviceConfig.name:$SERVICE" 2>&1) != \ + "$SERVICE" ]]; then +echo "false" +else +echo "true" +fi diff --git a/src/cli.js b/src/cli.js index e3b0ff9..50e53e5 100644 --- a/src/cli.js +++ b/src/cli.js @@ -214,7 +214,10 @@ export async function cli(args) { // TODO: add better user education on what the prereqs are try { await runHelperScript("googleCloudPrereqs"); - console.log("✅", "You seem to have all the dependencies installed!"); + console.log( + "✅", + "You seem to have all the local dependencies installed!" + ); } catch (err) { console.log("❌", err.stderr); return; @@ -294,6 +297,45 @@ export async function cli(args) { } } + // check if the kubernetes service is enabled in google cloud + + const check_K8s = await runHelperScript("googleCloudK8sEnabled"); + + if (check_K8s && check_K8s != "true") { + if (!argv.skipConfirmPrompts) { + const runCommand = await inquirer.prompt({ + type: "confirm", + default: true, + name: "runIt", + message: + "Kubernetes (container.googleapis.com) is not enabled on this Google Cloud project. Enable?", + }); + + if (!runCommand.runIt) { + console.log( + `\n\nOk :) Kubernetes is required to run Coder. You can manually enable it here:\n`, + "\t👉 https://console.cloud.google.com/marketplace/product/google/container.googleapis.com\n" + ); + return; + } + } + + try { + console.log( + "⏳", + "Enabling the Kubernetes service for this project..." + ); + await execa("gcloud", [ + "services", + "enable", + "container.googleapis.com", + ]); + console.log("✅", "Enabled the Google Cloud Kubernetes service\n\n"); + } catch (err) { + console.log("❌", "Error enabling the Kubernetes service", err); + } + } + let gCloudCommand = generateGoogleClusterCommand(argv); // TODO: add info on what this cluster means From 279270371ae4c69b547787c8077fc771d05b8118 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 31 Mar 2021 19:46:29 -0400 Subject: [PATCH 35/40] remove k8s enable step --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 80d3374..eaad82a 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,6 @@ Preferred environment: Google Cloud Shell. It just works. No need to install: ```sh -# If you have never used GKE before: -gcloud services enable container.googleapis.com - # For a guided install: npx @bpmct/launch-coder @@ -48,14 +45,6 @@ On non-public Dev URLs: `An internal server error occurred`: - Wait patiently - Last resort: Make Dev URL public -`Customer should enable service:container.googleapis.com before proceeding`: - -- This is for brand new acounts accounts, the script will handle this in the future. For now, enable by typing: - - ```sh - gcloud services enable container.googleapis.com - ``` - --- Questions? Join Slack [https://cdr.co/join-community](https://cdr.co/join-community) From f47a0a819f3fe9a157832a80a6d4f7250e28f0e8 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 31 Mar 2021 21:17:15 -0400 Subject: [PATCH 36/40] fix checker --- shell-helpers/googleCloudK8sEnabled.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell-helpers/googleCloudK8sEnabled.sh b/shell-helpers/googleCloudK8sEnabled.sh index 10b9957..4cd44c8 100755 --- a/shell-helpers/googleCloudK8sEnabled.sh +++ b/shell-helpers/googleCloudK8sEnabled.sh @@ -4,8 +4,8 @@ SERVICE="container.googleapis.com" -if [[ $(gcloud services list --format="value(serviceConfig.name)" \ - --filter="serviceConfig.name:$SERVICE" 2>&1) != \ +if [[ $(gcloud services list --format="value(config.name)" \ + --filter="config.name:$SERVICE" 2>&1) != \ "$SERVICE" ]]; then echo "false" else From 964b0c991889a221caf48e4a37253848bf2d0edc Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 31 Mar 2021 21:21:17 -0400 Subject: [PATCH 37/40] fix version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ceaefb7..992427c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bpmct/launch-coder", - "version": "1.0.4", + "version": "1.0.5", "main": "src/index.js", "bin": { "launch-coder": "bin/launch-coder" From b173904e1b2a966ff4eaa2ca09f850333db76c58 Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Thu, 1 Apr 2021 14:58:40 -0500 Subject: [PATCH 38/40] fix: README typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eaad82a..9bbd0fb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # launch-coder -⚠️: This was a hackathon project and is not recommended for production deployments or in sensative environments. +⚠️: This was a hackathon project and is not recommended for production deployments or in sensitive environments. --- From e1698539f551fab77ecf0c9a02cc88632cd16ee6 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 5 Apr 2021 11:44:51 -0400 Subject: [PATCH 39/40] clarify "install" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bbd0fb..cde122b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Preferred environment: Google Cloud Shell. It just works. ## How to use -No need to install: +No need to install the CLI tool locally, run from NPM: ```sh # For a guided install: From 178abc5d96c6ac1d0dd39948bf1cf65dd5ed145c Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Wed, 8 Sep 2021 21:55:02 -0500 Subject: [PATCH 40/40] add 1.21 warning --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cde122b..bc48589 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ⚠️: This was a hackathon project and is not recommended for production deployments or in sensitive environments. +This does not work for Coder v1.21+. Specify a --version flag + --- Launch [Coder](https://coder.com) in a simple way. It can: