Skip to content

Commit

Permalink
Merge branch 'master' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
edosrecki committed Jan 23, 2022
2 parents cfbe0af + aa8c415 commit 2d2307e
Show file tree
Hide file tree
Showing 14 changed files with 70 additions and 36 deletions.
8 changes: 5 additions & 3 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]

---

Expand Down Expand Up @@ -55,7 +57,7 @@ google-cloud-sql configurations create
google-cloud-sql configurations run
## Connect to the instance on localhost
psql -h localhost -p <LOCAL_PORT> -U <USER>
psql -h localhost -p $LOCAL_PORT -U $USER
----

== Build
Expand Down
Binary file added screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions src/commands/configurations/create.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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'
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 () => {
Expand All @@ -17,6 +18,7 @@ export const createConfiguration = async () => {
const answers = await inquirer.prompt<ConfigurationCreateAnswers>([
googleCloudProjectPrompt,
googleCloudSqlInstancePrompt,
kubernetesContextPrompt,
kubernetesNamespacePrompt,
kubernetesServiceAccountPrompt,
localPortPrompt,
Expand Down
15 changes: 15 additions & 0 deletions src/commands/configurations/prompts/kubernetes-context.ts
Original file line number Diff line number Diff line change
@@ -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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
5 changes: 3 additions & 2 deletions src/lib/configurations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
9 changes: 1 addition & 8 deletions src/lib/configurations/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,16 @@ export const store = new Conf<Schema>({
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' },
Expand Down
11 changes: 9 additions & 2 deletions src/lib/kubectl/contexts.ts
Original file line number Diff line number Diff line change
@@ -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'
`)
})
3 changes: 2 additions & 1 deletion src/lib/kubectl/namespaces.ts
Original file line number Diff line number Diff line change
@@ -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}'
`)
})
28 changes: 17 additions & 11 deletions src/lib/kubectl/pods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { execCommand, execCommandAttached } from '../util/exec'

type CloudSqlProxyPod = {
name: string
context: string
namespace: string
serviceAccount: string
instance: string
Expand All @@ -14,36 +15,41 @@ 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} \
-- /cloud_sql_proxy -ip_address_types=PRIVATE -instances=${pod.instance}=tcp:${pod.remotePort}
`)
}

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}"
`)
}
11 changes: 7 additions & 4 deletions src/lib/kubectl/service-accounts.ts
Original file line number Diff line number Diff line change
@@ -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}'
`)
})
}
)
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GoogleCloudSqlInstance } from './gcloud/sql-instances'
export type Configuration = {
configurationName: string
googleCloudSqlInstance: Pick<GoogleCloudSqlInstance, 'connectionName' | 'port'>
kubernetesContext: string
kubernetesNamespace: string
kubernetesServiceAccount: string
localPort: number
Expand Down

0 comments on commit 2d2307e

Please sign in to comment.