Fullmenu null

 

09 March 2018

Seguramente este caso de uso no será muy común pero espero que te aporte ideas si trabajas con BitBucket

Hace unos días en B2Boost estabamos intentando executar un pipeline de integración contínua para un proyecto que dependía de otros y necesitaba clonarlos primeramente para construir el producto.

Sin embargo surgió un problema cuando nos dimos cuenta que necesitabamos configurar todos los proyectos involucrados y añadirle una clave ssh común para poder clonarlos. En un escenario ideal esto hubiera sido tan simple como ubicar esta clave a nivel de Organización y todos los proyectos que dependen de ella quedarían configurados a su vez. Pero BitBucket no permite usar una misma clave ssh en dos proyectos y/o organizaciones a la vez y por razones históricas nosotros la estabamos usando en algunos repositorios. El problema era que no sabíamos en cuales

Así pues, teníamos dos opciones:

  • generar una nueva clave ssh desde nuestro sistema de CI y añadirla a nivel de organización

  • buscar qué proyectos usaban la clave y eliminarla

La primera opción probablemente era la más rápida pero se nos quedarían repositorios con claves sin usar y además corríamos el riesgo de que otros proyectos se vieran afectados.

La segunda opción era fácil y limpia pero tediosa porque conllevaba navegar entre los repos buscando la clave en los settings de cada uno …​ y eran más de 45

Rest al rescate

BitBucket tiene un interface REST (https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/) con dos versiones (/1.0 and /2.0) con las que puedes manejar tus repositorios tras haber obtenido un token de autentificación.

Si consultas el manual verás que puedes usar herramientas de línea como curl pero implementar toda la lógica que necesitamos en un bash puede convertirlo en dificil de entender, depurar y reutilizar. Pero con Groovy y HttpBuilder-ng lo puedes hacer en un script de menos de 50 lineas

OAuth Consumer

En primer lugar necesitas crear un Oauth Consumer en tu cuenta de BitBucket (Profile, Settings, Oauth)

Después de crearlo obtendrás un Key y un Secret que se lo proporcionaremos al script vía argumentos de línea junto con el nombre de nuestra organización

Dependencias

Sólo vamos a necesitar http-builder y slf4j:

@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.3')
@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')
import groovyx.net.http.*
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson

Autorización

Esta parte fue la más difícul, no por sus detalles técnicos sino porque el mecanismo de autentificación usado por HttpBuilder-NG no es compatible 100% con BitBucket.

HttpBulder-Ng tiene el método basic in the auth para enviar la cabecera Authorization: Basic xxxx pero sólo lo envía si el servidor lo requiere y BitBucket lo requiere en la primera petición.

La solución pasa por construir nosotros la cabecera de forma manual codificando el usuario y la password en base64 (donde usuario y password son el Key`y el `Secret). Así mismo tenemos que enviar la petición en formato form porque no hemos conseguido que BitBucket reconozca esta petición como JSON

organization = args[0] // organization
username = args[1]  //secretId
password = args[2]  //token

creds = "$username:$password".bytes.encodeBase64()

def http = configure {
    request.uri = 'https://bitbucket.org/'
    request.headers['Authorization'] = "Basic $creds"
    request.contentType = JSON[0]
}

def token = http.post{
    request.uri.path='/site/oauth2/access_token'
    request.body=[grant_type:'client_credentials']  //(1)
    request.contentType = 'application/x-www-form-urlencoded'
    request.encoder 'application/x-www-form-urlencoded', NativeHandlers.Encoders.&form
}.access_token
  1. send the grant_type as form because BitBucket doesn’t reconigze JSON in this request

BitBucket

Una vez obtenido un token podemos configurar un objecto bitbucket que usaremos a lo largo del script con los parámetros comunes a todas las peticiones:

def bitbucket = configure {
    request.uri = 'https://api.bitbucket.org/'
    request.headers['Authorization'] = "Bearer $token"
    request.accept=['application/json']
    request.contentType = JSON[0]
}

Listar repositorios

Podemos obtener los detalles de todos nuestros repos usando una petición paginada. Cada repo obtenido contiene gran cantidad de información de la que nosotros vamos a usar únicamente 'slug' como identificador del repo

def page=1
def repos=[]
while( true ) {
    def list = bitbucket.get{
        request.uri.path="/2.0/repositories/$organization"
        request.uri.query=[page:page]
    }
    repos.addAll list.values
    if( repos.size() >= list.size )
        break
    page++
}

Inspeccionar un repo

def keys=[:]    //(1)
repos.findAll{it.slug}.each{  repo->
    def repokeys = bitbucket.get{
        request.uri.path="/1.0/repositories/$organization/${repo.slug}/deploy-keys"
    }
    keys[repo.slug]=repokeys
}
  1. construimos un mapa con valores tipo repo-name:json

Report

Una vez obtenidos todos los repos iteraremos por todos ellos haciendo una petición en busca de las claves ssh que tiene y si alguna es el ID que buscamos

println "Total repos founded : ${repos.size()}"
println "Total keys founded : ${keys.size()}"
println prettyPrint(toJson(keys))

def remove = keys.findAll{ it.value.find{ it.key.indexOf('XXXXXXXXXXXXXXXXXXX')!=-1 } } //(1)
println "Remove: ${remove*.key}"
  1. filtrar claves que contienen nuestro ID

Conclusion

Tras ejecutar el script encontramos que sólo unos pocos repos estaban configurados con esta clave ssh pero sin el script probablemente hubieramos perdido una cantidad de tiempo importante, navegando por todos ellos corriendo el riesgo de olvidar alguno lo que nos obligaría a volver a comenzar


Script
//tag::dependencies[]
@Grab(group='io.github.http-builder-ng', module='http-builder-ng-apache', version='1.0.3')
@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')
import groovyx.net.http.*
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson
//end::dependencies[]

//tag::login[]
organization = args[0] // organization
username = args[1]  //secretId
password = args[2]  //token

creds = "$username:$password".bytes.encodeBase64()

def http = configure {
    request.uri = 'https://bitbucket.org/'
    request.headers['Authorization'] = "Basic $creds"
    request.contentType = JSON[0]
}

def token = http.post{
    request.uri.path='/site/oauth2/access_token'
    request.body=[grant_type:'client_credentials']  //(1)
    request.contentType = 'application/x-www-form-urlencoded'
    request.encoder 'application/x-www-form-urlencoded', NativeHandlers.Encoders.&form
}.access_token
//end::login[]

//tag::bitbucket[]
def bitbucket = configure {
    request.uri = 'https://api.bitbucket.org/'
    request.headers['Authorization'] = "Bearer $token"
    request.accept=['application/json']
    request.contentType = JSON[0]
}
//end::bitbucket[]

//tag::repos[]
def page=1
def repos=[]
while( true ) {
    def list = bitbucket.get{
        request.uri.path="/2.0/repositories/$organization"
        request.uri.query=[page:page]
    }
    repos.addAll list.values
    if( repos.size() >= list.size )
        break
    page++
}
//end::repos[]

//tag::v10[]
def keys=[:]    //(1)
repos.findAll{it.slug}.each{  repo->
    def repokeys = bitbucket.get{
        request.uri.path="/1.0/repositories/$organization/${repo.slug}/deploy-keys"
    }
    keys[repo.slug]=repokeys
}
//end::v10[]

//tag::report[]
println "Total repos founded : ${repos.size()}"
println "Total keys founded : ${keys.size()}"
println prettyPrint(toJson(keys))

def remove = keys.findAll{ it.value.find{ it.key.indexOf('XXXXXXXXXXXXXXXXXXX')!=-1 } } //(1)
println "Remove: ${remove*.key}"
//end::report[]