diff --git a/README.adoc b/README.adoc index 76bb173..8d1b5da 100644 --- a/README.adoc +++ b/README.adoc @@ -9,9 +9,11 @@ image:https://img.shields.io/github/workflow/status/edosrecki/google-cloud-sql-c A CLI app which establishes a connection to a private Google Cloud SQL instance and port-forwards it to a local machine. -Connection is established by running a Google Cloud SQL Auth Proxy pod in a Google Kubernetes Engine cluster which runs in the same VPC network as the private Cloud SQL instance. Connection is then port-forwarded to the local machine, where a user can connect to the instance on localhost. Configurations in the app can be saved for practical future usage. +Connection is established by running a Google Cloud SQL Auth Proxy pod in a Google Kubernetes Engine cluster which runs in the same VPC network as the private Cloud SQL instance. Connection is then port-forwarded to the local machine, where a user can connect to the instance on localhost. Corresponding workload identity has to be configured in the cluster, with service account which has Cloud SQL User role on the given SQL instance. Configurations in the app can be saved for practical future usage. -The app relies on local `gcloud` and `kubectl` commands which have to be configured and authenticated with the proper Google Cloud user and Kubernetes cluster. +The app relies on local `gcloud` and `kubectl` commands which have to be configured and authenticated with the proper Google Cloud user and GKE Kubernetes cluster. + +image::screenshot.png[] --- @@ -55,7 +57,7 @@ google-cloud-sql configurations create google-cloud-sql configurations run ## Connect to the instance on localhost -psql -h localhost -p -U +psql -h localhost -p $LOCAL_PORT -U $USER ---- == Build diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..1196516 Binary files /dev/null and b/screenshot.png differ diff --git a/src/commands/configurations/create.ts b/src/commands/configurations/create.ts index 6dc76cf..e70907d 100644 --- a/src/commands/configurations/create.ts +++ b/src/commands/configurations/create.ts @@ -1,4 +1,4 @@ -import { green, red, bold } from 'chalk' +import { bold, green, red } from 'chalk' import inquirer from 'inquirer' import autocomplete from 'inquirer-autocomplete-prompt' import { saveConfiguration } from '../../lib/configurations' @@ -6,9 +6,10 @@ import { ConfigurationCreateAnswers } from '../../lib/types' import { configurationNamePrompt } from './prompts/configuration-name' import { confirmationPrompt } from './prompts/confirmation' import { googleCloudProjectPrompt } from './prompts/google-cloud-project' -import { googleCloudSqlInstancePrompt } from './prompts/google-cloud-sql-instances' +import { googleCloudSqlInstancePrompt } from './prompts/google-cloud-sql-instance' +import { kubernetesContextPrompt } from './prompts/kubernetes-context' import { kubernetesNamespacePrompt } from './prompts/kubernetes-namespace' -import { kubernetesServiceAccountPrompt } from './prompts/kubernetes-service-accounts' +import { kubernetesServiceAccountPrompt } from './prompts/kubernetes-service-account' import { localPortPrompt } from './prompts/local-port' export const createConfiguration = async () => { @@ -17,6 +18,7 @@ export const createConfiguration = async () => { const answers = await inquirer.prompt([ googleCloudProjectPrompt, googleCloudSqlInstancePrompt, + kubernetesContextPrompt, kubernetesNamespacePrompt, kubernetesServiceAccountPrompt, localPortPrompt, diff --git a/src/commands/configurations/prompts/google-cloud-sql-instances.ts b/src/commands/configurations/prompts/google-cloud-sql-instance.ts similarity index 100% rename from src/commands/configurations/prompts/google-cloud-sql-instances.ts rename to src/commands/configurations/prompts/google-cloud-sql-instance.ts diff --git a/src/commands/configurations/prompts/kubernetes-context.ts b/src/commands/configurations/prompts/kubernetes-context.ts new file mode 100644 index 0000000..c5f393d --- /dev/null +++ b/src/commands/configurations/prompts/kubernetes-context.ts @@ -0,0 +1,15 @@ +import { fetchKubernetesContexts } from '../../../lib/kubectl/contexts' +import { ConfigurationCreateAnswers } from '../../../lib/types' +import { search } from '../../../lib/util/search' + +const source = (answers: ConfigurationCreateAnswers, input?: string) => { + const instances = fetchKubernetesContexts() + return search(instances, input) +} + +export const kubernetesContextPrompt = { + type: 'autocomplete', + name: 'kubernetesContext', + message: 'Choose Kubernetes context:', + source, +} diff --git a/src/commands/configurations/prompts/kubernetes-namespace.ts b/src/commands/configurations/prompts/kubernetes-namespace.ts index 92da050..bbafa88 100644 --- a/src/commands/configurations/prompts/kubernetes-namespace.ts +++ b/src/commands/configurations/prompts/kubernetes-namespace.ts @@ -3,7 +3,7 @@ import { ConfigurationCreateAnswers } from '../../../lib/types' import { search } from '../../../lib/util/search' const source = (answers: ConfigurationCreateAnswers, input?: string) => { - const instances = fetchKubernetesNamespaces() + const instances = fetchKubernetesNamespaces(answers.kubernetesContext) return search(instances, input) } diff --git a/src/commands/configurations/prompts/kubernetes-service-accounts.ts b/src/commands/configurations/prompts/kubernetes-service-account.ts similarity index 80% rename from src/commands/configurations/prompts/kubernetes-service-accounts.ts rename to src/commands/configurations/prompts/kubernetes-service-account.ts index 67b685c..5567a6a 100644 --- a/src/commands/configurations/prompts/kubernetes-service-accounts.ts +++ b/src/commands/configurations/prompts/kubernetes-service-account.ts @@ -3,7 +3,10 @@ import { ConfigurationCreateAnswers } from '../../../lib/types' import { search } from '../../../lib/util/search' const source = (answers: ConfigurationCreateAnswers, input?: string) => { - const instances = fetchKubernetesServiceAccounts(answers.kubernetesNamespace) + const instances = fetchKubernetesServiceAccounts( + answers.kubernetesContext, + answers.kubernetesNamespace + ) return search(instances, input) } diff --git a/src/lib/configurations/index.ts b/src/lib/configurations/index.ts index 347ef01..2fe6046 100644 --- a/src/lib/configurations/index.ts +++ b/src/lib/configurations/index.ts @@ -36,6 +36,7 @@ export const deleteConfiguration = (configuratioName: string): void => { export const execConfiguration = (configuration: Configuration) => { const pod = { name: `sql-proxy-${configuration.configurationName}-${randomString()}`, + context: configuration.kubernetesContext, namespace: configuration.kubernetesNamespace, serviceAccount: configuration.kubernetesServiceAccount, instance: configuration.googleCloudSqlInstance.connectionName, @@ -44,10 +45,10 @@ export const execConfiguration = (configuration: Configuration) => { } exitHook(() => { - deletePod(pod.name, pod.namespace) + deletePod(pod) }) runCloudSqlProxyPod(pod) - waitForPodReady(pod.name, pod.namespace) + waitForPodReady(pod) portForward(pod) } diff --git a/src/lib/configurations/store.ts b/src/lib/configurations/store.ts index 07c63b6..9d98b04 100644 --- a/src/lib/configurations/store.ts +++ b/src/lib/configurations/store.ts @@ -14,23 +14,16 @@ export const store = new Conf({ default: [], items: { type: 'object', - required: [ - 'configurationName', - 'googleCloudSqlInstance', - 'kubernetesNamespace', - 'kubernetesServiceAccount', - 'localPort', - ], properties: { configurationName: { type: 'string' }, googleCloudSqlInstance: { type: 'object', - required: ['connectionName', 'port'], properties: { connectionName: { type: 'string' }, port: { type: 'number' }, }, }, + kubernetesContext: { type: 'string' }, kubernetesNamespace: { type: 'string' }, kubernetesServiceAccount: { type: 'string' }, localPort: { type: 'number' }, diff --git a/src/lib/kubectl/contexts.ts b/src/lib/kubectl/contexts.ts index fefcf3d..249f37e 100644 --- a/src/lib/kubectl/contexts.ts +++ b/src/lib/kubectl/contexts.ts @@ -1,5 +1,12 @@ -import { execCommand } from '../util/exec' +import memoize from 'memoizee' +import { execCommand, execCommandMultiline } from '../util/exec' -export function fetchCurrentContext(): string { +export const fetchKubernetesCurrentContext = (): string => { return execCommand(`kubectl config current-context`) } + +export const fetchKubernetesContexts = memoize((): string[] => { + return execCommandMultiline(` + kubectl config get-contexts --output='name' + `) +}) diff --git a/src/lib/kubectl/namespaces.ts b/src/lib/kubectl/namespaces.ts index 5fa3756..97ecaba 100644 --- a/src/lib/kubectl/namespaces.ts +++ b/src/lib/kubectl/namespaces.ts @@ -1,9 +1,10 @@ import memoize from 'memoizee' import { execCommandMultiline } from '../util/exec' -export const fetchKubernetesNamespaces = memoize((): string[] => { +export const fetchKubernetesNamespaces = memoize((context: string): string[] => { return execCommandMultiline(` kubectl get namespaces \ + --context="${context}" \ --output='jsonpath={range .items[*]}{.metadata.name}{"\\n"}{end}' `) }) diff --git a/src/lib/kubectl/pods.ts b/src/lib/kubectl/pods.ts index f2207f1..4db929b 100644 --- a/src/lib/kubectl/pods.ts +++ b/src/lib/kubectl/pods.ts @@ -3,6 +3,7 @@ import { execCommand, execCommandAttached } from '../util/exec' type CloudSqlProxyPod = { name: string + context: string namespace: string serviceAccount: string instance: string @@ -14,7 +15,8 @@ export const runCloudSqlProxyPod = (pod: CloudSqlProxyPod): string => { return execCommand(` kubectl run \ --image=gcr.io/cloudsql-docker/gce-proxy \ - --namespace ${pod.namespace} \ + --context="${pod.context}" \ + --namespace="${pod.namespace}" \ --serviceaccount=${pod.serviceAccount} \ --labels=app=google-cloud-sql \ ${pod.name} \ @@ -22,28 +24,32 @@ export const runCloudSqlProxyPod = (pod: CloudSqlProxyPod): string => { `) } -export const deletePod = (pod: string, namespace: string) => { - console.log(`Deleting pod '${bold(cyan(pod))}'.`) +export const deletePod = (pod: CloudSqlProxyPod) => { + console.log(`Deleting pod '${bold(cyan(pod.name))}'.`) execCommand(` - kubectl delete pod ${pod} --namespace=${namespace} + kubectl delete pod ${pod.name} \ + --context="${pod.context}" \ + --namespace="${pod.namespace}" `) - console.log(`Pod '${bold(cyan(pod))}' deleted.`) + console.log(`Pod '${bold(cyan(pod.name))}' deleted.`) } -export const waitForPodReady = (pod: string, namespace: string) => { - console.log(`Waiting for pod '${bold(cyan(pod))}'.`) +export const waitForPodReady = (pod: CloudSqlProxyPod) => { + console.log(`Waiting for pod '${bold(cyan(pod.name))}'.`) execCommand(` - kubectl wait pod ${pod} \ + kubectl wait pod ${pod.name} \ --for=condition=ready \ - --namespace=${namespace} + --context="${pod.context}" \ + --namespace="${pod.namespace}" `) - console.log(`Pod '${bold(cyan(pod))}' is ready.`) + console.log(`Pod '${bold(cyan(pod.name))}' is ready.`) } export const portForward = (pod: CloudSqlProxyPod) => { console.log(`Starting port forwarding to pod '${bold(cyan(pod.name))}'.`) execCommandAttached(` kubectl port-forward ${pod.name} ${pod.localPort}:${pod.remotePort} \ - --namespace=${pod.namespace} + --context="${pod.context}" \ + --namespace="${pod.namespace}" `) } diff --git a/src/lib/kubectl/service-accounts.ts b/src/lib/kubectl/service-accounts.ts index cdf7999..34829b2 100644 --- a/src/lib/kubectl/service-accounts.ts +++ b/src/lib/kubectl/service-accounts.ts @@ -1,10 +1,13 @@ import memoize from 'memoizee' import { execCommandMultiline } from '../util/exec' -export const fetchKubernetesServiceAccounts = memoize((namespace: string): string[] => { - return execCommandMultiline(` +export const fetchKubernetesServiceAccounts = memoize( + (context: string, namespace: string): string[] => { + return execCommandMultiline(` kubectl get serviceaccounts \ - --namespace ${namespace} \ + --namespace="${namespace}" \ + --context="${context}" \ --output='jsonpath={range .items[*]}{.metadata.name}{"\\n"}{end}' `) -}) + } +) diff --git a/src/lib/types.ts b/src/lib/types.ts index e1c0d62..ffa682a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,6 +3,7 @@ import { GoogleCloudSqlInstance } from './gcloud/sql-instances' export type Configuration = { configurationName: string googleCloudSqlInstance: Pick + kubernetesContext: string kubernetesNamespace: string kubernetesServiceAccount: string localPort: number