godaddy / kubernetes-client

Simplified Kubernetes API client for Node.js.
MIT License
961 stars 192 forks source link

Use within Google Cloud Functions #262

Open gsf opened 6 years ago

gsf commented 6 years ago

I wanted to kick off a job in my GKE cluster from a Google Cloud Function reacting to changes in Cloud Storage. Since there's no .kube/config it wasn't clear to me how to do that with the new fromKubeConfig initialization. I got it working by piecing together a config object from data retrieved by google-auth-library and @google-cloud/container clients. I'll leave it here to both ask if this way makes sense and to serve as an example for others trying to do something similar.

const {auth} = require('google-auth-library');
const container = require('@google-cloud/container');
const job = require('./job.json');
const kube = require('kubernetes-client');

const clusterClient = new container.v1.ClusterManagerClient();
const request = {
  projectId: process.env.GCLOUD_PROJECT,
  zone: 'us-east4-b',
  clusterId: 'cluster-2',
};

function getCluster() {
  return clusterClient.getCluster(request)
    .then(results => {
      return results[0];
    });
}

function getToken() {
  return auth.getClient()
    .then(client => {
      return client.getAccessToken();
    })
    .then(tokenResponse => {
      return tokenResponse.token;
    });
}

// kubeClient is a lazy global used for caching.
// https://cloud.google.com/functions/docs/bestpractices/tips#use_global_variables_to_reuse_objects_in_future_invocations
let kubeClient;

function getKubeClient(cluster, token) {
  kubeClient = new kube.Client({config: kube.config.fromKubeconfig({
    apiVersion: 'v1',
    kind: 'Config',
    preferences: {},
    'current-context': 'a',
    contexts: [
      {
        name: 'a',
        context: {
          cluster: 'a',
          user: 'a'
        }
      }
    ],
    clusters: [
      {
        name: 'a',
        cluster: {
          'certificate-authority-data': cluster.masterAuth.clusterCaCertificate,
          server: `https://${cluster.endpoint}`
        }
      }
    ],
    users: [
      {
        name: 'a',
        user: {
          'auth-provider': {
            config: {
              'access-token': token,
              'cmd-args': 'config config-helper --format=json',
              'cmd-path': '/google/google-cloud-sdk/bin/gcloud',
              'token-key': '{.credential.access_token}'
            },
            name: 'gcp'
          }
        }
      }
    ]
  })});
  return kubeClient.loadSpec();
}

exports.updateJob = (event) => {
  return (kubeClient && Promise.resolve(kubeClient) ||
    Promise.all([getCluster(), getToken()]).then(([cluster, token]) => {
      return getKubeClient(cluster, token);
    }))
    .then(kubeClient => {
      // Strip extension from filename to get job name
      const name = event.data.name.replace(/\.[^/.]+$/, "");
      job.metadata.generateName = `${name}-update-`;
      job.spec.template.spec.volumes[0].configMap.name = name;
      return kubeClient.apis.batch.v1.namespaces('default').jobs.post({body: job});
    })
    .then(res => {
      console.log('Job created:', res.body.metadata.name);
    });
};
silasbw commented 6 years ago

Very cool. There's a a .getInClusterConfig() (https://github.com/godaddy/kubernetes-client/blob/master/lib/config.js#L25) for getting the config from a Pod. We could add a .getGoogleCloudConfig() based off your snippet above. Thoughts?

gsf commented 6 years ago

Awesome. I'd love to see a getGoogleCloud method on config accepting an object like the request passed into getCluster above.

silasbw commented 6 years ago

do you have time to whip up a PR and help with testing in your scenario? If you do a first pass, I'm happy to help tie off any loose ends.

gsf commented 6 years ago

I moved on to another piece in our stack but may be able to come back to this next week.

chrisatomix commented 6 years ago

We have this exact use-case right now, need to trigger a Kubernetes Job when a file is uploaded to a GCS bucket. Unfortunately I couldn't get this example working at all, most of it goes over my head. I was getting all kinds of error messages I didn't have time to troubleshoot (TypeError: kube.Client is not a constructor etc), had to drop it for now. My Javascript skills are not strong enough to be able to figure it out.

Just adding my comment so it's known there's a real world need for this (it'd make a great blog post for someone, just saying! :smile: )

tobinski commented 5 years ago

With some minor modification the snipped is working for me. You need to define the scope in auth.getClient({ scopes: 'https://www.googleapis.com/auth/cloud-platform' })

darweshsinghcgi commented 5 years ago

This would be incredibly useful if it were integrated in. I'm currently using kubernetes-client in App Engine. I'll give the snippet a try and see if it works.

darweshsinghcgi commented 5 years ago

Here is code I used to get authentication to work locally (I did rm ~/.kube/config before trying this to make sure):

const {auth} = require('google-auth-library')
const container = require('@google-cloud/container')
const Client = require('kubernetes-client').Client
const config = require('kubernetes-client').config

let clusterClient = new container.v1.ClusterManagerClient({})

async function getCluster () {
  try {
    let request = {
      name: '/PROJECT/-/ZONE/-/CLUSTER/'
    }
    let responses = await clusterClient.getCluster(request).catch((err) => {
      console.error(err)
    })
    let response = responses[0]
    return new Promise(resolve => {
      resolve(response)
    })
  } catch (err) {
    console.error(err)
  }
}

async function getToken () {
  try {
    let authResult = await auth.getClient({ scopes: 'https://www.googleapis.com/auth/cloud-platform' }).catch((err) => {
      console.error(err)
    })
    let accessToken = await authResult.getAccessToken().catch((err) => {
      console.error(err)
    })
    let token = accessToken.token
    return new Promise(resolve => {
      resolve(token)
    })
  } catch (err) {
    console.error(err)
  }
}

async function getClient (cluster, token) {
  try {
    let kubeConfig = {
      apiVersion: 'v1',
      kind: 'Config',
      preferences: {},
      'current-context': 'a',
      contexts: [
        {
          name: 'a',
          context: {
            cluster: 'a',
            user: 'a'
          }
        }
      ],
      clusters: [
        {
          name: 'a',
          cluster: {
            'certificate-authority-data': cluster.masterAuth.clusterCaCertificate,
            server: `https://${cluster.endpoint}`
          }
        }
      ],
      users: [
        {
          name: 'a',
          user: {
            'auth-provider': {
              config: {
                'access-token': token,
                'cmd-args': 'config config-helper --format=json',
                'cmd-path': '/google/google-cloud-sdk/bin/gcloud',
                'token-key': '{.credential.access_token}'
              },
              name: 'gcp'
            }
          }
        }
      ]
    }
    let client = new Client({ config: config.fromKubeconfig(kubeConfig)})
    let result = await client.loadSpec()
    return new Promise(resolve => {
      resolve(result)
    })
  } catch (err) {
    console.error(err)
  }
}

async function authenticatedClient () {
  try {
    let cluster = await getCluster()
    let token = await getToken()
    let newClient = await getClient(cluster, token)

    return new Promise(resolve => {
      resolve(newClient)
    })
  } catch (err) {
    console.error(err)
  }
}

async function main() {
  try {
    let client = await authenticatedClient()
    let nodes = await client.api.v1.nodes.get()
    console.log(nodes)
  } catch (err) {
    console.error(err)
  }
}

main()
garyo commented 4 years ago

I'll just chime in here -- I need this exact thing. Using GCF as a back end for my web app, and need to trigger a long-running GKE Kubernetes job from my function. I'll try @darweshsinghcgi 's solution, but it would be great if this were integrated.

parth-thakkar commented 4 years ago

Would it be the same authentication mechanism in case of GKE cluster is private ?

lakhansamani commented 4 years ago

@parth-thakkar yes it would be same mechanism

lakhansamani commented 4 years ago

@gsf thank you for the solution this was really handy ❤️