<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>Jorge Aguilera</title>
    <link href="https://blog.jagedn.dev/"/>
    <link rel="self" type="application/atom+xml" href="https://blog.jagedn.dev/feed.xml"/>
    <subtitle>Open source Open mind</subtitle>
    <updated>2026-06-11T08:37:31Z</updated>
    <id>tag:blog.jagedn.dev,2026:06</id>
    <entry>
        <title>FediPhoto III</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/fediphoto-iii.html"/>
        <updated>2026-06-11T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/fediphoto-iii.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <category term="fediverso"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta serie de artículos voy a explicar cómo funciona &quot;FediPhoto&quot;, una aplicación Java hecha desde cero y (casi)
sin dependencias que se integra en el Fediverso, a.k.a. Mastodon usando el protocolo ActivityPub&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este tercer post vamos a centrarnos en implementar un endpoint que permita a cualquier instancia del fediverso
conocer los detalles de los usuarios de la nuestra&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Parte 1: Introducción &lt;a href=&quot;fediphoto-i.html&quot; class=&quot;bare&quot;&gt;fediphoto-i.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Parte 2: Business &lt;a href=&quot;fediphoto-ii.html&quot; class=&quot;bare&quot;&gt;fediphoto-ii.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;fediphoto_core&quot;&gt;FediPhoto Core&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya vimos en el anterior post, FediPhoto no usa ninguna base de datos y simplemente usa las carpetas que se
creen manualmente a partir de una carpeta configurada como root. Cada carpeta representa el nombre de un usuario
y al arrancar la aplicación comprueba si el usuario tiene un par de claves generadas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Esta aplicación es un ejemplo y NO se deberían dejar las claves de forma tan accesible&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;webfinger&quot;&gt;Webfinger&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;&quot;WebFinger es un protocolo especificado por el IETF para el descubrimiento de información sobre personas y entes. La información puede ser descubierta por medio de un URI &quot;acct:&quot;, que es un URI similar a una dirección de correo electrónico.&quot;
https://es.wikipedia.org/wiki/WebFinger&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente es un endpoint HTTP que devuelve un json con un formato específico sobre el usuario &quot;acct&quot; sobre el que
se consulta&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;webfingercontroller&quot;&gt;WebfingerController&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como FediPhoto es una aplicación super simple implementa Webfinger en un simple Controller&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/activitypub/WebfingerController.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Controller(&quot;/.well-known/webfinger&quot;)
public class WebfingerController {

    private final String domain;
    private final UsersRepository usersRepository;

    public WebfingerController(@Value(&quot;${fediphoto.domain}&quot;) String domain, UsersRepository usersRepository) {
        this.domain = domain;
        this.usersRepository = usersRepository;
    }

    @Get
    public HttpResponse&amp;lt;?&amp;gt; webfinger(@QueryValue(&quot;resource&quot;) String resource) {
        if (resource == null || !resource.startsWith(&quot;acct:&quot;)) {
            return HttpResponse.badRequest();
        }

        String username = resource.replace(&quot;acct:&quot;, &quot;&quot;).split(&quot;@&quot;)[0];

        var user = usersRepository.getUser(username);
        if (user == null) {
            return HttpResponse.notFound();
        }

        var selfLink = new WebfingerLink(
                &quot;self&quot;,
                &quot;application/activity+json&quot;,
                &quot;https://&quot; + domain + &quot;/users/&quot; + username
        );

        return HttpResponse.ok(new WebfingerResponse(
                resource,
                List.of(selfLink)
        ));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usamos un par de DTO  &lt;code&gt;WebfingerResponse&lt;/code&gt; y  &lt;code&gt;WebfingerLink&lt;/code&gt; para que Micronaut lo renderize como JSON&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/activitypub/model/WebfingerLink.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Serdeable
public record WebfingerLink(
        String rel,
        String type,
        String href
) {}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/activitypub/model/WebfingerResponse.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Serdeable
public record WebfingerResponse(
        String subject,
        List&amp;lt;WebfingerLink&amp;gt; links) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejemplo&quot;&gt;Ejemplo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así si nuestra instancia tien un usuario &lt;code&gt;demo&lt;/code&gt; (representado como una carpeta &quot;demo&quot; bajo la carpeta definida como raiz) podremos consultar los detalles del usuario:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;&lt;a href=&quot;https://fediphoto.jagedn.dev/.well-known/webfinger?resource=acct:demo@fediphoto.jagedn.dev&quot; class=&quot;bare&quot;&gt;https://fediphoto.jagedn.dev/.well-known/webfinger?resource=acct:demo@fediphoto.jagedn.dev&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;obtendremos un JSON similar a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;{
  &quot;subject&quot;: &quot;acct:demo@fediphoto.jagedn.dev&quot;,
  &quot;links&quot;: [
    {
      &quot;rel&quot;: &quot;self&quot;,
      &quot;type&quot;: &quot;application/activity+json&quot;,
      &quot;href&quot;: &quot;https://fediphoto.jagedn.dev/users/demo&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes observar &quot;links&quot; es un array de enlaces, cada uno con un &quot;type&quot;. Las instancias que quieren
consultar información sobre nuestros usuarios filtrarán buscando el &quot;type&quot; &quot;application/activity+json&quot; y de ahí
obtendrán la URL del usuario&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;userscontroller&quot;&gt;UsersController&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último nos toca implementar un controller que devuelve la información específica del usuario.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El controller en este caso es tan simple como buscar el usuario que se solicita en la url en nuestra &quot;base de datos&quot;
y devolver un &quot;Actor&quot; con los detalles del usuario, como el nombre, la URL a su inbox y su clave pública:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/activitypub/UsersController.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Controller(&quot;/users&quot;)
public class UsersController {

    private final String domain;
    private final UsersRepository usersRepository;

    public UsersController(@Value(&quot;${fediphoto.domain}&quot;) String domain, UsersRepository usersRepository) {
        this.domain = domain;
        this.usersRepository = usersRepository;
    }

    @Get(value = &quot;/{username}&quot;, produces = &quot;application/activity+json&quot;)
    public HttpResponse&amp;lt;?&amp;gt; getActor(String username) {

        var user = usersRepository.getUser(username);
        if (user == null) {
            return HttpResponse.notFound();
        }
        var publicKeyRecord = new PublicKeyRecord(
                &quot;https://&quot; + domain + &quot;/users/&quot; + username + &quot;#main-key&quot;,
                &quot;https://&quot; + domain + &quot;/users/&quot; + username,
                user.pubKey()
        );
        var actor = new Actor(
                &quot;https://www.w3.org/ns/activitystreams&quot;,
                &quot;Person&quot;,
                &quot;https://&quot; + domain + &quot;/users/&quot; + username,
                username,
                &quot;https://&quot; + domain + &quot;/users/&quot; + username + &quot;/inbox&quot;,
                &quot;https://&quot; + domain + &quot;/users/&quot; + username + &quot;/outbox&quot;,
                publicKeyRecord);

        return HttpResponse.ok(actor);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Actor&lt;/code&gt; y &lt;code&gt;PublicKeyRecord&lt;/code&gt; son a su vez dos DTO que nos sirven para que Micronaut renderize el JSON correcto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/activitypub/model/PublicKeyRecord.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Serdeable
public record PublicKeyRecord(
String id,
String owner,
String publicKeyPem
) {}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/activitypub/model/Actor.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Serdeable
public record Actor(
    @JsonProperty(&quot;@context&quot;) Object context, // Puede ser String o List
    String type,
    String id,
    String preferredUsername,
    String inbox,
    String outbox,
    PublicKeyRecord publicKey
) {}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente, remarcar que el atributo &quot;context&quot; debe renderizarse como &quot;@context&quot; y por eso usamos JsonProperty&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejemplo_2&quot;&gt;Ejemplo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así nuestra aplicación es capaz de proporcionar información al exterior y devolvería algo como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;{
  &quot;@context&quot;: &quot;https://www.w3.org/ns/activitystreams&quot;,
  &quot;type&quot;: &quot;Person&quot;,
  &quot;id&quot;: &quot;https://fediphoto.jagedn.dev/users/demo&quot;,
  &quot;preferredUsername&quot;: &quot;demo&quot;,
  &quot;inbox&quot;: &quot;https://fediphoto.jagedn.dev/users/demo/inbox&quot;,
  &quot;outbox&quot;: &quot;https://fediphoto.jagedn.dev/users/demo/outbox&quot;,
  &quot;publicKey&quot;: {
    &quot;id&quot;: &quot;https://fediphoto.jagedn.dev/users/demo#main-key&quot;,
    &quot;owner&quot;: &quot;https://fediphoto.jagedn.dev/users/demo&quot;,
    &quot;publicKeyPem&quot;: &quot;-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh...ePIOfL7ZLOR\n/QIDAQAB\n-----END PUBLIC KEY-----&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con esta información &quot;mínima&quot; una instancia remota puede por un lado mostrar el perfil del usuario &quot;demo&quot; así como
tener la información necesaria para interactuar con él (a través del endpoint &quot;inbox&quot;) y validar los mensajes
que este usuario emita (validando la firma usando la publicKeyPem)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>FediPhoto: una aplicación java de ejemplo para el fediverso</summary>
    </entry>
    <entry>
        <title>FediPhoto II</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/fediphoto-ii.html"/>
        <updated>2026-06-10T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/fediphoto-ii.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <category term="fediverso"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta serie de artículos voy a explicar cómo funciona &quot;FediPhoto&quot;, una aplicación Java hecha desde cero y (casi)
sin dependencias que se integra en el Fediverso, a.k.a. Mastodon usando el protocolo ActivityPub&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este segundo post vamos a centrarnos en la parte &quot;pura&quot; de la aplicación sin llegar a entrar en las
funcionalidades de ActivityPub para poder tener un &quot;producto&quot; sobre el que trabajar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Parte 1: Introducción &lt;a href=&quot;fediphoto-i.html&quot; class=&quot;bare&quot;&gt;fediphoto-i.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;fediphoto_core&quot;&gt;FediPhoto Core&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya vimos en el anterior post, FediPhoto no usa ninguna base de datos y simplemente usa las carpetas que se
creen manualmente a partir de una carpeta configurada como root. Cada carpeta representa el nombre de un usuario
y al arrancar la aplicación comprueba si el usuario tiene un par de claves generadas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Esta aplicación es un ejemplo y NO se deberían dejar las claves de forma tan accesible&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;repository&quot;&gt;Repository&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Siguiendo las buenas prácticas, aunque la aplicación no use una base de datos vamos a crear un User y un UserRepository
que nos sirvan para abstraer la funcionalidad&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/data/User.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package com.incsteps.fediphoto.data;

public record User(
        String username,
        String pubKey,
        String privKey) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/data/UsersRepository.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package com.incsteps.fediphoto.data;

import java.util.List;


public interface UsersRepository {

    List&amp;lt;User&amp;gt; getUsers();

    User getUser(String username);

    void setKey(String username, String publicKey, String privateKey);

    void addFollower(User user, String followerUrl);

    void removeFollower(User user, String followerUrl);

    List&amp;lt;String&amp;gt; getFollowerUrls(String username);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y la implementación basada en fichero&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/data/file/FileUsersRepository.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Singleton
public class FileUsersRepository implements UsersRepository {

    private final String usersDir;

    public FileUsersRepository(@Value(&quot;${fediphoto.users}&quot;) String usersDir) throws IOException {
        this.usersDir = usersDir;
        Files.createDirectories(Path.of(usersDir));
    }

    public List&amp;lt;User&amp;gt; getUsers() {
        return Arrays.stream(
                Objects.requireNonNull(new File(this.usersDir).listFiles(File::isDirectory))
        )
                .map(this::fromDir)
                .toList();
    }

    @Override
    public User getUser(String username) {
        var dir = new File(this.usersDir, username);
        if(!dir.exists()) return null;
        return fromDir(dir);
    }

    @Override
    public void setKey(String username, String publicKey, String privateKey) {
        saveFile(username, &quot;public.key&quot;, publicKey);
        saveFile(username, &quot;private.key&quot;, privateKey);
    }
        @Override
    public synchronized void addFollower(User user, String followerUrl) {
        var followers = readFile(user.username(), &quot;followers.txt&quot;);
        if( followers == null) followers = &quot;&quot;;
        followers += followerUrl+&quot;\n&quot;;
        saveFile(user.username(), &quot;followers.txt&quot;, followers+&quot;\n&quot;);
    }

    @Override
    public synchronized void removeFollower(User user, String followerUrl) {
        var followers = readFile(user.username(), &quot;followers.txt&quot;);
        if( followers == null) followers = &quot;&quot;;
        var list = Arrays.stream(followers.split(&quot;\n&quot;)).filter(f -&amp;gt; !f.equals(followerUrl)).toList();
        saveFile(user.username(), &quot;followers.txt&quot;, String.join(&quot;\n&quot;, list));
    }

    @Override
    public List&amp;lt;String&amp;gt; getFollowerUrls(String username) {
        var user = getUser(username);
        if( user == null) return List.of();
        var followers = readFile(user.username(), &quot;followers.txt&quot;);
        return List.of(followers.split(&quot;\n&quot;));
    }

    private User fromDir(File dir) {
        var publicKey = getPublicKey(dir.getName());
        var privKey = getPrivateKey(dir.getName());
        return new User(dir.getName(), publicKey, privKey);
    }

    protected String getPublicKey(String username) {
        return readFile(username,  &quot;public.key&quot;);
    }

    protected String getPrivateKey(String username) {
        return readFile(username,  &quot;private.key&quot;);
    }

    protected String readFile(String username, String fileName) {
        var file = new File(usersDir+File.separator+username,  fileName);
        if (!file.exists()) {
            return null;
        }
        try {
            return String.join(&quot;\n&quot;, Files.readAllLines(file.toPath()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    protected void saveFile(String username, String fileName, String content) {
        var file = new File(usersDir+File.separator+username,  fileName);
        try {
            Files.write(file.toPath(), content.getBytes());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se puede ver el repository usa una carpeta root y para cada usuario maneja un fichero de
texto &lt;code&gt;followers.txt&lt;/code&gt; y un par &lt;code&gt;public.key&lt;/code&gt; &lt;code&gt;private.key&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;crypt&quot;&gt;Crypt&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para generar las claves de un usuario vamos a usar un Singleton sencillo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/crypt/CryptService.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Singleton
public class CryptService  implements ApplicationEventListener&amp;lt;StartupEvent&amp;gt; {

    private final UsersRepository usersRepository;

    public CryptService(UsersRepository usersRepository) {
        this.usersRepository = usersRepository;
    }

    @Override
    public void onApplicationEvent(StartupEvent event) {
        for(var user : usersRepository.getUsers()){
            if( user.privKey()==null || user.pubKey()==null){
                var keys = generateUserKeyPair();
                usersRepository.setKey(user.username(), keys[0], keys[1]);
            };
        }
    }

    private String[] generateUserKeyPair() {
        try {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance(&quot;RSA&quot;);
            kpg.initialize(2048);
            KeyPair kp = kpg.generateKeyPair();

            String[] result = new String[2];
            result[0] = formatKey(kp.getPublic().getEncoded(), &quot;PUBLIC&quot;);
            result[1] = formatKey(kp.getPrivate().getEncoded(), &quot;PRIVATE&quot;);

            return result;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(&quot;Error: No se encontró el algoritmo RSA&quot;, e);
        }
    }

    private String formatKey(byte[] encoded, String type) {
        String base64 = Base64.getMimeEncoder(64, new byte[]{&apos;\n&apos;}).encodeToString(encoded);
        return String.format(&quot;-----BEGIN %s KEY-----\n%s\n-----END %s KEY-----&quot;,
                type, base64, type);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es un singleton que al levantar la aplicación busca los usuarios y genera un par de claves para los que no las
tengan creadas todavia&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usa clases incluídas en el JDK estándar para crear claves RSA&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;business&quot;&gt;Business&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La lógica de negocio del aplicativo es bien simple. Escanea cada X segundos las carpetas de los usuarios y
si existe alguna foto nueva publicará una &quot;activity&quot; en la red del usuario (una notificación a cada follower)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/business/ImageService.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Singleton
public class ImageService {
    private static final Logger LOG = LoggerFactory.getLogger(ImageService.class);

    private final String usersDir;
    private final UsersRepository usersRepository;
    private final FederationService federationService;

    ImageService(@Value(&quot;${fediphoto.users}&quot;) String usersDir,
                 FederationService federationService,
                 UsersRepository usersRepository) {
        this.usersDir = usersDir;
        this.usersRepository = usersRepository;
        this.federationService = federationService;
    }

    @Scheduled(fixedDelay = &quot;30s&quot;)
    void scanForImages() {
        for (var user : usersRepository.getUsers()) {
            scanForUserImages(user);
        }
    }

    void scanForUserImages(User user) {
        File inputDir = new File(usersDir+File.separator+user.username(), &quot;input&quot;);
        if (!inputDir.exists()) inputDir.mkdirs();

        File[] files = inputDir.listFiles((dir, name) -&amp;gt;
                name.toLowerCase().endsWith(&quot;.png&quot;) || name.toLowerCase().endsWith(&quot;.jpg&quot;));

        if (files != null) {
            for (File file : files) {
                publishAndProcess(user.username(), file);
            }
        }
    }

    private void publishAndProcess(String username, File file) {
        try {
            LOG.info(&quot;Imagen detectada: {}&quot;, file.getName());

            File processedDir = new File(usersDir+File.separator+username, &quot;processed&quot;);
            if (!processedDir.exists()) processedDir.mkdirs();

            String now = new SimpleDateFormat(&quot;yyyyMMdd-HHmmss&quot;).format(new Date());
            File dest = new File(processedDir + &quot;/&quot; + now + &quot;.png&quot;);
            Files.move(file.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);

            publishToFediverse(username, dest);

            LOG.info(&quot;Imagen procesada y movida a: {}&quot;, dest.getPath());
        } catch (Exception e) {
            LOG.error(&quot;Error procesando {}&quot;, file.getName(), e);
        }
    }

    private void publishToFediverse(String username, File file) {
        federationService.sendNewPhoto(username, file.getName());
    }

    public byte[]getImage(String username, String image) throws IOException {
        File processedDir = new File(usersDir+File.separator+username, &quot;processed&quot;);
        if (!processedDir.exists()) processedDir.mkdirs();
        File dest = new File(processedDir + &quot;/&quot; + image);
        if( !dest.exists()){
            return null;
        }
        return Files.readAllBytes(dest.toPath());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El servicio tiene una dependencia con FederationService que aún no hemos visto&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se puede ver el servicio es super sencillo y lo único que hace es para cada imagen encontrada
la mueve de &quot;input&quot; a &quot;processed&quot; para poder ser vista via navegador.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez ejecutado la &quot;lógica de negocio&quot; se invoca al federationService para que notifique a los followers&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;controller&quot;&gt;Controller&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para que un usuario externo pueda ver las imagenes de los usuarios del aplicativo vamos a crear un Controller&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/java/com/incsteps/fediphoto/business/ImageController.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Controller(&quot;/images&quot;)
public class ImageController {

    private final ImageService imageService;

    public ImageController(ImageService imageService) {
        this.imageService = imageService;
    }

    @Get(&quot;{username}/{image}&quot;)
    @Produces(MediaType.IMAGE_PNG)
    public HttpResponse&amp;lt;?&amp;gt; getImage(String username, String image) {
        try{
            var bytes = imageService.getImage(username,image);
            return HttpResponse.ok(bytes);
        }catch (Exception e){
            return HttpResponse.notFound();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En una aplicación más completa el UI será más complejo, a lo mejor una aplicación cliente javascript llamando
a un API Rest, o usando Views de Micronaut crear un interface completo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En nuestro caso el UI es simplemente un endpoint que recibe un Http Get &quot;/{username}{id}&quot; para buscar la foto a mostrar&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>FediPhoto: una aplicación java de ejemplo para el fediverso</summary>
    </entry>
    <entry>
        <title>FediPhoto I</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/fediphoto-i.html"/>
        <updated>2026-06-09T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/fediphoto-i.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <category term="fediverso"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta serie de artículos voy a explicar cómo funciona &quot;FediPhoto&quot;, una aplicación Java hecha desde cero y (casi)
sin dependencias que se integra en el Fediverso, a.k.a. Mastodon usando el protocolo ActivityPub&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;fediverso&quot;&gt;Fediverso&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Esto es un resumen muy particular&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El llamado Fediverso es un conjunto de aplicaciones distribuidas que &quot;hablan&quot; un mismo protocolo abierto. Estas
aplicaciones, en el 99%, se orientan a formar redes sociales donde un usuario con cuenta en una de ellas
puede interactuar con usuarios en otras aplicaciones&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los casos de aplicaciones más conocidas en el fediverso pueden ser Mastodon, una aplicación de microblogging tipo
Twitter (paso de llamarle X) o Peertube una aplicación orientada a la publicación de vídeos similar a Youtube&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como estas aplicaciones usan el protocolo ActivityPub pueden dialogar entre sí de tal forma que un usuario de una
de ellas puede seguir a un usuario en la otra y ver en su timeline la actividad de este&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;activitypub&quot;&gt;ActivityPub&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El protocolo ActivityPub, implementado por estas aplicaciones, es el que especifica el qué y el cómo
tienen que enviarse actividades las aplicaciones para los usuarios de cada una pueda interactuar con las de otras.
Todo ello de forma abierta y siguiendo las especificaciones del W3C&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dicho de forma burda, el protocolo especifica de qué forma un usuario en una instancia Mastodon, por ejemplo,
puede obtener información sobre otro usuario en otra instancia, tanto Mastodon como Peertube por ejemplo, y cómo
estos dos servidores (en nombre de esos usuarios) se intercambian &quot;actividades&quot; de sus usuarios&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ActivityPub se basa en conceptos muy simples:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;sólo especifica la publicación de actividades. NO en cómo transmitir o visualizar el resultado de esta actividad&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;las partes de una activity son: un actor, un objeto y una acción o verbo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el descubrimiento de usuarios usa la spec Webfinger (la veremos en otro post)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;los mensajes de un usuario entre instancia van firmados con las claves publico-privadas de ese usuario&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;fediphoto&quot;&gt;FediPhoto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esa serie vamos a ir viendo cómo desarrollar una aplicación super-super-super-simple en Java que se integre
en el Fediverso, de tal forma que cualquier usuario podrá:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;descubrir los detalles de un usuario de esta instancia&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;seguir (y abandonar) a este usuario&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;recibir una activity en su timeline cuando el usuario publica una foto&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por su parte, el usuario &quot;propietario&quot; de la instancia podrá simplemente publicar fotos copiándola en un directorio
&quot;input&quot; y el sistema se encargará de notificar que hay una nueva foto a sus followers de forma automática&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;stack&quot;&gt;Stack&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mi framework favorito de Java es de hace unos años Micronaut por lo que lo usaremos para llegar a construir un binario Linux mediante GraalVM&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo mi build tool preferida es Gradle (por encima de Maven). En principio sería fácil usar Maven simplemente
teniendo en cuenta los puntos que detallaré a continuación&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La aplicación es super sencilla por lo que no usará ni base de datos, sólo carpetas y ficheros locales.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tampoco tendrá un interface web.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para comenzar usaremos el starter kit que ofrece Micronaut y crearemos un proyecto simple sin ninguna &quot;feature&quot;
especial&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez descomprimido el zip en una carpeta de trabajo, revisaremos el build.gradle prestando atención a estas partes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;dependencies {
    annotationProcessor(&quot;io.micronaut:micronaut-http-validation&quot;)
    annotationProcessor(&quot;io.micronaut.openapi:micronaut-openapi&quot;)
    annotationProcessor(&quot;io.micronaut.openapi:micronaut-openapi-adoc&quot;)
    annotationProcessor(&quot;io.micronaut.serde:micronaut-serde-processor&quot;)
    implementation(&quot;io.micronaut.serde:micronaut-serde-jackson&quot;)
    implementation(&quot;io.micronaut:micronaut-http-client&quot;)
    compileOnly(&quot;io.micronaut.openapi:micronaut-openapi-annotations&quot;)
    runtimeOnly(&quot;ch.qos.logback:logback-classic&quot;)
    testImplementation(&quot;io.micronaut:micronaut-http-client&quot;)

    implementation(&quot;org.tomitribe:tomitribe-http-signatures:1.1&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Incluiremos la ultima dependencia para que nos facilite la parte de generar la firma http, por lo demás como se
puede ver las dependencias son mínimas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para poder interactuar e incluso depurar la aplicación en desarrollo tenemos que poder acceder
desde &quot;fuera&quot; y además vía SSL. Yo uso ngrok que me crea un túnel entre mi máquina e internet&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;application.properties&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;fediphoto.domain=tu-dns.ngrok-free.app
fediphoto.users=data/users&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En domain tienes que poner el dns que te haya asignado Ngrok o el proxy que uses&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;próximos_pasos&quot;&gt;Próximos pasos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En los siguientes artículos veremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;el core, que inspecciona automaticamente las carpetas de usuarios y crea las claves publico/privada para
cada uno, así como las carpetas de cada uno para detectar si hay que publicar una foto&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el descubrimiento de usuarios&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el Inbox/Outbox de usuarios&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Recibiendo un Follow por usuario y guardándola&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enviando una notificación a los followers&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>FediPhoto: una aplicación java de ejemplo para el fediverso</summary>
    </entry>
    <entry>
        <title>K3s Networking: Solving the Source IP SNAT Issue</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/k3s-real-ip.html"/>
        <updated>2026-05-11T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/k3s-real-ip.html</id>
        <category term="k3"/>
        <category term="kubernetes"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;This article was written entirely by Google Gemini and supervised by a human to ensure technical
accuracy and real-world applicability. It serves as a testament to how AI can assist developers in documenting complex infrastructure
challenges and sharing solutions with the global community.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;issue&quot;&gt;Issue&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When transitioning a K3s cluster from a combined &apos;all-in-one&apos; node to a dedicated Control Plane and Agent architecture, many developers encounter a frustrating hurdle: the loss of the client&amp;#8217;s original source IP address. Suddenly, every request to your backend appears to originate from the cluster’s internal node IP, breaking geolocation, rate-limiting, and logging.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This post explores why Kubernetes&apos; default networking behavior (SNAT) masks the source IP during inter-node hops and provides a step-by-step guide to fixing it. We’ll dive into configuring Traefik as a DaemonSet, leveraging hostNetwork mode, and adjusting externalTrafficPolicy to ensure your application sees the real user behind the request, not just the cluster&amp;#8217;s internal proxy.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the_problem_the_mysterious_node_ip&quot;&gt;The Problem: The &quot;Mysterious&quot; Node IP&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Everything was working perfectly. My K3s cluster was a single-node powerhouse where the Control Plane and Agent roles lived together. My Node.js backend accurately logged client IPs using X-Forwarded-For.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Then, I decided to grow. I separated the roles: one dedicated Control Plane and one Agent node.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Immediately, my logs broke. Instead of seeing the real user&amp;#8217;s IP, every single request appeared to come from the internal IP of the Control Plane node. If you’ve ever tried to implement rate-limiting or geolocation, you know this is a nightmare.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;why_did_the_ip_disappear&quot;&gt;Why did the IP disappear?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In a standard Kubernetes setup, when traffic hits a node that doesn&amp;#8217;t host the destination Pod, the cluster uses Source Network Address Translation (SNAT) to route the packet to the correct node.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When my setup was a single node, the traffic hit the node and stayed there. No hops, no SNAT. Once I moved the Pod to a separate Agent node, the Control Plane had to &quot;forward&quot; the traffic. To ensure the response could find its way back, the Control Plane replaced the Client&amp;#8217;s IP with its own.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the_solution_bringing_traefik_to_the_edge&quot;&gt;The Solution: Bringing Traefik to the Edge&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To fix this, we need to ensure that the entry point (Traefik) is present on every node and that it talks directly to the host&amp;#8217;s network.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;transform_traefik_into_a_daemonset&quot;&gt;Transform Traefik into a DaemonSet&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By default, K3s installs Traefik as a Deployment. This means it might only run on one node. If traffic hits Node A but Traefik is on Node B, you get SNAT.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By changing it to a DaemonSet, we ensure a Traefik instance runs on every single node, including your Control Plane.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;bypass_the_nat_with_hostnetwork&quot;&gt;Bypass the NAT with HostNetwork&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We want Traefik to listen directly on the host&amp;#8217;s ports (80 and 443) rather than through the Kubernetes virtual networking proxy. This allows Traefik to see the &quot;wire&quot; IP of the packet before Kubernetes touches it.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;set_the_local_traffic_policy&quot;&gt;Set the Local Traffic Policy&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We need to tell the Service to only route traffic to pods on the same node that received the request. This preserves the source IP because it eliminates the inter-node hop.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the_configuration&quot;&gt;The Configuration&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In K3s, the cleanest way to apply this is by creating a HelmChartConfig. Edit (or create) the following file on your Control Plane:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;/var/lib/rancher/k3s/server/manifests/traefik.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    deployment:
      kind: DaemonSet &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
    hostNetwork: true &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
    service:
      spec:
        externalTrafficPolicy: Local &lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
    additionalArguments:
      - &quot;--entryPoints.web.forwardedHeaders.insecure=true&quot;  &lt;i class=&quot;conum&quot; data-value=&quot;4&quot;&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
      - &quot;--entryPoints.websecure.forwardedHeaders.insecure=true&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Runs Traefik on every node.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Binds Traefik directly to the host&amp;#8217;s network interface.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Forces the packet to stay within the node, preserving the IP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;4&quot;&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Not sure if required but&amp;#8230;&amp;#8203;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Once edited and saved K3S redeploy Traefik automagically in all nodes&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Separating your Control Plane and Agents is a great step for cluster stability,
but it changes the rules of the game for networking.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By moving Traefik to a DaemonSet and using HostNetwork, you remove the middleman that&amp;#8217;s masking your data.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now, my logs are back to normal, and I can see where my users are actually coming from.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Happy hacking!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Obtaining the remote IP in a cluster with K3s y Traefik</summary>
    </entry>
    <entry>
        <title>Auto alojando Writefreely</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/writefreely.html"/>
        <updated>2026-05-05T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/writefreely.html</id>
        <category term="writefreely"/>
        <category term="selfhosting"/>
        <category term="docker"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;writefreely&quot;&gt;Writefreely&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;An open source platform for building a writing space on the web.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente, Writefreely es una aplicación muy ligera (y superminimalista) para escribir un blog en Internet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;em&gt; Cuando digo super minimalista no exagero. No subes fotos, no creas tablas, ni layouts, ni nada parecido,
sólo escribes texto y publicas&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al ser open source cualquiera puede descargarla e instalarla para tener su propio blog aunque también existe
la posibilidad de crear una cuenta en cualquiera de las instancias existentes y no complicarse la vida.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La diferencia es obvia: si la alojas tú mismo puedes usar tu propio dominio (random.jagedn.dev, yo.es, &amp;#8230;&amp;#8203;)
, mientras que si usas la oficial tu blog será algo como nombredelainstancia.com/usuario&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a ver cómo alojar Writefreely en nuestra propia instancia. Para ello necesitamos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;una máquina donde alojarla. Lo más fácil es contratar una por 4€ al mes en cualquier proveedor
(Yo uso Hostinger y va como un tiro). Seguramente te sobrará máquina pero suele ser lo mínimo que se despacha&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el nombre de un dominio, el mío por ejemplo es &quot;jagedn.dev&quot;. Dependiendo del dominio y donde lo registres
el precio varía pero puedes conseguir un .com por unos 10€ al año&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;acceso a la máquina. Yo uso la consola con SSH pero muchos proveedores te permiten acceder vía navegador&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;docker. NO es necesario pero para mí es un imprescindible porque me permite instalar, desinstalar, actualizar&amp;#8230;&amp;#8203;
todo tipo de aplicaciones de una forma sencilla&lt;/p&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Esta guía instala Writefreely en el stack de aplicaciones que tengo (ver otros posts como
&quot;Self-hosting multiples blogs&quot; o &quot;Self-hosting Snikket and GoToSocial behind Caddy&quot;) de tal forma que en un
mismo VPS ejecuto diferentes aplicaciones&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación&quot;&gt;Preparación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Definimos (y creamos) un directorio donde va a residir nuestra aplicación, por ejemplo /blog&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este directorio crearemos los siguientes ficheros:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;services:

  caddy:
    image: caddy:2.11.1-alpine
    container_name: caddy
    restart: always
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./caddy_data:/data
      - ./caddy_config:/config
      - ./acme_challenges:/var/www/challenges

  db:
    image: mariadb:latest
    container_name: db
    init: true
    volumes:
      - ./db_data:/var/lib/mysql
    env_file:
      - .env_write
    restart: unless-stopped

  writefreely:
    build:
      context: ./writefreely
      dockerfile: Dockerfile.prod
    image: writefreely-local-prod:latest
    container_name: writefreely-app
    depends_on:
      - db
    volumes:
      - ./app_data:/data
    restart: unless-stopped&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Caddy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;... Otros dominios

random.jagedn.dev { &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
   reverse_proxy writefreely:8080
}
&amp;lt;1&amp;gt; el nombre de tu dominio&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.env_write&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;MARIADB_ROOT_PASSWORD=unapasswordalgocomplicada
MARIADB_DATABASE=writefreely
MARIADB_USER=writefreely
MARIADB_PASSWORD=otrapasswordcomplicada&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;git_writefreely&quot;&gt;Git Writefreely&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En lugar de la imagen docker publicada que no es para producción, vamos a usar el código fuente
para construir la imagen. De esta forma será fácil actualizar de versión&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;git clone https://github.com/writefreely/writefreely.git&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;db&quot;&gt;DB&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar debemos levantar la base de datos &lt;strong&gt;antes&lt;/strong&gt; que Writefreely por lo que
ejecutamos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker compose up -d db&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si queremos ver cómo (y cúando) se levanta la base de datos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker compose logs -f db&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando veamos que la bbdd está levantada pasaremos a crear la imagen de Writefreely&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;writefreely_2&quot;&gt;Writefreely&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar vamos a &quot;construir&quot; la imagen&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker compose build writefreely&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Después le diremos que cree la configuración inicial:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker compose run --rm writefreely writefreely config start&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El primer writefreely se refiere al nombre del container (ver docker-compose.yml) mientras que el
segundo se refiere al comando a ejecutar &quot;dentro de él&quot;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Writefreely nos hará unas cuantas preguntas muy simples de responder, siendo la más &quot;rara&quot; si estamos
usando un reverse proxy. Como es el caso (Caddy) contestaremos que sí&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usaremos &quot;db&quot; como nombre donde está corriendo la base de datos (pues es el nombre que le hemos dado en
el docker-compose)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último, una vez completada la configuración y creado el usuario, crearemos las claves:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker compose run writefreely writefreely keys generate&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien sólo resta levantar el servicio:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;escribir&quot;&gt;Escribir&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En unos pocos segundos podremos acceder a nuestra instancia con el usuario y password que indicamos en el setup.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vía interface web puedes configurar si quieres federar la instancia de tal forma que usuarios del fediverso
(Mastodon, Pleroma, Loops) puedan seguirte y enterarse cuando publicas un artículo nuevo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Writefreely es realmente minimalista y muy centrado en la escritura sencilla. Yo recién he añadido una
instalación en mi stack y creado un par de posts pero espero seguir usándolo cada vez más&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Autoalojar una instancia de WriteFreely</summary>
    </entry>
    <entry>
        <title>GitOps con Terraform y Github</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/gitops-tf-gh.html"/>
        <updated>2026-04-01T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/gitops-tf-gh.html</id>
        <category term="terraform"/>
        <category term="gitops"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;projecto&quot;&gt;Projecto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Actualmente, estoy &quot;ejerciendo&quot; las labores de DevOps/Infra en un equipo multidisciplinar, o sea lo que viene siendo varias personas trabajando en varios
microservicios que se despliegan en un mismo sitio, en este caso
y por &quot;imperativo legal&quot; un cluster ECS de Amazon (yo hubiera preferido un
EKS porque me manejo mejor con kubernetes).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde el principio hemos aplicado que todo el tema de infra tiene que ser bajo
Terraform y hemos reducido las intervenciones manuales (tanto consola como web)
al mínimo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así nuestro proyecto terraform despliega todo lo relativo a la red, DNS, bases
de datos, secretos y cómo no, para bien o para mal, la definición de todos los microservicios.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ignorando los detalles, básicamente tenemos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Un proyecto de terraform que define los elementos &quot;globales&quot; entre ellos el repositorio
Docker de cada servicio, exponiendo los arn como outputs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Diferentes proyectos de terraform con los recursos necesarios para desplegar
cada servicio dentro del cluster, diferenciando además entre staging y prod&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A cada proyecto terraform de cada servicio, entre otras cosas,
se le define la imagen Docker y el tag de la misma a desplegar junto con otras
configuraciones como la lista de secretos y environments a usar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De esta forma, y como la arquitectura en esta fase está &quot;viva&quot;, cada servicio se despliega de forma independiente
y si es necesario se elimina sin interferir en los otros proyectos.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;servicio&quot;&gt;Servicio&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así, cada servicio cuenta con su &lt;code&gt;locals.tf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;locals {
  project           = &quot;xxxxx&quot;
  container_image   = &quot;${data.terraform_remote_state.global.outputs.ecr_servicexxxx_url}:${var.service_tag}&quot;
  ....
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y en un fichero &lt;code&gt;versions.auto.tfvars&lt;/code&gt; definimos el tag a desplegar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;service_tag = 1234567&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desplegar una nueva version es &quot;tan fácil&quot; como editar este fichero con el nuevo commit y ejecutar un
&lt;code&gt;terraform apply&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gitops&quot;&gt;GitOps&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente, una vez que este sistema está funcionando y desplegar versiones se convierte en algo tan fácil
como esperar a que una versión nueva se encuentre subida al ECR, cambiar el fichero y ejecutar el terraform,
surge la &quot;necesidad&quot; de &lt;code&gt;esto hay que automatizarlo&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La primera parte para la automatización es &quot;trivial&quot;: cada vez que en un servicio se mergee a &lt;code&gt;main&lt;/code&gt;, un pipeline
construirá y publicará la imagen.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como estamos usando Github, esto lo resolvemos mediante una GH Action similar a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;deploy.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: CD - Deploy Develop
on:
  push:
    branches: [main]

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: servicexxxx

jobs:
    ....
  build:
    name: Build &amp;amp; Push
    runs-on: ubuntu-latest
    needs: test
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Get short SHA
        id: shortsha
        run: echo &quot;sha7=$(echo ${GITHUB_SHA} | cut -c1-7)&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ steps.shortsha.outputs.sha7 }}
        run: |
          docker build --build-arg GITHUB_TOKEN=${{ steps.generate_token.outputs.token  }} -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
          IMAGE_URI=&quot;$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG&quot;
          echo &quot;image=$IMAGE_URI&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT
          echo &quot;✅ Built and pushed image: $IMAGE_URI&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ahora que tenemos la imagen creada y subida (recuerda, esto es en cada servicio/proyecto) lo que querríamos es
poder &quot;notificar&quot; al repositorio de Terraform que existe una versión nueva para desplegar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como esta parte es &quot;común&quot; a todos los servicios hemos creado un repositorio &lt;code&gt;platform-ci&lt;/code&gt; donde, entre otros
pipelines, tenemos uno para crear una PR en el repositorio de Terraform que modifique únicamente el &lt;code&gt;versions&lt;/code&gt;
correspondiente. De esta forma &quot;reutilizamos&quot; pipelines y no nos repetimos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;platform_ci&quot;&gt;Platform CI&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El pipeline en este repo es similar a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;deploy-version.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: Deploy service

on:
  workflow_call:
    secrets:
      GH_TOKEN_APP_ID:
        required: true
      GH_TOKEN_PRIVATE_KEY:
        required: true
    inputs:
      environment:
        required: true
        type: string
      project:
        required: true
        type: string
      variable:
        required: true
        type: string
      value:
        required: true
        type: string

jobs:
  update-version:
    runs-on: ubuntu-latest
    steps:
      - name: Generate GitHub App Token
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.GH_TOKEN_APP_ID }}
          private-key: ${{ secrets.GH_TOKEN_PRIVATE_KEY }}

      - name: Checkout Infra Repo
        uses: actions/checkout@v4
        with:
          repository: OUR_ORGANIZATION/tr-infra
          token: ${{ steps.generate_token.outputs.token }}

      - name: Update Version File
        run: |
          cd environments/${{ inputs.environment }}/${{ inputs.project }}
          sed -i &apos;s/${{ inputs.variable }} = .*/${{ inputs.variable }} = &quot;${{ inputs.value }}&quot;/&apos; versions.auto.tfvars

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v5
        with:
          token: ${{ steps.generate_token.outputs.token }}
          commit-message: &quot;deploy: update ${{ inputs.variable }} (${{ inputs.environment }}) to ${{ inputs.value }}&quot;
          title: &quot;🚀 Deploy ${{ inputs.project }} (${{ inputs.environment }})&quot;
          body: &quot;Deploy new version of **${{ inputs.project }}** \n ${{inputs.variable}}=${{inputs.value}}&quot;
          branch: &quot;gitops/${{ inputs.variable }}-${{ inputs.value }}&quot;
          base: main&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este pipeline requiere 4 parámetros:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;environment (staging o production), referencia a una de las dos carpetas &quot;padres&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;project (service1, service2), referencia a una de las multiples subcarpetas donde tenemos definidos los servicios&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;variable, por ahora usamos &quot;service_tag&quot; pero lo dejamos abierto para futuros usos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;value, el valor a usar que en este caso será el tag pero asi esta abierto para futuros usos&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El pipeline simplemente clona el repo terraform, se ubica en la carpeta &lt;code&gt;{environment}/{project}&lt;/code&gt;, modifica el valor
proporcionado usando un simple &lt;code&gt;sed&lt;/code&gt; y crea una PR&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Por ahora las PR que se crean deben ser aprobadas de forma manual hasta que todo vaya más rodado&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues, simplemente tenemos que incluir en el pipeline de cada servicio una invocación a este una vez que la imagen
se encuentra subida:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;deploy.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  notify-infra:
    needs: build
    uses: OUR_ORGANIZATION/platform-ci/.github/workflows/deploy-version.yml@main
    secrets:
      GH_TOKEN_APP_ID: ${{ secrets.GH_TOKEN_APP_ID }}
      GH_TOKEN_PRIVATE_KEY: ${{ secrets.GH_TOKEN_PRIVATE_KEY }}
    with:
      environment: staging  &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
      project: service1
      variable: service_tag
      value: ${{ needs.build.outputs.image_tag }}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;En este caso desplegamos en staging, pero cuando es un merge versionado usamos production&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando la imagen ha sido publicada y este último steps se ejecuta, tenemos una PR creada en el repo de Terraform.
En un proceso típico simplemente se mergea y el pipeline del proyecto Terraform despliega la última versión.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este mecanismo nos permite de una forma fácil hacer &lt;strong&gt;rollback&lt;/strong&gt; a la versión anterior, pues simplemente es hacer
otra PR (esta vez manual) indicando el tag a usar, pero lo más importante (en mi opinión) permite al equipo ser
&quot;dueño&quot; del proceso de despliegue sin depender de nadie.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La propuesta primera era incluir en cada servicio los comandos aws necesarios para modificar la Task Definition
proporcionándole el tag a usar, pero esto creaba que el proyecto terraform se quedará &quot;desactualizado&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La otra opción era &quot;mover&quot; la parte terraform a cada servicio, pero esto nos dificulta la reutilización de módulos
de terraform y de alguna manera tendríamos la infra &quot;distribuida&quot; en varios proyectos. Por ahora nos sentimos más
cómodos teniéndolo unificado en un único repo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En cualquier caso la idea es conseguir proporcionar herramientas y procedimientos simples al equipo lo más automatizados
posible para que los despliegues sean fluídos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Creando un flujo GitOps con Terraform y Github Actions</summary>
    </entry>
    <entry>
        <title>Self-hosting multiples blogs</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/apache-docker.html"/>
        <updated>2026-02-18T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/apache-docker.html</id>
        <category term="apache"/>
        <category term="selfhosting"/>
        <category term="docker"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Recientemente he movido el blog desde mi proveedor de hosting en USA a otro en Europa y me estoy migrando todos
los sites que tenía en aquel al nuevo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una de las ventajas del proveedor que tenía era la facilidad para crear sites con un par de clicks. Dado mi dominio
&quot;jagedn.dev&quot; por ejemplo, era sumamente fácil crear otro site &quot;blog.jagedn.dev&quot;,  &quot;maps.jagedn.dev&quot;
o &quot;whatever.jagedn.dev&quot;. Cada site tenía su directorio e incluía PHP&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este blog voy a explicar cómo he hecho la migración de estos sites a mi instancia de tal forma, que aunque
no sea con un simple click, me permita crear sites de una forma fácil&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además algunos sites podrán usar carpetas &quot;public&quot; y &quot;private&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un dominio (por ejemplo &quot;jagedn.dev&quot;) y que los DNS apunten a la máquina donde vamos a instalar el multisite&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente, voy a usar Docker en la máquina donde voy a alojar mis sites.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente, el otro requisito es poder acceder por ssh a la instancia.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación&quot;&gt;Preparación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el directorio de trabajo elegido vamos a crear directorios por cada site que queramos hostear:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;./blog&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;./maps&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;./calendar&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;blog&quot;&gt;Blog&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la carpeta blog copio los ficheros de mi static-site. Como es todo html+css simplemente copio los ficheros manteniendo
la estructura del mismo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;maps&quot;&gt;Maps&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta es una aplicación que usa PHP y que usa una carpeta pública donde hay html y un api en PHP y una carpeta private
donde guarda ficheros sensibles&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;calendar&quot;&gt;Calendar&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podría ser por ejemplo un Calendy o similar. La incluyo solo a modo de ejemplo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;caddy&quot;&gt;Caddy&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el directorio de trabajo crearemos un fichero &lt;code&gt;Caddyfile&lt;/code&gt; similar a :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Caddyfile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;maps.jagedn.dev,
blog.jagedn.dev,
jagedn.dev {
    reverse_proxy apache:80
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;docker&quot;&gt;Docker&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el directorio de trabajo crearemos un dichero &lt;code&gt;Dockerfile&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Dockerfile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;FROM php:8.4-apache

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    sudo \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

RUN chown -R www-data:www-data /var/www/html \
    &amp;amp;&amp;amp; chmod -R 755 /var/www/html

RUN a2enmod rewrite

USER www-data&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y el fichero &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;services:

  caddy:
    image: caddy:latest
    container_name: caddy
    restart: always
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
      - acme_challenges:/var/www/challenges

  apache:
    container_name: apache
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./blog:/var/www/html/blog
      - ./maps:/var/www/html/maps
      - ./apache2/sites-available:/etc/apache2/sites-available
      - ./apache2/sites-enabled:/etc/apache2/sites-enabled&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último crearemos los directorios &lt;code&gt;./apache2/sites-available&lt;/code&gt; y &lt;code&gt;./apache2/sites-enabled&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En &lt;code&gt;sites-available&lt;/code&gt; mantendremos la configuracion de todos los sites y en &lt;code&gt;sites-enabled&lt;/code&gt; los que queremos en cada
momento activos. Lo normal es usar enabled como enlaces simbólicos pero yo simplemente hago un copy porque nunca
sé cómo usar &lt;code&gt;ln -s&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;apache2/sites-enabled/blog.conf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;lt;VirtualHost *:80&amp;gt;
ServerName blog.jagedn.dev

        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html/blog

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
&amp;lt;/VirtualHost&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;apache2/sites-enabled/maps.conf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;lt;VirtualHost *:80&amp;gt;
ServerName maps.jagedn.dev

        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html/maps/public

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
&amp;lt;/VirtualHost&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque son casi similares hay diferencias:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ServerName: sirve para que Apache &quot;machee&quot; la petición del usuario con la config a usar&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DocumentRoot: sirve para indicar qué carpeta debe servir ese server name. Por ejemplo maps usa &quot;maps/public&quot;
mientras que blog usa directamente la carpeta &quot;blog&quot; montada&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nuevo_site&quot;&gt;Nuevo Site&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente si tengo que añadir un nuevo site los pasos a seguir serían:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;crear el DNS en mi proveedor y apuntarlo a la IP de la máquina. Por ejemplo &quot;chat.jagedn.dev&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crear una carpeta al mismo nivel que blog o map, donde alojar la aplicación&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crear una site-enabled similar a los existentes indicando el ServerName y la ruta&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;montar la nueva carpeta en el docker-compose&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y simplemente reconfigurar todo con &lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Autoalojar multiples sitios webs con Docker y Apache</summary>
    </entry>
    <entry>
        <title>Creando un Map en PHP y Drive</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/drive-maps-php.html"/>
        <updated>2026-01-05T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/drive-maps-php.html</id>
        <category term="google"/>
        <category term="drive"/>
        <category term="php"/>
        <category term="leaflet"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es bien simple. Vamos a usar una carpeta de Google Drive, donde habremos subido fotos
de diferentes sitios y &lt;strong&gt;que contienen el metadata GPS&lt;/strong&gt;, para crear una página html que las
ubique en un map&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como la mayoría de los provedores de hosting ofrecen PHP gratuito vamos a crear un pequeño API
que sirva para realizar la integración con Google (proceso de autentificación y obtención de
las fotos)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De esta forma los requisitos de la aplicación son mínimos, pues las fotos serán servidas por
un enlace (read-only) de google y el php tendrá una carga mínima.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además, vamos a implementar un pequeño caché para evitar llamadas excesivas a Google.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;consola_google&quot;&gt;Consola Google&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero (aparte de tener cuenta en Google y una carpeta con fotos en Google Drive) es crear
un proyecto en la consola de Google, por ejemplo misandanzas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://console.cloud.google.com/welcome?project=misandanzas&quot; class=&quot;bare&quot;&gt;https://console.cloud.google.com/welcome?project=misandanzas&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Una vez creado (y seleccionado) debemos habilitar el API de Drive, con lo que iremos a &quot;APIs and services&quot;, &quot;Library&quot;, buscaremos Drive y lo añadiremos al proyecto&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lo siguiente es crear una &quot;Oauth Screen consent&quot;. Como la aplicación será de consumo &quot;propio&quot; y
su objetivo no es ser usada por el público nos servirá el estado &quot;testing&quot; en que se crea y que no
requiere aprobación por parte de Google (Si vas a Audience verás que está en estado Testing)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;En la pantalla de Audience añadiremos (abajo del todo) nuestro correo como usuario de test&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;En la pantalla de Data access añadiremos un nuevo scope pulsando &quot;Add or remove scopes&quot;. Añadiremos
&quot;&amp;#8230;&amp;#8203;/auth/drive.readonly&quot; (tal cual) en el campo de entrada inferior y pulsamos añadir.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Por último crearemos un Oauth Client en la pantalla de &quot;Clients&quot;. El cliente será del tipo &quot;Web application&quot; y deberemos añadir nuestra web como &quot;Authorised Javascript&quot; (&lt;a href=&quot;https://maps.jagedn.dev&quot; class=&quot;bare&quot;&gt;https://maps.jagedn.dev&lt;/a&gt; en mi caso) y en &quot;Authorised redirect URIs&quot;&quot; (&lt;a href=&quot;https://maps.jagedn.dev/api/callback&quot; class=&quot;bare&quot;&gt;https://maps.jagedn.dev/api/callback&lt;/a&gt;)&lt;/p&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Si el mapa lo vas a &quot;consumir&quot; en local puedes usar &lt;a href=&quot;http://localhost:xxxx&quot; class=&quot;bare&quot;&gt;http://localhost:xxxx&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez creado el OAUTH descargaremos el JSON que nos proporciona. ¡OJO! Hay que descargarlo en el modal
recién generado. Si no lo descargas justo en este momento te toca generar otro OAuth de nuevo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;drive&quot;&gt;Drive&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya hemos mencionado en nuestro Google Drive crearemos una carpeta y subiremos fotos que tengan
el metadata de GPS para poder ubicarlas en el mapa.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Lo interesante del proyecto es que podamos
añadir fotos posteriormente y que aparezcan sin tener que hacer nada, pero para probar que todo va bien
deberíamos subir al menos un par de ellas al principio&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi caso la carpeta la he llamado &quot;MisAndanzas&quot; pero puede ser cualquier nombre&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;proyecto&quot;&gt;Proyecto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para proteger los datos sensibles (el fichero json bajado con las credenciales) vamos a crear dos carpetas
en nuestro hosting: public y private&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Public será la carpeta que configuraremos como root de la aplicación web y private estará &quot;fuera&quot; de ella.
Yo las he puesto las dos en el mismo directorio y configurado &quot;public&quot; como root en el Apache&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A su vez en la carpeta public crearemos otra llamada &quot;api&quot; y dentro de ella otra llamada &quot;src&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;|-- root
|   |-- public
|       |-- api
|           |-- src
|   |-- private
|       |-- credentials.json&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La carpeta &lt;code&gt;root/public/api&lt;/code&gt; será donde estará nuestro api PHP. Usaremos Slim como framework por
ser ligero y sencillo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la carpeta &lt;code&gt;root/public/api&lt;/code&gt; creamos un fichero &lt;code&gt;composer.json&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;root/public/api/composer.json&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;{
    &quot;require&quot;: {
        &quot;slim/slim&quot;: &quot;^4.0&quot;,
        &quot;slim/psr7&quot;: &quot;^1.8&quot;
    },
    &quot;autoload&quot;: {
        &quot;psr-4&quot;: {
            &quot;App\\&quot;: &quot;src/&quot;
        }
    }
}&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y ejecutaremos &lt;code&gt;composer install&lt;/code&gt; (desde esta carpeta) para instalar las dependencias.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la misma carpeta crearemos un &lt;code&gt;.htaccess&lt;/code&gt; que nos sirva para proteger nuestro api:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;root/public/api/.htaccess&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;&amp;lt;IfModule mod_rewrite.c&amp;gt;
    RewriteEngine On

    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d

    # Todo lo que llegue aquí lo procesa index.php
    RewriteRule ^ index.php [QSA,L]
&amp;lt;/IfModule&amp;gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(De esta forma todas las peticiones se resuelven en el index.php que crearemos)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y por último crearemos nuestro index.php:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;root/public/api/index.php&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;&amp;lt;?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use App\GooglePhotos;

// El autoload está en la misma carpeta api/
require __DIR__ . &apos;/vendor/autoload.php&apos;;

$app = AppFactory::create();

// IMPORTANTE
$app-&amp;gt;setBasePath(&apos;/api&apos;);

$app-&amp;gt;addRoutingMiddleware();
$app-&amp;gt;addErrorMiddleware(true, true, true);

$google = new GooglePhotos();

// Rutas...
$app-&amp;gt;get(&apos;/setup&apos;, function (Request $request, Response $response) use ($google) {
    return $response-&amp;gt;withHeader(&apos;Location&apos;, $google-&amp;gt;getAuthUrl())-&amp;gt;withStatus(302);
});

$app-&amp;gt;get(&apos;/callback&apos;, function (Request $request, Response $response) use ($google) {
    $params = $request-&amp;gt;getQueryParams();
    $google-&amp;gt;authenticate($params[&apos;code&apos;]);
    $response-&amp;gt;getBody()-&amp;gt;write(&quot;Conectado con éxito.&quot;);
    return $response;
});

$app-&amp;gt;get(&apos;/photos&apos;, function (Request $request, Response $response) use ($google) {
    $params = $request-&amp;gt;getQueryParams();
    $token = $params[&apos;nextPageToken&apos;] ?? null;
    $photos = $google-&amp;gt;getPhotos($token);
    $response-&amp;gt;getBody()-&amp;gt;write(json_encode($photos));
    return $response
        -&amp;gt;withHeader(&apos;Content-Type&apos;, &apos;application/json&apos;)
        -&amp;gt;withHeader(&apos;Cache-Control&apos;, &apos;no-store, no-cache, must-revalidate, max-age=0&apos;)
        -&amp;gt;withHeader(&apos;Pragma&apos;, &apos;no-cache&apos;);
});

$app-&amp;gt;run();&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver el index.php &quot;simplemente&quot; indica las rutas de nuestra aplicación y las
dirige a una clase &lt;code&gt;GooglePhotos&lt;/code&gt; que crearemos a continuación (en la subcarpeta &lt;code&gt;src&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente, la API tendrá una llamada &lt;code&gt;setup&lt;/code&gt; que ejecutaremos una vez para obtener un token
de Google (que nos la proporcionará en el endpoint &lt;code&gt;callback&lt;/code&gt;) y una llamada &lt;code&gt;photos&lt;/code&gt; para
devolver un json con el detalle de las fotos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;service&quot;&gt;Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Toda la lógica (tampoco es que sea mucha) la ubicaremos en el fichero &lt;code&gt;public/api/src/GooglePhotos.php&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a verla por partes&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;public/api/src/GooglePhotos.php&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;&amp;lt;?php
namespace App;

class GooglePhotos {
    private $tokenPath;
    private $credentialsPath;
    private $scope = &quot;https://www.googleapis.com/auth/drive.readonly&quot;;

    public function __construct() {
        $this-&amp;gt;tokenPath = __DIR__ . &apos;/../../../private/token.json&apos;;
        $this-&amp;gt;credentialsPath = __DIR__ . &apos;/../../../private/credentials.json&apos;;
    }&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ajusta las rutas si no has usado las mismas que yo. Simplemente, indicamos donde leer las
credenciales y donde guardar el token que se obtenga&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;public/api/src/GooglePhotos.php&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;    private function getCredentials() {
        if (!file_exists($this-&amp;gt;credentialsPath)) throw new \Exception(&quot;Falta credentials.json&quot;);
        $data = json_decode(file_get_contents($this-&amp;gt;credentialsPath), true);
        return $data[&apos;web&apos;] ?? $data[&apos;installed&apos;] ?? $data;
    }

    public function getAuthUrl() {
        $creds = $this-&amp;gt;getCredentials();

        if (empty($creds[&apos;client_id&apos;])) {
            die(&quot;ERROR: client_id está vacío. Estructura del JSON: &quot; . print_r($creds, true));
        }

        $params = [
            &apos;client_id&apos;     =&amp;gt; $creds[&apos;client_id&apos;],
            &apos;redirect_uri&apos;  =&amp;gt; $creds[&apos;redirect_uris&apos;][0],
            &apos;scope&apos;         =&amp;gt; $this-&amp;gt;scope,
            &apos;response_type&apos; =&amp;gt; &apos;code&apos;,
            &apos;access_type&apos;   =&amp;gt; &apos;offline&apos;,
            &apos;prompt&apos;        =&amp;gt; &apos;consent&apos;
        ];
        return &quot;https://accounts.google.com/o/oauth2/v2/auth?&quot; . http_build_query($params);
    }&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando se llama al método &lt;code&gt;getAuthUrl&lt;/code&gt; se construye una URL para iniciar el flujo OAUTH contra
Google indicandole los parámetros necesarios (como client_id y scope)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que Google ha validado el proceso y autentificado al usuario (a tí) nos proporciona un code
que debemos aceptar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;    public function authenticate($code) {
        $creds = $this-&amp;gt;getCredentials();
        return $this-&amp;gt;postToTokenEndpoint([
            &apos;client_id&apos;     =&amp;gt; $creds[&apos;client_id&apos;],
            &apos;client_secret&apos; =&amp;gt; $creds[&apos;client_secret&apos;],
            &apos;code&apos;          =&amp;gt; $code,
            &apos;redirect_uri&apos;  =&amp;gt; $creds[&apos;redirect_uris&apos;][0],
            &apos;grant_type&apos;    =&amp;gt; &apos;authorization_code&apos;
        ]);
    }

    private function postToTokenEndpoint($params) {
        $ch = curl_init(&apos;https://oauth2.googleapis.com/token&apos;);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
        $response = json_decode(curl_exec($ch), true);
        curl_close($ch);

        $tokenToSave = [
            &apos;access_token&apos;  =&amp;gt; $response[&apos;access_token&apos;],
            &apos;expires_at&apos;    =&amp;gt; time() + $response[&apos;expires_in&apos;],
            &apos;refresh_token&apos; =&amp;gt; $response[&apos;refresh_token&apos;] ?? (json_decode(file_get_contents($this-&amp;gt;tokenPath), true)[&apos;refresh_token&apos;] ?? null)
        ];
        file_put_contents($this-&amp;gt;tokenPath, json_encode($tokenToSave));
        return $tokenToSave[&apos;access_token&apos;];
    }&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien deberíamos tener en nuestra carpeta privada un fichero &lt;code&gt;token.json&lt;/code&gt;
que nos servirá para identificar las sucesivas llamadas a Drive. Además, el PHP se encargará
de ir renovándolo cuando sea necesario&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;    private function getValidAccessToken() {
        if (!file_exists($this-&amp;gt;tokenPath)) throw new \Exception(&quot;Falta token.json&quot;);
        $tokenData = json_decode(file_get_contents($this-&amp;gt;tokenPath), true);
        if (time() &amp;gt; ($tokenData[&apos;expires_at&apos;] - 30)) {
            return $this-&amp;gt;refreshAccessToken($tokenData[&apos;refresh_token&apos;]);
        }
        return $tokenData[&apos;access_token&apos;];
    }

    private function refreshAccessToken($refreshToken) {
        $creds = $this-&amp;gt;getCredentials();
        return $this-&amp;gt;postToTokenEndpoint([
            &apos;client_id&apos;     =&amp;gt; $creds[&apos;client_id&apos;],
            &apos;client_secret&apos; =&amp;gt; $creds[&apos;client_secret&apos;],
            &apos;refresh_token&apos; =&amp;gt; $refreshToken,
            &apos;grant_type&apos;    =&amp;gt; &apos;refresh_token&apos;
        ]);
    }&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;api_photos&quot;&gt;API Photos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La lógica principal, una vez que podemos &quot;dialogar&quot; con Google la ubicamos en una método getPhotos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este método comprueba si ya hemos generado un fichero de cache y si no existe o es un poco antiguo
lo recrea&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;obtenemos el token&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;obtenemos el ID de Drive a partir del nombre de la carpeta y el token&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;iteramos llamando al api de google hasta que &lt;code&gt;nextPageToken&lt;/code&gt; nos indique que no hay más fotos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;para cada iteración extraemos la información y la vamos añadiendo a un json nuestro&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;    public function getPhotos($nextPageToken = null, $folderName = &quot;MisAndanzas&quot;, $seconds = 30) {
        $cacheFile = __DIR__ . &apos;/../../../private/photos_cache.json&apos;;

        // Si el archivo de caché existe y es reciente, lo devolvemos
        if (file_exists($cacheFile) &amp;amp;&amp;amp; (time() - filemtime($cacheFile) &amp;lt; $seconds)) {
            return json_decode(file_get_contents($cacheFile), true);
        }

        $accessToken = $this-&amp;gt;getValidAccessToken();

        $folderId = $this-&amp;gt;getFolderIdByName($folderName, $accessToken);
        if (!$folderId) return [&quot;error&quot; =&amp;gt; &quot;No se encontró la carpeta &apos;$folderName&apos;&quot;];

        $allPhotos = [];
        $pageToken = null;

        do {
            $query = urlencode(&quot;&apos;$folderId&apos; in parents and mimeType contains &apos;image/&apos; and trashed = false&quot;);
            // Pedimos el máximo por página (100) para terminar antes
            $url = &quot;https://www.googleapis.com/drive/v3/files?q=$query&quot;
                . &quot;&amp;amp;fields=nextPageToken,files(id,name,thumbnailLink,imageMediaMetadata,createdTime)&quot;
                . &quot;&amp;amp;pageSize=100&quot;;

            if ($pageToken) {
                $url .= &quot;&amp;amp;pageToken=&quot; . $pageToken;
            }

            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HTTPHEADER, [&apos;Authorization: Bearer &apos; . $accessToken]);
            $res = json_decode(curl_exec($ch), true);
            curl_close($ch);

            if (isset($res[&apos;files&apos;])) {
                foreach ($res[&apos;files&apos;] as $file) {
                    $meta = $file[&apos;imageMediaMetadata&apos;] ?? null;
                    $location = $meta[&apos;location&apos;] ?? null;

                    // Solo guardamos las que tienen GPS para no llenar el JSON de basura
                    if ($location) {
                        $allPhotos[] = [
                            &apos;id&apos;      =&amp;gt; $file[&apos;id&apos;],
                            &apos;name&apos;    =&amp;gt; $file[&apos;name&apos;],
                            &apos;url&apos;     =&amp;gt; str_replace(&apos;=s220&apos;, &apos;=s800&apos;, $file[&apos;thumbnailLink&apos;] ?? &apos;&apos;),
                            &apos;lat&apos;     =&amp;gt; (float)$location[&apos;latitude&apos;],
                            &apos;lng&apos;     =&amp;gt; (float)$location[&apos;longitude&apos;],
                            &apos;date&apos;    =&amp;gt; $meta[&apos;time&apos;] ?? $file[&apos;createdTime&apos;] ?? null,
                            &apos;date_human&apos; =&amp;gt; isset($meta[&apos;time&apos;]) ? date(&quot;d/m/Y H:i&quot;, strtotime($meta[&apos;time&apos;])) : null
                        ];
                    }
                }
            }

            // Si hay un token, el bucle se repite; si no, termina.
            $pageToken = $res[&apos;nextPageToken&apos;] ?? null;

        } while ($pageToken);

        // Ordenamos por fecha antes de enviar al cliente
        usort($allPhotos, function($a, $b) {
            return strtotime($a[&apos;date&apos;]) - strtotime($b[&apos;date&apos;]);
        });

        return $allPhotos;
    }

    private function getFolderIdByName($name, $accessToken) {
        $query = urlencode(&quot;name = &apos;$name&apos; and mimeType = &apos;application/vnd.google-apps.folder&apos; and trashed = false&quot;);
        $url = &quot;https://www.googleapis.com/drive/v3/files?q=$query&amp;amp;fields=files(id)&quot;;

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [&apos;Authorization: Bearer &apos; . $accessToken]);
        $res = json_decode(curl_exec($ch), true);
        curl_close($ch);

        return $res[&apos;files&apos;][0][&apos;id&apos;] ?? null;
    }&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y básicamente esto es todo lo que necesitamos para consumir el API de Drive con PHP&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;front&quot;&gt;Front&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta parte la puedes hacer tan &quot;profesional&quot; como quieras.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Yo por mi parte voy a crear un simple &lt;code&gt;index.html&lt;/code&gt; que use Vue y Leaflet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Las partes mas importantes del html serian&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;root/public/index.html&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;&amp;lt;head&amp;gt;
    ...
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/leaflet@1.9.4/dist/leaflet.css&quot; /&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div id=&quot;app&quot;&amp;gt;
    &amp;lt;div id=&quot;map&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div v-if=&quot;selectedPhoto&quot; class=&quot;lightbox&quot; @click=&quot;selectedPhoto = null&quot;&amp;gt;
        &amp;lt;span class=&quot;close-btn&quot;&amp;gt;&amp;amp;times;&amp;lt;/span&amp;gt;
        &amp;lt;img :src=&quot;getFullSizeUrl(selectedPhoto.url)&quot; @click.stop /&amp;gt;
        &amp;lt;div class=&quot;lightbox-caption&quot;&amp;gt;
            &amp;lt;span class=&quot;date-badge&quot;&amp;gt;&amp;amp;nbsp;&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script src=&quot;https://unpkg.com/vue@3/dist/vue.global.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;https://unpkg.com/leaflet@1.9.4/dist/leaflet.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El html crea dos divs: map y selectedPhoto. El primero se usará para que Leaflet monte el mapa
y el segundo para mostrar un modal cuando se seleccione una foto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El javascript que llama al api y monta el mapa es muy simple:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;const { createApp, onMounted, ref } = Vue;

    createApp({
        setup() {
            const map = ref(null);
            const photos = ref([]);
            const selectedPhoto = ref(null);
            const getFullSizeUrl = (url) =&amp;gt; {
                return url ? url.replace(/=s\d+/, &apos;=s0&apos;) : &apos;&apos;;
            };

            const openImage = (id) =&amp;gt; {
                console.log(&quot;Intentando abrir foto ID:&quot;, id); // Para debug
                const photo = photos.value.find(p =&amp;gt; p.id === id);
                if (photo) {
                    selectedPhoto.value = photo;
                }
            };
            window.openImage = openImage;

            const initMap = () =&amp;gt; {
                map.value = L.map(&apos;map&apos;, {
                    minZoom: 2,
                    worldCopyJump: true
                }).setView([17, 0], 2);

                L.tileLayer(&apos;https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png&apos;, {
                    attribution: &apos;© OpenStreetMap&apos;
                }).addTo(map.value);
            };

            const loadPhotos = async () =&amp;gt; {
                try {
                    const response = await fetch(&apos;/api/photos&apos;);
                    const data = await response.json();

                    // Filtrar solo las que tienen coordenadas
                    photos.value = data
                        .filter(p =&amp;gt; p.lat &amp;amp;&amp;amp; p.lng)
                        .sort((a, b) =&amp;gt; new Array(a.date).toLocaleString().localeCompare(new Array(b.date).toLocaleString()));

                    // Añadir marcadores
                    photos.value.forEach(photo =&amp;gt; {
                        const marker = L.marker([photo.lat, photo.lng]).addTo(map.value);

                        // Contenido del Popup
                        const popupContent = `
                            &amp;lt;div style=&quot;text-align:center&quot; id=&quot;pop-${photo.id}&quot;&amp;gt;
                                &amp;lt;strong&amp;gt;${photo.date_human}&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;
                                &amp;lt;img src=&quot;${photo.url}&quot;
                                     class=&quot;img-click&quot;
                                     style=&quot;width:150px; cursor:pointer; border-radius:4px; margin-top:5px;&quot; /&amp;gt;
                            &amp;lt;/div&amp;gt;
                        `;
                        marker.bindPopup(popupContent);
                        marker.on(&apos;popupopen&apos;, function() {
                            const container = document.querySelector(`#pop-${photo.id} .img-click`);
                            if (container) {
                                container.onclick = () =&amp;gt; {
                                    window.openImage(photo.id);
                                };
                            }
                        });
                    });

                } catch (error) {
                    console.error(&quot;Error cargando fotos:&quot;, error);
                }
            };

            onMounted(() =&amp;gt; {
                initMap();
                loadPhotos();
                window.addEventListener(&apos;keydown&apos;, (e) =&amp;gt; {
                    if (e.key === &apos;Escape&apos;) {
                        selectedPhoto.value = null;
                    }
                });
            });

            return { photos, selectedPhoto, openImage, getFullSizeUrl};
        }
    }).mount(&apos;#app&apos;);&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente llama al api, y recorriendo el JSON crea Markups en el mapa&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusiones&quot;&gt;Conclusiones&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A parte de la (nula) utilidad del proyecto, este desarrollo me ha venido bien para investigar un poco
más de PHP y su integración con Google&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Las primeras tortas fueron usando el SDK de PHP, en concreto el de GooglePhotos, pero &quot;gracias&quot;
a que es un dolor de muelas usarlo al final he optado por una aproximación más simple que me ha servido
para ver lo fácil que es usar PHP&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Creando un mapa a partir de fotos en Google Drive</summary>
    </entry>
    <entry>
        <title>Self-hosting Snikket and GoToSocial behind Caddy</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2026/gotosocial-snikket-caddy.html"/>
        <updated>2026-01-05T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2026/gotosocial-snikket-caddy.html</id>
        <category term="fediverse"/>
        <category term="gotosocial"/>
        <category term="snikket"/>
        <category term="xmpp"/>
        <category term="docker"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After a couple of months running my GotoSocial instance, I wanted to add more federated-opensource
services to my stack, so I decided to add an XMPP server. After a little research (a simple Google search) I decided to use Snkikket.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As my instance is a 5€ monthly machine, my first intent was to create another similar instance and
install Snikket on it. But I wondered if all the stack could work in the same machine and maybe save
so money&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As I deployed my GotoSocial instance using Docker, and the Snikket&amp;#8217;s installation guide refers to Docker,
I thought it would be easy to add to my docker-compose.yml&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;initial&quot;&gt;Initial&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Deploying a GotoSocial instance using Docker is as &quot;simple&quot; as running a docker-compose.yml similar to&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;services:
  gotosocial:
    image: docker.io/superseriousbusiness/gotosocial:latest
    container_name: gotosocial
    user: 1000:1000
    networks:
      - gotosocial
    environment:
      GTS_HOST: social.jagedn.dev
      GTS_DB_TYPE: sqlite
      GTS_DB_ADDRESS: /gotosocial/storage/sqlite.db
      GTS_LETSENCRYPT_ENABLED: &quot;false&quot;
      GTS_PORT: &quot;8080&quot;
      GTS_TRUSTED_PROXIES: &quot;172.18.0.0/16&quot;
      GTS_LETSENCRYPT_EMAIL_ADDRESS: &quot;jorge@xxxx.xxxx&quot;
      GTS_WAZERO_COMPILATION_CACHE: /gotosocial/.cache
      GTS_STATUSES_MAX_CHARS: 512
      TZ: Europe/Madrid
      GTS_MEDIA_LOCAL_MAX_SIZE: 60MiB
      GTS_INSTANCE_EXPOSE_PUBLIC_TIMELINE: &quot;true&quot;
    expose:
      - &quot;8080&quot;
    volumes:
      - /home/ubuntu/gotosocial/data:/gotosocial/storage
    restart: &quot;always&quot;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;caddy&quot;&gt;Caddy&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;My first (easy) step was to include Caddy in my stack and let Caddy work as a proxy-reverse against
my GotoSocial.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Add Caddy to the docker-compose&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;  caddy:
    image: caddy:latest
    container_name: caddy
    restart: always
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
      - acme_challenges:/var/www/challenges
    networks:
      - gotosocial&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Caddyfile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;social.jagedn.dev {
    reverse_proxy gotosocial:8080
}&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now Caddy will negociate the Let&amp;#8217;s Encrypt SSL Certificate and redirect all requests to the
gotosocial instance&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simple.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;As a plus, this configuration allows me to add more applications&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the_challenge_the_snikket_monolith&quot;&gt;The Challenge: The Snikket &quot;Monolith&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;While Snikket is often deployed as a single Docker image,
running it behind an existing reverse proxy like Caddy requires breaking it down into its core components:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The Server (Prosody): The brain of the XMPP operations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Portal: The web interface for users and admins.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Cert-Manager: To handle XMPP-specific encryption&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Following Snikket&amp;#8217;s tutorial, it&amp;#8217;s straightforward to deploy all these services in a docker-composer
&lt;strong&gt;but only if they act as a hole and not behind a reverse proxy&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;  snikket_certs:
    container_name: snikket-certs
    image: snikket/snikket-cert-manager:stable
    networks:
      - gotosocial
    env_file: snikket.conf
    volumes:
      - snikket_data:/snikket
      - acme_challenges:/var/www/.well-known/acme-challenge
    restart: &quot;unless-stopped&quot;

  snikket_portal:
    container_name: snikket-portal
    image: snikket/snikket-web-portal:stable
    networks:
      - gotosocial
    env_file: snikket.conf
    restart: &quot;unless-stopped&quot;

  snikket_server:
    container_name: snikket
    image: snikket/snikket-server:stable
    networks:
      - gotosocial
    expose:
      - &quot;5280&quot;
      - &quot;5281&quot;
    ports:
      - &quot;5222:5222&quot; # XMPP Client
      - &quot;5269:5269&quot; # XMPP Federation
      - &quot;3478:3478&quot; # TURN
      - &quot;3478:3478/udp&quot;
      - &quot;5000:5000/tcp&quot;
      - &quot;5000:5000/udp&quot;
    volumes:
      - snikket_data:/snikket
    env_file: snikket.conf
    restart: &quot;unless-stopped&quot;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;snikket.conf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;SNIKKET_DOMAIN=chat.jagedn.dev
SNIKKET_ADMIN_EMAIL=jorge@edn.es&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(I will omit the part you need to configure DNS in your internet provider as the aim of the post
is not a HOWTO install Snikket)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Caddyfile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;social.jagedn.dev {
    reverse_proxy gotosocial:8080
}

chat.jagedn.dev,
groups.chat.jagedn.dev,
share.chat.jagedn.dev {

    handle_path /.well-known/acme-challenge/* {
        root * /var/www/challenges
        file_server
    }

    handle {
        reverse_proxy snikket_portal:5765
    }
}&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As the Cert-Manager is responsible to negociate the certificate for mobile comms, we have
to align it with Caddy. This part was solved easily sharing the &lt;code&gt;acme_challenges&lt;/code&gt; folder&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also, the docker-compose uses typical ports 443 and 80, but in my case Caddy manages these ports&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Another issue is that with this default mapping, the portal tries to talk to the server via 127.0.0.1, which fails in a multi-container Docker setup, and we need to find the way to configure the services.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After several tries and errors, and digging a lot into the Snikket GitHub repo, I was able to figure
how to configure all the pieces&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the_aha_moment&quot;&gt;The &quot;Aha!&quot; Moment&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;At the end it was so easy as configure snikket.conf:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;snikket.conf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;SNIKKET_DOMAIN=chat.jagedn.dev
SNIKKET_ADMIN_EMAIL=jorge@edn.es

SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE=0.0.0.0
SNIKKET_TWEAK_INTERNAL_HTTP_INTERFACE=0.0.0.0

SNIKKET_WEB_PROSODY_ENDPOINT=http://snikket:5280
SNIKKET_TWEAK_INTERNAL_HTTP=true&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In sum up:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;configure portal to listen in all interfaces not only 127.0.0.1, so Caddy can redirect calls to it&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;configure server to listen in all interfaces so portal can validate requests against it&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;configure portal to call snikket container instead to 127.0.0.1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(to be honest, not sure if SNIKKET_TWEAK_INTERNAL_HTTP is required)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Docker networking can be a real pain if you don&amp;#8217;t keep an eye on where your traffic is going.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I spent too much time fighting against 502 errors just to realize I was pointing Caddy to the wrong port or the wrong container name.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the end, keeping it simple and forcing the internal endpoints was all it took. Now I have a full social stack running on a tiny VPS, and it actually works!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;But the most important part is, since this is all Open Source, I didn&amp;#8217;t have to just &quot;guess&quot; why it was failing. When the logs kept showing the portal was stuck on 127.0.0.1, I could actually look into the code and the internal scripts to see which environment variables it was looking for.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now I have a full social stack running on a tiny VPS, and it actually works because I took the time to understand what was happening under the hood.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Self-hosting Snikket and GoToSocial behind Caddy</summary>
    </entry>
    <entry>
        <title>Tooteando cámaras de Madrid</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/camaras-madrid-fedi.html"/>
        <updated>2025-12-14T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/camaras-madrid-fedi.html</id>
        <category term="fediverse"/>
        <category term="fediverso"/>
        <category term="toot"/>
        <category term="google"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Spoiler&lt;/strong&gt; : este script usa Google App Script. Si Google te da asco seguramente no quieras seguir leyendo. Avisado estás&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;opendata&quot;&gt;OpenData&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dentro del catálogo de datos abiertos del ayuntamiento de Madrid hay uno que publica un xml con los datos de todas las cáramas que controlan el tráfico (y a nosotros por ende) de la ciudad&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada elemento indica la posición, nombre etc y una URL hacia la última imagen tomada (creo que se refrescan cada 5 minutos aprox)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post voy a explicar cómo uso este OpenData para elegir una cámara aleatoria y subirla a mi cuenta de &quot;Mastodon&quot; (en realidad GotoSocial) junto con un texto. Tal vez te pueda servir de ejemplo/inspiración para crear tus scripts.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script se ejecuta en Google Script pero podría tenerlo en una raspberry, en mi ordenador &amp;#8230;&amp;#8203; Y por otra parte está hecho en Javascript pero podrías hacerlo hasta con puro Bash con un curl&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;crear_fedi_aplicación&quot;&gt;Crear fedi-aplicación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero a hacer es crearnos una aplicación en nuestra cuenta del Fediverso. Cada sistema (mastodon, gotosocial, pleroma) lo hacen de forma diferente pero muy parecida.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente en settings crearás una &quot;application&quot;, le das permisos de read-write y una vez que la autorizas te devuelve un token.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este token hay que guardarlo aunque en cualquier momento puedes borrar la application y volver a recrearlo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El nombre de la aplicación que creas aparecerá como metadato y se podrá &quot;ver&quot; en tu timeline.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;crear_script&quot;&gt;Crear Script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si tienes cuenta en Google (gmail por ejemplo) puedes crear un proyecto en &lt;a href=&quot;https://script.google.com&quot; class=&quot;bare&quot;&gt;https://script.google.com&lt;/a&gt; y configurar &quot;disparadores&quot; que lo ejecuten&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En nuestro caso vamos a usar un disparador de tiempo que ejecutará
el script todos los días entre las 8 y las 9 de la mañana&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el editor que nos abre Google copiaremos este código (el cual explico
a continuación)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;const url = &quot;https://datos.madrid.es/egob/catalogo/202088-0-trafico-camaras.kml&quot;
const instancia = &apos;social.jagedn.dev&apos;;
const bootToken = &apos;ZTMYYTQ4-----recuerda no compartir tu token ni en un post&apos;;

function myFunction() {

  let xml = UrlFetchApp.fetch(url).getContentText(&apos;ISO-8859-1&apos;);
  let document = XmlService.parse(xml);
  let root = document.getRootElement();
  let ids = new Array()
  root.getChildren().forEach(document=&amp;gt;{
    document.getChildren().forEach(item=&amp;gt;{
      if(item.getName()==&quot;Placemark&quot;){
        item.getChildren().forEach(extendedData=&amp;gt;{
          if( extendedData.getName()==&apos;ExtendedData&apos;){
            extendedData.getChildren().forEach(data=&amp;gt;{
              if(data.getAttribute(&quot;name&quot;).getValue()==&quot;Numero&quot;){
                data.getChildren().forEach(value=&amp;gt;{
                    ids.push(value.getText())
                })
              }
            })
          }
        })
      }
    })
  })
  let id = ids[Math.floor(Math.random() * ids.length)];
  let image = UrlFetchApp.fetch(&quot;https://informo.madrid.es/cameras/Camara&quot;+id+&quot;.jpg?v=&quot;+new Date().getMilliseconds()).getBlob()
  sendMedia(image, &quot;Una #random webcam de #Madrid al día\n(Enviada con un script, podriamos decir que es un bot o no, qué se yo)&quot;)
}

function sendMedia(blob, toot) {

  var urlMedia = `https://${instancia}/api/v1/media`;
  var url = `https://${instancia}/api/v1/statuses`;

  var boundary = &quot;xxxxxxxxxx&quot;;
  var data = &quot;&quot;;

  data += &quot;--&quot; + boundary + &quot;\r\n&quot;;
  data += &quot;Content-Disposition: form-data; name=\&quot;file\&quot;; filename=\&quot;chart.png\&quot;\r\n&quot;;
  data += &quot;Content-Type:image/png\r\n\r\n&quot;;

  var payloadImage = Utilities.newBlob(data).getBytes()
    .concat(blob.getBytes())
    .concat(Utilities.newBlob(&quot;\r\n--&quot; + boundary + &quot;--&quot;).getBytes());

   var optionsImage = {
    method : &quot;post&quot;,
    contentType : &quot;multipart/form-data; boundary=&quot; + boundary,
    payload : payloadImage,
    headers:{
      &apos;Authorization&apos;: `Bearer ${bootToken}`
    },
    muteHttpExceptions: true,
  };
  var respImage = UrlFetchApp.fetch(urlMedia, optionsImage).getContentText();
  Logger.log(respImage)
  var jsonImage = JSON.parse(respImage);

  var payload = {
    status: toot,
    media_ids: [jsonImage.id]
  }
  var options = {
    &apos;method&apos; : &apos;post&apos;,
    &apos;contentType&apos;: &apos;application/json&apos;,
    &apos;payload&apos;: JSON.stringify(payload),
    &apos;headers&apos;: {
      &apos;Authorization&apos;: `Bearer ${bootToken}`
    },
    &apos;muteHttpExceptions&apos;:true
  };
  Logger.log(payload)

  const resp = UrlFetchApp.fetch(url, options);
  Logger.log(resp)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;myfunction&quot;&gt;myFunction&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;este es el punto de entrada a nuestro proyecto. Simplemente se descarga la
última versión del XML y lo parsea, extrayendo los ids de todas las cámaras&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como el XLM es un KML (xml con etiquetas de posicionar elementos geográficos) es un poco raruno pero se lee fácil.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente elegimos un id al azar y nos descargamos la imagen
(la url es la misma para todas las cámaras cambiando el id). Le añado
además un parámetro en la url con la fecha actual para evitar temas de cacheo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez tenemos el &quot;blog&quot;, o sea el binario remoto, llamamos a la funcion
que lo enviará al fediverso de nuestra instancia&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si usas otro lenguaje (tipo PHP o Java) o incluso con Javascript usando axios, puedes usar sus funciones para subir un fichero mediante
&lt;code&gt;multipart/form-data&lt;/code&gt; (formato requerido por las instancias)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mi ejemplo es una implementación &quot;a pelo&quot; de cómo subir un multipart
usando &quot;boundary&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez enviada la imagen a la url &lt;code&gt;/api/v1/media&lt;/code&gt; el servidor nos
devolverá un json con la info del recurso creado por lo que nos
guardamos su id&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación creamos un toot llamando a la url &lt;code&gt;/api/v1/statuses&lt;/code&gt;
pasando un json con el payload requerido. Simplemente proporcionamos
un texto (en este caso fijo, en otros podría ser generado al vuelo)
y añadimos el &lt;code&gt;media_ids&lt;/code&gt; con el id que nos devolvió de la imagen&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y eso es todo, si todo va bien habremos publicado un toot con una imagen
callejera&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;otras_ideas&quot;&gt;Otras ideas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tomando una hoja de GoogleSheet como fuente de datos puedes hacer un
script que tootee un chiste, o que genere un gráfico de barras
según los datos del excel y lo subas al fediverso&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;O pasar de Google y tenerlo autoalojado en una raspberry&amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La imaginación es el límite&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Un pequeño script para tootear imágenes de Madrid</summary>
    </entry>
    <entry>
        <title>Writing (and publishing) a Nextflow Plugin III</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/writing-nextflow-plugin-iii.html"/>
        <updated>2025-09-30T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/writing-nextflow-plugin-iii.html</id>
        <category term="nextflow"/>
        <category term="plugin"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Welcome to this comprehensive four-part series on developing and publishing your own Nextflow plugins!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow plugins allow you to extend the core functionality of Nextflow, making your pipelines more powerful,
flexible, and integrated with external systems. Whether you&amp;#8217;re looking to add custom executors, integrate cloud services,
or enhance reporting, this series has you covered from initial concept to final release.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Use the links below to easily navigate the entire series:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Part 1: Introduction and Creating a Nextflow Plugin &lt;a href=&quot;writing-nextflow-plugin.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 2: Adding Configuration to Your Nextflow Plugin &lt;a href=&quot;writing-nextflow-plugin-ii.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-ii.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 3: Testing Nextflow Plugins with Spock (You Are Here)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 4: Publishing Documentation and Generating a GitHub Release &lt;a href=&quot;writing-nextflow-plugin-iv.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-iv.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Robust testing is key to reliability. Here, you&amp;#8217;ll learn how to leverage the powerful Spock Framework to write
effective unit and integration tests for your Nextflow plugin components.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;This kind of tests can&amp;#8217;t be used in a complete pipeline. For example, you can&amp;#8217;t test a full Kubernetes
executor (or at least not easily) but using this approach you can validate how your plugin works in a more realistic
situation than simple unit tests&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nf_math&quot;&gt;nf-math&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post we&amp;#8217;ll use the nf-math plugin we created in the first post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We created a &lt;code&gt;test.nf&lt;/code&gt; (and a &lt;code&gt;nextflow.config&lt;/code&gt;) and we show our plugin in action executing nextflow in a terminal.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now we&amp;#8217;ll create a &lt;code&gt;NfDslSpec&lt;/code&gt; Spock test to integrate this kind of validations into our build process&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Spock is a test framework similar to JUnit. You can create tests with JUnit, but Nexflow provides some
utilities that make our tests straightforward to use&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a &lt;code&gt;NfDslSpec.groovy&lt;/code&gt; file at &lt;code&gt;src/main/test/incsteps/plugin&lt;/code&gt; (where incsteps is the package name I choosed
for my plugin. Use your package instead)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Declare the class&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;class NfDslSpec extends Dsl2Spec{ &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

    @Shared String pluginsMode &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Instead from Specification we&amp;#8217;ll use a Dsl2Speec class from Nextflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;We&amp;#8217;ll use to start and stop our plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a setup and cleanup. Spock will execute these methods every time it start/finish a test&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    def setup() {
        PluginExtensionProvider.reset()
        pluginsMode = System.getProperty(&apos;pf4j.mode&apos;)
        System.setProperty(&apos;pf4j.mode&apos;, &apos;dev&apos;)
        def root = Path.of(&apos;.&apos;).toAbsolutePath().normalize()
        def manager = new TestPluginManager(root){
            @Override
            protected PluginDescriptorFinder createPluginDescriptorFinder() {
                return new TestPluginDescriptorFinder(){
                    @Override
                    protected Manifest readManifestFromDirectory(Path pluginPath) {
                        def manifestPath= getManifestPath(pluginPath)
                        final input = Files.newInputStream(manifestPath)
                        return new Manifest(input)
                    }
                    protected Path getManifestPath(Path pluginPath) {
                        return pluginPath.resolve(&apos;build/tmp/jar/MANIFEST.MF&apos;)
                    }
                }
            }
        }
        Plugins.init(root, &apos;dev&apos;, manager)
    }

    def cleanup() {
        Plugins.stop()
        PluginExtensionProvider.reset()
        pluginsMode ? System.setProperty(&apos;pf4j.mode&apos;,pluginsMode) : System.clearProperty(&apos;pf4j.mode&apos;)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically we&amp;#8217;ll integrate our tests with the Pf4j library used by Nextflow instead to use the &quot;installed&quot; plugin&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now create as many tests as you think will be necessary to ensure quality to your plugin&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For example, we&amp;#8217;ll validate the plugin calculate and return values for a list of doubles:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    def &apos;should calculate some values&apos; () {
        when:
        def SCRIPT = &quot;&quot;&quot;
            include { calculate_stats } from &apos;plugin/nf-math&apos;

            Channel.of( [1.0, 2, 3.0, 21.2] )
                .map{ values -&amp;gt;
                    calculate_stats( values )
                }
                .view()
            &quot;&quot;&quot;
        and:
        def result = new MockScriptRunner([
                math:[
                        max: -1
                ]
        ]).setScript(SCRIPT).execute()
        then:
        result.val == [max:21.2, min:1.0, mean:6.8, median:2.5, stddev:9.634659654947166]
        result.val == Channel.STOP
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;def &apos;should calculate some values&apos; ()&lt;/code&gt; is the starting point of our plugin. As you realize the name of the method
can be a sentence describing the intent of the test&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SCRIPT is a simple String where we define the pipeline to be executed. As you can see we use the &lt;code&gt;include&lt;/code&gt; directive,
Channel objet and operators as &lt;code&gt;map&lt;/code&gt; or &lt;code&gt;view&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;View is a good operator to use as we&amp;#8217;ll validate the values returned by the pipeline&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We use a &lt;code&gt;MockScriptRunner&lt;/code&gt; provided by Nextflow to create a &quot;light&quot; version of the nextflow environment where run
the pipeline.  As you can see the first parameter is a Map you can use to provide configuration to the execution
similar to a &lt;code&gt;nextflow.config&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As our pipeline only returns a row (and them the pipeline ends) we can validate the values of the return&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can/must create more test and validate as many situations as you can, simply create more &quot;def should&quot; methods&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Starting a Nexflow plugin from scratch, part III</summary>
    </entry>
    <entry>
        <title>Writing (and publishing) a Nextflow Plugin IV</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/writing-nextflow-plugin-iv.html"/>
        <updated>2025-09-30T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/writing-nextflow-plugin-iv.html</id>
        <category term="nextflow"/>
        <category term="plugin"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Welcome to this comprehensive four-part series on developing and publishing your own Nextflow plugins!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow plugins allow you to extend the core functionality of Nextflow, making your pipelines more powerful,
flexible, and integrated with external systems. Whether you&amp;#8217;re looking to add custom executors, integrate cloud services,
or enhance reporting, this series has you covered from initial concept to final release.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Use the links below to easily navigate the entire series:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Part 1: Introduction and Creating a Nextflow Plugin &lt;a href=&quot;writing-nextflow-plugin.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 2: Adding Configuration to Your Nextflow Plugin &lt;a href=&quot;writing-nextflow-plugin-ii.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-ii.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 3: Testing Nextflow Plugins with Spock &lt;a href=&quot;writing-nextflow-plugin-iii.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-iii.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 4: Publishing Documentation and Generating a GitHub Release (You Are Here)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This article guides you through generating clear documentation and standardizing your release process
using GitHub Actions and features, ensuring your work is easily discoverable and adoptable by the community.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;It&amp;#8217;s important to note that the approach presented here—specifically the use of GitHub Actions for
documentation and releases—is my personal, non-official workflow.
Nextflow doesn&amp;#8217;t mandate a specific release tool, so feel free to adapt this process to your own preferred platform.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;build_and_test&quot;&gt;Build and Test&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We can use GitHub to check every change running our tests.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a folder in the root of the project &lt;code&gt;.github/workflows/&lt;/code&gt; (&lt;strong&gt;pay attention to the dot in github folder&lt;/strong&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a &lt;code&gt;build.yaml&lt;/code&gt; file (name doesn&amp;#8217;t matter only the extension)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: build
on:
  push:
    branches:
      - &apos;*&apos;
    tags-ignore:
      - &apos;*&apos;
  pull_request:
    branches:
      - &apos;*&apos;
jobs:
  build:
    name: Build
    if: &quot;!contains(github.event.head_commit.message, &apos;[ci skip]&apos;)&quot;
    runs-on: ubuntu-latest
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        java_version: [17]

    steps:
      - name: Environment
        run: env | sort

      - name: Checkout
        uses: actions/checkout@v1
        with:
          fetch-depth: 1
          submodules: true

      - name: Setup Java ${{ matrix.java_version }}
        uses: actions/setup-java@v1
        with:
          java-version: ${{matrix.java_version}}
          architecture: x64

      - name: Compile
        run: ./gradlew assemble

      - name: Tests
        run: ./gradlew check
        env:
          GRADLE_OPTS: &apos;-Dorg.gradle.daemon=false&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once merged this file, GitHub will run a &lt;code&gt;./gradlew assemble check&lt;/code&gt; in every commit, in all branches, all PR, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If some test fails, we&amp;#8217;ll receive a notification.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;asciidoctor&quot;&gt;Asciidoctor&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;m a big (big big) fan of Asciidoctor (&lt;a href=&quot;https://asciidoctor.org/&quot; class=&quot;bare&quot;&gt;https://asciidoctor.org/&lt;/a&gt;) and as it has a Gradle plugin one of the &quot;extra&quot;
features I like to include in my plugins documentation using it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;gradle&quot;&gt;Gradle&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Add the plugin into &lt;code&gt;plugins&lt;/code&gt; closure of &lt;code&gt;build.gradle&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins {
    id &apos;org.asciidoctor.jvm.convert&apos; version &apos;3.3.2&apos;  &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
    id &apos;io.nextflow.nextflow-plugin&apos; version &apos;1.0.0-beta.9&apos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Add the asciidoctor plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create the asciidoctor configuration&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;asciidoctor{ &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
    resources {
        from(&apos;src/docs/asciidoc/images&apos;) {
            include &apos;**/*.png&apos;
        }

        into &apos;./images&apos;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Add to the end of build.gradle&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;index_adoc&quot;&gt;Index.adoc&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create your first &lt;code&gt;index.adoc&lt;/code&gt; page at &lt;code&gt;src/docs/asciidoc&lt;/code&gt; folder&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;index.adoc&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;= nf-csvext
Jorge Aguilera &amp;lt;jorge@incsteps.com&amp;gt;
:toc: left
:imagesdir: images

== Install

To install the plugin, add the plugin into your `nextflow.config`

blablabla&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Asciidoctor will convert all &lt;code&gt;.adoc&lt;/code&gt; files to &lt;code&gt;.html&lt;/code&gt; so you can create several pages and link all of them,
for example&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;github_pages&quot;&gt;Github Pages&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you use GitHub (in case you use others as Gitlab for example process is very similar) you need to prepare your
repository:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;go to settings&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;select pages&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;set &quot;Build and deployment Source&quot; as &quot;Github Actions&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Them create a &lt;code&gt;ghpages.yml&lt;/code&gt; file (ghpages can be whatever
but extension needs to be &lt;code&gt;yml&lt;/code&gt; or &lt;code&gt;yaml&lt;/code&gt;) in the &lt;code&gt;.github/workflows/&lt;/code&gt; folder:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ghpages.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: ghpages
on:
  push:
    branches:
      - &apos;main&apos; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: &quot;pages&quot;
  cancel-in-progress: false

jobs:
  build:
    name: Build docu
    runs-on: ubuntu-latest
    timeout-minutes: 90
    steps:
      - name: Environment
        run: env | sort
      - uses: actions/checkout@v4
      - name: Set up JDK for x64
        uses: actions/setup-java@v3
        with:
          java-version: &apos;17&apos;
          distribution: &apos;temurin&apos;
          architecture: x64
      - name: Generate
        run: ./gradlew asciidoctor
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3.0.1
        with:
          path: ./build/docs/asciidoc

  publish-ghpages:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;adjust it in case you use other as &lt;code&gt;master&lt;/code&gt; for example&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once committed and merged in the main branch, next merges will fire a GitHub action to build a static site and publish
it in the repository GitHub pages. For example, if my repo is &lt;code&gt;github.com/incsteps/nf-csvext&lt;/code&gt; the url will be
&lt;a href=&quot;https://incsteps.github.io/nf-csvext&quot; class=&quot;bare&quot;&gt;https://incsteps.github.io/nf-csvext&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;tag_and_release&quot;&gt;Tag and Release&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Another feature GitHub Actions allows us is to automatically generate and publish a new release when we create a &lt;strong&gt;tag&lt;/strong&gt;
in our repo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;secret&quot;&gt;Secret&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;First, we need to add our NPR_API_KEY token to our repository as a &lt;strong&gt;secret&lt;/strong&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Goto settings&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Secrets and Variables&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Actions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;New repository secret&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And create a new secret with your token. We&amp;#8217;ll use NPR_API_KEY but can be whatever&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;github_action&quot;&gt;Github Action&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Them, in your repo, create a &lt;code&gt;release.yaml&lt;/code&gt; file in the &lt;code&gt;.github/workflows&lt;/code&gt; folder (as with ghpages the name doesn&amp;#8217;t matter only
the extension)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;release.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: release
on:
  push:
    tags:
      - &apos;[0-9]+.[0-9]+.[0-9]+&apos;
      - &apos;[0-9]+.[0-9]+.[0-9]+-edge[0-9]+&apos;

permissions:
  contents: write

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    timeout-minutes: 90
    strategy:
      fail-fast: false
      matrix:
        java_version: [19]

    steps:
      - name: Environment
        run: env | sort

      - name: Checkout
        uses: actions/checkout@v1
        with:
          fetch-depth: 1
          submodules: true

      - name: Setup Java ${{ matrix.java_version }}
        uses: actions/setup-java@v1
        with:
          java-version: ${{matrix.java_version}}
          architecture: x64

      - name: Compile
        run: ./gradlew assemble

      - name: Tests
        run: ./gradlew check
        env:
          GRADLE_OPTS: &apos;-Dorg.gradle.daemon=false&apos;

  publish-gpr:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 19 for x64
        uses: actions/setup-java@v3
        with:
          java-version: &apos;19&apos;
          distribution: &apos;temurin&apos;
          architecture: x64

      - name: build artifacts
        run: ./gradlew clean installPlugin -x test -P version=${GITHUB_REF#refs/tags/}

      - name: Upload artifact and release
        uses: softprops/action-gh-release@v2
        with:
          draft: false
          prerelease: false
          body_path: CHANGELOG.md
          files: |
            ./build/distributions/*

      - name: publish release
        env:
          NPR_API_KEY: ${{ secrets.NPR_API_KEY }}
        run: ./gradlew releasePlugin -x test -P version=${GITHUB_REF#refs/tags/}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically, we&amp;#8217;re telling to GitHub we want to run this pipeline when a new &lt;strong&gt;tag&lt;/strong&gt; is created. This tag needs to follow
a semver pattern (0.0.1, 1.22.33), also we&amp;#8217;ll allow &quot;release candidates&quot; identified by &lt;code&gt;edge-XXX&lt;/code&gt; suffix.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can change this behavior and use your pattern if you want&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;create_a_release&quot;&gt;Create a Release&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When you&amp;#8217;re ready to publish a new release click in your GitHub repository page at &quot;Releases&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Draft a new release&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a tag (remember to use the semver notation)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Give a title&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Generate a release notes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Publish the release&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once you publish the release GitHub will create the tag and your pipeline will be executed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The pipeline will build and test last version of your plugin and if all goes well it will send your to the registry
( &lt;code&gt;./gradlew releasePlugin -x test -P version=${GITHUB_REF#refs/tags/}&lt;/code&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Also, the action will attach the artifacts to the release as future references&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And this is all. Once the plugin is approved, it will be ready to be used by the community!!!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Starting a Nexflow plugin from scratch, part IV</summary>
    </entry>
    <entry>
        <title>Writing (and publishing) a Nextflow Plugin II</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/writing-nextflow-plugin-ii.html"/>
        <updated>2025-09-28T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/writing-nextflow-plugin-ii.html</id>
        <category term="nextflow"/>
        <category term="plugin"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Welcome to this comprehensive four-part series on developing and publishing your own Nextflow plugins!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow plugins allow you to extend the core functionality of Nextflow, making your pipelines more powerful,
flexible, and integrated with external systems. Whether you&amp;#8217;re looking to add custom executors, integrate cloud services,
or enhance reporting, this series has you covered from initial concept to final release.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Use the links below to easily navigate the entire series:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Part 1: Introduction and Creating a Nextflow Plugin &lt;a href=&quot;writing-nextflow-plugin-iv.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-iv.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 2: Adding Configuration to Your Nextflow Plugin (You Are Here)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 3: Testing Nextflow Plugins with Spock &lt;a href=&quot;writing-nextflow-plugin-iii.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-iii.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 4: Publishing Documentation and Generating a GitHub Release &lt;a href=&quot;writing-nextflow-plugin-iv.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-iv.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Moving beyond the basic setup, this post dives into how to define, manage, and access custom configuration within your plugin, making it truly adaptable for various user environments.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To have a more interesting plugin, we&amp;#8217;ll create a new &lt;code&gt;nf-llm&lt;/code&gt; plugin following the steps explained in the previous post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This new plugin will allow the user to store messages in an Embedded store and &quot;chat&quot; with an LLM at the end of the
pipeline about the messages collected during the execution&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For example, a simple pipeline can be&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;include { addMessage; chat } from &apos;plugin/nf-llm&apos;

channel.of( &apos;hi&apos;,
            &apos;this is a simple sentence generated at &apos;+new Date(),
            &apos;dont be shine and give me a hi&apos;
        )
        .subscribe(
                onNext: { v -&amp;gt;
                    addMessage v
                },
                onComplete: {
                    println chat(&quot;Generate a brief of the conversation&quot;)
                })&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Imagine instead a simple channel of strings you can call &lt;code&gt;addMessage&lt;/code&gt; in every process executed and once completed
the pipeline you can request to the LLM to generate a report, analyze times, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For the sake of simplicity, our plugin will use Google Gemini (but can be easily replaced with OpenAI, Ollama, etc),
so the user needs to provide his &lt;code&gt;apiKey&lt;/code&gt; in the &lt;code&gt;nextflow.config&lt;/code&gt; . Also, the user will be allowed to specify
which model to use in their pipeline:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins {
    id &quot;nf-llm@0.1.0&quot;
}

llm{
    model = &quot;gemini-2.5-flash&quot;
    apiKey = &quot;AIzaSy------&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;creating_our_new_plugin&quot;&gt;Creating our new plugin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Following the previous post, we have a new plugin created, so now is the time to add our logic:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;add dependencies&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    implementation &quot;dev.langchain4j:langchain4j:1.4.0&quot;
    implementation &quot;dev.langchain4j:langchain4j-embeddings-all-minilm-l6-v2:1.4.0-beta10&quot;
    implementation &quot;dev.langchain4j:langchain4j-google-ai-gemini:1.4.0&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;create an Assistant interface&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/groovy/incsteps.plugin/Assistant.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;package incsteps.plugin

interface Assistant {

    String chat(String prompt)

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;create an LlmFactory&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/groovy/incsteps.plugin/LlmFactory.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.memory.chat.MessageWindowChatMemory
import dev.langchain4j.model.chat.ChatModel
import dev.langchain4j.model.embedding.EmbeddingModel
import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever
import dev.langchain4j.service.AiServices
import dev.langchain4j.store.embedding.EmbeddingStore
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore

class LlmFactory {

    static EmbeddingModel embeddingModel(){
        new AllMiniLmL6V2EmbeddingModel()
    }

    static EmbeddingStore&amp;lt;TextSegment&amp;gt; embeddingStore() {
        new InMemoryEmbeddingStore()
    }

    static ChatModel chatModel(String model, String apiKey){
        return GoogleAiGeminiChatModel.builder()
                .apiKey(apiKey)
                .modelName(model)
                .temperature(0.8)
                .build();
    }

    static Assistant assistant(ChatModel model, EmbeddingStore embeddingStore){
        return AiServices.builder(Assistant.class)
                .chatModel(model)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(Integer.MAX_VALUE))
                .contentRetriever (EmbeddingStoreContentRetriever
                        .builder()
                        .maxResults(30)
                        .embeddingStore(embeddingStore)
                        .build())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Basically, we are creating our &quot;business logic&quot; following Langchain4j tutorials creating a series of
artifacts required to build an &lt;code&gt;Assistant&lt;/code&gt; how use an InMemory storage as knowledge repository&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By the moment, nothing special relates to our Nextflow Plugin&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;configuration&quot;&gt;Configuration&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll create a new class to represent our configuration requirements (remember, a model and an apiKey)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/groovy/incsteps.plugin/PluginConfig.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;class PluginConfig {

    final String model
    final String apiKey

    private PluginConfig(String model, String apiKey){
        this.model = model
        this.apiKey = apiKey
    }


    static PluginConfig fromMap(Map config){
        assert config.containsKey(&quot;apiKey&quot;)

        String model = config.model ? config.model.toString() : &quot;gemini-2.5-flash&quot;
        String apiKey = config.apiKey.toString()

        new PluginConfig(model, apiKey)
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see, this is a simple Java Bean. You can model as you want, but basically the idea is to retrieve values
from a Map. In my example, the only way to create a PluginConfig is using the &lt;code&gt;static fromMap&lt;/code&gt; method&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This Map can be retrieved from the &lt;code&gt;Session&lt;/code&gt; in our &lt;code&gt;NfLlmExtension&lt;/code&gt; (created by nextflow)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/main/groovy/incsteps.plugin/NfLlmExtension.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;class NfLlmExtension extends PluginExtensionPoint {

    private PluginConfig llmConfig

    @Override
    protected void init(Session session) {
        llmConfig = session.config.containsKey(&quot;llm&quot;) ? session.config.llm as Map : [:]
        ....
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once all plugins are loaded, Nextflow will initialize all Extensions calling their &lt;code&gt;init&lt;/code&gt; method. This is the moment
to check and validate our configuration&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;initializing_our_function&quot;&gt;Initializing our Function&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Our function requires to be initialized, maybe yours no, so you can skip this part&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As our function wants to store intermediate messages in the storage we need to initialize:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    protected void init(Session session) {
        def llmConfig = session.config.containsKey(&quot;llm&quot;) ? session.config.llm as Map : [:]
        initAssistant( PluginConfig.fromMap( llmConfig ) )
    }

    private Assistant assistant
    private EmbeddingModel embeddingModel
    private EmbeddingStore embeddingStore

    void initAssistant(PluginConfig config){
        embeddingModel = LlmFactory.embeddingModel()
        embeddingStore = LlmFactory.embeddingStore()

        def chatModel = LlmFactory.chatModel(config.model, config.apiKey)
        assistant = LlmFactory.assistant(chatModel, embeddingStore)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;exposing_functions&quot;&gt;Exposing functions&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now our plugin is fully configured and initialized we can expose our functions to the pipeline, as we did with
the nf-math plugin:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    @Function
    void addMessage(String target) {
        def segment = TextSegment.from(target)
        def embedding = embeddingModel.embed(segment)
        embeddingStore.add( embedding.content(), segment )
    }

    @Function
    String chat(String msg) {
        assistant.chat(msg)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;validating&quot;&gt;Validating&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once compiled and &quot;installed&quot; (remember &lt;code&gt;installPlugin&lt;/code&gt; Gradle task?) We can run our &lt;code&gt;test.nf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To validate config is applied, we&amp;#8217;ll try some &quot;invalid&quot; values. For examples remove apiKey from configuration&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins {
    id &quot;nf-llm@0.1.0&quot;
}

llm{
    model = &quot;gemini-2.5-flash&quot;
    // remove or set to null: apiKey = null
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Will fail as &lt;code&gt;apiKey&lt;/code&gt; is required by PluginConfig&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can change also, &lt;code&gt;model&lt;/code&gt; with &quot;gemini-2.5&quot; and compare the result with the flash model, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Remember, this is the &quot;dirty&quot;/&quot;quick&quot; way. You can/need to create tests and validate all uses cases&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;executing&quot;&gt;Executing&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once configured the plugin, I can run the pipeline and &quot;chat&quot; with my LLM:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;nextflow run test.nf

 N E X T F L O W   ~  version 25.04.7

Launching `test.nf` [extravagant_linnaeus] DSL2 - revision: 7e8c5f63b0


The conversation begins with a &quot;hi&quot; and describes itself as a simple sentence generated on Sun Sep 28 11:11:09 CEST 2025. It concludes by requesting a &quot;hi&quot; back, advising the recipient not to be shy.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Starting a Nexflow plugin from scratch, second part</summary>
    </entry>
    <entry>
        <title>Writing (and publishing) a Nextflow Plugin</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/writing-nextflow-plugin.html"/>
        <updated>2025-09-26T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/writing-nextflow-plugin.html</id>
        <category term="nextflow"/>
        <category term="plugin"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Welcome to this comprehensive four-part series on developing and publishing your own Nextflow plugins!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow plugins allow you to extend the core functionality of Nextflow, making your pipelines more powerful,
flexible, and integrated with external systems. Whether you&amp;#8217;re looking to add custom executors, integrate cloud services,
or enhance reporting, this series has you covered from initial concept to final release.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Use the links below to easily navigate the entire series:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Part 1: Introduction and Creating a Nextflow Plugin (You Are Here)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 2: Adding Configuration to Your Nextflow Plugin &lt;a href=&quot;writing-nextflow-plugin-ii.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-ii.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 3: Testing Nextflow Plugins with Spock &lt;a href=&quot;writing-nextflow-plugin-iii.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-iii.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Part 4: Publishing Documentation and Generating a GitHub Release &lt;a href=&quot;writing-nextflow-plugin-iv.adoc&quot; class=&quot;bare&quot;&gt;writing-nextflow-plugin-iv.adoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Some useful links as introduction reference:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nextflow.io/docs/latest/plugins/plugins.html&quot; class=&quot;bare&quot;&gt;https://www.nextflow.io/docs/latest/plugins/plugins.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nextflow.io/docs/latest/plugins/developing-plugins.html&quot; class=&quot;bare&quot;&gt;https://www.nextflow.io/docs/latest/plugins/developing-plugins.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nextflow.io/docs/latest/plugins/plugin-registry.html&quot; class=&quot;bare&quot;&gt;https://www.nextflow.io/docs/latest/plugins/plugin-registry.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;language&quot;&gt;Language&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow is mainly written in Apache Groovy (4.x) language. Groovy is one of the &quot;JVM language&quot; similar to Java,
Kotlin or Scala&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ll use Groovy as the main language, but if you feel more comfortable with Java steps and syntax are very similar,
so it&amp;#8217;s not complicated to code the plugin in Java and it&amp;#8217;ll work&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nextflow_plugin_features&quot;&gt;Nextflow plugin features&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A plugin can provide different features to the Nextflow ecosystem:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Function(s). Instead of populating and repeating the same code across all your pipelines, you can create a plugin
with several functions. For example, a &quot;validate_params&quot; function can validate if the provided params are ok following
your own logic&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Operator(s). Similar to a function but oriented to work with a DataFlow. It receives a DataflowReadChannel and
provides a DataflowWriteChannel. The operator will be notified when an input is ready in the read channel and it
can emit an output when considered necessary&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Factory(s). Similar to operators but oriented to produce data. It returns a DataflowWriteChannel where emit
values. Can be configured with params if you desired&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Observer(s). A plugin can be notified about the progress execution of the pipeline using Observers so you can
create your own report, abort the process, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Executor. Maybe the most complicated feature (at least to me). You can create a new Executor and it will work
with Core executing new Tasks when Core required. You need to maintain/provide also the status of every one&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Command. Your plugin can provide command to be executed &quot;outside&quot; the execution of a pipeline. For example
it can provide a &quot;send-reports-to-our-backend&quot; command to zip all the reports and upload it to your backend server&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;our_plugin&quot;&gt;Our plugin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For the sake of simplicity, in this post we&amp;#8217;ll create a simple plugin with some functions and operators.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Our plugin will be a Math utility with following features:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;some math functions to work with a List (as max, min, average, &amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a operator to calculate some stats of items in a channel. Similar to the functions but reading from a channel&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For sure, for this example, we can implement all the logic in our plugin but as an example of how to use open source
 libraries, I&amp;#8217;ll use Apache Math for math operations.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java 17 (you can use sdkman to install and manage different versions easily)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ Community Edition as IDE (I dont like VSCode, sorry)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Nextflow installed&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;create_the_plugin&quot;&gt;Create the plugin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In you dev folder execute:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow plugin create&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;plugin name: nf-math&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;organization: Incremental Steps (use yours)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;project path: nf-math&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open the nf-math folder with IntelliJ and compile the project using the &lt;code&gt;build&lt;/code&gt; gradle task (you can find it in
the &quot;Gradle&quot; toolbar, at the left)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If all goes well, now we&amp;#8217;ll try to &quot;install&quot; it in our local, so execute the &lt;code&gt;installPlugin&lt;/code&gt; gradle task under
&lt;code&gt;nextflow plugin&lt;/code&gt; section&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a subfolder &lt;code&gt;validation&lt;/code&gt; (or whatever) and create our first pipeline&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;nextflow.config&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins {
    id &quot;nf-math@0.1.0&quot; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;you can find current version value at &lt;code&gt;build.gradle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;test.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;include { sayHello } from &apos;plugin/nf-math&apos;

sayHello(&quot;hi&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now, you can run your pipeline:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow run test.nf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Launching `test.nf` [disturbed_bose] DSL2 - revision: 93fbf85726

Pipeline is starting! 🚀
Hello, hi!
Pipeline complete! 👋&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Congratulations!!! Your first plugin is alive&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;removing_not_desired_features&quot;&gt;Removing not desired features&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll remove some features created by Nextflow&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Delete following files:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;src/main/groovy/incrementalsteps/plugin/NfMathFactory&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;src/main/groovy/incrementalsteps/plugin/NfMathObserver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;src/test/groovy/incrementalsteps/plugin/NfMathObserverTest&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In &lt;code&gt;build.gradle&lt;/code&gt; change&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;extensionPoints = [
        &apos;incrementalsteps.plugin.NfMathExtension&apos;,
        &apos;incrementalsteps.plugin.NfMathFactory&apos;
    ]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;by&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;extensionPoints = [
        &apos;incrementalsteps.plugin.NfMathExtension&apos;,
    ]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;As our plugin will provide only function and operators, we remove Factory and Observer.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Rerun the &lt;code&gt;installPlugin&lt;/code&gt; task and execute again the &lt;code&gt;test.nf&lt;/code&gt; plugin. This time you will see only&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Launching `test.nf` [disturbed_bose] DSL2 - revision: 93fbf85726

Hello, hi!&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Great, we have a starting point for our plugin&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;add_commons_math_dependency&quot;&gt;Add commons-math dependency&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As our plugin will use Apache Math, we&amp;#8217;ll include it as a dependency in our &lt;code&gt;build.gradle&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;dependencies{
    implementation &apos;org.apache.commons:commons-math3:3.6.1&apos; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;You can put it at the end of the file, but I like to put after plugins block, at the beginning&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;our_first_function&quot;&gt;Our first Function&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open &lt;code&gt;NfMathExtension.groovy&lt;/code&gt; and remove the function sayHello (don&amp;#8217;t forget to remove also the annotation @Function)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create our first function&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Function
 Map calculate_stats( List&amp;lt;Double&amp;gt; values) {
     def descriptiveStatistics = new DescriptiveStatistics() &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
     for(double d : values){
         descriptiveStatistics.addValue(d)
     }

     [
             max: descriptiveStatistics.max,
             min: descriptiveStatistics.min,
             mean : descriptiveStatistics.mean,
             median: descriptiveStatistics.getPercentile(50),
             stddev: descriptiveStatistics.standardDeviation
     ]

 }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Include import if intellij doesn&amp;#8217;t include it automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically our function will receive a list of numbers and will return a map with some stats&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ejecute the &lt;code&gt;installPlugin&lt;/code&gt; again&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Reuse our &lt;code&gt;test.nf&lt;/code&gt; or create a new one:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;test.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;include { calculate_stats } from &apos;plugin/nf-math&apos;

println calculate_stats( [1.0, 2, 3.0, 21.2] )&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and execute it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow run test.nf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Launching `test.nf` [crazy_saha] DSL2 - revision: 9e046654f5

[max:21.2, min:1.0, mean:6.8, median:2.5, stddev:9.634659654947166]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;test&quot;&gt;Test&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you realize, We&amp;#8217;ve been testing our tests in a &quot;dirty&quot; way so now we&amp;#8217;ll create a test&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/test/groovy/incrementalsteps/plugin/NfFunctionSpec.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;class NfFunctionSpec extends Specification {

    def &apos;should calculate values for a list&apos; () {
        given:
        def list = [0, 1.2, 3, 23.1, 18]
        def functions = new NfMathExtension()

        when:
        def ret = functions.calculate_stats(list)

        then:
        ret == [1]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This time we&amp;#8217;ll execute the &lt;code&gt;build&lt;/code&gt; gradle to be sure our plugin is validated&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can gess, it will fail because result is not [1]&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Condition not satisfied:

ret == [1]
|   |
|   false
[max:23.1, min:0.0, mean:9.06, median:3.0, stddev:10.696167537954892]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Fix our test changing the condition expression&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;then:
ret == [max:23.1, min:0.0, mean:9.06, median:3.0, stddev:10.696167537954892]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;publish&quot;&gt;Publish&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now our plugin is working fine it&amp;#8217;s time to let the community use it!!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you use Github, you can create a repository and commit your changes on it. Then you create your first Release
using the Github UI and attach the plugin binary&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To generate the binary you need to execute the &lt;code&gt;packagePlugin&lt;/code&gt; and upload the zip located in &lt;code&gt;build/distributions&lt;/code&gt;
folder&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Users can execute your plugin using the NXF_PLUGINS_TEST_REPOSITORY env as explaining in
&lt;a href=&quot;https://www.nextflow.io/docs/latest/plugins/developing-plugins.html&quot; class=&quot;bare&quot;&gt;https://www.nextflow.io/docs/latest/plugins/developing-plugins.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;But if you&amp;#8217;re confidence with your plugin and want everyone uses it you can publish in the official repository:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Create an account at &lt;a href=&quot;https://registry.nextflow.io/&quot; class=&quot;bare&quot;&gt;https://registry.nextflow.io/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create an access token at &lt;a href=&quot;https://registry.nextflow.io/access-tokens&quot; class=&quot;bare&quot;&gt;https://registry.nextflow.io/access-tokens&lt;/a&gt; (grab it in a secure place and never include
it in any repository)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Register/Claim the plugin &lt;a href=&quot;https://registry.nextflow.io/claim-plugin&quot; class=&quot;bare&quot;&gt;https://registry.nextflow.io/claim-plugin&lt;/a&gt; (Pay attention the Provider &lt;strong&gt;must&lt;/strong&gt; to be
same you use when created the plugin and it&amp;#8217;s specified in your &lt;code&gt;build.gradle&lt;/code&gt; ). Approval is requiered, but it
doesn&amp;#8217;t take so much, at least in my recent claims.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open a terminal console and navigate to your plugin folder:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;export NPR_API_KEY=your_token_value
./gradlew clean build installPlugin&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If all goes well, your last version plugin will be uploaded to the registry and someone at Sequera will review it
and hopefully approve it !!!!!!!!!!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now be ready to receive feedback and issues!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;next_step&quot;&gt;Next Step&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the next post we&amp;#8217;ll see how to implement an Operator to consume the double values from a Channel instead as parameters&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Starting a Nexflow plugin from scratch</summary>
    </entry>
    <entry>
        <title>Debugging Nextfow core/plugins</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/debug-nextflow.html"/>
        <updated>2025-09-02T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/debug-nextflow.html</id>
        <category term="groovy"/>
        <category term="nextflow"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Say you&amp;#8217;re developing your own Nextflow plugin (or want to learn how Nextflow works internally) and don&amp;#8217;t want to
populate your code with thousands of &lt;code&gt;println &quot;passing 1&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Say you want to debug your code to understand these corner cases that tests are not able to reproduce&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post, I&amp;#8217;ll show you how easily it is using Intellij (Community Edition is ok). So basically the requirements are:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Intellij as IDE&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A repository with your code (or the Nextflow core repo)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A pipeline to be executed&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ll use &lt;code&gt;nf-parquet&lt;/code&gt; as the plugin I want to debug&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cloning_and_installing_repo&quot;&gt;Cloning and installing repo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In a fresh directory you need to grab the project&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;git clone &lt;a href=&quot;https://github.com/nextflow-io/nf-parquet&quot; class=&quot;bare&quot;&gt;https://github.com/nextflow-io/nf-parquet&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Next, we need to build and install the plugin in our machine&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew installPlugin&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This command will compile and install last version of the plugin in our &lt;code&gt;$HOME/.nextflow/plugins&lt;/code&gt; folder&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;intellij&quot;&gt;IntelliJ&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open the &lt;code&gt;nf-parquet&lt;/code&gt; directory with Intellij and let it to reindex the project (hope a few seconds)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the &lt;code&gt;Project&lt;/code&gt; view (at the left) navigate and open &lt;code&gt;ParquetExtension.groovy&lt;/code&gt; file&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2025/debug-nextflow-1.png&quot; alt=&quot;debug nextflow 1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Scroll down until line &lt;code&gt;44&lt;/code&gt; and click on the line number. A red dot will show the breakpoint is activated in this line&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2025/debug-nextflow-2.png&quot; alt=&quot;debug nextflow 2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now we need to create a &quot;JVM Remote Debug configuration&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Select &quot;Edit configuration&quot;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2025/debug-nextflow-3.png&quot; alt=&quot;debug nextflow 3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and create a new JVM Debug configuration:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2025/debug-nextflow-4.png&quot; alt=&quot;debug nextflow 4&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;press OK and accept default values&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;running_the_pipeline&quot;&gt;Running the pipeline&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nf-parquet&lt;/code&gt; comes with some simple pipelines to validate it. Open a console terminal and navigate to the &lt;code&gt;validation&lt;/code&gt;
folder and execute one of them:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;export PARQUET_PLUGIN_VERSION=0.2.1 &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
nextflow -remote-debug run read.nf &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;nextflow.config allows to test different versions of the plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;pay attention where &quot;-remote-debug&quot; is specified&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If all goes well, the pipeline will be stopped at the start and is waiting for the debug session showing the message
&lt;code&gt;Listening for transport dt_socket at address: 5005&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Go to the Intellij editor and click in the &quot;bug&quot; green button close to the &quot;Unnamed&quot; configuration (in case you didnt provided one)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As soon both JVMs are connected your pipeline will start. When the execution reach the breakpoint it will be stopped
and you can inspect the variables, for example:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2025/debug-nextflow-5.png&quot; alt=&quot;debug nextflow 5&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Uses the debug buttons to &quot;continue&quot;, &quot;step by step&quot;, etc to continue with the execution of your pipeline&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2025/debug-nextflow-6.png&quot; alt=&quot;debug nextflow 6&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;stop the debug process only &quot;detached&quot; your session, the pipeline will be running&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;You dont need to create a new JVM Debug configuration.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;fixing_code&quot;&gt;Fixing code&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Say you find where your code is failing, or you want to try something new. Simple change your code, execute again
the &lt;code&gt;installPlugin&lt;/code&gt; task and repeat the process&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>How to debug your Nextflow's plugin</summary>
    </entry>
    <entry>
        <title>Using parquet files in Nextflow</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/nextflow-parquet.html"/>
        <updated>2025-08-10T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/nextflow-parquet.html</id>
        <category term="parquet"/>
        <category term="csv"/>
        <category term="nextflow"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;csv_vs_parquet&quot;&gt;CSV vs Parquet&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CSV is a common plain text file format where each line represents a row and fields in each row are separated by a
&quot;special&quot; character, usually a comma, tab or pipe&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Parquet, by opposite, is a columnar storage file format optimized for analytics. It&amp;#8217;s highly efficient in both storage and performance&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post I&amp;#8217;ll show you how to deal with Parquet files in your Nextflow pipelines using the &lt;code&gt;nf-parquet&lt;/code&gt; plugin&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;csv_generator&quot;&gt;CSV generator&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll use a Nextflow script to generate a csv file with random values. By default it will generate a
one million rows with 21 fields:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;UUID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;field1, field2, &amp;#8230;&amp;#8203; field10 as random string&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;field11, field12, &amp;#8230;&amp;#8203; field20 as random float&lt;/p&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;You can specify how many rows you want using `--rows&apos; parameter&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;generator.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;import java.util.UUID;

params.out = &quot;$baseDir/data/example.csv&quot;
params.rows = 1_000_000

alphabet = ((&apos;A&apos;..&apos;Z&apos;)+(&apos;0&apos;..&apos;9&apos;)).join()

String randomString(int n){
  new Random().with {
    (1..n).collect { alphabet[ nextInt( alphabet.length() ) ] }.join()
  }
}

String randomFloat(){
    new Random().with{
        nextFloat()
    }.toString()
}

outFile = new File(params.out)
outFile.text = ([&quot;uuid&quot;]+(1..20).collect{ &quot;field${it}&quot;}).join(&quot;,&quot;)+&quot;\n&quot;

(1..params.rows).each{
    outFile &amp;lt;&amp;lt; (
            [ UUID.randomUUID().toString() ]
            +
            (1..10).collect{ randomString(new Random().nextInt(10)) }
            +
            (1..10).collect{ randomFloat() }
        ).join(&quot;,&quot;)+&quot;\n&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow run generator.nf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In my case the script create, after 1-minute aprox, a &lt;code&gt;data/example.csv&lt;/code&gt; with &lt;strong&gt;191Mb&lt;/strong&gt; and one million of rows (plus header)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;reading_a_csv_file&quot;&gt;Reading a CSV file&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To parse a CSV file, we can use the &lt;code&gt;splitCSV&lt;/code&gt; operator provided by Nextflow:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;csv.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;params.input = &quot;$baseDir/data/example.csv&quot;

workflow {
    Channel.fromPath(params.input)
        .splitCsv(header:true)
        .count()
        .view()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ nextflow run csv.nf

 N E X T F L O W   ~  version 25.02.3-edge

Launching `csv.nf` [prickly_heisenberg] DSL2 - revision: 192c2f35f2

1000000&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;reading_a_parquet_file&quot;&gt;Reading a Parquet file&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Before to work with parquet format we need to convert our CSV.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;We can use online tools to convert a CSV file to Parquet, for example &lt;a href=&quot;https://www.tablab.app/csv/to/parquet&quot; class=&quot;bare&quot;&gt;https://www.tablab.app/csv/to/parquet&lt;/a&gt;,
or with a simple Nextflow script. By the moment we&amp;#8217;ll use online tool&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Follow instructions to upload the csv generated and download the &quot;parquet version&quot; into &lt;code&gt;data&lt;/code&gt; subfolder, close to the csv version&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you compared the sizes of both files, you can find first advantage of parquet format:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;example.csv &lt;strong&gt;191Mb&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;example.parquet &lt;strong&gt;112Mb&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;In case your csv contains only a few hundreds of Kb, the size difference will be not notable.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To use parquet files we need to install the &lt;code&gt;nf-parquet&lt;/code&gt; plugin:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;nextflow.config&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins {
     id &quot;nf-parquet&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The plugin contains a &lt;code&gt;splitParquet&lt;/code&gt; operator similar to CSV:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;parquet.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;include { splitParquet } from &apos;plugin/nf-parquet&apos;

params.input = &quot;$baseDir/data/example.parquet&quot;

workflow {
    Channel.fromPath(params.input)
        .splitParquet()
        .count()
        .view()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see, You need to add the &lt;code&gt;splitParquet&lt;/code&gt; function defined in the nf-parquet plugin and use it very similar
as &lt;code&gt;splitCSV&lt;/code&gt; (they differ in the allowed parameters)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;first_benchmark&quot;&gt;First benchmark&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now that we have both format files, we can run a simple comparison:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;time nextflow run csv.nf
....
real    0m9,085s
user    0m27,703s
sys     0m2,415s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Twenty-seven seconds to parse the csv file&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;time nextflow run parquet.nf
....
real    0m5,814s
user    0m20,854s
sys     0m1,542s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Twenty seconds to parse the parquet file&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Only&quot; 7 seconds of difference but it&amp;#8217;s the beginner&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;reading_data&quot;&gt;Reading data&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll dump a tuple of field1 and field20 (for example) in both format&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;csv_view.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;params.input = &quot;$baseDir/data/example.csv&quot;

workflow {
    Channel.fromPath(params.input)
        .splitCsv(header:true)
        .map{ row -&amp;gt; tuple(row.field1, row.field20) }
        .view()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;parquet_view.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;include { splitParquet } from &apos;plugin/nf-parquet&apos;

params.input = &quot;$baseDir/data/example.parquet&quot;

workflow {
    Channel.fromPath(params.input)
        .splitParquet()
        .map{ row -&amp;gt; tuple(row.field1, row.field20) }
        .view()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see, both pipelines look very similar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Running the benchmark on my computer:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;csv took &lt;code&gt;user    0m42,274s&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;parquet took &lt;code&gt;user    0m32,426s&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;defining_a_schema&quot;&gt;Defining a Schema&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Although nf-parquet allows parsing a parquet file in &quot;raw&quot; format as a &lt;code&gt;Map&lt;/code&gt; in a similar way of splitCSV, the
power of parquet format comes when you define a schema to instruct the library about the structure of the file&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a &lt;code&gt;schema&lt;/code&gt; directory in your project and create these files:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;module-info.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;module records { &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
    opens records; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;We&amp;#8217;ll use a &quot;records&quot; package name. Can be anything but need to be the same in the pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Row.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package records; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

record Field1Field2( &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
  String uuid,
  String field1,
  String field2
){}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;the package we specified in module-info.java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;a java record with the names and type of every field&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lastly, we need to compile our schema:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;javac --release 17 -d lib/ schemas/*&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;this process is only required one time meantime your schema doesn&amp;#8217;t change&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;In case you&amp;#8217;re versioning your pipeline, you need to version at least the schema (java files) and
generate the binary before run the pipeline&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now we&amp;#8217;ll run a &quot;raw&quot; count&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;workflow {
    Channel.fromPath(params.input)
        .splitParquet()
        .count()
        .view()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;vs a &quot;schema&quot; count&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;workflow {
    Channel.fromPath(params.input)
        .splitParquet( record: Field1Field20 )
        .count()
        .view()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see, the only difference is we provide a &lt;code&gt;record&lt;/code&gt; params to indicate which fields we&amp;#8217;re
interested to read (parquet not only will ignore other but also will not read them)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;csv took &lt;code&gt;user    0m32,118s&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;raw&quot; took &lt;code&gt;user    0m20,470s&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;schema&quot; took &lt;code&gt;user  0m14,590s&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;writing&quot;&gt;Writing&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;nf-parquet plugin not only allows us to read parquet files but also create&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;writing requires a Schema (implemented in a record Java as shown in the previous section)&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a &lt;code&gt;Row.java&lt;/code&gt; record with your schema:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Row.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;package records;

record Row(
  String id,
  String field1,
  String field2,
  String field3,
  String field4,
  String field5,
  String field6,
  String field7,
  String field8,
  String field9,
  String field10,

  Float field11,
  Float field12,
  Float field13,
  Float field14,
  Float field15,
  Float field16,
  Float field17,
  Float field18,
  Float field19,
  Float field20
){}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;you can use nested records as for example &lt;code&gt;position&lt;/code&gt; with &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt; fields, or more
complex case that it&amp;#8217;s impossible to represent with CSV&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and compile it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;javac --release 17 -d lib/ schemas/*&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;lib folder will contain now a &lt;code&gt;record&lt;/code&gt; subfolder with two classes, Field1Field20 and Row&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;convert.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;include { splitParquet; toParquet } from &apos;plugin/nf-parquet&apos; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

import records.* &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;

params.index = &quot;$baseDir/data/example.csv&quot;
params.output = &quot;$baseDir/data/converted.parquet&quot;

workflow {
    Channel.fromPath(params.index)
        .splitCsv(header:true) \
        .map{ row-&amp;gt;
		    new Row( &lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
                row.uuid,
                row.field1,row.field2,row.field3,row.field4,row.field5,
                row.field6,row.field7,row.field8,row.field9,row.field10,
                row.field11 as float,
                row.field12 as float,
                row.field13 as float,
                row.field14 as float,
                row.field15 as float,
                row.field16 as float,
                row.field17 as float,
                row.field18 as float,
                row.field19 as float,
                row.field20 as float,
	        )
	    }
        .toParquet( params.output, [record:Row])
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;include new function from plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;import the package you specified in module-info.java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;instance a Java record with the desired info&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If all goes well you&amp;#8217;ll have a &lt;code&gt;data/converted.parquet&lt;/code&gt; file.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Also, if you have a parquet viewer, you can see
parquet generated online specifies all columns as String, but our converted file uses Float for last columns
following our Schema&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;comparing_with_real_data&quot;&gt;Comparing with real data&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Someone notices me that using random values doesn&amp;#8217;t use some compress features of Parquet format, so I&amp;#8217;ve
looked for a more realistic example and I run another benchmark. You can read more about it at&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jagedn/nf-parquet-benchmark&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/nf-parquet-benchmark&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>From CSV to Parquet in Nextflow, a tutorial</summary>
    </entry>
    <entry>
        <title>Deploying a Nomad cluster with Terraform and Oracle for Nextflow</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/nomad-nextflow-oracle.html"/>
        <updated>2025-07-22T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/nomad-nextflow-oracle.html</id>
        <category term="nomad"/>
        <category term="nextflow"/>
        <category term="oracle"/>
        <category term="terraform"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;empowering_nextflow_workflows_for_small_teams_with_terraform_and_nomad&quot;&gt;Empowering Nextflow Workflows for Small Teams with Terraform and Nomad&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you&amp;#8217;re part of a small team in bioinformatics or computational science, getting your Nextflow pipelines up and running can be a real headache.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You&amp;#8217;re trying to do awesome science, but instead, you&amp;#8217;re wrestling with servers, trying to figure out how to scale things, and constantly worrying about whether your data&amp;#8217;s in the right place.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;That&amp;#8217;s where tf-nomad project comes in.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We, at Incremental Steps, are working in an IAC (Infrastructure-As-Code) project with the idea to offer a super easy way to spin up the infrastructure you need.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;re using Terraform to automate all the setup – think of it as writing down exactly what you want, and Terraform just makes it happen. Our repo will provision
all Networks, Security Rules, instances, etc the stack requires with one command but most important, if you need to change some resources (more instances,
memory&amp;#8230;&amp;#8203;) Terraform will apply changes automatically.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once Networks are deployed, Terraform will aprovisionate instances and will create a Nomad cluster on it, all running in a private network.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;re also deploying a MinIO instance right inside your cluster. Why? So all your important data stays local, preventing those annoying data transfer fees and speeding up your Nextflow runs significantly.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And for the cherry on top, this solution sets up a Tailscale network. This means you can securely and easily run your pipelines directly from your local machine, connecting seamlessly to the cluster as if it were right next door.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;But wait, there is more: do you have several teams/customers and want to replicate same infra for every one? The project allows to create
as many &quot;clients&quot; you need, every one with their space&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-695287f8335110e5ffa9c72700571694.png&quot; alt=&quot;Diagram&quot; width=&quot;499&quot; height=&quot;772&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the_repository&quot;&gt;The repository&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here you can find the GitHub repository to the project&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/incsteps/nomad-oracle&quot; class=&quot;bare&quot;&gt;https://github.com/incsteps/nomad-oracle&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This repo is structured in modules, for example &lt;code&gt;vcn&lt;/code&gt; describe the network with their security rules, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also, it includes a simple nextflow pipeline to validate every infrastructure once deployed&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;An Oracle Cloud Account. (Why? Basically because we&amp;#8217;ve a free account;) to play with it)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Good news is Terraform makes very similar to deploy the same solution in different providers (AWS, Google, etc)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;An Internet Domain (can be a subdomain or your organization). As your team will work from their local and the stack will be deployed in a remote cloud,
we need a &quot;DNS&quot; to join all parts&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;deploying_a_cluster&quot;&gt;Deploying a cluster&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Say you want to create a stack for TeamA, this is the steps you need to follow to provision it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;create a folder at &lt;code&gt;clients/team_a&lt;/code&gt; and copy files from &lt;code&gt;clients/incsteps&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;set your vars at &lt;code&gt;terraform.tfvars&lt;/code&gt; file as for example &lt;code&gt;client_name&lt;/code&gt;, &lt;code&gt;headscale_domain_name&lt;/code&gt;, or &lt;code&gt;nomad_client_count&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Plan your Terraform and if all looks fine, apply it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ terraform init
$ terraform plan
$ terraform apply&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If all goes well, Terraform will create all required artifacts and output interesting IP&amp;#8217;s. For example, &quot;public IP&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;set your &quot;A&quot; record (DNS) pointing to the new public IP created&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;join_the_cluster&quot;&gt;Join the cluster&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As an admin, you can ssh to the bastion machine using the private key pem you provided when create the stack&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;From this machine you can, for example, approve users to join the private network:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Say our dns is &lt;code&gt;nomad.incsteps.com&lt;/code&gt; and one member of our team want to use the cluster.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The user executes on their computer:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;tailscale up --accept-routes --login-server=https://nomad.incsteps.com&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and a URL with a token is generated&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The user provides to you the token so you can approve the login using &lt;code&gt;headscale accept&lt;/code&gt; command in bastion&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once accepted, the user is able to consume resources in this network (plus their own network)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nextflow_configuration&quot;&gt;Nextflow configuration&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now, the nextflow project configuration can use private IPs:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;nextflow.config&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins {
    id &quot;nf-nomad@0.3.1&quot;
}

process.executor = &quot;nomad&quot;
docker.enabled = true
wave.enabled=true
fusion.enabled=true
fusion.exportStorageCredentials=true

aws {
    accessKey = &apos;minioadmin&apos;
    secretKey = &apos;minioadmin&apos;
    client {
      endpoint = &quot;http://10.10.2.201:9000&quot; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
      s3PathStyleAccess = true
    }
}

nomad {

    client {
        address = &quot;http://10.10.2.188:4646&quot; &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
    }

    jobs {
        deleteOnCompletion = false
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Minio private IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Nomad private IP&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And run their pipelines:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow run hello&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nomad&quot;&gt;Nomad&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The Nomad cluster can be accessed via web browser at &lt;a href=&quot;http://&amp;lt;&amp;lt;nomad-server-ip&amp;gt;:4646&quot; class=&quot;bare&quot;&gt;http://&amp;lt;&amp;lt;nomad-server-ip&amp;gt;:4646&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Current deployment doesn&amp;#8217;t provide any login but, as you as admin can ssh into the machines, you can configure
what you want&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;minio&quot;&gt;Minio&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The stack deploys a Minio instance, so you can use the S3 features from Nextflow without needing an Amazon account.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Moreover, the stack provisions and mount an Oracle File System in the Minio instance so data is persisted outside
the instance.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This project offers a complete, easy-to-use solution for small scientific teams.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By combining Terraform for automated infrastructure, Nomad for efficient workflow orchestration, MinIO for secure, local data storage, and Tailscale for seamless remote access, we&amp;#8217;ve built an environment that lets you focus on what truly matters: your research.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Infrastructure as code for Nextflow pipelines on Nomad</summary>
    </entry>
    <entry>
        <title>Sincronizar serverless functions con CosmosDB</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/cosmosdb-lock.html"/>
        <updated>2025-05-04T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/cosmosdb-lock.html</id>
        <category term="azure"/>
        <category term="cosmosdb"/>
        <content type="html">
            &lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando utilizas &lt;code&gt;serverless&lt;/code&gt; (Azure Functions en mi caso) uno de los problemas recurrentes es cómo sincronizar
el acceso a ciertos recursos para que dos &quot;functions&quot; no se ejecuten a la vez sobre el mismo recurso.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que estamos implementando una aplicación &lt;code&gt;serverless&lt;/code&gt; donde en un momento determinado múltiples usuarios
pueden acceder a la misma.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dependiendo del número de ellos, de la capacidad de la aplicación, etc la infra nos puede crear otra instancia (
en otra máquina diferente, e incluso en otra región) para permitir que nuestra aplicación escale sin interrupción
del servicio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En un escenario típico, donde tenemos el control de las instancias, usaríamos algun mecanismo de bloqueo para que
cada ejecución pueda tomar el control sobre el recurso. Por ejemplo si sólo es una instancia usaríamos variables
estáticas, o semáforos en memoria. Si es en un cluster usaríamos Redis (entre muchas otras opciones), etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero en el caso serverless es más complicado porque no tenemos el control y además las instancias no tienen porqué
&quot;verse&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el mundo Azure contamos con CosmosDB como base de datos distribuida &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
        </content><summary>Cómo bloquear acceso a recursos usando CosmosDB</summary>
    </entry>
    <entry>
        <title>QaQatua</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/qaqatua-1.html"/>
        <updated>2025-03-21T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/qaqatua-1.html</id>
        <category term="qaqatua"/>
        <category term="qa"/>
        <category term="php"/>
        <category term="laravel"/>
        <category term="petproject"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hace unas semanas le eché una mano a un amigo QA, Oscar Islas, a &quot;aterrizar&quot; una idea con la que andaba trabajando y que
me resultó muy atractiva desde el primer momento.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como QA muchas veces tienes que preparar flujos de pruebas contra servicios que manejan información &quot;sensible&quot;,
por ejemplo &quot;cuenta bancaria&quot;, &quot;password&quot;, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algunos de estos servicios requieren el uso de criptografía de tal forma que estos campos van encriptados junto
con el resto y a su vez son devueltos encriptados.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De cara al desarrollo no deja de ser &quot;una capa más&quot;. Quiero decir, cuando desarrollo mi programa para enviar/recibir
estos payloads, configuro mi aplicación con las claves publicas/privadas que el admin del entorno me proporcione
y con unas cuantas líneas de código encripto/desencripto los mensajes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El trabajo del QA es más laborioso. Debe preparar juegos de prueba que contemplen todos los escenarios así que le toca
o bien pregenerar los mensajes o incluir algun tipo de librería que le haga lo mismo que hace el programador lo cual
tampoco es fácil&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Oscar Islas se enfrenta casi a diario a este tipo de situaciones. Como usa Postman pudo desarrollar una pequeña
librería Groovy que embeber a la que invoca para que le haga el trabajo de encriptar/desencriptar payloads.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras echarle una mano revisando y poniendo a punto la librería Groovy se nos ocurrió que esta funcionalidad podría
ser una buena herramienta para la comunidad y así comenzamos QaQatua (un giño a QA y a la funcionalidad que implementa
que no deja de ser una cacatua que repite lo que le dices)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente QaQatua es una aplicación web que, tras configurarla, permite transformar las partes de un payload que le
indiques tanto para encriptar como para desencriptar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues el &quot;flujo&quot; de un usuario (QA principalmente) sería:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;preparar una prueba con datos &quot;en claro&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;invocar al endpoint QaQatua de encriptación, el cual le devolverá el payload transformado&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;invocar al servicio con el payload obtenido de la llamada a QaQatua&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;invocar al endpoint QaQatua de desencriptación, el cual le devolverá el payload transformado&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;validar que los campos retornados, ya &quot;en claro&quot;, son los esperados&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usando Postman, SoapUI, o simple scripts con &lt;code&gt;curl&lt;/code&gt; el uso es sencillo y evita tener que añadir dependencias a la
herramienta&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;qaqatua&quot;&gt;QaQatua&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;QaQatua  permite a un usuario registrarse en el sistema mediante email/password.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez identificado, el usuario puede crear hasta 3 proyectos, proporcionando un nombre identificativo a cada uno.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada proyecto consta de los siguientes campos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;fields, una lista de campos, separados por comas, a modificar en los payloads.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;priv/pub keys, una pareja de clave pública/privada para operar con el payload.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si el usuario no quiere &quot;complicarse&quot; con la gestión de claves puede solicitar a QaQatua que genere un juego de claves.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En cualquier caso estos tres campos son editables en todo momento por lo que puede empezar a diseñar sus casos de
prueba y posteriormente proporcionar las claves que requiera el servicio a invocar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo, al permitir más de un proyecto, es fácil crear casos de pruebas donde intervienen más de un servicio
cada uno con claves diferentes.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;api&quot;&gt;Api&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;QaQatua ofrece un API realmente sencillo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;/api/projects:&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;get&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;post&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;put&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;delete&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/api/encrypt:&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;post&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/api/decrypt:&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;post&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además de contar con un interface Web, el api de projects permite la gestión vía REST&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los endpoints de encrypt/decrypt son altamente configurables y permiten especificar qué proyecto usar en cada
petición (si el usuario tiene sólo un proyecto se usa por defecto) e incluso añadir &lt;code&gt;fields&lt;/code&gt; a usar en una petición
específica&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En los próximos posts iremos contando más detalles de este proyecto, como tecnología usada, casos de éxito,
nuevas funcionalidades, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Qaqatua, un proyecto OpenSource para QAs</summary>
    </entry>
    <entry>
        <title>How I Feel Today, una app para el fediverso</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2025/fediverse-hift.html"/>
        <updated>2025-03-04T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2025/fediverse-hift.html</id>
        <category term="vue"/>
        <category term="fediverse"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hace poco he publicado una aplicación web que permite publicar mensajes en el fediverso sin salir de tu navegador.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La aplicación se llama How I Feel Today y es una aplicación VueJS que se conecta a tu servidor de Mastodon
(en realidad tu servidor del Fediverso pero como todo el mundo lo conoce como Mastodon&amp;#8230;&amp;#8203;.) para publicar
un diagrama &quot;radar&quot; en formato PNG con tus estados de salud, financiero, social y emocional.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin texto, sin explicaciones, solamente una imagen que refleja tu estado de ánimo en ese momento.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ui&quot;&gt;UI&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El interface de la aplicación es muy sencillo, solamente tienes que seleccionar el &quot;nivel&quot; de cada uno de los
estados y la imagen se va ajustando en tiempo real.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2025/hift-web.png&quot; alt=&quot;hift web&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando has ajustado los niveles a cómo te sientes en cada uno puedes publicarlos pulsando simplemente en &quot;publish&quot;
y la aplicación publicará un toot con la imagen&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver, la aplicación es muy sencilla de usar.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;oauth&quot;&gt;OAuth&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para poder publicar tu HIFT lo primero que debes hacer es identificarte en tu instancia y permitir a la aplicación
obtener un &lt;code&gt;access token&lt;/code&gt; para poder publicar en tu cuenta&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La aplicación detecta cuando no está logeada (más detalles a continuación) y te muestra una pantalla para que indiques
la instancia que usas (i.e. mastodon.social, jvm.social, etc)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2025/hift-login.png&quot; alt=&quot;hift login&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando has puesto tu instancia y pulsado Submit, la aplicación te redirige a tu instancia para que la apruebes
y ella se encarga de completar el proceso de autorización guardando el &lt;code&gt;access token&lt;/code&gt; en tu navegador&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi opinión, esta es (una de) la &quot;gracia&quot; de esta aplicación: no requiere de un servidor donde guardar secretos
ni completar de forma oscura el proceso de autorización. Todo se realiza en tu navegador a base de &quot;redirects&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;vue&quot;&gt;Vue&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El código de la aplicación se encuentra publicado en &lt;a href=&quot;https://github.com/jagedn/hift/&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/hift/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La aplicación está desarrollada con Vue 3 y se ejecuta completamente en el navegador por lo que no se guarda nada
en servidores externos (El servidor donde está alojada es de Github pero por tener el código publicado junto
con la aplicación web. En realidad podrías descargarte el código y ejecutarla en tu máquina)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver en el fichero &lt;code&gt;package.json&lt;/code&gt; tiene pocas dependencias:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&quot;bootstrap&quot;: &quot;^5.3.3&quot;,
&quot;chart.js&quot;: &quot;^3.9.1&quot;,
&quot;masto&quot;: &quot;^6.7.0&quot;,
&quot;pinia&quot;: &quot;^2.1.7&quot;,
&quot;vue&quot;: &quot;^3.5.10&quot;,
&quot;vue-chart-3&quot;: &quot;^3.1.8&quot;,&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Bootstrap para poder &quot;enmaquetar&quot; de una forma responsive la página&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Chart.js para generar el diagrama en tiempo real y Vue-Chart-3 para integrarlo en Vue3&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pinia para mantener el estado de la aplicación y compartir los cambios en el modelo de datos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente la aplicación se compone de un &lt;code&gt;parent&lt;/code&gt; HomeView.vue y dos componentes &lt;code&gt;FeelingForm.vue&lt;/code&gt; y &lt;code&gt;FeelingChart.vue&lt;/code&gt;
, así como 2 &lt;code&gt;stores&lt;/code&gt;, uno para mantener la configuración de la instancia a la que te has logeado y otro para mantener
tus &quot;feelings&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;propagación_de_eventos_y_compartir_estado&quot;&gt;Propagación de eventos y compartir estado&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Debido a mis escasos conocimientos de Vue una de las cosas donde me he peleado más ha sido en conseguir sincronizar
los cambios entre el formulario donde seleccionas el nivel de cada sentimiento y la generación del Chart, pero una
vez puesta en marcha la verdad es que muy simple con Vue3&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el parent simplemente usamos &quot;@evento&quot; (@save en este caso) y proporcionamos una funcion a ejecutar ante este evento&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;col-lg-4 col-md-12 col-sm-12 px-0&quot;&amp;gt;
      &amp;lt;FeelingForm @save=&quot;publishFeeling&quot; v-model=&quot;feeling&quot; @logoff=&quot;logoff&quot; :topics=&quot;topics&quot;/&amp;gt;
    &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y en el child emitimos el evento&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const submitForm = () =&amp;gt; {
  emits(&apos;save&apos;);
};

  ...

   &amp;lt;form @submit.prevent=&quot;submitForm&quot;&amp;gt;
    ....
   &amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo, cuando el usuario pulsa Submit se captura el evento y se emite a su vez uno propio &quot;save&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para compartir el estado entre el padre y los hijos Vue lo hace realmente fácil&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el padre usamos &lt;code&gt;ref&lt;/code&gt; para crear objetos &quot;referenciados&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const feeling = {
  a: ref(0),
  b: ref(0),
  c: ref(0),
  d: ref(0),
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y los pasamos a los hijos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt; v-model=&quot;feeling&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En los hijos simplemente definimos el modelo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;defineModel(&apos;modelValue&apos;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y los usamos donde sea necesario&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt; &amp;lt;input type=&quot;range&quot; class=&quot;form-range&quot; id=&quot;a&quot;  @change=&quot;updated&quot; v-model=&quot;modelValue.a.value&quot; min=&quot;1&quot; max=&quot;5&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;fediverse&quot;&gt;Fediverse&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tengo la imagen (en memoria) que quieres publicar es muy sencillo subirla a tu instancia. En realidad
el protocolo es muy simple y se podría hacer con unos simples &lt;code&gt;fetch&lt;/code&gt; pero ya que existe una librería &lt;code&gt;masto&lt;/code&gt; que lo
hace por mí, todo se vuelve más fácil&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  const masto = createRestAPIClient({
    url: url,
    accessToken: accessToken,
  });

  const response = await masto.v1.media.create({
    file: currentImage,
    description:`A radar chart shows four categories:
        ${topics.a} (light red),
        ${topics.b} (light teal),
        ${topics.c} (light gray),
        and ${topics.d} (gold).
    The ${topics.a} category has a value of ${feeling.a.value}
    The ${topics.b} category has a value of ${feeling.b.value}
    The ${topics.c} category has a value of ${feeling.c.value}
    The ${topics.d} category has a value of ${feeling.d.value}
    `
  })
  const mediaId = response.id;

  await masto.v1.statuses.create({
    status: `#HIFT`,
    visibility: &quot;public&quot;,
    mediaIds:[mediaId]
  });&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente creamos un objeto &lt;code&gt;masto&lt;/code&gt; usando la url+accessToken y subimos la imagen con &lt;code&gt;media.create&lt;/code&gt; añadiendo
un texto alternativo (que no falte)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El servidor nos devuelve el id de la imagen que ha guardado y simplemente creamos un toot añadiendo el mediaId&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y eso es todo para publicar un toot con una imágen!!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último simplemente guardamos en el navegador el HIFT publicado para usarlos como punto de partida
la próxima vez que abrás la aplicación&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;github&quot;&gt;Github&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La aplicación está publicada en Github (podría estar alojada en cualquier servidor de contenido estático) y esto
plantea un pequeño reto para aplicaciones Vue y similares que se basan en la ruta del navegador para mantener el estado
además de los redirects necesarios de esta aplicación&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el caso concreto de Github hay dos pequeños &quot;hacks&quot; a tener en cuenta&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El index.html generado por Vue hay que añadirle un pequeño script:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;index.html&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;   &amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
      // Single Page Apps for GitHub Pages
      // MIT License
      // https://github.com/rafgraph/spa-github-pages
      // This script checks to see if a redirect is present in the query string,
      // converts it back into the correct url and adds it to the
      // browser&apos;s history using window.history.replaceState(...),
      // which won&apos;t cause the browser to attempt to load the new url.
      // When the single page app is loaded further down in this file,
      // the correct url will be waiting in the browser&apos;s history for
      // the single page app to route accordingly.
      (function(l) {
        if (l.search[1] === &apos;/&apos; ) {
          var decoded = l.search.slice(1).split(&apos;&amp;amp;&apos;).map(function(s) {
            return s.replace(/~and~/g, &apos;&amp;amp;&apos;)
          }).join(&apos;?&apos;);
          window.history.replaceState(null, null,
                  l.pathname.slice(0, -1) + decoded + l.hash
          );
        }
      }(window.location))
    &amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;además de añadir un .htaccess&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.htaccess&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Options All -Indexes

&amp;lt;Files .htaccess&amp;gt;
order allow,deny
deny from all
&amp;lt;/Files&amp;gt;

&amp;lt;IfModule mod_rewrite.c&amp;gt;
  # Redirect to the public folder
  RewriteEngine On
  # RewriteBase /
  RewriteRule login index.html [L]

  # Redirect to HTTPS
  # RewriteEngine On
  # RewriteCond %{HTTPS} off
  # RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así conseguimos que cuando la instancia en la que hacemos login nos haga el redirect a &lt;code&gt;login&lt;/code&gt; , página que no existe
en nuestra aplicación, sea redirigida a &lt;code&gt;index.html&lt;/code&gt; y que se ejecute nuestra aplicacion Vue&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Una aplicacion para el fediverso sin salir de tu navegador</summary>
    </entry>
    <entry>
        <title>Cómo "tracear" visitas al blog con Telegram</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/ping-telegram.html"/>
        <updated>2024-11-10T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/ping-telegram.html</id>
        <category term="telegram"/>
        <category term="php"/>
        <content type="html">
            &lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por si no lo he dicho antes, mi blog es un &quot;static site&quot;. Básicamente es un conjunto de páginas HTML (más imágenes,
css, etc) preconstruido previamente de tal forma que no necesito base de datos ni un servidor especial para
visualizarlo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mi blog está alojado en un plan Shared en DreamHost pero al ser todo generado previamente,
en principio podría usar casi cualquier alojamiento básico.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para el tema de estadísticas (a todos nos gusta saber si nos leen para qué engañarnos) tiro del viejo sistema que
analiza los logs de acceso. DreamHost lo provee con un sólo click pero casi todos los alojamientos lo tienen
de una forma u otra porque es más viejo que el hilo negro y no requiere de grandes recursos por su parte.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi plan con DreamHost también está incluido el alojamiento de PHP que en dos palabras serían ficheros que se
ejecutan en el servidor cuando los pides y que se &quot;salida&quot; se envía a tu navegador. PHP también es más viejo que el
hilo negro y suele ser incluido en casi todos los planes de hosting. Por eso es tan popular, por ejemplo, WordPress
(el cual me parece un monstruo que ha crecido de tal forma que es una burrada usarlo para un blog)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así qe aprovechando esta funcionalidad le he añadido un pequeño PHP a mi blog para que me envíe una notificación a
un canal de Telegram privado cuando un usuario accede al blog&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ping.php&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;&amp;lt;?php
$apiToken = &apos;TU_API_TOKEN&apos;;

$json = file_get_contents(&apos;php://input&apos;);
$body = json_decode($json);

$data = [
    &apos;chat_id&apos; =&amp;gt; &apos;TU_CHAT&apos;,
    &apos;text&apos; =&amp;gt; &quot;un usuario esta leyendo $json&quot;
];

$response = file_get_contents(&quot;http://api.telegram.org/bot$apiToken/sendMessage?&quot; . http_build_query($data) );&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y en el static site le incluyo un pequeño javascript, que invoca a &lt;code&gt;ping.php&lt;/code&gt; en cada página que me interesa &quot;tracear&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;api.js&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;$( document ).ready(function() {
    var obj = {
        url:window.location.href
    };
    $.ajax({
        url: &apos;/ping.php&apos;,
        type: &apos;post&apos;,
        dataType: &apos;json&apos;,
        contentType: &apos;application/json&apos;,
        data: JSON.stringify(obj)
    });
});&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De esta forma recibo un aviso cada vez que leéis una entrada en el blog pero sin recolectar información acerca de
navegador, origen, sesion ni nada parecido&lt;/p&gt;
&lt;/div&gt;
        </content><summary>Traceando visitas al blog estático con Telegram y PHP</summary>
    </entry>
    <entry>
        <title>Apisix+KeyCloak+Active Directory</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/apisix-keycloak-ldap.html"/>
        <updated>2024-09-29T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/apisix-keycloak-ldap.html</id>
        <category term="apisix"/>
        <category term="keycloak"/>
        <category term="apigateway"/>
        <category term="docker"/>
        <category term="ldap"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En post anteriores hemos visto cómo montar un ApiGateway con Apisix el cual
protega las peticiones a los servicios del backend comprobando si las peticiones
están autorizadas. Para la autorización hemos usado Keycloak y creado usuarios
&quot;a mano&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El post anterior lo puedes encontrar en &lt;a href=&quot;apisix-keycloak-docker.html&quot; class=&quot;bare&quot;&gt;apisix-keycloak-docker.html&lt;/a&gt;]&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a seguir avanzando con esta idea pero ahora los
usuarios/passwords Keycloak los validará contra un Active Directory de Windows (
o cualquier otro sistema que use LDAP)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De forma visual la arquitectura será algo parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-ebee2a095b9cb9617b1283cfdf749b12.png&quot; alt=&quot;Diagram&quot; width=&quot;699&quot; height=&quot;536&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se puede ver, es exactamente la situación del post anterior salvo que
delegamos la gestión de usuarios en un LADP&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ldap&quot;&gt;LDAP&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;

&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;realm_en_keycloak&quot;&gt;Realm en Keycloak&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;

&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;apisix&quot;&gt;Apisix&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;

&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este ejemplo NO debería de usarse como tal en producción pero creo que sirve de ejemplo para ver cómo
encajan las diferentes piezas al montar una arquitectura de microservicios con un ApiGateway y con autentificadión
delegada&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Protegiendo el accesso a tu API mediante autentificacion LDAP (Active Directory)</summary>
    </entry>
    <entry>
        <title>RemoveCookieWall, una extension de Firefox</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/remove-cookiewall.html"/>
        <updated>2024-09-11T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/remove-cookiewall.html</id>
        <category term="javascript"/>
        <category term="firefox"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;¿Harto del banner que se ha puesto de moda en las webs para que aceptes cookis de terceros o pases por caja?
En este post explico cómo me hecho (y publicado) una extensión de Firefox para evitarlo en la mayoría de sites&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El código de esta extensión está publicado en &lt;a href=&quot;https://github.com/jagedn/removecookiewall-addon&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/removecookiewall-addon&lt;/a&gt;
y lo puedes instalar en Firefox (también en móvil) desde &lt;a href=&quot;https://addons.mozilla.org/es/firefox/addon/removecookiewall/&quot; class=&quot;bare&quot;&gt;https://addons.mozilla.org/es/firefox/addon/removecookiewall/&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde hace unos meses, y por una exigencia de Europa (creo), la mayoría de las webs te muestran un banner
la primera vez que accedes a ellas que no te dejan continuar hasta que no decides entre:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;te voy a colocar miles de cookies de terceros en tu navegador que van a espiar lo que navegas&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pasa por caja y págame para que no lo haga&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La mayoría de estas librerias ejecutan un javascript nada más cargar la página que leen tus cookies. Si ven que
no has pasado por caja te muestran un dialogo HTML y bloquean el body cambiando el style a &quot;block&quot; (o similar)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este diálogo no te deja leer lo que hay debajo pero &amp;#8230;&amp;#8203; no deja de ser un elemento DOM del HTML, así que,
como los navegadores te permiten abrir una consola de desarrollo e inspeccionar el HTML, me surgió la idea de
eliminar manualmente el díalogo (simplemente le das a inspeccionar, buscas en el HTML donde está definido
y le das a suprimir) y chimpón, el diálogo desaparece. Luego busco la declaración del &quot;body&quot; y dando doble click
en el atributo style le quito la propiedad que lo bloquea y ya puedo hacer scroll.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Poca magia.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Qué es lo que está pasando entonces? Pues simplemente el código javascript sigue esperando que le llegue un
evento de usuario diciendole qué botón has pulsado, pero estos botones ya no están, así que nunca le llegará
y no te instalará cookies de terceros.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ok, pero ¿y si refresco la página?. Pues vuelta a empezar &amp;#8230;&amp;#8203; así que esto es perfecto para una nueva
extensión del navegador que lo haga por mí.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;extension_removecookiewall&quot;&gt;Extension RemoveCookieWall&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una extensión Firefox, de forma resumida, es un espacio de memoria del navegador reservado donde se ejecuta
un código javascript que puede dialogar con él.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puede (si le concede permisos el usuario) inyectar código en las páginas que visitas, abrir pestañas, cerrarlas,
comunicarse con servicios remotos, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;RemoveCookieWall es una extensión de Firefox que lo &quot;único&quot; que necesita es que el navegador inyecte
en todas las páginas que el usuario visita un pequeño código javascript.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este javascript según se ha cargado la página inspeccionará si existe un elemento DOM que coincida con alguno
de los que he investigado que ese están usando. Si lo detecta usará funciones standard de Javascript para borrarlo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como el banner a veces puede aparecer (mili)segundos después de que nuestro código se ejecute lo que hace
el script es repetir la búsqueda durante un par de segundos. Pasado este tiempo si el banner no ha aparecido la
extensión asume que la página no tiene un CookieeWall y termina&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y esto es todo. Sólo queda empaquetar el código, añadir un fichero Manifest que indique los permisos que
requiere nuestra extensión y publicarlo en Firefox&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;código&quot;&gt;Código&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El código JS básicamente es:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;var readyStateCheckInterval;
var counter = 0;

function sanitizeBody() {
    document.body.style.overflow = &quot;unset&quot;
    document.body.classList.remove(&apos;sxnlzit&apos;)
    document.body.classList.remove(&apos;didomi-popup-open&apos;)
    document.body.parentNode.classList.remove(&apos;sp-message-open&apos;)
}

function removeMe(element) {
    element.remove();
    sanitizeBody();
}

readyStateCheckInterval = setInterval(function() {
    if (document.readyState === &quot;complete&quot;) {
        counter++;
        const removeParent = [&apos;div.pmConsentWall&apos;]; //elpais
        [...removeParent].forEach(s =&amp;gt; {
            var divs = document.body.querySelectorAll(s);
            [...divs].forEach(element =&amp;gt; {
                removeMe(element.parentNode);
            });
        });
        const removeThis = [
            &apos;div[data-nosnippet=&quot;data-nosnippet&quot;]&apos;,
            &apos;#mrf-popup&apos;,
            &apos;#didomi-popup&apos;,
            &apos;[id^=&quot;sp_message_container_&quot;]&apos;,
            &apos;#cl-consent&apos;,
            &apos;dialog.cookie-policy&apos;
        ];
        [...removeThis].forEach(s =&amp;gt; {
            var divs = document.body.querySelectorAll(s);
            [...divs].forEach(element =&amp;gt; {
                removeMe(element);
            });
        });
        if (counter &amp;gt; 30) {
            clearInterval(readyStateCheckInterval);
        }
    }
}, 100);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nada más ser inyectado el código en la página se inicia un &lt;code&gt;interval&lt;/code&gt; cada 100 milis&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El scrip busca si el &lt;code&gt;document.body.querySelectorAll&lt;/code&gt; encuentra algún elmento tipo &lt;code&gt;#mrf-popup&lt;/code&gt;, &lt;code&gt;#didomi-popup&lt;/code&gt;,
etc. Si lo encuentra simplemente lo elimina con &lt;code&gt;element.remove()&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Después de unos cuantos intentos termina borrando el &lt;code&gt;interval&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Toda extensión tiene que tener un fichero Manifest. El de esta extensión es simplemente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;{

    &quot;description&quot;: &quot;Remove CookieWall&quot;,
    &quot;manifest_version&quot;: 2,
    &quot;name&quot;: &quot;RemoveCookieWall&quot;,
    &quot;version&quot;: &quot;0.11&quot;,
    &quot;homepage_url&quot;: &quot;https://github.com/jagedn/removecookiewall-addon&quot;,
    &quot;icons&quot;: {
        &quot;48&quot;: &quot;icons/border-48.png&quot;
    },
    &quot;content_scripts&quot;: [{
        &quot;matches&quot;: [
            &quot;*://*/*&quot;
        ],
        &quot;js&quot;: [&quot;removeCookieWall.js&quot;]
    }],
    &quot;browser_specific_settings&quot;: {
        &quot;gecko&quot;: {
            &quot;id&quot;: &quot;remove-cookiewall@aguilera.soy&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ves, content_scripts indica que queremos inyectar el js en todas las páginas. Otras extensiones pueden
indicar sólo un site, otras ejecutar un javascript en backuground, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;build_and_publish&quot;&gt;Build and publish&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para publicar en Firefox simplemente tenemos que proveer un zip donde esten todos los ficheros que requiere
la extension. Para hacerlo fácil me he hecho un &lt;code&gt;build.sh&lt;/code&gt; que simplemente ejecuta el zip:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;zip -r -FS ../remove-cookiewall.zip * --exclude &apos;&lt;strong&gt;.git&lt;/strong&gt;&apos; --exclude &apos;build.sh&apos;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Publicar una extension en Firefox no tiene ninguna complicación y es gratis. Lo único que tu extensión tiene
que pasar una revisión inicial que puede tardar uno (o varios) días&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Crear una extension de Firefox para quitar el mensajito de aceptar cookies o pagar</summary>
    </entry>
    <entry>
        <title>Apisix Gateway con autentificación Keycloak (y SSL con Caddy)</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/apisix-keycloak-docker.html"/>
        <updated>2024-09-06T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/apisix-keycloak-docker.html</id>
        <category term="apisix"/>
        <category term="keycloak"/>
        <category term="apigateway"/>
        <category term="docker"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post voy a instalar en una máquina (linux obviamente) desde cuasi-cero un stack que nos permita tener
un acceso &lt;code&gt;https&lt;/code&gt; a unos microservicios pasando por un api gateway (con Apisix) que dialogará con Keycloack para
gestionar la autentificación y autorización del usuario a los mismos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De forma visual la arquitectura será algo parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-48e3624df831c83b3b10f65d36a64899.png&quot; alt=&quot;Diagram&quot; width=&quot;558&quot; height=&quot;504&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;proyecto&quot;&gt;Proyecto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea del proyecto es crear un stack en Internet de tal forma que se expongan unos servicios a los que el
usuario accederá previa identificación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dicha identificación podrá ser mediante usuario/password o bien mediante un proveedor de identidades, en este caso
Linkedin (de igual forma puede ser Github, Gitlab, Google, etc.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El acceso a los servicios se realizará mediante https y la gestión de la autentificación residirá
en una pieza ajena a los servicios. Estos recibirán una cabecera con un JWT donde podrán extraer la información
del usuario sin preocuparse de cómo se ha obtenido.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Una máquina Linux (la mia tiene 2GB de memoria y unos pocos gigas de disco)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tener acceso como root (seguramente podrías con otro usuario pero no me voy a complicar)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Docker instalado&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Un dominio en internet (para este post usaré &lt;code&gt;apisix.edn.es&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Una entrada en el DNS que apunte a la IP pública de la máquina&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;acceso ssh a la máquina.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;si se desea probar la integración con Linkedin (o similar) se necesita crear una App en &lt;a href=&quot;https://developer.linkedin.com/&quot; class=&quot;bare&quot;&gt;https://developer.linkedin.com/&lt;/a&gt; y generar un ApiClient y SecretClient&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crear un dominio/subdominio suele tardar unos minutos, al igual que la propagación de la entrada DNS. Recomiendo
hacer esto lo primero para que se vaya realizando la propagación DNS y así Caddy aprovisionará un certificado sin esperas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Recomiendo tener todos los puertos de la máquina expuesta a internet cerrados (e incluso cambiar el puerto por
defecto 22 de ssh a otro) salvo el 80 y el 443. El puerto 80 es necesario para que Caddy pueda gestionar la
provisión del certificado&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;servicios_web&quot;&gt;Servicios Web&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es tener nuestros microservicios &quot;limpios&quot; de cualquier lógica de identificar a un usuario. Simplemente
recibirán un JWT donde podrá extraer el nombre, email, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para este ejemplo vamos a usar una imagen &lt;code&gt;httpbin.org&lt;/code&gt; que simplemente muestra los parámetros que se le envían
en la petición y así comprobar que el servicio recibe dichos parámetros.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo vamos a crear dos servicios, &lt;code&gt;web1&lt;/code&gt; y &lt;code&gt;web2&lt;/code&gt; para darle más contenido y poder explorar cómo manejarlos
con Apisix&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;services:
  web1:
    image: kennethreitz/httpbin

  web2:
    image: kennethreitz/httpbin&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;caddy&quot;&gt;Caddy&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Caddy es un reverse proxy que gestiona de forma automática el aprovisionar un certificado SSL con Let&amp;#8217;s Encrypt
de tal forma que será nuestra &quot;puerta de entrada&quot; al sistema mediante &lt;code&gt;https&lt;/code&gt;. Una vez que securiza la conexión
con el cliente mediante SSL realiza la labor de reverse proxy y le pasa la petición al servicio que se le indique&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  caddy:
   image: caddy
   ports:
     - &quot;80:80&quot;
     - &quot;443:443&quot;
   volumes:
     - ./caddy/data:/data/
     - ./caddy/config:/config/
     - ./caddy/Caddyfile:/etc/caddy/Caddyfile&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La configuración es además bastante simple, al menos para el alcance de este artículo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;caddy/Caddyfile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;{
  email TU_EMAIL@TU_EMAIL
}

apisix.edn.es {
  reverse_proxy http://apisix:9080
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este fichero indicarás tu correo y el dominio a usar ( &lt;code&gt;apisix.edn.es&lt;/code&gt; en mi ejemplo)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que Caddy resuelve el SSL realizará el reverse proxy hacia el servicio &lt;code&gt;apisix&lt;/code&gt; que crearemos a continuación&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;keycloak&quot;&gt;Keycloak&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Keycloak es un sistema de autenticación y autorización seguro (Open Source). Es decir, es el encargado
de &quot;gestionar los usuarios&quot; y los &quot;accesos a los recursos&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En su uso más básico podremos dar de alta usuarios vía interface web y los servicios podrán dialogar con
él para validar si un usuario es quien dice ser y saber a qué tiene acceso.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;También puedes conectarlo con directorios de usuarios típicos en empresa (Kerberos, Active Directory, etc.)
e incluso usar &lt;code&gt;identity providers&lt;/code&gt; como Google, Linkedin, Github, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para este ejemplo vamos a crear un usuario con el interface web y además añadir Linkedin como identity provider
(es decir, un usuario podrá identificarse mediante user/password o usando su cuenta de Linkedin)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  keycloak:
    image: quay.io/keycloak/keycloak:25.0
    container_name: keycloak
    env_file:
      - .env
    command: start-dev
    depends_on:
      - keycloakdb
    ports:
      - 8080:8080

  keycloakdb:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data
    env_file:
     - .env&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El interface de Keycloak realiza dos funciones:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;mostrar el dialogo para autentificar un usuario&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;mostrar el dashboard de admin que permite la configuración y gestion del propio Keycloak&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente NO queremos que el dashboard esté accesible a todo el mundo así usaremos las variables de
entorno que nos ofrece para configurarle dos URLs diferentes. Una será accesible a todos los usuarios
que quieran acceder a nuestro sistema y otra sólo estará disponible para administradores&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para nuestro ejemplo estas URLs serán:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://apisix.edn.es/keycloak&quot; class=&quot;bare&quot;&gt;https://apisix.edn.es/keycloak&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://localhost:8080&quot; class=&quot;bare&quot;&gt;http://localhost:8080&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;env&quot;&gt;.env&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usamos un fichero &lt;code&gt;.env&lt;/code&gt; para tener en un sitio las variables de entorno de configuración, así como usuarios
y password de sistema, así que este fichero NO debería ser versionado&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.env&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;KC_DB=postgres
KC_DB_URL=jdbc:postgresql://keycloakdb:5432/keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=password

KC_HOSTNAME=https://apisix.edn.es/keycloak
KC_HOSTNAME_PORT=443
KC_HOSTNAME_STRICT=true
KC_HOSTNAME_STRICT_HTTPS=true

KC_HOSTNAME_ADMIN=http://localhost:8080

KC_LOG_LEVEL=info

KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin

POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=password&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Básicamente, las variables que empiezan por &lt;code&gt;KC_&lt;/code&gt; son propias de Keycloak y las que empiezan por &lt;code&gt;POSTGRES&lt;/code&gt;
son del motor de la base de datos. De ahí que estén duplicados los valores&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparando_keycloak&quot;&gt;Preparando Keycloak&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A estas alturas estamos listos para levantar la primera fase de nuestro proyecto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo va bien, Caddy gestionará el tema del SSL y Keycloak preparará la base de datos &amp;#8230;&amp;#8203; pero cómo acceder
a la consola de Keycloak si está corriendo en una máquina en nuestro proveedor?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;ssh -p 2222 -L 8080:localhost:8080 &lt;a href=&quot;mailto:root@TU.IP&quot;&gt;root@TU.IP&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es decir, abrimos una conexión ssh con nuestra máquina (yo uso el puerto 2222) y hacemos enrutado de puertos
8080 entre la máquina remota y la nuestra.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Esta NO es la manera de segurizar un acceso remoto en una máquina en internet, pero para este
artículo no me voy a complicar.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Existen tutoriales más detallados sobre cómo manejar Keycloak. En este artículo simplemente voy a enumerar
los pasos a realizar, pues son realmente muy fáciles e intuitivos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;crear un realm &lt;code&gt;apisix_test&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;en este realmn crear un client &lt;code&gt;apisix&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;configurar en este cliente &lt;code&gt;valid redirect URL&lt;/code&gt; con &lt;code&gt;&lt;a href=&quot;https://apisix.edn.es/web1/anything&quot; class=&quot;bare&quot;&gt;https://apisix.edn.es/web1/anything&lt;/a&gt;&lt;/code&gt; y &lt;code&gt;&lt;a href=&quot;https://apisix.edn.es/web2/anything&quot; class=&quot;bare&quot;&gt;https://apisix.edn.es/web2/anything&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;asegurarnos en este cliente que está marcada la opción &quot;Client authentication&quot;. De esta forma la autentificación
en este realm es gestionada entre servicios. Si estuviera OFF sería para aplicaciones Javascript por ejemplo&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez creado, Keycloak nos creará un client y un secret para este client que deberemos proporcionar a Apisix
(paso siguiente)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;crearemos un usuario &lt;code&gt;test&lt;/code&gt; con password &lt;code&gt;test&lt;/code&gt; y marcaremos como que su email ha sido validado y que no
necesita cambiar la password en el primer acceso. (Por no complicarnos)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Si quieres añadir Linkedin como Identity Provider, en el menú de la izquierda te permitirá elegirlo y
añadir las claves creadas desde Linkedin&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;apisix&quot;&gt;Apisix&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Apisix es un ApiGateway Open Source (la lógica de negocio no reside en él, sino que se dedica a enrutar, transformar, etc.
peticiones hacia los servicios).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post lo vamos a usar en modo &quot;standalone&quot; de tal forma que la configuración
y gestión sea lo más sencilla posible.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Puedes instalarlo con varias instancias, que use &lt;code&gt;etcd&lt;/code&gt; como respaldo de la configuración, puedes
instalarlo en kubernetes, etc.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  apisix:
    image: apache/apisix:${APISIX_IMAGE_TAG:-3.10.0-debian}
    volumes:
      - ./apisix_conf/apisix-standalone.yaml:/usr/local/apisix/conf/apisix.yaml
    env_file:
      - .env
    environment:
      - APISIX_STAND_ALONE=true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Añadiremos en el fichero &lt;code&gt;.env&lt;/code&gt; la configuración creada por Keycloak&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.env&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;KC_OIDC_CLIENTID=apisix
KC_OIDC_SECRET=myhg------------
KC_OIDC_ISSUER=apisix_test&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último el fichero &lt;code&gt;apisix-standalone.yml&lt;/code&gt; donde ocurre toda la magia:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;apisix-standalone.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;routes:
  -
      uris: [ &quot;/keycloak/js/*&quot;, &quot;/keycloak/resources/*&quot;, &quot;/keycloak/realms/*&quot;, &quot;/keycloak/robots.txt&quot; ]
      upstream_id: 3
      plugins:
         proxy-rewrite:
           regex_uri: [&quot;/keycloak/(.*)&quot;,&quot;/$1&quot;]

  -
      uri: /web1/*
      upstream_id: 1
      plugins:
        proxy-rewrite:
           regex_uri: [&quot;/web1/(.*)&quot;]
        openid-connect:
           client_id: ${{KC_OIDC_CLIENTID}}
           client_secret: ${{KC_OIDC_SECRET}}
           discovery: https://apisix.edn.es/keycloak/realms/${{KC_OIDC_ISSUER}}/.well-known/openid-configuration
           redirect_uri: https://apisix.edn.es/web1/anything
           scope: openid profile

  -
      uri: /web2/*
      upstream_id: 2
      plugins:
        proxy-rewrite:
           regex_uri: [&quot;/web2/(.*)&quot;]
        openid-connect:
           client_id: ${{KC_OIDC_CLIENTID}}
           client_secret: ${{KC_OIDC_SECRET}}
           discovery: https://apisix.edn.es/realms/${{KC_OIDC_ISSUER}}/.well-known/openid-configuration
           redirect_uri: httsp://apisix.edn.es/web2/anything
           scope: openid profile



upstreams:
  -
      id: 1
      nodes:
          &quot;web1:80&quot;: 1
      type: roundrobin


  -
      id: 2
      nodes:
          &quot;web2:80&quot;: 1
      type: roundrobin

  -
      id: 3
      nodes:
         &quot;keycloak:8080&quot;: 1
      type: roundrobin

#END&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero: El fichero debe acabar en &quot;#END&quot; !!!! Esto es así porque estamos usando la versión standalone.
En una instalación en producción NO usaríamos este modo y la gestión de rutas, etc las tendríamos en ficheros
separados por ejemplo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ves este fichero es donde configuramos tanto las rutas, como los backends (upstreams) y plugins.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para nuestro ejemplo vamos a usar 3 upstreams:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;web1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;web2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;los endpoints de keycloak &quot;abiertos&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo en este ejemplo sencillo vamos a configurar 3 tipos de rutas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Los endpoints abiertos de keycloak los dirigimos tal cual&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Creamos rutas para web1 y web2, y para complicar el ejemplo cada uno podría estar configurado para
usar diferentes realm (web1 por ejemplo permitiría accesos a un realm y web2 a otros)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el caso de web1 (idem para web2) podríamos dirigir las llamadas al endpoint del servicio &lt;code&gt;/api&lt;/code&gt; por ejemplo.
Como estamos usando la imagen de &lt;code&gt;httpbin.org&lt;/code&gt; usamos un endpoint suyo llamado &lt;code&gt;anything&lt;/code&gt; que simplemente
vuelca los parámetros de la llamada (y así poder comprobar que recibe el JWT entre otros)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejecutando&quot;&gt;Ejecutando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si levantamos todos los servicios &lt;code&gt;docker compose up -d&lt;/code&gt; nuestro stack debería estar completo ahora:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;caddy&lt;/code&gt; recibe una petición https y la pasa a &lt;code&gt;apisix&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;apisix&lt;/code&gt; evalúa que &lt;code&gt;route&lt;/code&gt; despachar&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;si es para &lt;code&gt;web1&lt;/code&gt; el plugin &lt;code&gt;openid&lt;/code&gt; valida si hay una autentificadion valida en la petición&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;si no la hay o es inválida &lt;code&gt;openid&lt;/code&gt; se la envía a &lt;code&gt;keycloak&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;keycloak&lt;/code&gt; presenta el interface para que el usuario haga login y dialoga con el usuario&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una vez validado el acceso, &lt;code&gt;keycloak&lt;/code&gt; redirige la petición original de &lt;code&gt;web1&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;apisix&lt;/code&gt; la recibe y la enruta a &lt;code&gt;web1&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;web1&lt;/code&gt; puede acceder al JWT&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes probarlo accediendo a &lt;code&gt;&lt;a href=&quot;http://apisix.edn.es/web1/index.html&quot; class=&quot;bare&quot;&gt;http://apisix.edn.es/web1/index.html&lt;/a&gt;&lt;/code&gt; (al menos mientras tenga esta máquina
de pruebas levantada, que cuesta sus dineros)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/apisix-keycloak.png&quot; alt=&quot;apisix keycloak&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este ejemplo NO debería de usarse como tal en producción pero creo que sirve de ejemplo para ver cómo
encajan las diferentes piezas al montar una arquitectura de microservicios con un ApiGateway y con autentificadión
delegada&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Creando un ApiGateway con autentificacion usando: Apisix, Keycloak, Docker, Caddy</summary>
    </entry>
    <entry>
        <title>Executing Nextflow pipelines with Nomad by Hashicorp</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/nomad-nextflow.html"/>
        <updated>2024-08-30T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/nomad-nextflow.html</id>
        <category term="nextflow"/>
        <category term="nomad"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow enables scalable and reproducible scientific workflows using software containers. It allows the adaptation of pipelines written in the most common scripting languages.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nomad is a simple and flexible scheduler and orchestrator to deploy
and manage containers and non-containerized applications across
on-premises and clouds at scale.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post, I&amp;#8217;ll explore how to use a Nomad cluster to run Nextflow pipelines.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;about_nomad&quot;&gt;About Nomad&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In some ways, Nomad is an alternative to Kubernetes (and similar orchestrators) without its complexity. You can create
a cluster of low-resources machines and Nomad orchestrates how/where deploy your workloads.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Workloads are defined in Nomad by a &lt;code&gt;Job&lt;/code&gt; witch contains 1 or more &lt;code&gt;TaskGroup&lt;/code&gt;, and every &lt;code&gt;TaskGroup&lt;/code&gt; contains 1 or more &lt;code&gt;Task&lt;/code&gt;.
This task can be a Jar application, a system process or a Docker image&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also, you can share volumes across your cluster. These volumes can be a local folder, in case you have
only one machine, or use some of the available plugins to share CSI volumes as NFS, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;about_nf_nomad&quot;&gt;About nf-nomad&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can imagine, this is enough to run Nextflow pipelines (a container orchestrator and shared volumes) and the &quot;only&quot;
missing piece is some Nextflow &lt;code&gt;Executor&lt;/code&gt; to submit tasks into the cluster and control their execution and this missing
piece is the &lt;code&gt;nf-nomad&lt;/code&gt; plugin&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nf-nomad&lt;/code&gt; is a new Nextflow plugin, similar to the &lt;code&gt;nf-k8s&lt;/code&gt; kubernetes plugin, implementing the bridge logic between
Nextflow and Nomad.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When you execute a pipeline in Nextflow using this plugin as &lt;code&gt;executor&lt;/code&gt;, it will translate, create and submit the
Nextflow process to the cluster the Nomad &lt;code&gt;Job&lt;/code&gt; specification.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once submitted, the plugin will maintain the status of every task in the Nexflow session.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;The plugin is open source, and you can find it at &lt;a href=&quot;https://github.com/nextflow-io/nf-nomad&quot; class=&quot;bare&quot;&gt;https://github.com/nextflow-io/nf-nomad&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;running_a_nomad_cluster&quot;&gt;Running a Nomad Cluster&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Install and run a Nomad cluster it&amp;#8217;s as easy as download and execute the binary from the official website. In the same
way, configure and attach clients to the cluster are straightforward, and it only requires a text configuration file.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For simplicity, In this post, we&amp;#8217;ll create a cluster with only one machine on (our computer). We&amp;#8217;ll use, also, a local folder as shared volume.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;em&gt;Steps in this post have been tested using a Linux machine, not sure if they will work on a Mac. Surely it will not work
in a Windows OS&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Download the Git repo &lt;a href=&quot;https://github.com/nextflow-io/nf-nomad&quot; class=&quot;bare&quot;&gt;https://github.com/nextflow-io/nf-nomad&lt;/a&gt; and unzip it (or clone with &lt;code&gt;git clone&lt;/code&gt;) in some folder&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open a terminal console and navigate to the &lt;code&gt;validation&lt;/code&gt; sub-folder and run on it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./start-nomad.sh&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This sh will perform the following steps:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;download the &lt;code&gt;nomad&lt;/code&gt; binary from the official website&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;create a &lt;code&gt;server&lt;/code&gt; and a &lt;code&gt;client&lt;/code&gt; configuration files&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;create a &lt;code&gt;nomad_temp&lt;/code&gt; folder and configure it as a &quot;shared&quot; volume called &lt;code&gt;scratchdir&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;run the nomad executable&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If all goes well, you have running a Nomad cluster into your computer. You see the UI at &lt;a href=&quot;http://localhost:4646/&quot; class=&quot;bare&quot;&gt;http://localhost:4646/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;running_a_hello_world_pipeline&quot;&gt;Running a &quot;hello world&quot; pipeline&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this &lt;code&gt;validation&lt;/code&gt; folder you can find several pipelines examples. They are used to validate different plugin features.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll try to run a simple &quot;hello world&quot; nextflow pipeline.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open a terminal console and navigate to the &lt;code&gt;validation&lt;/code&gt; sub-folder and run on it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;export NOMAD_PLUGIN_VERSION=0.2.0 &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
nextflow run -w $(pwd)/nomad_temp/scratchdir/ -c basic/nextflow.config basic/main.nf &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Last version at this moment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;This post assumes you have nextflow 24.x.x installed in your PATH&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Using the NOMAD_PLUGIN_VERSION environment variable, we instruct to &lt;code&gt;basic/nextflow.config&lt;/code&gt; about which version we
want to use. Feel free to use a fixed value in &lt;code&gt;basic/nextflow.config&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If all goes well, you will see the typical `Bonjour world&apos;, &apos;Ciao world&apos;, &apos;Hello world&apos;, &apos;Hola world&apos; output&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;basic/main.nf&lt;/code&gt; is the typical Nextflow &quot;hello world&quot;, nothing special here.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;basic/nextflow.config&lt;/code&gt; configures &lt;code&gt;nomad&lt;/code&gt; as the default executor. Also, it contains the minimal configuration required
by the plugin:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process {
    executor = &quot;nomad&quot;
}

nomad {
    client {
        address = &quot;http://localhost:4646&quot;
    }
    jobs {
         volume = { type &quot;host&quot; name &quot;scratchdir&quot; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see, the plugin requires the endpoint to the cluster (localhost in our case) and a volume to attach to
the &lt;code&gt;Job&lt;/code&gt;. This volume was created previously by the &lt;code&gt;start-nomad.sh&lt;/code&gt;. In a more realistic situation probably this volume will
be a &lt;code&gt;csi&lt;/code&gt; type&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nf_coredemo&quot;&gt;nf-core/demo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the same terminal and in the &lt;code&gt;validation&lt;/code&gt; folder run:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;export NXF_ASSETS=$(pwd)/nomad_temp/scratchdir/assets
export NXF_CACHE_DIR=$(pwd)/nomad_temp/scratchdir/cache
nextflow run -w $(pwd)/nomad_temp/scratchdir/ -c basic/nextflow.config nf-core/demo -profile test,docker --outdir $(pwd)/nomad_temp/scratchdir/outdir&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;....
* Software dependencies
  https://github.com/nf-core/demo/blob/master/CITATIONS.md
------------------------------------------------------
executor &amp;gt;  nomad (7)
[21/35f6ba] process &amp;gt; NFCORE_DEMO:DEMO:FASTQC (SAMPLE1_PE)     [100%] 3 of 3 ✔
[9b/5e0157] process &amp;gt; NFCORE_DEMO:DEMO:SEQTK_TRIM (SAMPLE3_SE) [100%] 3 of 3 ✔
[da/0eadbc] process &amp;gt; NFCORE_DEMO:DEMO:MULTIQC                 [100%] 1 of 1 ✔
-[nf-core/demo] Pipeline completed successfully-&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;if all goes well, you&amp;#8217;ll be able to see the &lt;code&gt;nf-core/demo&lt;/code&gt; outputs at &lt;code&gt;./nomad_temp/scratchdir/outdir/pipeline_info&lt;/code&gt; folder&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;volumes&quot;&gt;Volumes&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In all these examples, we&amp;#8217;ve used a local folder as a shared volume between our nextflow command line and containers
created into the nomad cluster. However, you can also use a S3 bucket (with wave+fusion), or install some of the
available Nomad plugins and mount an NFS server, for example.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nf-nomad&lt;/code&gt; plugin allows also mounting more than one volume:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jobs {
    volumes = [
        { type &quot;host&quot; name &quot;scratchdir&quot; },
        { type &quot;host&quot; name &quot;scratchdir&quot; path &quot;/var/data&quot; },  // can mount same volume in different path
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;delete_jobs&quot;&gt;Delete Jobs&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Using the &lt;code&gt;jobs.deleteOnCompletion&lt;/code&gt; boolean configuration you can specify if the jobs are removed once completed or
maintain them for posterior inspection (using the nomad UI for example)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;secrets&quot;&gt;Secrets&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;One feature of Nomad is to store variables into the cluster (and configure which roles can access to them,) and you
can use them in your &lt;code&gt;Job&lt;/code&gt; definition avoiding using fixed values into it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nf-nomad&lt;/code&gt; plugin use this feature to provide a Nextflow &lt;code&gt;SecretsProvider&lt;/code&gt; as a bridge between your pipelines and these
variables:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a key=value variable into the cluster using the nomad cli.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./nomad var put secrets/nf-nomad/MY_ACCESS_KEY MY_ACCESS_KEY=TheAccessKey&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;use this variable as a secret into your pipeline&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;workflow.onComplete {
    println(&quot;The secret is: ${secrets.MY_ACCESS_KEY}&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;The &lt;code&gt;start-nomad.sh&lt;/code&gt; shell create a namespace and two variables. You can see how to use them in the &lt;code&gt;secrets/main.nf&lt;/code&gt; pipeline&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;stop_and_clean&quot;&gt;Stop and clean&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Stop the cluster is so easy as kill the &lt;code&gt;nomad&lt;/code&gt; process. You can use the &lt;code&gt;stop-nomad.sh&lt;/code&gt; shell to do it and clean the
temporal folder&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;configuration&quot;&gt;Configuration&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Using the &lt;code&gt;nomad&lt;/code&gt; closure you can configure different aspects of the plugin as:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;endpoint to the cluster&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;access token (in case you&amp;#8217;ve protected the access with a token)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;list of datacenters to use&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;region where allocate jobs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;namespace to use in jobs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;list of volume specs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;list of affinities and constraints&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;attempts in case of failure&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also, in current version, some of them can be overwritten using environment variables:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;NOMAD_ADDRESS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NOMAD_TOKEN&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NOMAD_DC (datacenters)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NOMAD_REGION&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NOMAD_NAMESPACE&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;raspberry_pi&quot;&gt;Raspberry Pi&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As a side/fun note, I would like to comment I was able to run the &lt;code&gt;bactopia&lt;/code&gt; pipeline in a raspberry pi attached to the
cluster&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;At this moment the plugin is in version &lt;code&gt;0.2.0&lt;/code&gt; and it is being testing by the Universitey Gent team.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The Current version covers almost all the requirements to run typical pipelines, but surely new features will be included
in the plugin&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Playing to run Nextflow pipelines with Nomad</summary>
    </entry>
    <entry>
        <title>Machine Learning with Spark and Groovy</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/ml-spark-groovy.html"/>
        <updated>2024-07-21T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/ml-spark-groovy.html</id>
        <category term="machine learning"/>
        <category term="groovy"/>
        <category term="spark"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In a previous post, &lt;a href=&quot;machine-learning-groovy.html&quot; class=&quot;bare&quot;&gt;machine-learning-groovy.html&lt;/a&gt; (spanish), I was playing
to group similar customers using Groovy + Ignite.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To be honest, although it works, I think the script is very obscure, so I was looking
for another approach, and I&amp;#8217;ve reached Spark. In this post, I&amp;#8217;ll cover the same issue
but using it but &lt;strong&gt;using local mode&lt;/strong&gt; I mean, I&amp;#8217;ll not use many nodes as my data
is a small files with 1000 records +- and can suite on my computer&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To recap:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We have collected different features of our customer as number of users, number of
documents finished, time to complete, web or api, and so.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As our main data comes from a big MySQL database, we have created a &quot;feature&quot; table
 fed with several &quot;groups by&quot;, so we have something similar to&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2858%;&quot;&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;CustomerId&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Users&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Finished&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Days&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;API&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Web&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;21221&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;22&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2212&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;18000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;221&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;21&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;200&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;project&quot;&gt;Project&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This time I&amp;#8217;ll use a &lt;code&gt;gradle&lt;/code&gt; project (you can use Maven also) instead a GroovyScript. I founded some problems
with Ivy downloading dependencies, so I decided to create a project with only 1 class
(yes, I know, I know &amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;dependencies {
    // Use the latest Groovy version for building this library
    implementation &apos;org.apache.groovy:groovy-all:4.0.11&apos;

    implementation &apos;mysql:mysql-connector-java:5.0.5&apos;
    implementation &apos;org.apache.spark:spark-core_2.13:3.5.1&apos;
    implementation &apos;org.apache.spark:spark-mllib_2.13:3.5.1&apos;
    implementation &apos;org.apache.spark:spark-sql_2.13:3.5.1&apos;

}
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(Java version 17 doesn&amp;#8217;t work with Spark)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll define a correlation map between &quot;visual labels&quot; and mysql column:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def labels = [
    &apos;Users&apos;    : &apos;nusers&apos;,
    &apos;Documents&apos;: &apos;ndocuments&apos;,
    &apos;Finished&apos; : &apos;docusfinished&apos;,
    &apos;Days&apos;     : &apos;daystofinish&apos;,
    &apos;API&apos;      : &apos;template&apos;,
    &apos;Web&apos;      : &apos;web&apos;,
    &apos;Workflow&apos; : &apos;workflow&apos;
]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And we&amp;#8217;ll generate a CSV file:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def rows = mlDB.rows(&apos;select * from ml.clientes order by nombre&apos;)

def file = new File(&quot;out.csv&quot;)
file.text = ([&quot;Company&quot;]+labels.keySet()).join(&quot;;&quot;) + &quot;\n&quot;
rows.eachWithIndex { row , idx-&amp;gt;
    List&amp;lt;String&amp;gt; details = []
    labels.entrySet().eachWithIndex { entry, i -&amp;gt;
        details &amp;lt;&amp;lt; (row[entry.value] ?: 0.0).toString()
    }
    file &amp;lt;&amp;lt; &quot;${idx+1};&quot;+details.join(&apos;;&apos;)+&quot;\n&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By the moment nothing special, only a CSV file with a header using &quot;;&quot; as separator&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;spark&quot;&gt;Spark&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Next we&amp;#8217;ll create a &quot;local&quot; spark session and read the csv&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def spark = SparkSession
        .builder()
        .appName(&quot;CustomersKMeans&quot;)
        .config(new SparkConf().setMaster(&quot;local&quot;))
        .getOrCreate()

def dataset = spark.read()
        .option(&quot;delimiter&quot;, &quot;;&quot;)
        .option(&quot;header&quot;, &quot;true&quot;)
        .option(&quot;inferSchema&quot;, &quot;true&quot;)
        .csv(&quot;out.csv&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;transforming_origin&quot;&gt;Transforming origin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We need to transform our dataset, adding some new columns and normalizing others:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def assembler = new VectorAssembler(inputCols: labels.keySet(), outputCol: &quot;features&quot;)

dataset = assembler.transform(dataset)

def scaler = new StandardScaler(inputCol: &quot;features&quot;, outputCol: &quot;scaledFeatures&quot;, withStd: true, withMean: true)

def scalerModel = scaler.fit(dataset)

dataset = scalerModel.transform(dataset)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We add a new &quot;features&quot; column and write on it all the labels (defined at the beginning), so &quot;features&quot; column will contain &quot;Users, Documents, API,&amp;#8230;&amp;#8203; &quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also, we&amp;#8217;ll transform the data using a StandardScalar so all data will be standarized
using their media&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;running_a_kmean&quot;&gt;Running a kMean&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def kmeans = new KMeans(k:5 ,seed:1, predictionCol: &quot;Cluster&quot;, featuresCol: &quot;scaledFeatures&quot; )

def kmeansModel = kmeans.fit(dataset)

// Make predictions
def predictions = kmeansModel.transform(dataset)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We want to have 5 groups (this is a &quot;business&quot; requirements. They are ways to find
the optimal number). We want to &quot;create&quot; a new column called &quot;Cluster&quot; where indicate
in which group the data is (by default the column is called &quot;prediction,&quot;) and also
we indicate which columns used, &quot;scaledFeatures&quot; in this case, created previously&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;showing_the_cluster&quot;&gt;Showing the cluster&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll create a copy of the original dataset, and we&amp;#8217;ll join to it a new column
&lt;code&gt;Cluster&lt;/code&gt; using an inner join from &lt;code&gt;predictions&lt;/code&gt; dataset&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def copy = dataset.alias(&quot;copy&quot;)
copy = copy.join(predictions.select(&quot;Company&quot;, &quot;Cluster&quot;), &quot;Company&quot;, &quot;inner&quot;)
copy.show(3)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;+-------+-----+---------+--------+----+---+---+--------+--------------------+--------------------+-------+
|Company|Users|Documents|Finished|Days|API|Web|Workflow|            features|      scaledFeatures|Cluster|
+-------+-----+---------+--------+----+---+---+--------+--------------------+--------------------+-------+
|      1|238.0|  26906.0| 20987.0| 4.0|0.0|1.0|     0.0|[238.0,26906.0,20...|[5.09496005230008...|      0|
|      2|  1.0|     16.0|     9.0| 0.0|0.0|0.0|     0.0|(7,[0,1,2],[1.0,1...|[-0.3286794172192...|      0|
|      3| 80.0|      0.0|     0.0| 0.0|0.0|0.0|     0.0|      (7,[0],[80.0])|[1.47920040595383...|      0|
+-------+-----+---------+--------+----+---+---+--------+--------------------+--------------------+-------+
only showing top 3 rows&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;as you can see, we have a &lt;code&gt;copy&lt;/code&gt; dataset with original data plus a
&lt;code&gt;Cluster&lt;/code&gt; column indicating in which cluster this customer is&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;From here you can use this information to iterate over the dataset and generate
some diagrama, update a database, &amp;#8230;&amp;#8203; or create some HTML charts&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;chart_js&quot;&gt;Chart.js&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post we want to create an HTML visualization using &lt;code&gt;chart.js&lt;/code&gt; so we&amp;#8217;ll create
a &lt;code&gt;data.js&lt;/code&gt; containing a Javascript object to embed in an index.html&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def json = [
    labels:labels.keySet(),
    datasets:[]
]
kmeansModel.clusterCenters().eachWithIndex{v,i-&amp;gt;
    json.datasets &amp;lt;&amp;lt; [
            label:&quot;Cluster ${i+1}&quot;,
            data: v.toArray(),
            fill: true
    ]
}
new File(&quot;data.js&quot;).text = &quot;const dataArr = &quot;+JsonOutput.prettyPrint(JsonOutput.toJson(json))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Most important part here is &lt;code&gt;kmeansModel.clusterCenters()&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We create a JSON where datasets is an array of objects (required by chart.js) and
everyone has an array of doubles with their centers&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lastly we have an &lt;code&gt;index.html&lt;/code&gt; with &lt;code&gt;chart.js&lt;/code&gt; and &lt;code&gt;data.js&lt;/code&gt; included&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Clustering Customers&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;https://momentjs.com/downloads/moment.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/chart.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;./data.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
...
    &amp;lt;div style=&quot;width:75%; height: 40rem&quot;&amp;gt;
        &amp;lt;canvas id=&quot;canvas&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
    &amp;lt;/div&amp;gt;
...
&amp;lt;script&amp;gt;
    const config = {
        type: &apos;radar&apos;,
        data: dataArr,
        options: {
            elements: {
                line: {
                    borderWidth: 3
                }
            }
        },
    };

    window.onload = function() {
        var ctx = document.getElementById(&apos;canvas&apos;).getContext(&apos;2d&apos;);
        window.myLine = new Chart(ctx, config);
    };
&amp;lt;/script&amp;gt;
....&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nothing special but visually very attractive:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/kmeans.png&quot; alt=&quot;kmeans&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;source&quot;&gt;Source&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here you can find the source code&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/jagedn/184302ac4f89def14410f8a6f54a93ea&quot; class=&quot;bare&quot;&gt;https://gist.github.com/jagedn/184302ac4f89def14410f8a6f54a93ea&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Clustering customers with Spark + Groovy</summary>
    </entry>
    <entry>
        <title>A fancy pdf CV with Asciidoctor</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/cv-asciidoctor.html"/>
        <updated>2024-07-13T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/cv-asciidoctor.html</id>
        <category term="asciidoctor"/>
        <category term="doc-as-code"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post I&amp;#8217;ll show you how to create a CV in PDF using a simple text file.
No Word, no Google Docs, no Acrobat &amp;#8230;&amp;#8203; only text&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/cv.png&quot; alt=&quot;cv&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;asciidoctor_web_pdf&quot;&gt;asciidoctor-web-pdf&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Install &lt;code&gt;asciidoctor-web-pdf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;npm i -g @asciidoctor/core asciidoctor-pdf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;assets&quot;&gt;assets&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;in a folder, grab these files:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ggrossetie/asciidoctor-web-pdf/blob/main/examples/resume/style.css&quot; class=&quot;bare&quot;&gt;https://github.com/ggrossetie/asciidoctor-web-pdf/blob/main/examples/resume/style.css&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ggrossetie/asciidoctor-web-pdf/blob/main/examples/resume/template.js&quot; class=&quot;bare&quot;&gt;https://github.com/ggrossetie/asciidoctor-web-pdf/blob/main/examples/resume/template.js&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ggrossetie/asciidoctor-web-pdf/blob/main/examples/resume/phone.svg&quot; class=&quot;bare&quot;&gt;https://github.com/ggrossetie/asciidoctor-web-pdf/blob/main/examples/resume/phone.svg&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ggrossetie/asciidoctor-web-pdf/blob/main/examples/resume/mail.svg&quot; class=&quot;bare&quot;&gt;https://github.com/ggrossetie/asciidoctor-web-pdf/blob/main/examples/resume/mail.svg&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cv&quot;&gt;CV&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;in the same folder, create a text file, JorgeAguileraCV.adoc, for example, and write:
(feel free to change with your data)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;header&quot;&gt;Header&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;= Software Developer Solutions
Jorge Aguilera González

[.info]
== !

=== Jorge Aguilera

[contact]
- image:mail.svg[role=&quot;picto&quot;] jorge@edn.es&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The First line is the title of your CV&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The Second line will not be visible, so you can omit but be sure to let a blank line&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;[.info]&lt;/code&gt; creates a section where you&amp;#8217;ll insert your name&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;[contact]&lt;/code&gt; will be used to write your email&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can add another item with your phone, for example:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;- image:mail.svg[role=&quot;picto&quot;] jorge@edn.es
- image:phone.svg[role=&quot;picto&quot;] +3491xxxxxx&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;left&quot;&gt;Left&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll use the left bar of our CV to include some tips about us, as knowledge, education,
style of life &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So append to the file following text:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;==== Knowledge

- 30+ years as Developer
- C, C++, *Java*, Groovy
- *Javascript*, TypeScript, NodeJS
- Maven, Gradle
- Asciidoctor

==== OpenSource Projects

- K8s Caos Operator
- Gradle extensions
- Google DSL (Groovy)
- Asciidoctor Extensions
- MicronautRaffle

==== Publications

101-groovy-scripts blog&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;about_me&quot;&gt;About me&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now it&amp;#8217;s time to write some sentences about us:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[.chronologie]
== !

=== About me

With more than 30 years of experience in IT, I’ve worked in many different sectors where always I
focused to bring quality and innovation ideas in every project I’ve been participated. During several
years I’ve run a one-person company offering services as technical leader and providing my
experience to improve the skills of the team, teaching good practices and applying productive tools
and languages.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;[.chronologie]&lt;/code&gt; will position &quot;the cursor&quot; at the top of the right part and we&amp;#8217;ll
start writing an &quot;about me&quot; sentences&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;professional_career&quot;&gt;Professional career&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;==== Professional experience

*Software Architect* at EDN (2024)

- Helping different customers in the digital transformation of their systems, migrating from monolithic application
to microservice architecture, implement best practices as code review and clean code

*Software Architect/DevOps* at Baraka (Dec 2022)

- Leading the migration of a NodeJS *Javascript* application deployed in AWS ECS to a *microservice architecture* in Kubernetes.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;building_our_cv&quot;&gt;Building our CV&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It&amp;#8217;s time to see how it looks:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;asciidoctor-web-pdf JorgeAguilera.adoc --template-require ./template.js&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;This command will generate a temp HTML file&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Surely you&amp;#8217;ll need to iterate several times. My advice is to be conscious and write
only a couple of sentences in every position, so the CV will have only one page&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is how mine looks&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;JorgeAguilera.pdf&quot; class=&quot;bare&quot;&gt;JorgeAguilera.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now is the time to send it and wait the headhunter&amp;#8217;s call. Good luck&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Build a fancy pdf CV using asciidoctor</summary>
    </entry>
    <entry>
        <title>Groogle Intro</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/groogle/groogle-1.html"/>
        <updated>2024-07-11T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/groogle/groogle-1.html</id>
        <category term="java"/>
        <category term="groogle"/>
        <category term="groovy"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Groogle is a DSL (Domain Specific Language) oriented to interact with Google Cloud services in an easy way. It provides a concise language so you can create scripts or integrate into your application and consume Google Cloud services as Drive, Sheet or Gmail&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post series I&amp;#8217;ll (try) to explain the origin, aim and how to use it with several real examples&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirement&quot;&gt;Requirement&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A Google account&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java 11+ and Google 4.x installed&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A Google credentials file (more details above)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;example&quot;&gt;Example&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;With Groogle you can create a script, for example, to list all the files in your &lt;code&gt;Drive&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;groogle = GroogleBuilder.build {
    withOAuthCredentials {
        applicationName &apos;test&apos;
        scopes DriveScopes.DRIVE
        usingCredentials &quot;client_secret.json&quot;
        storeCredentials true
    }
}

groogle.with {
    service(DriveService).with {
        findFiles {
            eachFile {
                println &quot;$id = $file.name&quot;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;origin&quot;&gt;Origin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A few years ago I was fascinated about how easily you can write your own DSL using ApacheGroovy so I started a project called Groogle (Groovy + Google)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I started it using the &lt;code&gt;com.puravida-software.groogle&lt;/code&gt; maven coordinates but as PuraVida Software company run out, I&amp;#8217;ve decided to rewrite/improve it under the new &lt;code&gt;es.edn.groogle&lt;/code&gt; coordinates&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;first_steps&quot;&gt;First steps&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Before to start you need to have a Google Cloud account and a project created, say &lt;code&gt;QuickStart&lt;/code&gt; for example&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create an Oauth 2.0 credential (service credentials will be covered in another post):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oyv3nvx16j574k884a54.png&quot; alt=&quot;oyv3nvx16j574k884a54&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b5qlgt48duwxgznh63h0.png&quot; alt=&quot;b5qlgt48duwxgznh63h0&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Download the &lt;code&gt;.json&lt;/code&gt; file but &lt;strong&gt;don&amp;#8217;t share them nor store in your repo&lt;/strong&gt;. For our examples this file will be called &lt;code&gt;client_secret.json&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;first_script&quot;&gt;First script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the same folder you downloaded the json create a &lt;code&gt;example.groovy&lt;/code&gt; file:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;import com.google.api.services.sheets.v4.SheetsScopes
import com.google.api.services.drive.DriveScopes
import es.edn.groogle.*

@Grab(&quot;es.edn:groogle:4.0.0-rc4&quot;)
@GrabConfig(systemClassLoader=true)

groogle = GroogleBuilder.build {
    withOAuthCredentials {
        applicationName &apos;test&apos;
        scopes DriveScopes.DRIVE, SheetsScopes.SPREADSHEETS
        usingCredentials &quot;client_secret.json&quot;
        storeCredentials true
    }
}

groogle.with {
    service(DriveService).with {
        findFiles {
            eachFile {
                println &quot;$id = $file.name&quot;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;in a terminal console execute:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;groovy example.groovy&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;if all goes well a browser will be opened and you need to indicate which Google account you want to use&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wl1k1kjl22dkit2ghzfz.png&quot; alt=&quot;wl1k1kjl22dkit2ghzfz&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and allow access to the application&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/01cj5vk58mnks9wc1iwa.png&quot; alt=&quot;01cj5vk58mnks9wc1iwa&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now you can close the browser and see how the script was able to iterate over your files&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2snq47fbrcf1l68ujrp7.png&quot; alt=&quot;2snq47fbrcf1l68ujrp7&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the following posts we&amp;#8217;ll see how to create or download files from your Drive, create and modify Sheets or send emails&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Intro to Groogle</summary>
    </entry>
    <entry>
        <title>Creando un producto por menos de 10€</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/recargo-equivalencia.html"/>
        <updated>2024-06-19T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/recargo-equivalencia.html</id>
        <category term="startup"/>
        <category term="product"/>
        <category term="php"/>
        <category term="laravel"/>
        <category term="raspberrypi"/>
        <category term="micronaut"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Recientemente, he &quot;sacado&quot; al mercado un nuevo servicio que permite a las empresas que facturan a minoristas saber
si alguno de ellos está acogido a un régimen especial de IVA, lo que le obliga a aplicar un tipo impositivo más.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente, la AEAT (la Hacienda española) ofrece un servicio para realizar esta consulta, de tal forma que rellenando
un formulario, y tras identificarte con tu certificado electrónico, puedes consultar si un NIF está acogido a este
servicio. Así mismo ofrece un servicio SOAP (ahí es nada) para realizar esta consulta mediante programación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El caso es que a pocos clientes que manejes el tema de consultar uno a uno no es trivial por lo que he creado
&lt;code&gt;recargo-de-equivalencia.es&lt;/code&gt;, un servicio en el que tras darte de alta, subes un fichero con los NIFs y de forma
periódica el servicio los chequeará y te notificará si encuentra alguno acogido al servicio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La &quot;gracia&quot; del servicio es que no requiere un chequeo en tiempo real por lo que se puede realizar un escaneo
semanal o mensual, por ejemplo, y notificar vía email (más adelante podría ser también mediante un WebHook).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues los requisitos del servicio serían:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;dominio personalizado&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una landing page donde explicar el servicio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un proceso de subscripcion al servicio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un aplicativo con un dashboard simple donde el usuario pueda subir un fichero y consultar aquellos que
estén acogidos al sistema de recargo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un proceso que periódicamente consulte &lt;strong&gt;todos&lt;/strong&gt; los NIFs y actualice su estado&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una notificación via email&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y el requisito más &quot;importante&quot;: el MVP tiene que tener el costo de mantenimiento lo más reducido posible&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;infraestructura&quot;&gt;Infraestructura&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La mayor parte del servicio se ha hecho en PHP y alojado en DreamHost (DH). ¿Porqué? porque es un proveedor donde tengo
alojados varios dominios (pero de sites estáticos básicamente) y que me ofrece hosting con base de datos y email
ilimitados. Más o menos pago al año unos 120 € y puedo alojar todos los sites que quiera.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hasta hace poco permitían desplegar aplicaciones con NodeJS, Ruby y PHP pero hace poco han cambiado y sólo permiten
PHP (8.3 eso sí), lo cual casi agradezco porque así me he obligado a hacer algo en PHP&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para el proceso que interactua con el servico SOAP de la AEAT he optado por hacer un programa de línea de comando,
perfecto para ser ejecutado por un planificador de tareas de sistema (un cron) con Micronaut &amp;#8230;&amp;#8203; y que se ejecuta
en una RaspberryPi que tenía criando polvo en la estantería !!!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Es decir que el coste del MVP es prácticamente cero&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El problema de DH es que no me sirve para registrar dominios &lt;code&gt;.es&lt;/code&gt; directamente pero sí puedo registrarlo
con un proveedor español (Strato en mi caso) y luego configurar los DNS para que use los de DH. A partir de ahí
puedo crear tantos subdominios como quiera y alojarlos en DH, todo por el mismo precio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Porqué Strato? pues básicamente cuando he buscado proveedores para registrar el dominio el precio de casi todos
era de unos 35 € pero en Strato era de &amp;#8230;&amp;#8203;. 1€!!!!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;landing_page&quot;&gt;Landing Page&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que he registrado el dominio &lt;code&gt;recargo-de-equivalencia.es&lt;/code&gt; y configurado los DNS para usar los de DH
he creado &lt;code&gt;info.recargo-de-equivalencia.es&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este es un static-site creado con Hugo usando una plantilla OpenSource y que me ha llevado un par de días ir retocando
y añadiendo mi contenido.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez tenía la landing en mi local simplemente la copiaba a la carpeta correspondiente de DH y ya estaba disponible&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;subscripcion&quot;&gt;Subscripcion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como es un MVP he creado un form de Google con 4 preguntas para conocer a las empresas que les pueda interesar el
servicio y tener su email para ponerme en contacto con ellas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;El servicio se encuentra actualmente en Beta, es gratis y cuenta con 2 clientes&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;aplicación&quot;&gt;Aplicación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta parte ha sido la más ardua pero probablemente por desconocimiento de PHP y el framework Laravel. Sin embargo
en cierta forma ha sido la más placentera&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Actualmente la aplicación es un simple dashboard donde, una vez registrado, puedes ver el total de NIFs que has
subido y cúales de ellos están en recargo de equivalencia. Además puedes subir un fichero con más NIFs que serán
incluidos en tu espacio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como nota técnica sobre Laravel comentaré que me ha encantado. Tiene muchos conceptos similares a los frameworks
Java y es un ecosistema bastante maduro. Comentar que gracias a que DH también me permite ejecutar tareas
planificadas ha sido muy fácil añadir Workers a la aplicación de tal forma que una vez el usuario sube un fichero
el aplicativo lo guarda en un directorio temporal y un worker lo procesa al poco&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/recargo.png&quot; alt=&quot;recargo&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;micronaut&quot;&gt;Micronaut&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para la parte de integración con AEAT he recurrido a lo &quot;seguro&quot;: un Micronaut + Apache Axis para hacer llamadas al
SOAP de la AEAT y que lee/escribe en la base de datos alojada en DH&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este Micronaut lo he convertido a binario con GraalVM y encima lo puedo ejecutar en una RaspberryPi por lo que
el consumo energético del proceso es mínimo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es que este aplicativo está configurado para &quot;peinar&quot; los NIFs de los clientes y marcarlos si encuentra
alguno en el régimen.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Posteriormente un worker en el aplicativo anterior detectará estos registrados marcados y los empaquetará en un
email de notificación al cliente&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Actualmente el servicio se encuentra, como he dicho, en fase Beta pero ya maneja unos 35.000 NIFs&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los siguientes pasos, según el interés que despierte, sería crear algunas integraciones por ejemplo para
FacturaScript, WordPress o incluso en avanzar la parte de WebHook&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Creando un servicio SaaS para comprobar si un minorista está acogido al recargo de equivalencia</summary>
    </entry>
    <entry>
        <title>Executing Nextflow pipelines with K3d</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/k3s-nextflow.html"/>
        <updated>2024-05-01T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/k3s-nextflow.html</id>
        <category term="nextflow"/>
        <category term="kubernetes"/>
        <category term="k3s"/>
        <category term="k3d"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;em&gt;Nextflow enables scalable and reproducible scientific workflows using software containers. It allows the adaptation of pipelines written in the most common scripting languages.&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post I&amp;#8217;ll explore how to create a &quot;work environment&quot; where run Nextflow pipelines using Kubernetes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The user will be able to create and edit the pipeline, configuration and assets into their computer and run the pipelines in the
cluster in a fluent way.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The idea is to provide to the user the most complete environment in their computer
so, once tested and validated, it will run in a real cluster.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;problem&quot;&gt;Problem&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When you&amp;#8217;re deploying Nextflow&amp;#8217;s pipelines in kubernetes you need to find a way to share the workdir. Options are basically
use &lt;code&gt;volumes&lt;/code&gt; or use &lt;code&gt;fusion&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Fusion is very easy to use (basically &lt;code&gt;enable=true&lt;/code&gt; in the nextflow.config) but you create an external dependency.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Volumes are more &quot;native&quot; solution, but you need to fight with the infrastructure, providers, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Another challenge working with pipelines in kubernetes is to retrieve outputs once the pipeline is completed. Probably
you need to run some &lt;code&gt;kubectl cp&lt;/code&gt; commands&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post I&amp;#8217;ll create a cluster (with only 1 node) from scratch and run some pipelines on it. We&amp;#8217;ll see how pods
are created and how we can edit the pipeline and/or configuration using our preferred IDE (notepad, vi, VSCode,&amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a computer (and internet connection of course)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We need to have installed following command line tools:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;kubectl (if you work in kubernetes you&amp;#8217;ve it)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;skaffold &lt;a href=&quot;https://skaffold.dev/docs/install/&quot; class=&quot;bare&quot;&gt;https://skaffold.dev/docs/install/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;k3d &lt;a href=&quot;https://k3d.io/v5.6.3/#releases&quot; class=&quot;bare&quot;&gt;https://k3d.io/v5.6.3/#releases&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;k9s. Is &lt;strong&gt;not required&lt;/strong&gt; but very useful&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;create_a_cluster&quot;&gt;Create a cluster&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;k3d cluster create nextflow --port 9999:80@loadbalancer&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;re creating a new cluster called &lt;code&gt;nextflow&lt;/code&gt; (can be whatever). We&amp;#8217;ll use 9999 port to access to our results&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;kubectl cluster-info
Kubernetes control plane is running at https://0.0.0.0:40145
CoreDNS is running at https://0.0.0.0:40145/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://0.0.0.0:40145/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparing_our_environment&quot;&gt;Preparing our environment&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a folder &lt;code&gt;test&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;nextflow_area&quot;&gt;Nextflow area&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a subfolder &lt;code&gt;project&lt;/code&gt; (will be our nextflow working area)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a &lt;code&gt;nextflow.config&lt;/code&gt; in this subfolder&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;k8s {
   context = &apos;k3d-nextflow&apos;  &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

   namespace = &apos;default&apos; &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;

   runAsUser = 0
   serviceAccount = &apos;nextflow-sa&apos;
   storageClaimName = &apos;nextflow&apos;
   storageMountPath = &apos;/mnt/workdir&apos;
}

process {
   executor = &apos;k8s&apos;
   container = &quot;quay.io/nextflow/rnaseq-nf:v1.2.1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;k3d-nextflow&lt;/code&gt; was created by k3d. If you chose another name you need to change it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;I&amp;#8217;ll use the &lt;code&gt;default&lt;/code&gt; namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;k8s_area&quot;&gt;K8s area&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a subfolder &lt;code&gt;k8s&lt;/code&gt; and create following files into it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;pvc.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nextflow
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 2Gi&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;admin.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nextflow-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: nextflow-role
rules:
  - apiGroups: [&quot;&quot;]
    resources: [&quot;pods&quot;, &quot;pods/status&quot;, &quot;pods/log&quot;, &quot;pods/exec&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;delete&quot;]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: nextflow-rolebind
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nextflow-role
subjects:
  - kind: ServiceAccount
    name: nextflow-sa&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;jagedn.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jagedn &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
  labels:
    app: jagedn
spec:
  selector:
    matchLabels:
      app: jagedn
  template:
    metadata:
      labels:
        app: jagedn
    spec:
      serviceAccountName: nextflow-sa
      terminationGracePeriodSeconds: 5
      securityContext:
        fsGroup: 0
        runAsGroup: 0
        runAsNonRoot: false
        runAsUser: 0
      containers:
      - name: nextflow
        image: jagedn
        volumeMounts:
        - mountPath: /mnt/workdir
          name: volume
      - name: nginx-container
        image: nginx:latest
        ports:
        - containerPort: 80
        volumeMounts:
          - name: volume
            mountPath: /usr/share/nginx/html
      volumes:
      - name: volume
        persistentVolumeClaim:
          claimName: nextflow&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jagedn&lt;/code&gt; is my nick, you can use whatever but pay attention to replace in all places&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;kustomization.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
 - pvc.yml
 - admin.yml
 - jagedn.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically we&amp;#8217;re creating:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;service account and their roles&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a persistent volume claim to share across pods&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a pod&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;skaffold&quot;&gt;Skaffold&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the &quot;parent&quot; folder create following files:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Dockerfile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;FROM nextflow/nextflow:24.03.0-edge
RUN yum install -y tar  &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

ADD project /home/project &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;

ENTRYPOINT [&quot;tail&quot;, &quot;-f&quot;, &quot;/dev/null&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Required by skaffold to sync files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Required by skaffold to sync files&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;skaffold.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: skaffold/v4beta10
kind: Config
metadata:
    name: nextflow

build:
    artifacts:
        - image: jagedn
          context: .
          docker:
            dockerfile: Dockerfile
          sync:
            manual:
                - src: &apos;project/**&apos;
                  dest: /home

manifests:
    kustomize:
        paths:
            - k8s

deploy:
    kubectl: {}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;watching&quot;&gt;Watching&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Only for the purpose to watch how pods are created and destroyed we&amp;#8217;ll run in a terminal console &lt;code&gt;k9s&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/k9s.png&quot; alt=&quot;k9s&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;go&quot;&gt;Go&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open the project with VSCode (for example)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/vscode.png&quot; alt=&quot;vscode&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open a &lt;code&gt;terminal&lt;/code&gt; tab and execute:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;skaffold dev&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/skaffold.png&quot; alt=&quot;skaffold&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;let the terminal running&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In another terminal console execute:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl exec -it jagedn-56b9fb64dc-2xw8f&amp;#8201;&amp;#8212;&amp;#8201;/bin/bash&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;You&amp;#8217;ll need to use the pod id skaffold has created&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and cd to &lt;code&gt;/home/project&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/nextflow-k3d-1.png&quot; alt=&quot;nextflow k3d 1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Using you VCode editor open &lt;code&gt;nextflow.config&lt;/code&gt; and change something (a comment for example). Save the change
and in the terminal run a &lt;code&gt;cat&lt;/code&gt; command to verify skaffold has been synced the file&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/nextflow-k3d-2.png&quot; alt=&quot;nextflow k3d 2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;run&quot;&gt;Run&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the terminal execute&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;NXF_ASSETS=/mnt/workdir/assets nextflow run nextflow-io/rnaseq-nf  -with-docker -w /mnt/workdir -cache false&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you change to the &lt;code&gt;k9s&lt;/code&gt; console, you&amp;#8217;ll see how pods are created&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/k9s-2.png&quot; alt=&quot;k9s 2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After a while the pipeline is completed !!!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/nextflow-k3d-3.png&quot; alt=&quot;nextflow k3d 3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;extra_ball&quot;&gt;Extra ball&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you inspect the folder &lt;code&gt;/home/project/results&lt;/code&gt; you&amp;#8217;ll find the outputs of the pipeline, so &amp;#8230;&amp;#8203; how we can inspect
them?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Execute &lt;code&gt;cp -R results/ /mnt/workdir/&lt;/code&gt; in the kubectl terminal and open a browser
to &lt;a href=&quot;http://localhost:9999/results/multiqc_report.html&quot; class=&quot;bare&quot;&gt;http://localhost:9999/results/multiqc_report.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/tadam.png&quot; alt=&quot;tadam&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;A better approach is to create another volume for the result so nginx sidecar pod can read directly&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;clean_up&quot;&gt;Clean up&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once you want to end with your cluster only finish (ctrl+c) the skaffold session and it will remove all resources&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To delete the cluster you can run &lt;code&gt;k3d cluster delete nextflow&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Don&amp;#8217;t know if this approach is the best or maybe a little complicate but I think can be a good approach to have a
very productive kubernetes environment without the need to have a full cluster&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;update_using_k3s&quot;&gt;Update: using k3s&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you have a k3s cluster with several nodes and Longhorm installed you can change the &lt;code&gt;pvc.yml&lt;/code&gt; file and you&amp;#8217;ll be
able to run your pipeline using them&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;pvc.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nextflow
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: longhorn
  resources:
    requests:
      storage: 2Gi&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Playing to run pipelines with k3d</summary>
    </entry>
    <entry>
        <title>Machine Learning con Groovy</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/machine-learning-groovy.html"/>
        <updated>2024-04-27T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/machine-learning-groovy.html</id>
        <category term="machine learning"/>
        <category term="groovy"/>
        <category term="ignite"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Recientemente he participado en lo que podría ser mi primer proyecto con Machine Learning.
En concreto el proyecto consiste en catalogar a unos mil clientes en función de una serie de atributos (unos 15)
de tal forma que nos ayude a comprender mejor sus necesidades.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proyecto no requiere (ahora mismo) de un análisis en real-time sino que vamos a iterando y mejorando las
características y sacando conclusiones en cada una.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta fase del proyecto no hace falta acudir a diferentes fuentes de datos sino que vamos a trabajar con los datos
&quot;consolidados&quot; que ya disponemos. Es decir, vamos a calcular para cada cliente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;el número de usuarios que utilizan nuestra aplicación (hay clientes con un sólo usuario y otros con un pool de ellos)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el número de documentos que empiezan y el número de documentos que completan&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el tiempo, en días, que emplean para completarlos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;si usan el interface web o el api&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;etc&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación_de_datos&quot;&gt;Preparación de datos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues la primera tarea es revisar las fuentes de datos y &quot;su calidad&quot;. En nuestro proyecto hemos revisado las
diferentes tablas de las qe disponemos y preparado una base de datos donde consolidarlas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esto ha sido por la comodidad de usar algo que es compartido entre los diferentes miembros del proyecto y su rapidez.
En otros proyectos o artículos verás que usan ficheros CSV o sistemas más o menos complejos según sus necesidades.
En realidad, al menos para nuestro caso, esto es irrelevante, lo único que queremos es tener un sitio que sea cómodo
de donde leer los datos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues se han creado una serie de &quot;select count, group, sum, diff&quot; que nos permitan obtener los diferentes contadores
por clientes e insertarlos en una tabla&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;create table clientes
(
    id  int auto_increment primary key,
    cif varchar(20) not null,
    nombre varchar(200) not null,
    nusers int,
    ndocuments int,
    docusfinished int,
    daystofinish int,
    api int,
    web int,
... otros campos de interés
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este proceso ha sido incremental, es decir, hemos empezado con unos pocos campos e ido añadiendo según analizábamos
el modelo y las herramientas.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;datos&quot;&gt;Datos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea principal es disponer de una serie de registros donde cada uno representa los atributos de un cliente, lo
cual coincide con la idea de un query SQL y que Groovy nos permite leer al vuelo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Definimos un mapa &lt;code&gt;metadata&lt;/code&gt; donde podremos asociar los nombres de los campos MySQL con un nombre más legible
y prepararemos con los datos de tabla un &lt;code&gt;data&lt;/code&gt; :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;var metadata = [
    &apos;Users&apos;    : &apos;nusers&apos;,
    &apos;Documents&apos;: &apos;ndocuments&apos;,
    &apos;Finished&apos; : &apos;docusfinished&apos;,
    &apos;Days&apos;     : &apos;daystofinish&apos;,
    &apos;API&apos;      : &apos;template&apos;,
    &apos;Web&apos;      : &apos;web&apos;,
    ...
]

var rows = mlDB.rows(&quot;select * from ml.clientes order by nombre&quot;)

var fields = metadata.values()

var data = rows.collect { row -&amp;gt;
    fields.collect { row[it] ?: 0.0 } as double[]
} as double[][]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así en &lt;code&gt;data&lt;/code&gt; tenemos una matriz de NxM con los datos de los clientes&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;groovy_y_apache_ignite&quot;&gt;Groovy y Apache Ignite&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A diferencia de la mayoría de los post que encontrarás por ahí nosotros no hemos usado Python, sino Groovy. No voy
a detallar las razones, pero básicamente es porque puedo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por la parte de Machine Learning, existen diferentes frameworks con capacidades para ello, pero Ignite se integra
muy bien con Groovy y ha sido muy fácil usarlo. En mi equipo, un portátil normal, y con un script de Groovy de unas
cuantas líneas, el proceso de generar el cluster de clientes tarda apenas unos segundos sin necesidad de crear ninguna
infraestructura especial.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Grab(&quot;org.apache.ignite:ignite-core:2.15.0&quot;)
@Grab(&quot;org.apache.ignite:ignite-ml:2.15.0&quot;)
@Grab(&quot;org.apache.ignite:ignite-spring:2.15.0&quot;)

import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction
import org.apache.ignite.cache.query.ScanQuery
import org.apache.ignite.configuration.CacheConfiguration
import org.apache.ignite.configuration.IgniteConfiguration
import org.apache.ignite.ml.clustering.kmeans.KMeansTrainer
import org.apache.ignite.ml.dataset.feature.extractor.impl.DoubleArrayVectorizer
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi
import org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder
import static org.apache.ignite.Ignition.start

var cfg = new IgniteConfiguration(
        peerClassLoadingEnabled: true,
        discoverySpi: new TcpDiscoverySpi(
                ipFinder: new TcpDiscoveryMulticastIpFinder(
                        addresses: [&apos;127.0.0.1:47500..47509&apos;]
                )
        )
)

start(cfg).withCloseable { ignite -&amp;gt;
   // alimentar a Ignite
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque el script es más complejo (y lo iré desgranado a continuación) este código básicamente lo que hace es
levantar en nuestra máquina una instancia de Ignite de un sólo nodo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación creamos una cache&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;var dataCache = ignite.createCache(new CacheConfiguration&amp;lt;Integer, double[]&amp;gt;(
    name: &quot;TEST_${UUID.randomUUID()}&quot;,
    affinity: new RendezvousAffinityFunction(false, 10)))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y le alimentamos con los datos de &lt;code&gt;data&lt;/code&gt;. Insertaremos los datos de todos nuestros clientes porque no son millones
de registros, sólo unos miles&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;data.indices.each { int i -&amp;gt;
    dataCache.put(i, data[i])  // estamos insertando en la fila &quot;i&quot; un array de doubles
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;entrenando_el_modelo&quot;&gt;Entrenando el modelo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación vamos a decirle a Ignite que entrene al modelo con los datos proporcionados:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;var vectorizer = new DoubleArrayVectorizer()

var trainer = new KMeansTrainer()
            .withAmountOfClusters(CLUSTERS)
            .withMaxIterations(100)
            .withEpsilon(1.0E-14)

var mdl = trainer.fit(ignite, dataCache, vectorizer)

var centroids = mdl.centers*.all()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CLUSTERS es el número de agrupaciones en las que queremos categorizar a nuestros clientes. Para nuestro proyecto
hemos empezado con 3, pero ahora estamos probando con 10 para conseguir mayor &quot;granuralidad&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Esta es la parte central y más difícil de entender de este método de ML.&lt;/strong&gt; Lo que hace el algoritmo de KMeans
es tomar CLUSTERS puntos aleatorios y de forma repetitiva (nosotros forzamos a 100 veces) va analizando cada fila
de datos y viendo la distancia &quot;matemática&quot; que hay con esos puntos. En cada paso irá colocando a esa fila en un grupo u otro,
de tal forma que al final del bucle unos registros estarán en un grupo y otros en otro.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El elemento aleatorio de dónde ubicar los puntos (centroides) iniciales hace que si ejecutas el proceso diferentes
veces obtengas diferentes resultados. Por eso estoy forzando al trainer a que la distancia entre los elementos
sea muy muy reducida y a que se ejecute hasta 100 veces obteniendo así las mismas (o parecidas) agrupaciones todas
las veces que se ejecuta el script.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez ejecutado el trainer lo que obtenemos es un array de los puntos &quot;centrales&quot; de cada agrupación&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;alimentando_al_modelo&quot;&gt;Alimentando al modelo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último lo que vamos a hacer es, usando el modelo entrenado, &quot;predecir&quot; uno a uno cada cliente e ir acumulándolos
en un mapa en memoria &lt;code&gt;clusters&lt;/code&gt; , además vamos a guardar las observaciones de cada uno en otro mapa
&lt;code&gt;observationMap&lt;/code&gt; para tener un mayor detalle. Este segundo map lo usamos para generar un report más específico
pero por no complicar este post no voy a entrar en detalles&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;// Predict all data
var clusters = [:].withDefault { [] } as Map&amp;lt;Integer, ArrayList&amp;gt;
var observationsMap = [:].withDefault { [:].withDefault { [] as Set } }

dataCache.query(new ScanQuery&amp;lt;&amp;gt;()).withCloseable { observations -&amp;gt;
    observations.each { observation -&amp;gt;
        def (k, v) = observation.with { [getKey(), getValue()] }
        def vector = vectorizer.extractFeatures(k, v)
        int prediction = mdl.predict(vector)
        clusters[prediction] += companies[k]
        v.eachWithIndex { val, idx -&amp;gt;
            observationsMap[prediction][features[idx]] += val
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;generando_conclusiones&quot;&gt;Generando conclusiones&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez hemos realizado todas las predicciones tendremos en las variables &lt;code&gt;clusters&lt;/code&gt; y
&lt;code&gt;observationsMap&lt;/code&gt; suficiente información de cada grupo así como por cada cliente y podremos sacar conclusiones e
incluso podremos generar gráficas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    int clusterIdx = 1
    clusters.sort { e -&amp;gt; e.value.size() }.each { k, v -&amp;gt;
        println &quot;\nCluster ${clusterIdx}: ${v.sort().join(&apos;, &apos;)}&quot;

        def chart = new RadarChartBuilder().width(1024).height(768).title(&quot;Cluster ${clusterIdx}: ${v.size()} Empresas&quot;).build()
        chart.radiiLabels = features as String[]
        chart.styler.with {
            legendVisible = true
            seriesColors = [
                    new Color(255, 51, 151, 50)
            ] as Color[]
        }

        chart.addSeries(&quot;Serie&quot;.toString(), normalized)
        def bufferedImage = BitmapEncoder.getBufferedImage(chart)
        var outputfile = new File(&quot;cluster_${clusterIdx}.png&quot;)
        ImageIO.write(bufferedImage, &quot;png&quot;, outputfile)

        clusterIdx++
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejemplo&quot;&gt;Ejemplo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras ejecutar el script obtenemos gráficas de cada cluster (podríamos tenerlos todos en  una pero interpretar tanta
info resulta un poco confuso así que hemos preferido crear una imagen por cluster) como por ejemplo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/cluster1.png&quot; alt=&quot;cluster1&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. Ejemplo de cluster&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estas agrupaciones nos ayudan a determinar qué empresas en concreto están tardando muchos días en completar
el proceso de documentos y que además tienen una baja tasa de terminados lo que indica que están teniendo problemas
de usabilidad con la aplicación&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2024/cluster2.png&quot; alt=&quot;cluster2&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. Ejemplo de cluster&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;O a ver que un par de empresas son capaces de terminar todo el flujo de creación de documentos pero empleando muchos
usuarios por lo que podríamos ayudarles en mejorar las integraciones con API&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta fase del proyecto tenemos 10 agrupaciones de clientes y cada una revela ciertas características y/o patrones
que nos pueden ayudar a enfocar el diálogo de mejoras.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo trabajando &quot;un poco&quot; más en el script hemos parametrizado qué atributos nos interesan en cada ejecución
de tal forma que en pocos segundos, de una forma sencilla, podemos obtener diferentes agrupaciones que nos descubren comportamientos de los
clientes&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post NO muestro todo el script, simplemente las partes de interés (además de que puede haber algún dato
sensible) pero espero que sirva para exponer la idea principal de cómo agrupar clientes en función de múltiples atributos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Personalmente lo mejor para mí es poder seguir aplicando mis conocimientos de Apache Groovy y descubrir nuevos
frameworks de Machine Learning sin tener que tocar una sóla línea de Python&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;En este post encontrarás una explicación mejor de cómo hacer una agrupación de marcas de Wisky (que es en el que
me he inspirado para este proyecto)&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://groovy.apache.org/blog/whiskey-clustering-with-groovy-and&quot; class=&quot;bare&quot;&gt;https://groovy.apache.org/blog/whiskey-clustering-with-groovy-and&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Agrupando clientes con Machine Learning (Groovy+Apache Ignite)</summary>
    </entry>
    <entry>
        <title>Deploying a K3s cluster with SSL</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/k3s-ssl.html"/>
        <updated>2024-04-20T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/k3s-ssl.html</id>
        <category term="apisix"/>
        <category term="kubernetes"/>
        <category term="apigateway"/>
        <category term="k3s"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post I&amp;#8217;ll explain how to deploy a ready to prod k8s cluster (with only 1 node)
using k3s and exposing a service using https with an auto-provisioned certificate using
Let&amp;#8217;s Encrypt&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As a plus we&amp;#8217;ll install Apisix, an OpenSource ApiGateway, to allow grow up our stack with more
nodes+applications&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirement&quot;&gt;Requirement&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A Linux server with (min) 2Gb&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A registered DNS, for example &lt;code&gt;api.jorge.io&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;kubectl installed&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;helm installed&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;install_k3s&quot;&gt;Install k3s&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;K3s is a light implementation of kubernetes ready to production. You can find more information
at &lt;a href=&quot;https://docs.k3s.io/installation&quot; class=&quot;bare&quot;&gt;https://docs.k3s.io/installation&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;curl -sfL &lt;a href=&quot;https://get.k3s.io&quot; class=&quot;bare&quot;&gt;https://get.k3s.io&lt;/a&gt; | INSTALL_K3S_EXEC=&quot;--tls-san api.jorge.io&quot; sh -&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;install_apisix&quot;&gt;Install Apisix&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Apisix is an ApiGateway. You can use it as standalone, with docker or integrate into your cluster.
It has a lot of plugins and features ready to be used in production&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll use a namespace for Apisix:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl create namespace ingress-apisix&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll install a basic/typical implementation:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;helm repo add apisix https://charts.apiseven.com
helm update
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm install apisix apisix/apisix --set gateway.tls.enabled=true --set ingress-controller.enabled=true --namespace ingress-apisix&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;install_certmanager&quot;&gt;Install CertManager&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CertManager is the agent able to create and provision certificates. It allows to create
diferent kinds of certificates and talk with different CAs to create and install them as secrets
into our cluster. For our we&amp;#8217;ll use it to install free SSL certificates from Let&amp;#8217;s Encrypt&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;helm repo add jetstack https://charts.jetstack.io --force-update
helm update
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm install   cert-manager jetstack/cert-manager   --namespace cert-manager   --create-namespace   --version v1.14.4&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;checking&quot;&gt;Checking&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll check if Apisix is ready&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl get all -n ingress-apisix&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pods and services are up and running&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the_funny_part&quot;&gt;The funny part&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;First thing (can be also last, of course, it doesnt matter) will be to redirect all plain
http requests to https&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;traefik-https-redirect-middleware.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
    name: redirect-https
spec:
    redirectScheme:
        scheme: https
        permanent: true&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f traefik-https-redirect-middleware.yaml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Next, we&amp;#8217;ll create the issuer:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;cluster-issuer.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
    name: letsencrypt
    namespace: ingress-apisix
spec:
    acme:
        server: https://acme-v02.api.letsencrypt.org/directory
        email: jorge@edn.es
        privateKeySecretRef:
            name: letsencrypt
        solvers:
        - selector: {}
          http01:
            ingress:
            class: traefik&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f cluster-issuer.yaml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ve created a ClusterIssuer (so all nodes and all namespaces in the cluster will be able to use
it)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It will negociate with Lets Encrypt using the &lt;code&gt;http01&lt;/code&gt; method via traefik and will create a
secret into the cluster named &lt;code&gt;letsencrypt&lt;/code&gt; (or whatever you specify)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now, we&amp;#8217;ll send all trafic to Apisix&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ingress.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    annotations:
        spec.ingressClassName: traefik
        cert-manager.io/cluster-issuer: letsencrypt
        traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
    name: api-jorge-ingress
    namespace: ingress-apisix
spec:
    rules:
    - host: api.jorge.io
      http:
        paths:
        -   pathType: Prefix
            path: /
            backend:
                service:
                    name: apisix-gateway
                    port:
                        number: 80
    tls:
    - hosts:
        - api.jorge.io
      secretName: letsencrypt-cert&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f ingress.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pay attention to the annotations, this is the &quot;trick&quot;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;we will use the traekif ingress&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;we&amp;#8217;re instructing to the certificate manager to use the previous issuer created&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;we&amp;#8217;re instructing to traefil to redirect all plain http to https&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;we&amp;#8217;re instructing we want to use a certificate from a secret (created by cert-manager)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Now all traffic will be served from our k3s cluster using https with a certificate
created by Lets&amp;#8217;Encrypt&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviously we&amp;#8217;ll have a 404 as Apisix doesn&amp;#8217;t know what to do with the request&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;deploying_a_service&quot;&gt;Deploying a service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll deploy a typical service (httpbin)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl run httpbin --image kennethreitz/httpbin --namespace ingress-apisix&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(We are creating a deploying httpbin using the public image kennethreitz/httpbin)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and exposing it&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl expose pod httpbin -n ingress-apisix --port 80&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It only remains to &quot;link&quot; Apisix with the new httpbin service:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;routes.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
    name: httpbin
spec:
    http:
    - name: httpbin
      match:
        paths:
        - /*
        hosts:
            - api.jorge.io
      backends:
        - serviceName: httpbin
          servicePort: 8080&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We are creating an ApisixRoute to route all requests to &lt;code&gt;api.jorge.io&lt;/code&gt; to the httpbin service.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And this is all. Right now a request from outside will follow:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;traefik redirecting http to https&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cert-manager negociate, creates and store the certificate&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;traefik redirect all requests to Apisix&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apisix determine which service to use (httpbin in our case)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;httpbin response to the requests&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Deploying from scratch a basic k3s cluster with Let's Encrypt SSL</summary>
    </entry>
    <entry>
        <title>Is Spam?</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2024/is-spam-in-devto.html"/>
        <updated>2024-01-16T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2024/is-spam-in-devto.html</id>
        <category term="bash"/>
        <category term="dev.to"/>
        <category term="httpie"/>
        <category term="jq"/>
        <content type="html">
            &lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;These last days I&amp;#8217;ve reported to the Dev.to admin a few posts as spam, so I&amp;#8217;ve developed this small bash script
to detect posible posts&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;INFO: You need to have &lt;code&gt;httpie&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; installed. Also, an API-KEY is required&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;INFO: In fact, it&amp;#8217;s more to practice httpie and jq filtering capabilities than a useful tool&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;#!/bin/bash
API_KEY=$1

&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
latest=$(http  https://dev.to/api/articles api-key:$API_KEY accept:application/vnd.forem.api-v1+json  per_page==80)

&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
filtered=$(jq &apos;.[] | select(.reading_time_minutes==1 and .user.user_id &amp;gt; 4)&apos; &amp;lt;&amp;lt;&amp;lt; &quot;$latest&quot;)

echo Total Last articles $(jq -M -r &apos;.id&apos; &amp;lt;&amp;lt;&amp;lt; &quot;$filtered&quot; | wc -l)
echo &apos;-----&apos;

echo Number of authors $(jq -M -r &apos;.user.user_id&apos; &amp;lt;&amp;lt;&amp;lt; &quot;$filtered&quot; | uniq | wc -l)
echo &apos;-----&apos;

&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
users=$(jq -M -r &apos;.user | .user_id&apos; &amp;lt;&amp;lt;&amp;lt; &quot;$filtered&quot; | uniq)

for user_id in $(echo &quot;$users&quot;); do

   &lt;i class=&quot;conum&quot; data-value=&quot;4&quot;&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
   strjoined_at=$(http GET &quot;https://dev.to/api/users/$user_id&quot; api-key:$API_KEY accept:application/vnd.forem.api-v1+json | jq -r &apos;.joined_at&apos;)

   joined_at=$(date --date=&quot;$strjoined_at&quot; &quot;+%Y-%m-%d&quot;)
   days=$((($(date +%s) - $(date -d &quot;$joined_at&quot; +%s))/86400))

   &lt;i class=&quot;conum&quot; data-value=&quot;5&quot;&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
   if (( ${days:-2} &amp;lt; 3 )); then
        echo &quot;The $user_id user is suspect to be spam, see post:&quot;
        jq --arg jq_user_id ${user_id} &apos;.[] | select(.user.user_id == ($jq_user_id|tonumber)) | .url&apos; &amp;lt;&amp;lt;&amp;lt; &quot;$latest&quot;
   fi
done&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;retrieve last articles (80 max)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;filter by &lt;code&gt;reading_time_minutes&lt;/code&gt; as spam usually are short post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;extract uniques &lt;code&gt;user_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;4&quot;&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;find user details for &lt;code&gt;user_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;5&quot;&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;check if this account was recently created&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviously not all articles that meet these conditions are spam. Lot of people (as me) write a hello-world just created
the account so the script show the url, so I can read the post and decide if it&amp;#8217;s spam or not.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For next version, I have time, I would like to include some kind of &quot;IA&quot; to automatically read the post and decide if the post is spam&lt;/p&gt;
&lt;/div&gt;
        </content><summary>A 20 lines bash script to check if last posts in Dev.to can be spam</summary>
    </entry>
    <entry>
        <title>Groogle al rescate</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/groogle-al-rescate.html"/>
        <updated>2023-12-02T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/groogle-al-rescate.html</id>
        <category term="groovy"/>
        <category term="groogle"/>
        <category term="google"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hace ya unos años dediqué un tiempo en desarrollar un DSL (domain specific language) para poder usar los servicios
de Google de una forma simple. La implementación la hice, obviamente, en Groovy (de ahí el nombre Groogle
como contracción entre Groovy y Google)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Llegué a publicar bastantes releases cubriendo varios servicios como Drive, Sheet, Gmail, People y Calendar
pero para ser sincero como no veía mucho interés por parte de la comunidad en el DSL lo fui dejando y dedicandome
a otras cosas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero de repente y por casualidad, hablando con una amiga me dice que han tenido un problema con la base de datos y
que se va a pasar toda la mañana (con suerte) extrayendo datos de unas 120 hojas de cálculo Google para recuperarlos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente, tenía que ir una por una, buscar la hoja correspondiente a diciembre y extraer los 31 valores de una
columna, pasarlos a un fichero csv para al final del todo poder importarlos de nuevo a la base de datos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;No viene al caso el porqué una empresa maneja toda su información en hojas de Google. No creo que sean los únicos&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así que ví la oportunidad para desempolvar Groogle y echarle una mano.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar, lo que necesitaba era los identificadores de todas esas hojas de cálculo y como además necesitábamos
saber a quién correspondía cada una le dije a mi amiga que fuera recabando esta información (puesto que además
era ella la que tenía acceso a las hojas). Simplemente tenía que prepara un fichero parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Fulanito 123123123123sdafas12312
Meganito 92131231ewewrdsfs112321
....&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los IDs se pueden extraer fácilmente de la URL cuando estás editando la hoja.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mientras se ponía a esta tarea yo preparé el DSL:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;@Grab(&apos;com.puravida-software.groogle:groogle-core:3.1.2&apos;)
@Grab(&apos;com.puravida-software.groogle:groogle-sheet:3.1.2&apos;)
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.sheets.v4.SheetsScopes
import com.puravida.groogle.*
import java.text.SimpleDateFormat

lst = new File(&quot;lista.txt&quot;).text.split(&quot;\n&quot;)
sdf = new SimpleDateFormat(&quot;yyyy/MM/dd&quot;)
file = new File(&quot;data.csv&quot;)
file.text = &quot;id | fecha | valor\n&quot;

groogle = GroogleBuilder.build {
    withOAuthCredentials {
        applicationName &apos;test-sheet&apos;
        withScopes SheetsScopes.SPREADSHEETS
        usingCredentials &quot;client_secret.json&quot;
        storeCredentials true
    }
    service SheetServiceBuilder.build(), SheetService
}

centros.eachWithIndex { line, cidx -&amp;gt;
    def kv = line.split(&quot; &quot;)
    def id = kv[0]
    def sheetId = kv[1]

    println id

    groogle.service(SheetService).withSpreadSheet sheetId, {
        withSheet &apos;Dic23&apos;, {
            def day = sdf.parse( &quot;2023/01/01&quot;)
            def values = writeRange(&quot;J7&quot;, &quot;J37&quot;).get()
            values.eachWithIndex{ v, idx-&amp;gt;
                file &amp;lt;&amp;lt; &quot;${kv[0]} | ${sdf.format(day)} | ${v[0]}\n&quot;
                day++
            }
        }
    }
    if( !(cidx % 10) ){
        sleep 1000*60
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras importar las librerías correspondientes e inicializar un par de variables empezamos escribiendo en el
fichero a generar unas cabeceras&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usando &lt;code&gt;withOAuthCredentials&lt;/code&gt; le diremos a Groogle que el usuario se tendrá que identificar para lo que se abrirá
un navegador y el usuario podrá usar su cuenta para dar permisos al script.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esto lo que quiere decir es que no necesito ejecutar el script en mi equipo, sino que puedo pasarselo tal cual y
ella ejecutarlo en su máquina, usando su cuenta de Google (teniendo Groovy instalado claro)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El resto del script es bastante fácil:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;leemos línea a línea el fichero que ha preparado con los IDs de las hoja&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;withSpreadSheet sheetId&lt;/code&gt; abre la hoja con este ID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;withSheet &apos;Dic23&apos;&lt;/code&gt; abre la pestaña de esa hoja&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;def values = writeRange(&quot;J7&quot;, &quot;J37&quot;).get()&lt;/code&gt; leemos de una sola vez todo un rango&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vamos volcando al fichero cada valor junto con el origen y la fecha&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para evitar que Google nos dé un error por muchas lecturas en poco tiempo metemos una espera cada 10 hojas de un
minuto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras un par de pruebas tuvimos listo el fichero completo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tiempo ejecución 5-8 minutos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tiempo que nos llevó preparar el script 30-40 minutos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sé que puede ser una noñería, pero que de vez en cuando, acuda a un proyecto OpenSource que publiqué hace un tiempo
y que pueda resolverle a alguien el evitar estar todo un día realizando una tarea tan pesada como esta que se puede
resolver con unas pocas líneas me llenan de orgullo y satisfacción. ;)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Usando el DSL Groogle para extraer datos de 100 hojas de cálculo</summary>
    </entry>
    <entry>
        <title>Integrando un botón de Donar en React</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/stripe-react.html"/>
        <updated>2023-12-02T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/stripe-react.html</id>
        <category term="react"/>
        <category term="stripe"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;De react tengo cero conocimientos, lo único que sé copypastear muy bien&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Recientemente, he publicado una aplicación hecha totalmente en React (un framework
javascript para desarrollar aplicaciones web).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La aplicación en sí no es muy complicada, simplemente sirve para convertir unos
ficheros XML de la Seguridad Social a Excel y viceversa, pero quería darle un toque
moderno y hacerla visualmente atractiva así que he buscado algún ejemplo React que
me gustara y del que poder partir&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;La aplicación en concreto está en &lt;a href=&quot;https://creta.edn.es&quot; class=&quot;bare&quot;&gt;https://creta.edn.es&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que me peleé con el framework y entendí un poco cómo funciona pude desplegar
la aplicación y ya hay algún usuario usándola e incluso me han reportado algún bug
(que ha sido oportunamente corregido).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En su momento me planteé hacer la aplicación accesible mediante un pago, tipo
una subscripción o un pago único así que me creé una cuenta de Stripe. Sin embargo,
una vez me puse con la aplicación decidí que no me compensaba el esfuerzo de crear
todo el sistema de carrito de la compra para una aplicación tan simple así que la
publiqué abierta.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, ahora que ya está publicada y en uso me he planteado si la gente
que la usa se animaría a realizar un pequeño donativo, aunque fueran un par de euros
para ayudar en el mantenimiento del hosting. Realmente no lo necesitaría pero por un
lado me gustaría comprobar si la gente es consciente de que estas cosas nos cuestan
tiempo y esfuerzo y por otra me interesaba investigar la integración con Stripe.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Descartado el implementar un carrito de la compra descubrí que Stripe permite generar
un trozo de código HTML que puedes integrar en tu web para que el usuario pueda
hacer pagos simples, así que me propuse incluir un botón de &quot;Donar&quot; en la aplicación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como digo, Stripe cuenta con librerías para muchos lenguajes y componentes web para
integrar en tu aplicación, pero el caso concreto de insertar un código HTML en React
no era tan trivial (para un backend sin conocimientos como yo).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al final la solución ha sido así de sencilla:&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;componente_stripecard&quot;&gt;Componente StripeCard&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la carpeta component, he creado un subdirectorio &lt;code&gt;StripeCard&lt;/code&gt; con dos ficheros&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;types.ts&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;typescript&quot;&gt;export interface StripeCardProps {
  buttonId?: string;
  publishableKey?: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para definir los dos properties que requiere el componente (ambos proporcionados
por Stripe cuando creas el botón desde su dashboard)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;index.ts&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;typescript&quot;&gt;import * as React from &apos;react&apos;
import { StripeCardProps } from &apos;./types&apos;;

declare global {
  namespace JSX {
    interface IntrinsicElements {
      &apos;stripe-buy-button&apos;: any;
    }
  }
}

export const StripeCard = ({ buttonId, publishableKey }: StripeCardProps) =&amp;gt;{

  return (
    &amp;lt;div&amp;gt;
    &amp;lt;stripe-buy-button
      buy-button-id={buttonId}
      publishable-key={publishableKey}
    &amp;gt;
    &amp;lt;/stripe-buy-button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con esto ya cuento con un componente &quot;integrado&quot; en React llamado &apos;stripe-buy-button&apos;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;page&quot;&gt;Page&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente para incluirlo en una página&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Creta/index.ts&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;typescript&quot;&gt;import { lazy } from &quot;react&quot;;
// otros imports
import { StripeCard } from &apos;../../common/StripeCard&apos;;

const Home = () =&amp;gt; {

  return (
    &amp;lt;Container&amp;gt;
      &amp;lt;ScrollToTop /&amp;gt;
      &amp;lt;UnificarCreta id=&quot;unificar&quot; title={UnirLiquidaciones.title} content={UnirLiquidaciones.text}/&amp;gt;

      &amp;lt;StripeCard buttonId={process.env.REACT_APP_PUBLIC_STRIPE_BUTTON_ID} publishableKey={process.env.REACT_APP_PUBLIC_STRIPE_PUBLISHABLE_KEY}/&amp;gt;

      &amp;lt;SolicitudCalculosForm id=&quot;solicitud&quot; title={SolicitudCalculo.title} content={SolicitudCalculo.text}/&amp;gt;
    &amp;lt;/Container&amp;gt;
  );
};

export default Home;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ves es tan simple como hacer el import al igual que con otros componentes
y usarlo en tu página&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;stripe_javascript&quot;&gt;Stripe Javascript&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último simplemente tenemos que incluir el código propio de Stripe
que será el encargado de &quot;dar vida&quot; al webcomponent&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;public/index.html&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    ...
    &amp;lt;script async src=&quot;https://js.stripe.com/v3/buy-button.js&quot;&amp;gt;
    &amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque en este post hablo de Stripe en realidad es la forma que he encontrado
de integrar webcomponents ajenos a React dentro de este framework&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Añadiendo un botón "Donar" de Stripe en una aplicación React</summary>
    </entry>
    <entry>
        <title>Adding AI to your Micronaut search service</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/openai-milvus-micronaut.html"/>
        <updated>2023-10-17T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/openai-milvus-micronaut.html</id>
        <category term="openai"/>
        <category term="ai"/>
        <category term="micronaut"/>
        <category term="milvus"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post we&amp;#8217;ll see how to create an &quot;intelligent search service&quot; using Micronaut, Milvus and OpenAI.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;I&amp;#8217;ll show only most important part of the code to have an idea about the full picture&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Say we want to create a service to search across a bunch of product descriptions using a user input.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;ProductId&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;ProductDescription&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;This is long text with several info for product 1&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;This is long text with several info for product 2&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;n&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;This is long text with several info for product n&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Traditionally we will use some kind of `LIKE %str%&apos; approach, so we&amp;#8217;ll try to find descriptions containing
the input text. But we want to improve our service with a little intelligence and search using vector search
where information is represented as vectors instead to plain text so queries can retrieve &quot;closed&quot; documents
instead &quot;matched&quot; documents.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For this example, we&amp;#8217;ll use Milvus (&lt;a href=&quot;https://milvus.io/&quot; class=&quot;bare&quot;&gt;https://milvus.io/&lt;/a&gt;) as a &quot;similarity&quot; database. Another alternatives can
be Meilisearch or Postgresql with pg_vector plugin installed, for example&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;architecture&quot;&gt;Architecture&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-a970f120851b2be2551be21afa8af63b.png&quot; alt=&quot;Diagram&quot; width=&quot;757&quot; height=&quot;302&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;PostgreSQL (or another source where &quot;documents&quot; are stored). For our example documents will be stock descriptions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Milvus&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OpenAI account (we&amp;#8217;ll use free tier of OpenAI to vectorized documents)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;collection&quot;&gt;Collection&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We need to create the collection in Milvus. For this purpose we&amp;#8217;ll create a Micronaut command line (cli) application
and add the milvus dependency&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;gradle.build&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;dependencies{
    ...
    implementation &apos;io.milvus:milvus-sdk-java:2.2.5&apos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And we&amp;#8217;ll create a PicoCli Command java class. We can implement also others commands as &lt;code&gt;delete-collection&lt;/code&gt;, &lt;code&gt;create-user&lt;/code&gt;,
&amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;MilvusClient connect(){
    return new MilvusServiceClient(
            ConnectParam.newBuilder()
                    .withUri(url)
                    .withAuthorization(user, password)
                    .build()
    );
}

void createCollection() throws Exception {
    final var milvusClient = connect();

    final var collection = positional.get(1);
    final var createCollection = CreateCollectionParam.newBuilder()
        .withCollectionName(collection)
        .addFieldType(FieldType.newBuilder().withName(&quot;id&quot;)
                .withDataType(DataType.Int64).withPrimaryKey(true).withAutoID(true).build())
        .addFieldType(FieldType.newBuilder().withName(&quot;instrument_id&quot;)
                .withDataType(DataType.VarChar).withMaxLength(36).build())
        .addFieldType(FieldType.newBuilder().withName(&quot;stock_instrument&quot;)
                .withDataType(DataType.VarChar).withMaxLength(20000).build())
        .addFieldType(FieldType.newBuilder().withName(&quot;embedding&quot;)
                .withDataType(DataType.FloatVector).withDimension(1536).build())
        .build();

    final var response = milvusClient.createCollection(createCollection);
    if( response.getStatus().intValue() != R.success().getStatus() )
        throw response.getException();

    final var createIndex = CreateIndexParam.newBuilder()
            .withCollectionName(collection)
            .withFieldName(&quot;embedding&quot;)
            .withIndexType(IndexType.IVF_FLAT)
            .withMetricType(MetricType.L2)
            .withExtraParam(&quot;{\&quot;nlist\&quot;: 1024}&quot;)
            .build();
    final var index = milvusClient.createIndex(createIndex);
    if( index.getStatus().intValue() != R.success().getStatus() )
        throw response.getException();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically we&amp;#8217;re creating a Milvus collection with 4 fields, and auto generated Id, our stock Id, the
full description of the stock (for future reference) and an embedding vector to represent the description&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once we build the jar we can run something similar to&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;java -jar milvus-cli.jar --url milvus-instance --user user --password password create-schema&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;documents&quot;&gt;Documents&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We can use also the previous command line java to feed the collection but as we want to update every day we&amp;#8217;ll
create a service and use a daily scheduler to do it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Stock description will be extracted from our PostgreSQL but in your case can be text files, Excel, World, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@JdbcRepository(dialect = Dialect.POSTGRES)
public interface StocksRepository extends GenericRepository&amp;lt;StockInstrumentEntity, UUID&amp;gt; {
    Flux&amp;lt;StockInstrumentEntity&amp;gt;findAll();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;openai&quot;&gt;OpenAI&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;This service will require an authorization token&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We will use public OpenAPI endpoint to send descriptions and retrieve associated vectors.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Serdeable
public record EmbeddingsModel(List&amp;lt;String&amp;gt; input, String model) {
}

@Client(&quot;https://api.openai.com&quot;)
public interface OpenaiClient {
    @Post(&quot;/v1/embeddings&quot;)
    EmbeddingsResponse embeddings(
        @Body EmbeddingsModel model,
        @Header(HttpHeaders.AUTHORIZATION)String auth);
}

@Serdeable
public record EmbeddingData(String object, List&amp;lt;Float&amp;gt; embedding, int index) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;EmbeddingsModel is a POJO to send a list of Strings to be vectorized using a model, &lt;code&gt;&quot;text-embedding-ada-002&quot;&lt;/code&gt;
in this case, and receive a list of &lt;code&gt;EmbeddingData&lt;/code&gt; where every input was converted to a &lt;code&gt;List&amp;lt;Float&amp;gt;&lt;/code&gt; (the vector)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;feed_milvus&quot;&gt;Feed Milvus&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically we have a Micronaut service retrieving StockInstrumentEntities from the database, sending the
description to OpenAI and receiving a &lt;code&gt;List&amp;lt;Float&amp;gt;&lt;/code&gt; per each document. Now is time to store all information into
the collection:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;List&amp;lt;InsertParam.Field&amp;gt; fields = new ArrayList&amp;lt;&amp;gt;();
fields.add(new InsertParam.Field(&quot;instrument_id&quot;, ids));
fields.add(new InsertParam.Field(&quot;stock_instrument&quot;, stocks));
fields.add(new InsertParam.Field(&quot;embedding&quot;, vectors));

InsertParam insertParam = InsertParam.newBuilder()
        .withCollectionName(configuration.getCollection())
        .withFields(fields)
        .build();
var resp = milvusClient.insert(insertParam);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see Milvus allows feed collections using a batch of objects. In our example we&amp;#8217;re sending
a bunch of 100 Ids+Descriptions+Vectors&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;intelligent_search&quot;&gt;Intelligent Search&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To run the search we need vectorized the user input as we did with the description of the stocks, using the OpenAI
endpoint. Once we have the associated vector we can use Milvus to run a search:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;LoadCollectionParam.newBuilder().withCollectionName(collection).build());

if( load.getStatus().intValue() != R.success().getStatus() )
    throw load.getException();

List&amp;lt;String&amp;gt; query_output_fields = List.of(&quot;id&quot;,&quot;instrument_id&quot;, stock_instrument&quot;);

QueryParam queryParam = QueryParam.newBuilder()
        .withCollectionName(collection)
        .withConsistencyLevel(ConsistencyLevelEnum.STRONG)
        .withExpr(search)
        .withOutFields(query_output_fields)
        .withOffset(0L)
        .withLimit(100L)
        .build();

R&amp;lt;QueryResults&amp;gt; respQuery = milvusClient.query(queryParam);
QueryResultsWrapper wrapperQuery = new QueryResultsWrapper(respQuery.getData());
var ids = wrapperQuery.getFieldWrapper(&quot;instrument_id&quot;).getFieldData();
var stocks = wrapperQuery.getFieldWrapper(&quot;stock_instrument&quot;).getFieldData();
for(var i=0; i&amp;lt; ids.size(); i++){
    System.out.println(Map.of(&quot;id&quot;,ids.get(i),&quot;stock&quot;,stocks.get(i)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Using the search vector Milvus will run a query and find these related documents using the distances of the vectors.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;As you can see, we&amp;#8217;re retrieving our custom Id (&lt;code&gt;instrument_id&lt;/code&gt;) so we can use it and retrieve from our
Postgresql database more information&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Some search examples:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&quot;companies selling cars&quot;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&quot;Ford Motor Company is an automobile company that designs, manufactures, markets, and services a full line of Ford trucks, utility vehicles, cars as well as Lincoln luxury vehicles&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;Toyota Motor Corp is a Japan-based company engaged in the automobile business, finance business and other businesses.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;General Motors Company designs, builds and sells trucks, crossovers, cars and automobile parts worldwide&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;women care&quot;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The Wendy&amp;#8217;s Company (Wendy) is engaged in the business of operating, developing and franchising a system of quick-service restaurants&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JELD-WEN Holding, Inc. is a door and window manufacturer. The Company designs, produces and distributes a range of interior and exterior doors, wood, vinyl and aluminum windows&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see it&amp;#8217;s very easy to integrate new similarity databases, as Milvus in this case, to improve the
user search experience&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>An "improved" Micronaut search service using embeddings with OpenAI and Milvus</summary>
    </entry>
    <entry>
        <title>ApacheConf Halifax</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/apacheconf-halifax.html"/>
        <updated>2023-10-13T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/apacheconf-halifax.html</id>
        <category term="apache"/>
        <category term="conferencias"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por segunda vez he estado en la ApacheCon, (Conferencias de la fundación Apache
sobre Open Source) esta vez en Halifax, Canada. En este post, voy a intentar resumir mi experiencia&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cómo_por_qué_otra_vez&quot;&gt;¿Cómo? ¿Por qué? ¿otra vez?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El cómo ya lo expliqué en el post del año pasado sobre las conferencias en New Orleans. Básicamente, y a pesar de que
el año pasado me dije que había estado bien pero que era muy cansado, otra vez el &quot;impulso&quot; de vivir una
experiencia me llevó a enviar dos propuestas al track de Groovy.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta vez tenía claro que una propuesta iba a ser la de cómo hacer un operador de kubernetes en Groovy. Había aprendido
recientemente y tenía un par de ellos ya hechos y funcionando así que &amp;#8230;&amp;#8203; propuesta enviada!! Luego me acordé que
había estado hablando el año pasado con un programador sobre Micronaut y Kafka y tenía una charla que había dado &amp;#8230;&amp;#8203;
pero me parecía muy larga de dar y entonces se me ocurrió que podía mandar la versión corta de cómo empezar con
kubernetes usando Okteto &amp;#8230;&amp;#8203; ¡Segunda propuesta enviada!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como el año pasado todo estaba supeditado a que me aceptaran también como assistant porque así me costearían el viaje,
al menos el billete de avión (al final fue todo incluido!!!). No lo tenía claro porque había entendido el año pasado
que suelen aceptar a gente que no haya estado antes así que envié mi solicitud como assistant cruzando los dedos
y a esperar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otro lado ya empezaba a imaginar cómo compaginaría la asistencia con el trabajo pero en mi fuero interno sabía que
para cuando llegara la fecha lo más probable es que ya no estuviera en este sitio así que ya cruzaríamos ese puente
cuando llegáramos a ese río.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Copiado del post sobre el evento del año pasado:
He &quot;descubierto&quot; que existe un programa de voluntariado en el que, si te aceptan, puedes asistir a la
conferencia a gastos pagados (billete y hotel e incluso manutención). La labor que hay que hacer es básicamente
el primer día estar en recepción para dar las camisetas y luego ayudar en las charlas al ponente avisándole
del tiempo que le queda etc. No sé si lo entendí bien, pero suelen pillar a gente que no lo haya solicitado
anteriormente. Creo que es una oportunidad para poder asistir, conocer gente y vivir una experiencia bastante
enriquecedora.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este año parecía que me sentiría más seguro con el inglés, pero para ser sincero creo que nunca se está lo suficientemente
seguro, pero bueno he sobrevivido.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para más info sobre asistir como assistant &lt;a href=&quot;https://events.apache.org/involved/index.html&quot; class=&quot;bare&quot;&gt;https://events.apache.org/involved/index.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-1.jpg&quot; alt=&quot;apachecon 1&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. Primer registro impreso, el mío&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;la_conferencia&quot;&gt;La conferencia&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sobre la conferencia y las charlas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ser sinceros, este año la he visto más deslucida que el anterior. No, no me dejé deslumbrar por la novedad,
es que este año ha habido menos asistentes (tal vez no era un sitio tan fácil de llegar como el anterior) y
en mi opinión, menos &quot;ambiente&quot;. Aun así ha sido un evento muy interesante y se han hablado de un montón de temas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este año se han organizado los tracks por días. Por ejemplo Groovy y API los dos primeros, Inteligencia Artificial
e infra otro, etc. En general se han tratado muchos temas diferentes siendo el de AI el estrella, como era de
esperar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En lugar de un hotel, ha sido en un centro de convenciones y teníamos toda una planta para nosotros por lo que
moverse entre salas era superfácil, estaban todas a la vista y bien marcadas. Eran salas bastante amplias y
muy bien preparadas. ¡La mía incluso tenía un &quot;podium&quot; y atril para el speaker!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-2.jpg&quot; alt=&quot;apachecon 2&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. Primer registro impreso, el mío&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como en la edición anterior, no se han grabado todas salvo las keynotes y alguna por ahí. Creo que se ha
grabado el audio de muchas, pero no sé con qué intención la verdad.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A diferencia del año anterior, la comida estaba incluida y entre que si assistant, speaker y que &quot;os invitamos&quot;
las cenas también han estado incluidas. Vamos, que al final me he gastado unos 60 euros en total como mucho&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;las_charlas&quot;&gt;Las charlas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con lo de assistant me ha tocado estar atento de varios tracks, así que he podido oir cosas de las que no
tengo ni idea, y como en el chiste del perro mistetas, &quot;pero me gustaría&quot;. No solo AI, las charlas estrellas con
salas llenas de gente, sino también Spark, GeoSpatial y algunas de kubernetes&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi opinión y a las que yo he asistido, casi todas eran muy interesantes y bastante asequibles (me refiero
al tema de seguir al ponente, material, etc.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo, y como una especie de regalo, asistí a la última charla tras los 4 días, ya relajado, a una sobre
un plugin de Maven para comprobar si tus releases son &quot;reproducible builds&quot; superchula.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Intenté/Tomé un montón de fotos de cada una a las que fuí para luego repasarlas, espero tener tiempo para hacerlo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-3.jpg&quot; alt=&quot;apachecon 3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-4.jpg&quot; alt=&quot;apachecon 4&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-5.jpg&quot; alt=&quot;apachecon 5&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-6.jpg&quot; alt=&quot;apachecon 6&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-7.jpg&quot; alt=&quot;apachecon 7&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;socializar&quot;&gt;Socializar&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este año me propusé socializar más. No lo conseguí, pero tampoco es que fuera un ermitaño, así que bueno he
sobrevivido y hablado con gente de &quot;distinto pelaje&quot; a la par que he charlado con algunas de las figuras que llevan
Groovy, por ejemplo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-8.jpg&quot; alt=&quot;apachecon 8&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;o incluso con algunos &quot;legendarios&quot; del OpenSource&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-9.jpg&quot; alt=&quot;apachecon 9&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y muchos, muchos saraos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-10.jpg&quot; alt=&quot;apachecon 10&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;mis_charlas&quot;&gt;Mis charlas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este año, a diferencia del anterior, mis charlas fueron nada más empezar. El año anterior fue hacia el final
con lo que tuve tiempo de ir ganando confianza. Este año como eso ya estaba &quot;superado&quot; (mentira) me ha venido
bien que fuera nada más empezar y así ya poder dedicarme a la labor de assistant&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Voy a resumir mis dos charlas en dos palabras: fracaso absoluto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No acudió nadie. Pero nadie, nadie. Solo Paul, que es el que lleva el track de Groovy. El resto de personas
que habían asistido a la charla anterior, se levantaron y me quedé sólo. Como puedes imaginar todo un palo que me
dejó el resto del día echo polvo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;como hemos venido a jugar, ya el domingo más relajado lo puse en contexto y bueno, estas cosas pasan, a veces
tu propuesta no interesa al público y ya está. La diferencia con los Meetups es que vas viendo antes cómo va
la gente que se apunta, mientras que aquí vas a &quot;pecho palomo&quot;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tengo que decir que yo creo que no salieron mal. Todo funcionó y no hubo efecto demo, me trabé en alguna parte porque
por mucho que ensayes luego sale otra cosa, pero nada grave y en resumen, otro listón superado que de todo se aprende&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;lighthing_talk&quot;&gt;Lighthing talk&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como colofón a los 4 días y en un arrojo de valentía envié una propuesta de mini charla de 5 minutos y &amp;#8230;&amp;#8203; tuve
que subir al escenario casi al final, así que en mi corazoncito es como si yo hubiera cerrado las jornadas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-13.jpg&quot; alt=&quot;apachecon 13&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A ver como lo explico: es como si los astros se hubieran alineado.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El primer día de las conferencias, estando
en la mesa para registrarse la gente y darles las camisetas, oigo a un asistente hablar con una assistant sobre
que él había trabajado en el proyecto Apache POI. Por si no lo sabes, son unas librerías de Java que permiten
leer y escribir ficheros Microsoft Office como Word o Excel.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Justo hacía poco, un proyecto que había empezado hace 16 años para un cliente y que se basaba en estas librerías,
ya no tiene mantenimiento desde hace tiempo y llega a su fin.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente, me armé de valor y me presenté. Él era David Fisher y le enseñé la aplicación. Estuvimos hablando un
rato y resulta que uno de los que organizan el tema de assistants &amp;#8230;&amp;#8203; ¡Había sido también parte del equipo!!
Así que me ví en la obligación de intentar hacer un postmortem al proyecto delante de toda esta gente y darles
las gracias&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Bueno, en resumen, el último minuto lo empleé en reponerme porque iba viendo cómo la emoción me iba nublando los
ojos y la voz y veía que no iba a llegar a terminar la exposición sin echarme a llorar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que sí, que te parecerá una gilipollez, pero qué le vamos a hacer, a mi me emocionó muchísimo estar ahí delante&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-14.jpg&quot; alt=&quot;apachecon 14&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;halifax&quot;&gt;Halifax&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al principio me pareció muy pequeño (me dí una caminata el día antes de empezar las conferencias de unas 4 horas)
pero coqueto. El último día me dí otra caminata de otras 2-3 horas y bueno, es más grande de lo que creía, lo
único que es más desparramado de lo que yo creía. Obviamente no es un Madrid pero me resultó muy agradable
de visitar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-11.jpg&quot; alt=&quot;apachecon 11&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-12.jpg&quot; alt=&quot;apachecon 12&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No sé si repetiré otro año, falta mucho, pero que ha sido una experiencia inolvidable ya te digo que sí.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como &quot;primicia&quot; va a haber edición Europa en Bratislava y otra USA en Denver. Ya
veremos, queda mucho (aunque el Call For Papers ya está abierto)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como novedad, abrí un canal de Telegram donde fuí &quot;retransmitiendo&quot; lo que me iba pasando, por si quieres
verlo &lt;a href=&quot;https://t.me/apachecon2023&quot; class=&quot;bare&quot;&gt;https://t.me/apachecon2023&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/apachecon-15.jpg&quot; alt=&quot;apachecon 15&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>cuatro días de Apache conferencias en Halifax</summary>
    </entry>
    <entry>
        <title>Swagger-Operator, let groovy operate your cluster</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/swagger-operator.html"/>
        <updated>2023-10-10T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/swagger-operator.html</id>
        <category term="groovy"/>
        <category term="kubernetes"/>
        <category term="micronaut"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;This post is still under review&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post I&amp;#8217;ll (try to) explain how I&amp;#8217;ve created a kubernetes operator using Groovy
and Micronaut, because &amp;#8230;&amp;#8203; yes, you don&amp;#8217;t need to use Go for it!!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;situation&quot;&gt;Situation&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Say we are working in a microservice architecture with several services (nodejs, spring,
micronaut, quarkus, &amp;#8230;&amp;#8203;), and some (or all) of them are using OpenAPI to expose their API, and
they are deployed in a kubernetes cluster.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Basically an OpenApi spec is a resource (typically a JSON or YAML) the service serve via an http GET
where all endpoints, payloads, documentation, etc are structured following a well-know structure&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;From time to timme a new service is deployed, or deprecated and removed from the cluster.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Typical situation is every service include an html interface to render this spec in a human friendly
way and is very common to use swagger-ui for it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Swagger-ui is basically a Javascript application able to
understand an OpenApi spec and generate a playground on the fly&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Another posible solution is to use a single swagger-ui instance and configure a list of OpenApi spec
(for example configuring the SERVERS_URL environment) so using a single javascript application we can
play with different services&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usually QA and/or Frontend use this interface to check their implementation, and also to execute
some requests to the backend microservice (yes, yes, I know, is not the best practique, but &amp;#8230;&amp;#8203; ) so
it&amp;#8217;s important to have this swagger-ui updated with the right list of specs.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;manual_solution&quot;&gt;Manual solution&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Our current solution consists in 2 artifacts:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a pod running a standard swagger-ui docker image&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a configmap with a JavaScript file similar to the oficial but with the lists of servers&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;configmap&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;window.ui = SwaggerUIBundle({
    urls: [
        {
            name:&quot;user-rest&quot;,
            url:&quot;/user-rest/swagger/service-example-0.0.yml&quot;
        }
    ...
})&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When we deploy the swagger-ui overwriting the original javascript with this configmap we have a
Swagger playground where our QA team can use to send requests to every service.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When a new microservice is required by the architecture and is deployed in the cluster is so simple
as edit the configmap, add the new entry in the list, and delete the current pod. As soon the cluster detect
this pod was deleted a new one will be created using new configmap, so QA only need to refresh the browser
to see the new service in the list. (Same if what we want is to remove a microservice from the list)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can imagine, although is a simple process is very error-prone, and most of the time we react when
the QA report can&amp;#8217;t find the new service in the swagger-ui application.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;whats_and_operator&quot;&gt;What&amp;#8217;s and operator?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically an operator is a &quot;typical&quot; application deployed into the cluster who will be &quot;talking&quot; with the
cluster, no with the user.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The cluster will be asking to our operator to check if all looks good every few seconds and our operator need
to &quot;reconcile&quot; current status with the desired state.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Imagine we want to have running 2 instances of a deployment and suddenly one of them reach and exception and
finish.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A &quot;few seconds&quot; later the cluster will ask the operator to check if all looks good in the deployment so the operator
will retrieve the list of running instances, will compare with the desired state, and it will decide to create a
new pod.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The logic of our SwaggerOperator to decide if all looks good is similar.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;swagger_operator&quot;&gt;Swagger-Operator&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So basically what we&amp;#8217;ll create is a kubernetes operator to perform these actions:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;create a deployment (running the swagger-ui docker image) and a service in case they not exist&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;maintain a configmap updated with the list of current services present into the cluster.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;delete the current pod in case an update in the configmap is done&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As an operator is in fact a typical application we&amp;#8217;ll create our swagger-operator using the micronaut
command line to create it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;mn create-app --lang groovy --features kubernetes-informer swagger-operator&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;crd_custom_resource_definition&quot;&gt;CRD, Custom Resource Definition&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;First thing is to define a new Kind resource (and deploy it into the cluster). The CRD is where we&amp;#8217;ll instruct
to the cluster how the new resource will look, so yes, it&amp;#8217;s a kind of meta-resource.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In a CRD we need to specify the name of the kind, the group we want to include it and the properties the
user need to fill to deploy a new swagger-operator resource&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Swagger-operator CRD looks like:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: swaggers.puravida.com
spec:
  group: puravida.com
  scope: Namespaced
  names:
    plural: swaggers
    singular: swagger
    kind: Swagger
  versions:
    - name: v1
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                serviceSelector:
                  type: string
                configMap:
                  type: string
                deployment:
                  type: string
                service:
                  type: string
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Although a long file basically what we&amp;#8217;re doing is instructing to the cluster about a new kind of resource (Swagger).
When the user will want to create a new resource of this kind he will need to provide 4 properties called
&lt;code&gt;serviceSelector&lt;/code&gt;, &lt;code&gt;configMap&lt;/code&gt; &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;In our case these 4 properties will be used by the operator to create configurable &quot;named&quot; resources
instead to use hardcoded values&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To let the cluster manage this new resource we need to apply it into the cluster:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ kubectl apply -f crd.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;crd_to_java&quot;&gt;CRD to Java&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As we want to use Groovy/Java in our operator, we need to convert this CRD to Java objects. The easy way is to
use a command line from kubernetes-client project similar to:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v &quot;$(pwd)&quot;:&quot;$(pwd)&quot; \
      -ti --network host ghcr.io/kubernetes-client/java/crd-model-gen:v1.0.6  \
      /generate.sh -u $(pwd)/src/k8s/crd.yml -n com.puravida -p com.puravida \
      -o $(pwd)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mounting our local project as a volume the &lt;code&gt;generate.sh&lt;/code&gt; process can read the crd and generate some Java files. They
are basically a kind of POJO Java representations of the CRD.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;operator&quot;&gt;Operator&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Swagger-operator is a simple operator and requires only one class, &lt;code&gt;SwaggerOperator.groovy&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Operator(
    informer = @Informer(
        apiType = V1Swagger,
        apiListType = V1SwaggerList,
        apiGroup = V1SwaggerWrapper.GROUP,
        resourcePlural = V1SwaggerWrapper.PLURAL,
        resyncCheckPeriod = 10000L
    )
)
class SwaggerOperator implements ResourceReconciler&amp;lt;V1Swagger&amp;gt;{
    // The implementation
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see basically we need to annotate our class with &lt;code&gt;@Operator&lt;/code&gt; and implement &lt;code&gt;ResourceReconciler&amp;lt;V1Swagger&amp;gt;&lt;/code&gt;
interface&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This interface requires we implement only one method:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Override
Result reconcile(@NonNull Request request, @NonNull OperatorResourceLister&amp;lt;V1Swagger&amp;gt; lister) {
    //
    return new Result(false) &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Returning false we inform to the cluster we don&amp;#8217;t need a new reconcile &quot;right now&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;reconcile&lt;/code&gt; is the method will be called every X millis (10s in our case) by the cluster once a V1Swagger resource
is deployed by the user. Our operator must check if all resources are aligned with the desired state.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To do it the operator needs/can &quot;talk&quot; with different APIs exposed by the cluster as CoreApi or AppApi, so it can
list all services present in a namespace, create a configmap, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically the main logic of the swagger-operator reconcile method is:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;if a Swagger resource is present the operator need to check if the configmap, the deployment and the service exist&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;also, if the Swagger resource exist it must to check if the configmap is up to date checking the list of services
and if they is any different it needs to update the configmap and delete the current pod&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;if the resource is marked to be deleted the operator needs to &quot;clean&quot; the resources created deleting the configmap,
service and deployment&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;checking_the_list_of_services&quot;&gt;Checking the list of services&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The operator requests to the cluster a list of current services in the namespace and select all that contains the desired annotation:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def services = coreApi.listNamespacedService(wrapper.namespace)
    .items
    .findAll({ service-&amp;gt;
        service.metadata.annotations?.containsKey(wrapper.serviceSelector)
    })
def map = services.inject([:],{ map, it -&amp;gt;
    map[it.metadata.name] = it.metadata.annotations[wrapper.serviceSelector]
    map
}) as Map&amp;lt;String, String&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;services&lt;/code&gt; is the current list of services we want to show in the list of swagger-ui and &lt;code&gt;map&lt;/code&gt; is a Map to be &quot;injected&quot;
in the ConfigMap.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;checking_if_configmap_is_up_to_date&quot;&gt;Checking if ConfigMap is up to date&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def configMap = coreApi
        .readNamespacedConfigMap(wrapper.configMap,
                wrapper.namespace,null,null,null)

def currentJS = configMap.data[CONFIG_YML]

if( currentJS.contains(&quot;urls: [$urlServices]&quot;) ){
        return false
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The swagger-operator read current ConfigMap data entry CONFIG_YML and check if the current value contains a string
similar to the current list of services. If it is the same no action is required&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If they are not equals this means some service is not in the list or a new service was deployed so the swagger-operator
create a new updated ConfigMap&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;currentJS = currentJS.replaceFirst(/urls: \[(.*?)]/,&quot;urls: [$urlServices]&quot;)
configMap.data[CONFIG_YML] = currentJS

coreApi.replaceNamespacedConfigMap(wrapper.configMap,
        wrapper.namespace,
        configMap)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and mark current deployment to be restarted&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def deployment = deploymentList.items.first()
deployment.spec
        .template
        .metadata
        .annotations[V1SwaggerWrapper.RESTARTED_AT_ANNOTATION]=Instant.now().toString()

appsApi.replaceNamespacedDeployment(deployment.metadata.name,
        deployment.metadata.namespace,
        deployment)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;You can check out the repository (link at the end of the post) to see full code&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;roles_and_service_account&quot;&gt;Roles and Service account&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probably your operator will require to be run with an special service account how allow them to access to cluster
resources. For swagger-operator this is specify in this resource&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: swagger-operator-role
rules:
  - apiGroups: [&quot;&quot;, &quot;apps&quot;]
    resources: [&quot;services&quot;,  &quot;configmaps&quot;, &quot;deployments&quot;, &quot;pods&quot;]
    verbs: [&quot;get&quot;, &quot;watch&quot;, &quot;list&quot;, &quot;create&quot;, &quot;delete&quot;, &quot;update&quot;]
  - apiGroups: [&quot;coordination.k8s.io&quot;]
    resources: [&quot;leases&quot;]
    verbs: [&quot;get&quot;, &quot;create&quot;, &quot;update&quot;]
  - apiGroups: [&quot;puravida.com&quot;]
    resources: [&quot;swaggers&quot;]
    verbs: [&quot;*&quot;]

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: swagger-operator-sa

---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: swagger-operator-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: swagger-operator-role
subjects:
  - kind: ServiceAccount
    name: swagger-operator-sa
    namespace: default&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see we are creating a new service account &lt;code&gt;swagger-operator-sa&lt;/code&gt; and allowing to it different access to
different resources (full control for swaggers.puravida.com resources for example)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;deploy&quot;&gt;Deploy&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As a final step we need to build and deploy our application to a docker registry (in swagger-opeator case using
the gradle task &lt;code&gt;jib&lt;/code&gt; ) and deploy it into the cluster using a typical deployment&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: swagger-deployment-operator
  name: swagger-deployment-operator
spec:
  replicas: 1
  selector:
    matchLabels:
      app: swagger-deployment-operator
  template:
    metadata:
      labels:
        app: swagger-deployment-operator
    spec:
      serviceAccountName: swagger-operator-sa
      containers:
        - image: registry.localhost:5000/swagger-operator
          name: swagger-deployment-operator
          imagePullPolicy: Always&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see we specify the serviceAccountName &lt;code&gt;swagger-operator-sa&lt;/code&gt; created previously.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;For this example I&amp;#8217;m using a local docker registry. In a near future I hope to have time and deploy in
docker hub&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;last_step&quot;&gt;Last step&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So now our cluster is ready and waiting an user (admin, QA, &amp;#8230;&amp;#8203;) create a new Swagger resource specifying wich
services to include and wich deployment create&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: puravida.com/v1
kind: Swagger
metadata:
  name: swagger-operator
spec:
  serviceSelector: swagger-path
  configMap: swagger-config
  deployment: swagger
  service: swagger&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As soon the user apply this file into the cluster, swagger-operator will start receiving events from the cluster
to reconcile it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The operator will list all current services and select some of them, check the ConfigMap is not present and it
will create a new one using a classpath resource as template, check the Deployment is not present and create one, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After a few seconds we&amp;#8217;ll have a new pod running into our cluster with the swagger-ui interface and a list of
services configured&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If we remove some service (&lt;code&gt;kubectl delete -f user-rest&lt;/code&gt; for example) the operator will recreate all resources and
the swagger-ui will be updated automatically&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this article we are creating a Micronaut Kubernetes Operator using Groovy able to create and maintain resources
into the cluster&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ve created a repo with the code and a service to be used as example at &lt;a href=&quot;https://github.com/jagedn/swagger-operator&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/swagger-operator&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>How to create a k8s operator in Groovy (using Micronaut)</summary>
    </entry>
    <entry>
        <title>Jugando con Monos (de Reactor)</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/reactor-usecase.html"/>
        <updated>2023-09-28T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/reactor-usecase.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <category term="rxjava2"/>
        <category term="reactor"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada día la programación funcional está más en boga y hay que ponerse las pilas, así que me he
inventado este caso de uso para jugar un poco con ella y sobre todo por practicar con &lt;code&gt;Mono&lt;/code&gt; y &lt;code&gt;Flux&lt;/code&gt;
de Reactor (esta vez en Java)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;As usual, este post se basa en lo que yo he buscado y probado, pudiendo estar mal e
incluso rematadamente mal, pero &amp;#8230;&amp;#8203; en mi local funciona.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;caso_de_uso&quot;&gt;Caso de Uso&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que tenemos dos sistemas independientes (dos microservicios en tu arquitectura o dos servicios
de dos proveedores diferentes, o uno tuyo y otro externo, por ejemplo):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Stock Service. Gestiona la cantidad de artículos que tenemos en el almacén&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ecommerce Service. Gestiona algunos de artículos junto con su cantidad disponible más el precio de venta
(por ejemplo querríamos tener un catálogo de artículos y para cada uno querer tener en almacén 100 unidades pero en eCommerce solamente 10)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ambos sistemas son independientes, con sus bases de datos correspondientes, etc y son accesibles a través
de unos endpoints diferentes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es ejecutar un proceso que busque el stock actual de todos los artículos, llamando a Stock Service,
aplicar una regla de negocio para cada uno, e invocar al endpoint de eCommerce para actualizar la cantidad de
cada uno.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además, como eCommerce dispone de un subconjunto de artículos, deberemos consultar en eCommerce la lista de
artículos para no recorrer todo el catálogo que tenemos en Stock&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-7e53f76e7753639da499077cbfbcb429.png&quot; alt=&quot;Diagram&quot; width=&quot;455&quot; height=&quot;290&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No parece muy difícil &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;stock_service&quot;&gt;Stock Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    public record Stock(String sku, int cantidad) {
    }

    public record StockPage(int total, Collection&amp;lt;Stock&amp;gt; stocks) {
    }

    @Get(&quot;/&quot;)
    Mono&amp;lt;StockPage&amp;gt; get(@QueryValue int page)

    @Post(&quot;/update&quot;)
    void update(@Body List&amp;lt;Stock&amp;gt; stocks)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplificando mucho vamos a trabajar con un &lt;code&gt;Stock&lt;/code&gt; identificado por su sku y del que mantenemos
la cantidad que tenemos en stock&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;StockPage nos sirve para hacer una paginación básica devolviendo el total de artículos en la base de datos,
y unos cuantos stocks. El servicio devolverá páginas según se le pida por parámetro.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;También añadimos un endpoint para poder actualizar el stock (que nos servirá para jugar a cambiar cantidades
del stock y volver a ejecutar nuestro proceso)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;obviamente es un ejemplo muy simplificado&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ecommerce_service&quot;&gt;eCommerce Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    public record Articulo(String sku, int cantidad, double precio) {
    }

    public record ArticuloPage(int total, Collection&amp;lt;Articulo&amp;gt; articulos) {
    }

    @Get(&quot;/articulos&quot;)
    public Mono&amp;lt;ArticuloPage&amp;gt; get(@QueryValue int page)

    @Post(&quot;/update&quot;)
    public Mono&amp;lt;List&amp;lt;UpdateStock&amp;gt;&amp;gt; update(@Body List&amp;lt;UpdateStock&amp;gt; update)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Parecido al Stock Service, el eCommerce service nos permite paginar los artículos y (lo más
importante en este caso) actualizar la cantidad de stock de articulos en ecommmerce&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;aproximación_tradicional&quot;&gt;Aproximación tradicional&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Siguiendo el diagrama anterior una posible implementación sería:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;int offset = 0;
while( offset &amp;lt; maxArticles ){

    // obtener n articulos en ecomerce
    var articuloPage = ecommerceClient.get(offset).block();

    // extraer los ids
    var articulos = new ArrayList&amp;lt;String&amp;gt;();
    for(var articulo : articuloPage.articulos()){
        articulos.add(articulo.sku());
    }

    // ver el stock de estos articulos
    StockPage stockPage = stockClient.get( String.join(&quot;,&quot;,articulos) ).block();

    // logica de negocio para estos articulos
    List&amp;lt;UpdateStock&amp;gt; updateStocks = new ArrayList&amp;lt;&amp;gt;();
    for (Stock s : stockPage.stocks()) {
        updateStocks.add(new UpdateStock(s.sku(), s.cantidad()));
    }

    // actualizar ecommerce
    ecommerceClient.update(updateStocks).block();
    offset+=batchSize;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Bueno, un poco engorroso pero (semi)fácil de seguir. Pedidos unos pocos articulos a ecommerce,
pedimos la cantidad de estos al servicio de stock, y actualizamos ecommerce con la cantidad que tenemos
en stock&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El &quot;detalle&quot; de esta implementación es que estamos haciendo llamadas a servicios externos y estamos
esperando a que se completen (por eso el uso de &lt;code&gt;block()&lt;/code&gt; al final de las llamadas a endpoints)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si este bucle se está ejecutando en el hilo de un Controller, por ejemplo, este hilo se queda bloqueado
mientras se resuelven las llamadas pudiendo quedarse congelado si no disponemos de los hilos necesarios.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;implementación_reactiva_no_bloqueante&quot;&gt;Implementación Reactiva (no bloqueante)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La implementación reactiva (y funcional) va a hacer uso de los objetos &lt;code&gt;Mono&lt;/code&gt; y &lt;code&gt;Flux&lt;/code&gt; (si usas
reactor) y otros,
los cuales en realidad NO ejecutan en ese momento la lógica que le indicamos, sino que las van &quot;encadenando&quot;
y ejecutando en hilos diferentes coordinados por la implementación reactiva que usemos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Mono&amp;lt;ArrayList&amp;lt;UpdateStock&amp;gt;&amp;gt; reactive() {
    return Flux
        .generate(() -&amp;gt; 0, (offset, emitter) -&amp;gt; {
            if (offset &amp;lt; maxArticles) {
                emitter.next(offset);
            } else {
                emitter.complete();
            }
            return offset + batchSize;
        })
        .concatMap(page -&amp;gt; ecommerceClient.get((Integer) page))
        .map(ArticuloPage::articulos)
        .filter(items -&amp;gt; !items.isEmpty())
        .flatMap(items -&amp;gt; stockClient.get(items.stream().map(Articulo::sku).collect(Collectors.joining()))
                .map(stocks -&amp;gt; stocks.stocks()
                        .stream()
                        .map(s -&amp;gt; new UpdateStock(s.sku(), s.cantidad()))
                        .toList()))
        .concatMap(ecommerceClient::update)
        .reduce(new ArrayList&amp;lt;&amp;gt;(), (prev, items) -&amp;gt; {
            prev.addAll(items);
            return prev;
        });
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;No he conseguido resolver de forma satisfactoria el hacer una primera llamada que me de el numero de
páginas que hay en eCommerce por lo que voy a hacer un número de llamadas &quot;fijas&quot; y luego filtrar las respuestas
que no contengan datos.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta implementación:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;genera un Flux de números entre 0 y N&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cada &lt;code&gt;page&lt;/code&gt; es mapeado a una llamada asíncrona al eCommerce&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;De lo que devuelva (en un futuro) el eCommerce, nos quedamos con la lista de artículos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;filtramos aquellas respuestas que vienen vacía&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cada lista de artículos en eCommerce es convertida a una llamada al servicio de stock&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cada lista de stocks que nos devolverá (en un futuro) el stock la usaremos para actualizar eCommerce&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Por último, juntaremos todas las respuestas (futuras) de eCommerce en un List para devolver la lista de articulos
actualizados&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;repositorio&quot;&gt;Repositorio&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;He creado un repositorio con una aplicación Micronaut por si quieres jugar con estos servicios y las implementaciones.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jagedn/reactor-demo&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/reactor-demo&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para simplificar la implementación los dos servicios (stock y ecommerce) se encuentran en la misma aplicación,
pero de forma independiente. Cada uno se accede por unos endpoints diferentes y mantienen una &quot;base de datos&quot;
independiente (la base de datos es un simple List en memoria)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez clonado el repositorio podremos ejecutar la aplicacion:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew run&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y acceder a los servicios&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http localhost:8080/ecommerce/articulos&lt;/code&gt;, para ver la situacion actual de ecommerce&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http localhost:8080/stock/&lt;/code&gt;, para ver la situacion actual del stock&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http -v localhost:8080/stock/update [0][sku]=1 [0][cantidad]=1&lt;/code&gt; para actualizar la cantidad del
artículo &quot;1&quot; en la base de datos del stock&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este punto, eCommerce tiene una cantidad diferente que Stock así que ejecutaremos la lógica de negocio
que las &quot;sincroniza&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http localhost:8080/ejemplo/block&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;o&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http localhost:8080/ejemplo/reactive&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien, la cantidad disponible en stock habrá sido actualizada en eCommerce&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Encadenando llamadas a servicios con reactor (Mono, Flux,...)</summary>
    </entry>
    <entry>
        <title>JBake + Gradle + Linkedin</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/jbake-linkedin.html"/>
        <updated>2023-09-27T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/jbake-linkedin.html</id>
        <category term="jbake"/>
        <category term="linkedin"/>
        <category term="gradle"/>
        <content type="html">
            &lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Para poder replicar esta funcionalidad necesitarás una cuenta en Linkedin
y obtener un token. Puedes consultar el post que subí al respecto &lt;a href=&quot;2022/gmail-linkedin.html&quot; class=&quot;bare&quot;&gt;2022/gmail-linkedin.html&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Continuando con el post anterior de cómo publicar un toot en tu instancia del Fediverso
cuando tienes un artículo nuevo en tu blog, esta vez vamos a hacer lo mismo pero
notificandolo en Linkedin (ver &lt;a href=&quot;jbake-toot.html&quot; class=&quot;bare&quot;&gt;jbake-toot.html&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mi blog es un static site hecho con JBake usando
Gradle y Asciidoctor así que básicamente es crear una tarea nueva en el &lt;code&gt;buildSrc&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;buildSrc/src/main/groovy/LinkedinTask.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;import groovy.json.JsonOutput
import groovy.xml.XmlSlurper
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

import java.net.http.*

abstract class LinkedinTask extends DefaultTask{

    @Input
    abstract Property&amp;lt;String&amp;gt; getPublisher()

    @Input
    abstract Property&amp;lt;String&amp;gt; getToken()

    @Input
    abstract Property&amp;lt;String&amp;gt; getPostId()

    @TaskAction
    def runTask() {
        def xml = new XmlSlurper().parse(&quot;https://blog.jagedn.dev/feed.xml&quot;)
        def entry = xml.entry.find{ &quot;$it.id&quot;.endsWith( postId.get()+&quot;.html&quot;)}
        if( !entry ) {
            println(&quot;Post no encontrado&quot;)
            return
        }
        def hashtags = entry.category.collect{ &quot;#&quot;+it.&quot;@term&quot;}.join(&apos; &apos;)
        def content = &quot;&quot;&quot;
🗣️ Nuevo post en mi blog

$entry.title

📖 ${entry.summary}

$hashtags

${entry.id}
&quot;&quot;&quot;
        def json = JsonOutput.toJson([
                &quot;author&quot;: &quot;urn:li:person:&quot;+publisher.get(),
                &quot;commentary&quot;: content,
                &quot;visibility&quot;: &quot;PUBLIC&quot;,
                &quot;distribution&quot;: [
                        &quot;feedDistribution&quot;: &quot;MAIN_FEED&quot;,
                        &quot;targetEntities&quot;: [],
                        &quot;thirdPartyDistributionChannels&quot;: [],
                ],
                &quot;lifecycleState&quot;: &quot;PUBLISHED&quot;,
                &quot;isReshareDisabledByAuthor&quot;: false
        ])

        def client = HttpClient.newHttpClient()
        def request = HttpRequest.newBuilder()
                .header(&quot;Content-Type&quot;, &quot;application/json; charset=UTF-8&quot;)
                .header(&quot;Authorization&quot;, &quot;Bearer ${token.get()}&quot;)
                .header(&apos;X-Restli-Protocol-Version&apos;,&apos;2.0.0&apos;)
                .header(&apos;LinkedIn-Version&apos;,&apos;202308&apos;)
                .uri(URI.create(&apos;https://api.linkedin.com/rest/posts&apos;))
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build()
        def response = client.send(request, HttpResponse.BodyHandlers.ofString())
        println response
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y crear una task en el &lt;code&gt;build.gradle&lt;/code&gt; para llamarla en nuestro pipeline&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;tasks.register(&apos;linkedin&apos;, LinkedinTask){
    publisher = findProperty(&quot;LINKEDIN_PUBLISHER&quot;)
    token = findProperty(&quot;LINKEDIN_TOKEN&quot;)
    postId = findProperty(&quot;postId&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los parámetros como el publisher or el token puedes guardarlos en el fichero
$HOME/.gradle/gradle.properties y evitar así tener que recordarlos cada vez que
quieras ejecutar la tarea.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El parámetro postId es simplemente el nombre corto (sin extension) del post.
Por ejemplo en mi caso ejecutaría&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew linkedin -P postId=jbake-linkedin&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y se crearía una publicación en mi feed de Linkedin&lt;/p&gt;
&lt;/div&gt;
        </content><summary>Tarea de Gradle para avisar en Linkedin de un nuevo post en el blog</summary>
    </entry>
    <entry>
        <title>MapBlog with JBake</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/jbake-map.html"/>
        <updated>2023-09-27T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/jbake-map.html</id>
        <category term="jbake"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JBake is a static site generator (similar to Hugo, Gatsby, etc). Although is written in Java you don&amp;#8217;t need
special knowledge in Java only a little knowledge of Markdown or better Asciidoctor&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;this site is generated by JBake&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;With default installation you can write typical &quot;posts&quot; or &quot;pages&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pages are &quot;fixed&quot; documents as &quot;about&quot;, &quot;contact&quot;, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Posts are &quot;dynamic&quot; in the sense they are showed in a paginated view (typically recent post first,) and they
have attributes as published date, state, summary, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When JBake is generating your site it applies a different template for every content depending on the &quot;type&quot;.
For example &quot;index.tpl&quot; for the &quot;masterindex&quot; page, or &quot;post.tpl&quot; for every &quot;post&quot; document&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;JBake can use different template engines, and you need to select wich engine to use. I&amp;#8217;ve selected
Groovy’s MarkupTemplateEngine, but you can use Freemarker, Thymeleaf, &amp;#8230;&amp;#8203; This post only works with Groovy
MarkupTemplateEngine but surely is easy to convert to other&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I would like to create a new section in my blog talking about my trips around the world. It&amp;#8217;s easy to create
a post for every place, but I would like to have them in another &quot;page&quot; and no mix them with my current posts
about tech.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Instead to have an ordered list of posts I would like to have a map and show every place on it so the user
can see all of them and clicking in an icon show a popup with the summary and a link to the post&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I would like to tag every post with different coordinates so JBake generate the map automatically&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I would like to have the possibility to have different maps, for example one for trips, another one for talks,
etc and every one will show only the associate &quot;mappost&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;model&quot;&gt;Model&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Map (type=map) with attributes &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt; and &lt;code&gt;title&lt;/code&gt;. Only &lt;code&gt;published&lt;/code&gt; will be rendered.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MapPost (type=mappost). A typical post with new &lt;code&gt;coordinates&lt;/code&gt; and &lt;code&gt;map&lt;/code&gt; attributes&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;template&quot;&gt;Template&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ve created a new file, &lt;code&gt;map.tpl&lt;/code&gt; in the &quot;templates&quot; directory. Basically it renders a pure HTML with
all javascript/css/link required&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ll use a WebComponent &lt;code&gt;&amp;lt;leaflet-map id=&quot;blog-map&quot; fitToBounds&amp;gt;&amp;lt;/leaflet-map&amp;gt;&lt;/code&gt; from
&lt;a href=&quot;https://migupl.github.io/vanilla-js-web-component-leaflet-geojson&quot; class=&quot;bare&quot;&gt;https://migupl.github.io/vanilla-js-web-component-leaflet-geojson&lt;/a&gt; because it makes very easy to include and manage
a map&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JBake will create as many &lt;code&gt;xxx.html&lt;/code&gt; as content with type equal to &lt;code&gt;map&lt;/code&gt; are published&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The template will &quot;inject&quot; a Javascript array collecting all mappost associate to this map:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;published_mapposts.findAll{ it.map == content.map }.each{ post-&amp;gt;
    yieldUnescaped &quot;&quot;&quot;

    data.push({
        &quot;type&quot;: &quot;Feature&quot;,
        &quot;geometry&quot;: {
            &quot;type&quot;: &quot;${post.coordstype ?: &apos;Point&apos;}&quot;,
            &quot;coordinates&quot;: [${post.coordinates}]
        },
        &quot;properties&quot;: {
            &quot;popupContent&quot;: &apos;&amp;lt;a href=&quot;${post.uri}&quot;&amp;gt;${post.title}&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;${post.summary}&apos;,

        ... more attributes
    })
    &quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;mappost&quot;&gt;MapPost&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As I don&amp;#8217;t want any special (by the moment) in every mappost I&amp;#8217;ve duplicated the &lt;code&gt;post.tpl&lt;/code&gt; template&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;configuration&quot;&gt;Configuration&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To activate the new kind of content you need to configure jbake.properties:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;jbake.properties&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;template.map.file=map.tpl
template.mappost.file=mappost.tpl&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;mis_andanzas&quot;&gt;Mis-Andanzas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To create the trip map I&amp;#8217;ve created a new &quot;empty&quot; asciidoc file &lt;code&gt;mis-andanzas.adoc&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;= Mis Andanzas
Jorge Aguilera
2023-03-28
:jbake-type: map
:jbake-status: published
:jbake-map: mis-andanzas&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can imagine &lt;code&gt;jbake-type: map&lt;/code&gt; advise to JBake to use the &lt;code&gt;map.tpl&lt;/code&gt; so we have at the end
an &lt;code&gt;mis-andanzas.html&lt;/code&gt; file with an embedded map&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Next step is to write &quot;mappost&quot; as a typical post but including some attributes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;= Costa Rica 1999
Jorge Aguilera
2022-09-23
:jbake-type: mappost
:jbake-status: published
:jbake-tags: personales, viajes
:jbake-summary: Primer viaje transoceánico
:jbake-coordinates: -83.712266,8.6543759
:jbake-map: mis-andanzas
:idprefix:
:icons: font

A poco que hayas hablado conmigo seguro que me has tenido que aguantar la chapa sobre
blabla blabla&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see the type is &lt;code&gt;mappost&lt;/code&gt; and I&amp;#8217;ve add &lt;code&gt;coordinates&lt;/code&gt; attribute to locate this entry into the map&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;result&quot;&gt;Result&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is an example how it appears in my blog&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/jbakemap.png&quot; alt=&quot;jbakemap&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can see it in action at &lt;a href=&quot;https://blog.jagedn.dev/mis-andanzas.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/mis-andanzas.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Creating a MapBlog in JBake</summary>
    </entry>
    <entry>
        <title>JBake + Gradle + Mastodon</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/jbake-toot.html"/>
        <updated>2023-09-25T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/jbake-toot.html</id>
        <category term="jbake"/>
        <category term="fediverse"/>
        <category term="gradle"/>
        <content type="html">
            &lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Para poder replicar esta funcionalidad necesitaras una cuenta en una
instancia Mastodon o Pleroma y crear un Token con ella&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Se me ha ocurrido (idea nada brillante) hacerme una tarea Gradle en el proyecto
del blog para poder tootear el enlace a un post&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No es nada del otro mundo. Mi blog es un static site hecho con JBake usando
Gradle y Asciidoctor así que básicamente es crear una tarea nueva en el &lt;code&gt;buildSrc&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;buildSrc/src/main/groovy/MastodonTask.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import groovy.xml.XmlSlurper
import groovy.json.JsonOutput
import java.net.http.*

abstract class MastodonTask extends DefaultTask{

    @Input
    abstract Property&amp;lt;String&amp;gt; getInstance()

    @Input
    abstract Property&amp;lt;String&amp;gt; getToken()

    @Input
    abstract Property&amp;lt;String&amp;gt; getPostId()

    @TaskAction
    def runTask() {
        def xml = new XmlSlurper().parse(&quot;https://blog.jagedn.dev/feed.xml&quot;)
        def entry = xml.entry.find{ &quot;$it.id&quot;.endsWith( postId.get()+&quot;.html&quot;)}
        if( !entry ) {
            println(&quot;Post no encontrado&quot;)
            return
        }
        def hashtags = entry.category.collect{ &quot;#&quot;+it.&quot;@term&quot;}.join(&apos; &apos;)
        def toot = &quot;&quot;&quot;
🗣️ Nuevo post en el blog

$entry.title

📖 ${entry.summary}

$hashtags

${entry.id}
&quot;&quot;&quot;
        def json = JsonOutput.toJson([ status: toot])

        def client = HttpClient.newHttpClient()
        def request = HttpRequest.newBuilder()
                .header(&quot;Content-Type&quot;, &quot;application/json; charset=UTF-8&quot;)
                .header(&quot;Authorization&quot;, &quot;Bearer ${token.get()}&quot;)
                .uri(URI.create(instance.get()+&apos;/api/v1/statuses&apos;))
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build()
        client.send(request, HttpResponse.BodyHandlers.ofString())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y crear una task en el &lt;code&gt;build.gradle&lt;/code&gt; para llamarla en nuestro pipeline&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;tasks.register(&apos;tootea&apos;, MastodonTask){
    instance = &quot;https://jvm.social&quot;
    token = findProperty(&quot;BLOG_ACCESS_TOKEN&quot;)
    postId = findProperty(&quot;postId&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los parámetros como el publisher or el token puedes guardarlos en el fichero
$HOME/.gradle/gradle.properties y evitar así tener que recordarlos cada vez que
quieras ejecutar la tarea.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El parámetro postId es simplemente el nombre corto (sin extension) del post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente, una vez que hemos generado y publicado el blog con el ultimo post,
ejecutaremos la tarea de gradle para que tootee por nosotros&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew tootea -P postId=mi-ultimo-post&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
        </content><summary>Tarea de Gradle para anunciar en el Fediverso un nuevo post en el blog</summary>
    </entry>
    <entry>
        <title>Nextflow: Organizando fotos por geoposicion</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/use-case-images.html"/>
        <updated>2023-09-05T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/use-case-images.html</id>
        <category term="groovy"/>
        <category term="nextflow"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Nextflow es una herramienta orientada a casos de uso mucho más interesantes
pero este ejemplo creo que puede servir para prácticar un poco y ver algunas funcionalidades
de este DSL&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a desarrollar un pipeline de Nextflow para organizar las fotos de un directorio
y ordernarlas por el sitio que fueron tomadas respecto de un punto que indiquemos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello el pipeline accederá a la meta información de cada foto y buscará si tiene la posición
donde fue tomada. Si la foto no cuenta con esta info, la foto será ignorada.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez obtenida la información de todas las fotos, el pipeline las ordenará según lo lejos que
se encuentren de un punto dado (en formato &quot;latitud, longitud&quot;). Si no se proporciona ningun punto
se tomará el punto [0,0] como referencia.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es obtener al final del proceso un directorio con las imágenes copiadas pero renombradas como&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;0-imagexxxx.jpg
1-imageyyyy.png
2-imagezzzz.jpg
etc&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;groovy_util&quot;&gt;Groovy util&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para mejorar la legibilidad del pipeline (y separar la &quot;lógica de negocio&quot; del pipeline) vamos a crear
una clase Groovy con 2 métodos estáticos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Uno servirá para extraer la posición donde fue tomada la foto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;static def extractCoord( path ){
    def metadata = Imaging.getMetadata(path.toFile())
    if( !metadata || !metadata.metaClass.getMetaMethod(&quot;getExif&quot;) )
        return null

    def latitude = metadata.exif?.GPS?.latitudeAsDegreesNorth
    def longitude = metadata.exif?.GPS?.longitudeAsDegreesEast

    [latitude, longitude]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El otro método servirá para ordenar dos posiciones geográficas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;static double metersTo( a,  b) {
    double lat1 = a[0] as double
    double lng1 = a[1] as double
    double lat2 = b[0] as double
    double lng2 = b[1] as double
    double radioTierra = 6371;
    double dLat = Math.toRadians(lat2 - lat1);
    double dLng = Math.toRadians(lng2 - lng1);
    double sindLat = Math.sin(dLat / 2);
    double sindLng = Math.sin(dLng / 2);
    double va1 = Math.pow(sindLat, 2) + Math.pow(sindLng, 2) * Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2));
    double va2 = 2 * Math.atan2(Math.sqrt(va1), Math.sqrt(1 - va1));
    double meters = radioTierra * va2;
    meters;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;image&quot;&gt;Image&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para mejorar la legibilidad del código (y no estar usando mapas genéricos donde guardar la información) vamos a crear una
clase &lt;code&gt;Image&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;class Image{
    def file
    def coord
    def order
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nos servirá para guardar referencia a la imagen original, las coordenadas y su posicion en la lista&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;process&quot;&gt;Process&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El pipeline se va a componer de dos procesos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Uno para iterar sobre las fotos y extraer su posicion&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process EXIF_GPS{
    input:
        path fImage
    output:
        val image
    exec:
        coord = Images.extractCoord( file(&quot;$params.directory/$fImage&quot;) )
        image = new Image(file:fImage, coord: coord)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y otro para copiar la imagen original al destino, usando la posicion como nombre&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process PROCESS_IMAGE{
    input:
        each img
    output:
        val target
    exec:
        target = file(&quot;$params.directory/$img.file&quot;).copyTo(file(&quot;$params.outputDir/$img.order-$img.file&quot;))
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;pipeline&quot;&gt;Pipeline&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último el pipeline a ejecutar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Para todas las fotos que existan en un directorio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Extraeremos la posicion&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Filtraremos las que tengan información de la posición&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Las ordenaremos segun el punto de origin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Copiaremos a directorio de salida&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;workflow{
    def images = Channel.fromPath(&quot;$params.directory/*.jpg&quot;)

    images //read all images
        | EXIF_GPS // extract gpf information
        | branch { // diferenciate if exif information or not
            no_info: !it.coord?[0]
            with_info: it.coord
        }
        | set{ gps_images } // send to new channel

    gps_images.with_info // only images with gps info
        | toSortedList{ a, b-&amp;gt; // sort respect how far are from origin
            Images.metersTo(origin,a.coord) &amp;lt;=&amp;gt; Images.metersTo(origin, b.coord)
        }
        | map { // assign the index
            it.eachWithIndex{ img, idx-&amp;gt; img.order = idx}
        }
        | set{ images_sorted } //send to new channel

    images_sorted
        | PROCESS_IMAGE
        | view

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este workflow tiene algunas cosas interesantes como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;branch&lt;/code&gt; que nos permite crear un canal con canales &quot;hijos&quot; según el criterio que queramos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;set&lt;/code&gt; que nos permite crear canales &quot;al vuelo&quot;. En este caso creamos un canal &lt;code&gt;images_sorted&lt;/code&gt;
alimentándolo con un ArrayList de images y así poder ejecutar en pararelo cada imagen&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejecutando&quot;&gt;Ejecutando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si tienes instalado nextflow y tienes un directorio con imágenes puedes ejecutar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow run -r main &lt;a href=&quot;https://github.com/jagedn/nextflow-images.git&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/nextflow-images.git&lt;/a&gt; --directory &quot;/MI/DIRECTORIO/ORIGEN&quot; --outputDir &quot;/MI/DIRECTORIO/SALIDA&quot;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ves Nextflow es capaz de descargar desde un repositorio git un pipeline completo e incluso de poder especificarle qué rama del mismo
queremos ejecutar (&lt;code&gt;main&lt;/code&gt; en mi caso)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si quieres ordenar las fotos por algún punto que no sea [0,0] añade al final del comando `--origin &quot;20.23,12.13123&quot; ` o las coordenadas que quieras&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probablemente no sea un pipeline muy útil pero me ha servido para practicar un poco el cómo encadenar canales y procesos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Un caso de uso simple. Organizando fotos por geoposicion</summary>
    </entry>
    <entry>
        <title>He vuelto a fallar</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/vuelto-fallar.html"/>
        <updated>2023-08-01T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/vuelto-fallar.html</id>
        <category term="personal"/>
        <category term="introspeccion"/>
        <content type="html">
            &lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como ya habrás observado se me da mejor escribir sobre cosas técnicas, explicando mis
pajas mentales y pet projects, que hablar sobre las movidas que tengo en la cabeza pero creo que de vez
en cuando tengo sacarlo fuera&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En unos días hará ya un mes que renuncié en mi último trabajo, después de 8 meses bastante intensos
(tampoco mucho, ha habido otras etapas más intensas pero supongo que con la edad va costando más). Durante este
mes he estado de vacaciones y por cosas de la vida todavía me quedan unas semanas más de estar off.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este no era el plan. El plan era repetir la idea del año pasado y aprovechar el #remotework para trabajar unas
semanas desde Tailandia a la vez que hacíamos turismo. Pero &amp;#8230;&amp;#8203; las cosas cambiaron muy rápido y ha habido que
adaptarse.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para tener un poco de contexto, recapitularemos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El año pasado lo inicié en una empresa que reunía todo lo que me gustaba. Trabajar con Groovy, en un proyecto
OpenSource, con peña de otros paises y con un sueldo muy bueno. Todo pintaba genial y de hecho lo guardo con mucho cariño
pero me encontré con que el jefe (un tipo muy majo y buena persona además de técnico exceptional) no &quot;me dejaba espacio&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Muchas veces lo que hablábamos de hacer un viernes, llegaba el lunes y me lo encontraba ya medio montado por ejemplo.
O iba viendo cómo mis opiniones no se tenían (del todo) en cuenta y me tenía que ceñir a lo que otro había pensado
que era lo correcto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes imaginar eso me dejó muy tocado anímicamente. Creo que seguí los pasos correctos, comentándolo a la
persona indicada pero descubrí que aunque la mayoría de mis compañeros en privado compartían mi opinión, no la
manifestaban así que llegó un punto en que dije hasta aquí y recogí mis cosas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por suerte conseguí enlazarlo con un reto que un muy buen amigo me propuso. Había empezado como sheriff en una startup
de Dubai y necesitaban a un DevOps, pero de los de verdad: dev y ops, no el de infraestructura. Así que me tiré de
cabeza a por ello.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Han sido 8 meses de aprender un huevo de cosas. Terraform, AWS, clusters, Nodejs, Typescript, Github actions, ufff.,,
iba a continuar escribiendo más cosas que me he comido estos 8 meses pero &amp;#8230;&amp;#8203; no es de eso de lo que quiero
escribir, para eso esta Linkedin.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El problema es que una vez más me sentí que no compartía la visión con los de arriba y además no me gustaba la
actitud que tenían hacia el equipo. Me gustaría explayarme, pero creo que no toca, que mejor lo voy olvidando.
El caso es que una vez más el Aguilera que llevo dentro se empezó a pudrir rápidamente y una mañana se encontró
enviando la renuncia de forma abrupta.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Seguramente tú en tu trabajo estés aguantando tus mierdas (seguro que sí) y no entenderás muy bien cómo puedo
renunciar así (y más peinando canas como ya peino) si encima me pagaban muy bien &amp;#8230;&amp;#8203; Pues eso mismo me pasa a mí, que me lo
pregunto todas las mañanas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El caso es que siempre se me queda una sensación amarga, de &quot;fracaso&quot;, de creer que es que la gente va a pensar
&quot;que no aguanto nada&quot; y bueno aunque uno se diga que la gente piense lo que quiera pues oye tampoco somos de piedra.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así que nada, toca seguir disfrutando de estas semanas super interesantes por tierras extrañas y ya a la vuelta
enfrentarse a mi peor enemigo, yo mismo.&lt;/p&gt;
&lt;/div&gt;
        </content><summary>Pues después de 8 meses intensos ... he dimitido again</summary>
    </entry>
    <entry>
        <title>Programar es de trileros</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/programar-trileros.html"/>
        <updated>2023-08-01T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/programar-trileros.html</id>
        <category term="personal"/>
        <category term="introspeccion"/>
        <content type="html">
            &lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hoy 1 de Agosto hace exactamente 32 años que terminé la beca en C.P. Software para programata&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por hacer un poco de memoria, en aquella época estaba haciendo (más bien suspendiendo) segundo curso de Económicas y trabajando de vigilante los fines de semana para pagar las cervezas. Ya había descubierto el aula de informática de la Uni, siempre vacía, mi colega José Merchán me había dado algunas clasecillas de Cobol y me &quot;enchufó&quot; para la beca donde curraba. Una beca de dos meses y medio (medio Mayo, Junio y Julio) que me cuadró muy bien en el timing de los exámenes finales porque estaba previsto suspender casi todas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Ojo, beca pagada. Si mal no recuerdo me pagaron por los dos meses que duró unas 30.000 pesetas por mes, lo mismo que me sacaba de vigilante.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De aquella beca tengo varios recuerdos/anécdotas (seguramente distorsionadas tras 32 años) como la de mi primer traje, pero la que vengo a contar hoy no se la había contado a nadie, tal vez por verguenza, aunque lo dudo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El caso es que ya iniciado el curso y una vez que habíamos ido viendo que si el editor, que si el entorno, etc empezamos a meternos en faena con el Cobol, su sintáxis, etc y uno de los ejercicios que teníamos que resolver era algo así como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dadas dos variables A y B donde A vale 30 y B vale 20 hacer un programa que intercambie los valores de tal forma que A valga 20 y B 30 (crifras y enunciado aproximado)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Chupao, le meto&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;A VALUE 3

B VALUE 20

MOVE A TO B

MOVE B TO A&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y a correr&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No hace falta ser un ingeniero del MIT para entender que no funciona. Si &quot;mueves&quot; A a B pierdes el valor de B así que en el segundo move ya estás jodido.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Seguramente había recibido antes alguna bofetada a mi chulería informática, pero esta la he guardado en mi recuerdo. Cuando el profe nos/me hizo ver el error estuvo unos segundos dándole vueltas y pensando &quot;pero entonces cómo &amp;#8230;&amp;#8203;&quot;, o como se dice ahora &quot;guat de fak?&quot; hasta que nos dijo como si tal cosa que necesitábamos una variable temporal donde almacenar una de las dos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;A VALUE 3

B VALUE 20

MOVE B TO C

MOVE A TO B

MOVE C TO A&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y entonces mi cabeza hizo boom. Ahí es cuando lo ví claro: &lt;strong&gt;programar es de trileros&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No hay magia en programar, no existe la salsa secreta de Kunfu Panda (vale, esto no lo pensé en su día), es todo truco: Tienes unos recursos limitados y de lo que se trata es de hacer tocomochos y mantener las suficientes pelotas en el aire para que parezca magia fluida, pero es todo truco&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(poco tiempo después volví a tener la misma epifanía cuando descubrí la recursividad pero no tengo anécdota #AbueloCebolleta que recuerde)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pues eso, ya lo he soltado después de 32 años.&lt;/p&gt;
&lt;/div&gt;
        </content><summary>Hoy 1 de Agosto hace exactamente 32 años que terminé la beca que me enseño que prograrmar es de trileros</summary>
    </entry>
    <entry>
        <title>FediSearch</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/fedisearch.html"/>
        <updated>2023-06-30T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/fedisearch.html</id>
        <category term="groovy"/>
        <category term="openai"/>
        <category term="fediverso"/>
        <category term="fediverse"/>
        <category term="supabase"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;En este post voy a contar mis primeros pasos con OpenAI asi que como todo primer paso
estaré equivocado y seguramente cometeré alguna burrada &amp;#8230;&amp;#8203; pero a mí me ha funcionado&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque ya hay gente dedicada de lleno al tema de OpenAI y relacionados, la verdad es que yo todavía no he tocado
nada de esta tecnología y sólo conozco lo poquito de algún que otro artículo que haya podido leer asi como
los típicos post en Linkedin de gente haciendo prompts o como se diga. Supongo que algún día me tendré que
arremangar y ponerme más con ello.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, este mes en el trabajo me han &quot;encasquetado&quot; integrar un producto que no había oído nunca y dar
soporte al programador que lo necesitaba. El producto en cuestión es Milvus y la movida era potenciar el buscador
de nuestra aplicación usando OpenAI en lugar de los típicos &quot;where like %&quot; en la base de datos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Ahora ya sé que podíamos haber usado Postgresql con menos esfuerzo&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente la idea de lo que queríamos hacer era:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;tenemos una base de datos de artículos con varios campos de interés para buscar, como el típico de descripción,
sector, familia, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vamos a &quot;vectorizar&quot; la unión de todos ellos. Esto significa que le vamos a pasar todos estos campos a OpenAI (un
API disponible en Internet y que es propietario de una gente muy lista) para que le aplique un modelo suyo y nos
devuelva un array de números (el vector). Este vector &quot;representa&quot; ese texto en una matriz de muchísimas dimensiones&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;lo insertamos en Milvus y le decimos qué campo es el vector&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dada una búsqueda del usuario con &quot;texto libre&quot;, la vectorizamos y le decimos a Milvus que nos busque similitudes
con un grado de afinidad&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y mientras estaba liado con la parte de instalar e ir entendiendo Milvus, un buen amigo me pasó por otro lado
un artículo que por casualidad me ayudó a entender mejor la historia y que además me descubría una nueva plataforma
, Supabase, donde poder desplegar aplicaciones con una capa gratuita interesante.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El artículo en cuestión: &lt;a href=&quot;https://supabase.com/blog/openai-embeddings-postgres-vector&quot; class=&quot;bare&quot;&gt;https://supabase.com/blog/openai-embeddings-postgres-vector&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;supabase&quot;&gt;Supabase&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea de Supabase me ha enganchado y tengo que estrujarla más pero así, en dos palabras, me dan un Postgresql
donde puedo instalar con un simple click muchas de sus extensiones (Postgresql tiene más de 1.000 extensiones)
y desplegar functions que acceden a los datos creando así un API.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Digamos que lo veo (tengo que explorarlo más) como el backend para mi frontend en Netlify&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez registrado en la plataforma simplemente he creado un proyecto nuevo, he activado el plugin
pg-vector y creado una tabla &quot;users&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Siguiendo los pasos del articulo anterior y cambiando su documents por mi users he tenido que crear la funcion
de Postgresql y el índice, pero es basicamente un c&amp;amp;p del artículo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;openai&quot;&gt;OpenAI&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por lo que he entendido hay miles de proyectos que hacen lo mismo que OpenAI pero como este es el más famoso
y su API es realmente sencilla es el que voy a usar. Simplemente me registro en la plataforma y obtengo un API
key para poder hacer N llamadas por minuto (suficientes para este proyecto)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;fedisearch&quot;&gt;FediSearch&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente, parafraseando a Jose Luis Lopez Vazquez &quot;soy como un americano que si le das una pistola tiene que usarla&quot;,
así que me he hecho este pequeño proyecto para probarlo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Aquí tienes el video por si te quieres saltar la parte técnica y verlo en acción &lt;a href=&quot;https://fediverse.tv/w/iiH8mw2J9D3KV2eyxBJ4Rz&quot; class=&quot;bare&quot;&gt;https://fediverse.tv/w/iiH8mw2J9D3KV2eyxBJ4Rz&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es crear un buscador de perfiles en Mastodon (en realidad Fediverso pero la gente lo conoce más por esta
aplicación que lo usa).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A diferencia de Twitter, donde sólo hay un &quot;único servidor&quot;, en el Fediverso hay muchísimos (se conocen como
instancias, nodos, servidores &amp;#8230;&amp;#8203;) y todos exponen el directorio de usuarios registrados en una url. Para cada usuario
puedes ver el username, su bio y campos descriptivos que el usuario haya querido añadir&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si por ejemplo consultas esta url &lt;a href=&quot;https://mastodon.social/api/v1/directory&quot; class=&quot;bare&quot;&gt;https://mastodon.social/api/v1/directory&lt;/a&gt; puedes ver que te responde con&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[
    {
        &quot;id&quot;:&quot;109244873849193955&quot;,
        &quot;username&quot;:&quot;jikodesu&quot;,
        &quot;acct&quot;:&quot;jikodesu&quot;,
        &quot;display_name&quot;:&quot;Jiko&quot;,
        ...
        &quot;created_at&quot;:&quot;2022-10-28T00:00:00.000Z&quot;,
        &quot;note&quot;:&quot;\u003cp\u003eTrying out Mastodon. Part of 2022 Twitter migration\u003c/p\u003e\u003cp\u003eDuolingo user: Nihongo, ...
    },
    {
        &quot;id&quot;:&quot;......&quot;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada vez que consultas esta url te devuelve usuarios random.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De esta forma ya tenemos la primera parte del proyecto: obtener de cada usuario un campo de texto que lo describa.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La segunda parte del proyecto será enviarle este campo descriptivo a OpenAI para que lo vectorize&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La tercera parte del proyecto será insertar en nuestra base de datos de usuarios el username, el campo de texto y
el vector&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello he hecho un pequeño script (en Groovy of course, pero con un poco de maña lo podría haber hecho incluso con un bash) que dada una instancia del fediverso extrae la info y se la manda a OpenAI para luego insertar los
resultados en Supabase:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;@Grab(group=&apos;org.apache.commons&apos;, module=&apos;commons-lang3&apos;, version=&apos;3.12.0&apos;)

import groovy.json.*
import java.net.http.*
import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4

json = new JsonSlurper().parse(&quot;${args[0]}/api/v1/directory&quot;.toURL())

// extraemos los campos de interes de cada usuario
users = json.collect { user-&amp;gt;
	[username: user.username,
	 note: user.display_name +&quot;|&quot;+escapeHtml4(user.note)+&quot;|&quot;+user.fields.collect{it.name+&apos;|&apos;+it.value}.join(&apos;|&apos;),
	 url: user.url]
}

body = [
	input: users.collect{it.note},
	model: &quot;text-embedding-ada-002&quot;
]
// se los mandamos a openai para que los vectorize
request = HttpRequest.newBuilder(new URI(&quot;https://api.openai.com/v1/embeddings&quot;))
                .headers(
                        &quot;Content-Type&quot;, &quot;application/json&quot;,
                        &quot;Authorization&quot;, &quot;Bearer ${args[1]}&quot;
                )
                .POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(body).toString()))
                .build()
response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream())
result = response.body().text

// añadimos a cada usuario su vector
embeddings= new JsonSlurper().parseText(result) as Map
embeddings.data.each{ def entry -&amp;gt;
	users[entry.index].embedding = entry.embedding
}

// se los mandamos a Supabase con un POST
request = HttpRequest.newBuilder(new URI(&quot;https://TUPROJECTO.supabase.co/rest/v1/users&quot;))
		.headers(
				&quot;Content-Type&quot;, &quot;application/json&quot;,
				&quot;apikey&quot;, args[2]
		)
		.POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(users).toString()))
		.build()
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream())&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es un script muy basico y guarro que pide por parametros la instancia, la key de OpenAI y la key de Supabase&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;buscando_usuarios&quot;&gt;Buscando usuarios&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para la búsqueda me he peleado un poco con node, deno y el subase-cli para poder crear una función que se pueda
invocar para realizar la busqueda (lo que sería mi API).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez he conseguido crear el proyecto y entender cómo desplegar las functions he podido desplegar mi funcion
(copiada prácticamente del articulo de supabase):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;import { serve } from &apos;https://deno.land/std@0.170.0/http/server.ts&apos;
import &apos;https://deno.land/x/xhr@0.2.1/mod.ts&apos;
import { createClient } from &apos;https://esm.sh/@supabase/supabase-js@2.5.0&apos;
import { Configuration, OpenAIApi } from &apos;https://esm.sh/openai@3.1.0&apos;

export const corsHeaders = {
  &apos;Access-Control-Allow-Origin&apos;: &apos;*&apos;,
  &apos;Access-Control-Allow-Headers&apos;: &apos;authorization, x-client-info, apikey, content-type&apos;,
}

serve(async (req) =&amp;gt; {
  // Handle CORS
  if (req.method === &apos;OPTIONS&apos;) {
    return new Response(&apos;ok&apos;, { headers: corsHeaders })
  }

  // Search query is passed in request payload
  const { query } = await req.json()

  // OpenAI recommends replacing newlines with spaces for best results
  const input = query.replace(/\n/g, &apos; &apos;)
  console.log(`Searching users for ${input}`)
  const configuration = new Configuration({ apiKey: &apos;sk-----------&apos; })
  const openai = new OpenAIApi(configuration)

  // Generate a one-time embedding for the query itself
  const embeddingResponse = await openai.createEmbedding({
    model: &apos;text-embedding-ada-002&apos;,
    input,
  })

  const [{ embedding }] = embeddingResponse.data.data

  const supabaseClient = createClient(&apos;https://pqlvxllouutlnnnwnlxp.supabase.co&apos;, &apos;TU_API_TOKEN&apos;);

  // In production we should handle possible errors
  const { data: documents } = await supabaseClient.rpc(&apos;match_users&apos;, {
    query_embedding: embedding,
    match_threshold: 0.75, // Choose an appropriate threshold for your data
    match_count: 50, // Choose the number of matches
  })

  return new Response(JSON.stringify(documents.sort((a,b)=&amp;gt;a.similarity - b.similarity)), {
    headers: { ...corsHeaders, &apos;Content-Type&apos;: &apos;application/json&apos; },
  })
})&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En sí no tiene mucho interés la funcion salvo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Llama a OpenAPI usando mi token para vectorizar la query enviada por el usuario&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Usando la librería de supabase ejecuto un RPC llamando a la funcion embebida en el Postgres que realiza
la busqueda de simimilitud por vectores&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusiones&quot;&gt;Conclusiones&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A parte de jugar un poco con el api de OpenAI y entender un poco lo de vectorizar textos creo que Supabase tiene
mucho potencial pues poder disponer de un Postgresql con un solo click y añadirle funciones es muy interesante
para esos pet-projects que se me ocurren cada cierto tiempo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Buscando perfiles del Fediverso con OpenAI y Supabase</summary>
    </entry>
    <entry>
        <title>Apisix en Okteto</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/apisix-3.html"/>
        <updated>2023-05-15T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/apisix-3.html</id>
        <category term="apisix"/>
        <category term="kubernetes"/>
        <category term="apigateway"/>
        <category term="okteto"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post sobre APISIX vamos a desplegarlo en nuestro namespace de Okteto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Este post NO es la continuación de &lt;a href=&quot;apisix-1.html&quot; class=&quot;bare&quot;&gt;apisix-1.html&lt;/a&gt; o &lt;a href=&quot;apisix-2.html&quot; class=&quot;bare&quot;&gt;apisix-2.html&lt;/a&gt;
sino que esta vez vamos a usar un cluster que no estará en nuestro local.&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El código de estos post los puedes encontrar en el repo &lt;a href=&quot;https://github.com/jagedn/apisix-example&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;recapitulando&quot;&gt;Recapitulando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En posts anteriores vimos cómo instalar un cluster en nuestro local con &lt;code&gt;k3d&lt;/code&gt; y
cómo desplegar en este cluster APISIX. Así mismo creamos un servicio &lt;code&gt;whoami&lt;/code&gt;
y unas rutas para acceder a él a través de nuestro apigateway APISIX&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;apigateway&quot;&gt;Apigateway&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a hacer lo mismo pero en lugar de usar un cluster en nuestro local, usaremos nuestro
namespace en Okteto por lo que nuestra aplicación estará accesible en Internet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es desplegar en nuestro namespace dos servicios (custom-service y product-service) pero queremos que no se
puedan acceder a ellos de forma directa, sino a través de nuestro ApiGateway que será donde tendremos las configuraciones
de rutas, autentificaciones, etc que nos ofrece Apisix.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;arquitectura&quot;&gt;Arquitectura&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues nuestra arquitectura actual es algo parecido a&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-b9f840ad8e7db980307124f7b2c7b1cc.png&quot; alt=&quot;Diagram&quot; width=&quot;447&quot; height=&quot;293&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente este es un ejemplo simple con dos servicios donde vamos a usar la imagen de Docker whoami, pero en un
caso real los servicios serían aplicaciones dialogando con bases de datos e incluso entre ellas, todo de forma
interna.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;okteto&quot;&gt;Okteto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ya he publicado algunos post sobre Okteto y cómo crearnos una cuenta, obtener las credenciales y tal, así que desde
aquí asumo que tienes tu namespace preparado y configurado. En mi caso va a ser &lt;code&gt;example-pvidasoftware&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;product_y_customer_service&quot;&gt;Product y Customer Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para desplegar los dos servicios vamos a usar una funcionalidad de Okteto que nos permite desplegar mediante
un docker-compose.yml (recuerda tienes todo el codigo en el repositorio &lt;a href=&quot;https://github.com/jagedn/apisix-example&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El docker-compose es similar en ambos casos y es tan simple como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;version: &quot;3&quot;

services:
  customer-service:
    image: containous/whoami&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nos situamos en la carpeta &lt;code&gt;product-service&lt;/code&gt; y ejecutamos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ okteto deploy&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y lo mismo desde la carpeta &lt;code&gt;customer-service&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si vemos la consola de Okteto veremos que hemos desplegado dos stacks&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/okteto_apisix_1.png&quot; alt=&quot;okteto apisix 1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al no especificar ningun puerto ni endpoints, ninguno de los servicios está accesible desde fuera del cluster
(a no ser que hagas un port-forward a tu maquina claro)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;desplegando_apisix_en_okteto&quot;&gt;Desplegando Apisix en Okteto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si recuerdas, en los post anteriores instalamos Apisix usando el Helm chart oficial. El &quot;problema&quot; con Okteto
es que no contamos con todos los permisos para hacer el mismo despliegue así que mi primera opción fue desplegarlo
usando un docker-compose y especificando las imágenes oficiales más la configuración de ejemplo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo después de investigar un poco he visto que existe una forma más sencilla usando el Helm y simplemente
hay que &quot;tunearlo&quot; un poco (El fichero de configuración completo está en el repositorio. Sólo busca las líneas
marcadas con &quot;JORGE&quot; y reemplaza a tu gusto)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar añadiremos a nuestra configuración la URL del repositorio oficial &quot;https://charts.apiseven.com&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/okteto_apisix_2.png&quot; alt=&quot;okteto apisix 2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;volvemos a nuestro namespace y pinchamos en &quot;Launch Dev Environment&quot; seleccionando un helm char y buscando Apisix
en la lista.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Okteto nos permite personalizar la configuración mediante un campo de entrada donde podremos borrar toda
la configuracion por defecto y poner la nuestra (o buscar los campos directamente y modificarlos)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente los campos que he modificado son:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;etcd.persistentce.size a 1Gb. Por defecto son 8Gb y al crear 3 volumenes me deja sin espacio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ingress.hosts.host. Tienes que cambiarlo según tu namespace. Yo por ejemplo he tenido que poner
&quot;example-pvidasoftware.cloud.okteto.net&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dashboard.true. Este es solo para tener la consola habilitada y poder crear rutas de forma sencilla, pero
puedes deshabilitarlo con lo que te ahorras recursos (pero te tocará crear las rutas mediante curl y json)&lt;/p&gt;
&lt;div class=&quot;olist lowerroman&quot;&gt;
&lt;ol class=&quot;lowerroman&quot; type=&quot;i&quot;&gt;
&lt;li&gt;
&lt;p&gt;y ejecutamos &amp;#8230;&amp;#8203; y esperamos unos 3-4 minutos&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo va bien deberías tener algo parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/okteto_apisix_3.png&quot; alt=&quot;okteto apisix 3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ves he subrayado la url pública y &lt;strong&gt;único&lt;/strong&gt; punto de entrada a nuestra aplicación.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;creando_rutas&quot;&gt;Creando rutas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como he comentado he habilitado el dashboard de apisix, pero al no publicar el puerto no se puede acceder a él
desde fuera así que usando kubectl haremos un port-forward para poder acceder como si se estuviera ejecutando
en nuestra máquina.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Accedemos a la consola en &lt;a href=&quot;http://localhost:9000&quot; class=&quot;bare&quot;&gt;http://localhost:9000&lt;/a&gt; con el usuario admin/admin (obviamente tú habrás puesto otros
porque eres una persona más cuidadosa que yo) y creamos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;upstream customer-service&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;upstream product-service&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;route /customer/* al upstream customer-service&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;route /product/* al upstream product-service&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;probando&quot;&gt;Probando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si accedemos a &lt;a href=&quot;https://apisix-gateway-example-pvidasoftware.cloud.okteto.net/customer/1&quot; class=&quot;bare&quot;&gt;https://apisix-gateway-example-pvidasoftware.cloud.okteto.net/customer/1&lt;/a&gt; veremos que nos
devuelve algo como&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Hostname: customer-service-6fdbbdc48-nts96
GET /customer/1 HTTP/1.1
Host: apisix-gateway-example-pvidasoftware.cloud.okteto.net
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/114.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
X-Forwarded-Host: apisix-gateway-example-pvidasoftware.cloud.okteto.net
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Scheme: https
X-Request-Id: 861e9d2c73dba0fa01e8f9ea677129f3
X-Scheme: https&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y lo mismo con la ruta de product&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tenemos Apisix corriendo en nuestro cluster podemos desplegar en él los microservicios sabiendo que
están &quot;por detrás&quot; de nuestro ApiGateway, con toda la potencia y facilidad que ofrece&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Desplegando Apisix en Okteto</summary>
    </entry>
    <entry>
        <title>Graalvanizando un script de Groovy</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/groovy-graalvm.html"/>
        <updated>2023-04-21T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/groovy-graalvm.html</id>
        <category term="java"/>
        <category term="groovy"/>
        <category term="graalvm"/>
        <category term="linkedin"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post voy a publicar el código y pasos a seguir para convertir un script de Groovy
en un binario de tal forma que para su ejecución no se necesita ni Groovy ni Java instalado. Además, aunque es
simple, la velocidad de ejecución es sensiblemente mejor&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En concreto el aplicativo va a subir un post a Linkedin (por ahora texto, luego veré de adjuntar imágenes)
usando el nuevo api REST de esta plataforma. Para ello pedirá el &lt;code&gt;author&lt;/code&gt; y el &lt;code&gt;accessToken&lt;/code&gt; para poder publicar&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para completar todos los pasos se requiere tener instado:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;sdkman, &lt;a href=&quot;https://sdkman.io/install&quot; class=&quot;bare&quot;&gt;https://sdkman.io/install&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez instalado le indicaremos que nos instale las versiones de Java y Groovy necesarias para generar el binario:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sdk use groovy 4.0.11&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sdk use java 22.3.r19-grl&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;configuración&quot;&gt;Configuración&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para generar una imagen nativa de un script de groovy es necesario que este sea compilado en modo estático (lo que
resta mucho de la expresividad de Groovy, pero &amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;compiler.config&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;withConfig(configuration) {
    ast(groovy.transform.CompileStatic)
    ast(groovy.transform.TypeChecked)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;groovyscript&quot;&gt;GroovyScript&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para mejorar la legibilidad del código he dividido el script en 2 ficheros .groovy, uno de ellos con métodos estáticos
orientado a hacer las peticiones HTTP get y post y el otro que será el que contenga la &quot;logica de negocio&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;HttpUtil.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;import groovy.json.JsonOutput
import groovy.json.JsonSlurper

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

class HttpUtil{

    static Map postJson(Map map, String url, String token) {
        println &quot;-&amp;gt; ${JsonOutput.prettyPrint(JsonOutput.toJson(map).toString())}&quot;
        def request = HttpRequest.newBuilder(new URI(url))
                .headers(
                        &quot;Content-Type&quot;, &quot;application/json&quot;,
                        &quot;X-Restli-Protocol-Version&quot;, &quot;2.0.0&quot;,
                        &quot;LinkedIn-Version&quot;, &quot;202301&quot;,
                        &quot;Authorization&quot;, &quot;Bearer $token&quot;
                )
                .POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(map).toString()))
                .build()
        def response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream())
        if( response.statusCode() &amp;gt; 299 ){
            println response
            return null
        }
        def result = response.body().text
        println &quot;&amp;lt;-${result}&quot;
        if( !result ){
            return null
        }
        new JsonSlurper().parseText(result) as Map
    }

    static Map getJson(String url, String token){
        def request = HttpRequest.newBuilder(new URI(url))
                .headers(
                        &quot;Content-Type&quot;, &quot;application/json&quot;,
                        &quot;X-Restli-Protocol-Version&quot;, &quot;2.0.0&quot;,
                        &quot;LinkedIn-Version&quot;, &quot;202301&quot;,
                        &quot;Authorization&quot;, &quot;Bearer $token&quot;
                )
                .GET()
                .build()
        def response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())

        def result = response.body()
        println &quot;&amp;lt;-${result}&quot;
        new JsonSlurper().parseText(result) as Map
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque parece tener mucho código en realidad es una implementación básica para usar las clases Http de Java y
poder hacer un get y un post con unas cabeceras determinadas así como enviar y recibir Mapas como si fueran Json.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;PostContent.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@GrabConfig(systemClassLoader = true)
@Grab(&apos;info.picocli:picocli-groovy:4.7.3&apos;)

@picocli.groovy.PicocliScript2

import groovy.transform.Field
import static picocli.CommandLine.*

@Option(names = [&quot;-a&quot;, &quot;--author&quot;], description = &quot;nickname&quot;, required=true)
@Field String author = &apos;&apos;

@Option(names = [&quot;-k&quot;, &quot;--token&quot;], description = &quot;token&quot;, required=true)
@Field String token = &apos;&apos;

@Option(names = [&quot;-t&quot;, &quot;--test&quot;], description = &quot;run as test&quot;)
@Field boolean test = false

@Parameters
@Field List&amp;lt;String&amp;gt; content = []

String postURL = &quot;https://api.linkedin.com/rest/posts&quot;

if( test ){
        postURL=&quot;https://httpbin.org/delay/0&quot;
}

Map post = [
        &quot;author&quot;                   : &quot;urn:li:person:${author}&quot;,
        &quot;commentary&quot;               : content.join(&apos; &apos;),
        &quot;visibility&quot;               : &quot;PUBLIC&quot;,
        &quot;distribution&quot;             : [
                &quot;feedDistribution&quot;              : &quot;MAIN_FEED&quot;,
                &quot;targetEntities&quot;                : [],
                &quot;thirdPartyDistributionChannels&quot;: []
        ],
        &quot;lifecycleState&quot;           : &quot;PUBLISHED&quot;,
        &quot;isReshareDisabledByAuthor&quot;: false
]

HttpUtil.postJson(post, postURL, token)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este script usa Picocli para facilitar el parseo de comandos. Simplemente necesita un author y un token y además
podemos indicar que se ejecute en modo test (-t) con lo que usará el servidor de httpbin como backend para testearlo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por lo demás es un &quot;simple&quot; post con el formato que Linkedin espera para publicar un post&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;graalvm&quot;&gt;Graalvm&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Las fases para convertir este script en binario las he separado en:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Pasar de Groovy a Java usando groovyc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ejecutarlo con Java en modo test para que el agente genere la configuracion de Graalvm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ejecutar el native-image para generar el binario&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;compile.sh&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;#!/bin/bash
set -e

# sdk use groovy 4.0.11
# sdk use java 22.3.r19-grl

echo compiling script
groovyc --configscript=compiler.groovy -d out PostContent.groovy

CP=&quot;$CP:./out&quot;
CP=&quot;$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-4.0.11.jar&quot;
CP=&quot;$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-json-4.0.11.jar&quot;
CP=&quot;$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-cli-picocli-4.0.11.jar&quot;
CP=&quot;$CP:$HOME/.groovy/grapes/info.picocli/picocli/jars/picocli-4.7.3.jar&quot;
CP=&quot;$CP:$HOME/.groovy/grapes/info.picocli/picocli-groovy/jars/picocli-groovy-4.7.3.jar&quot;

echo generating graalvm configuration
java -Dgroovy.grape.enable=false -agentlib:native-image-agent=config-output-dir=conf/ \
    -cp &quot;$CP&quot; \
    PostContent -t -a test -k test test

echo building native image
native-image -Dgroovy.grape.enable=false \
    --no-server \
    --no-fallback \
    --report-unsupported-elements-at-runtime \
    --initialize-at-build-time \
    --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \
    -H:ConfigurationFileDirectories=out/conf/ \
    --enable-url-protocols=http,https \
    -cp &quot;$CP&quot; \
    -H:ConfigurationFileDirectories=conf/ \
    PostContent&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo va bien al final del proceso (poco más de 1 minuto) tendremos un binario &lt;code&gt;postcontent&lt;/code&gt; que podremos ejecutar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./postcontent -a NH123123 -k A_BEARER_TOKEN_YOU_CAN_USE_MY_PREVIOUS_POST Hi this is a post from groovy script&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Para obtener el token te remito a otro de mis post donde te cuento como generarlo, o si lo prefieres usar el servicio
que he publicado.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Las nuevas versiones de Groovy y Graalvm permiten (con un poco de esfuerzo lo admito) poder crear comandos de consola
binarios, lo que para mí abre la puerta a poder distribuir utilidades usando mi lenguaje favorito&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Queda pendiente para un futuro post el poder adjuntar imágenes, que con el nuevo api Linkedin lo ha complicado un poco más.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Convirtiendo un script de groovy a binario nativo</summary>
    </entry>
    <entry>
        <title>Primeros pasos con Apisix en Kubernetes: JWT Auth</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/apisix-2.html"/>
        <updated>2023-04-05T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/apisix-2.html</id>
        <category term="apisix"/>
        <category term="kubernetes"/>
        <category term="apigateway"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este segundo post sobre APISIX vamos a securizar el acceso a los endpoints de un microservicio que se encuentra tras el api-gateway usando JWT&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Este post es la continuación de &lt;a href=&quot;apisix-1.html&quot; class=&quot;bare&quot;&gt;apisix-1.html&lt;/a&gt; y da por supuesto que ya has instalado y desplegado ciertos artefactos en tu cluster.
Si no es así te recomiendo que en
primer lugar lo completes&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El código de estos post los puedes encontrar en el repo &lt;a href=&quot;https://github.com/jagedn/apisix-example&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;recapitulando&quot;&gt;Recapitulando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el post anterior vimos cómo instalar un cluster en nuestro local con &lt;code&gt;k3d&lt;/code&gt; y
cómo desplegar en este cluster APISIX. Así mismo creamos un servicio &lt;code&gt;whoami&lt;/code&gt;
y unas rutas para acceder a él a través de nuestro apigateway APISIX&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a ver cómo podemos seguir con la migración a microservicios de nuestro monolito en concreto aquellos endpoints securizados con JWT&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es decir, probablemente en nuestro monolito tengamos implementado un sistema de autentificación de usuarios que genere token JWT y que tengamos multitud
de endpoints de nuestra API que lo validen mirando la cabecera &lt;code&gt;authorization&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El &quot;problema&quot; a resolver es conseguir que nuestro api gateway valide esos JWT
antes de enrutar las peticiones al microservicio de tal forma que estos no tengan
que preocuparse de esta validación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo otro problema a resolver es cómo
les hacemos llegar a estos usuarios qué usuario es el que está &quot;detrás&quot; de esa
petición.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;arquitectura&quot;&gt;Arquitectura&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues nuestra arquitectura actual es algo parecido a&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-89c8eead60a7280b39668b8bd033bce2.png&quot; alt=&quot;Diagram&quot; width=&quot;408&quot; height=&quot;382&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mientras que en una arquitectura microservicios con un ApiGw sería como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-0ab8b5f511d26adc8df9ce558415fb49.png&quot; alt=&quot;Diagram&quot; width=&quot;862&quot; height=&quot;446&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;security_service&quot;&gt;Security Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para este post he creado un servicio Micronaut de ejemplo siguiendo el tutorial
&lt;a href=&quot;https://guides.micronaut.io/latest/micronaut-security-jwt-gradle-java.html&quot; class=&quot;bare&quot;&gt;https://guides.micronaut.io/latest/micronaut-security-jwt-gradle-java.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este servicio &quot;validarará&quot; cualquier login cuya password sea &quot;password&quot; y usará
el username como &lt;code&gt;sub&lt;/code&gt; en el JWT generado. Es decir, cualquier login con un
usuario cualquier y una password=password devolverá un JWT donde el &lt;code&gt;sub&lt;/code&gt; será el
usuario que indiquemos. De esta forma podremos simular diferentes logins y comprobar
que nuestra solución es capaz de identificarlos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;el plugin de Apisix valida que el JWT incluya un campo &lt;code&gt;key&lt;/code&gt; por lo que
el tutorial de micronaut no es 100% valido.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El código de nuestro servicio de autentificación lo puedes encontrar en
&lt;a href=&quot;https://github.com/jagedn/apisix-example/tree/main/micronaut-security-jwt-gradle-java&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example/tree/main/micronaut-security-jwt-gradle-java&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez compilado y generada la imagen la he subido a mi docker.io como &lt;code&gt;jagedn/monolito&lt;/code&gt; (ya, el nombre no es muy acertado)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez publicada la imagen la desplegamos en nuestro cluster&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/jagedn/apisix-example/blob/main/k3d/04-jwt/user-service.yml&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example/blob/main/k3d/04-jwt/user-service.yml&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: jagedn/monolito
        imagePullPolicy: Always
        ports:
          - containerPort: 8080
            protocol: TCP
            name: http
        env:
          - name: MICRONAUT_SECURITY_TOKEN_JWT_SIGNATURES_SECRET_GENERATOR_SECRET
            value: MY_APPLICATION_JWT_SECRET_KEY_DUMMY &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  ports:
  - name: http
    targetPort: http
    port: 8080
  selector:
    app: user-service&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;MY_APPLICATION_JWT_SECRET_KEY_DUMMY es la clave a usar para generar JWT firmados&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y creamos una ruta que redirija las peticiones de login a él&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
si nuestro monolito fuera el que está resolviendo las autentificaciones
y generando los JWT, NO desplegaríamos un servicio nuevo sino que usaríamos la
ruta que ya tengamos creada
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/jagedn/apisix-example/blob/main/k3d/04-jwt/apisix-login.yml&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example/blob/main/k3d/04-jwt/apisix-login.yml&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: apisix-login-route
  namespace: default
spec:
  http:
    - name: route-login
      match:
        paths:
          - /login
      backends:
        - serviceName: user-service
          servicePort: http&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ahora podemos probar a hacer un login con un usuario cualquiera:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;http localhost:8881/login username=jorgepayaso password=password

{
    &quot;access_token&quot;: &quot;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb3JnZXBheWFzbyIsIm5iZiI6MTY4MDg4MjMwMCwicm9sZXMiOltdLCJpc3MiOiJtaWNyb25hdXRndWlkZSIsImV4cCI6MTY4MDg4NTkwMCwiaWF0IjoxNjgwODgyMzAwLCJrZXkiOiJtYWluIn0.ALYLxJFNPYE-jmalF0cBGjTBse7DZFwzfd5DMEN1JLs&quot;,
    &quot;expires_in&quot;: 3600,
    &quot;refresh_token&quot;: &quot;eyJhbGciOiJIUzI1NiJ9.NjBlNTk3N2EtZjIyYi00MjFjLTk2MjktOGM5NzdjZTZkODE0.jysMcQJDEDHDTypmSOBnFA4YpkmyS-o3eqvmv_--c3U&quot;,
    &quot;token_type&quot;: &quot;Bearer&quot;,
    &quot;username&quot;: &quot;jorgepayaso&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;si validas ese &lt;code&gt;access_token&lt;/code&gt; en jwt.io por ejemplo verás que el payload es:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;jorgepayaso&quot;,
  &quot;nbf&quot;: 1680882300,
  &quot;roles&quot;: [],
  &quot;iss&quot;: &quot;micronautguide&quot;,
  &quot;exp&quot;: 1680885900,
  &quot;iat&quot;: 1680882300,
  &quot;key&quot;: &quot;main&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;securizando_el_api&quot;&gt;Securizando el API&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tenemos un servicio y una ruta para obtener tokens vamos a &quot;securizar&quot; nuestro API haciendo que el APISIX valide que todas las peticiones
incluyen una cabecera Authorization con un JWT válido (y así no tener que
implementarlo en TODOS los servicios):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para poder comparar con el post anterior vamos a configurar una nueva ruta &lt;code&gt;v2&lt;/code&gt;
que estará securizada, mientras que la del post anterior seguirá en &quot;abierto&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/jagedn/apisix-example/blob/main/k3d/04-jwt/apisix-secure-whoami.yml&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example/blob/main/k3d/04-jwt/apisix-secure-whoami.yml&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: apisix-whoami-route
  namespace: default
spec:
  http:
    - name: route-1
      match:
        paths:
          - /v2/*
      backends:
        - serviceName: whoami-service
          servicePort: http
      authentication:
        enable: true
        type: jwtAuth&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver con este objeto kubernetes le estamos diciendo a Apisix que
las peticiones a &lt;code&gt;/v2/*&lt;/code&gt; se envíen a nuestro servicio &lt;code&gt;whoami&lt;/code&gt; (como las de v1,
simplemente es por no crear otros servicios) pero que primero valide que
están autenticadas usando &lt;code&gt;jwtAuth&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras aplicar este objeto veremos que &lt;strong&gt;toda peticion a v2 sin un JWT es rechazada&lt;/strong&gt;
por APISIX sin llegar a ejecutarse la llamada al servicio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ahora bien, APISIX puede comprobar que viene la cabecera e incluso parsear el
JWT pero no tiene forma de validar que ha sido nuestro servicio &lt;code&gt;user-service&lt;/code&gt;
quien lo ha creado y que no es un intento de colarnos un JWT falso.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para eso configuraremos el plugin JWTAuth indicando la clave a usar para verificarlo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
En lugar de usar claves y passwords en claro como estoy haciendo
en estos artículos, lo suyo es usar algún servicio de Secrets que ofrezca el
cluster (y que soporte Apisix)
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/jagedn/apisix-example/blob/main/k3d/04-jwt/apisix-jwt-consumer.yml&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example/blob/main/k3d/04-jwt/apisix-jwt-consumer.yml&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
  name: jwt-consumer
spec:
  authParameter:
    jwtAuth:
      value:
        key: main  &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
        secret: MY_APPLICATION_JWT_SECRET_KEY_DUMMY &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;key=main está &quot;a pelo&quot; en customer service. No sé muy bien porqué
APISIX lo necesita&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;MY_APPLICATION_JWT_SECRET_KEY_DUMMY es la clave que hemos usado en
user-service para firmar los JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un ApisixConsumer representa a un cliente y APISIX maneja diferentes tipos
de consumers. Por ejemplo basicAuth sirve para identificar a usuarios
concretos mediante user+password, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ahora ya podemos usar el JWT contra &lt;code&gt;v2&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ TOKEN=$(http localhost:8881/login username=jorgepayaso password=password | jq -r .access_token)

$ http localhost:8881/v2/soy/yo Authorization:&quot;Bearer $TOKEN&quot;
HTTP/1.1 200 OK
GET /v2/soy/yo HTTP/1.1
Host: localhost:8881&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;identificar_el_usuario&quot;&gt;Identificar el usuario&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si has seguido hasta aquí la explicación (y si yo he conseguido explicarme),
tenemos implementado un api gateway que protege las rutas que digamos mediante
una validación JWT generado por nosotros.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo uno de los problemas comunes en una arquitectura microservicios
es que la petición, al ser enrutada al servicio en cuestión, necesita en la
mayoría de los casos ser &quot;personalizada&quot; con algún tipo de identificación
del usuario que la está realizando.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En un &quot;monolito&quot; lo normal es que deleguemos en el framework en el que está
implementado la autorización y la autentificación de tal forma que el framework
ante cada petición comprueba la firma del JWT y extrayendo el sub (por ejemplo)
del token puede ir a la base de datos y obtener toda la info del usuario en
cuestión.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En microservicios lo &quot;normal&quot; es adjuntar en una cabecera especial el ID del
usuario y dejar a cada servicio que lidie con ello. En la mayoría de los casos
este ID es suficiente para que el servicio pueda realizar su trabajo. En otros
usará este ID para invocar al servicio de usuarios y que le devuelva más
información como la fecha de creación, si está al orden de pago, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En nuestro ejemplo lo que vamos a hacer es que APISIX, una vez validado el JWT,
parsee el payload y nos incluya el &lt;code&gt;sub&lt;/code&gt; en una cabecera X-USER-ID que enviará
junto con la petición al microservicio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello simplemente &quot;enriqueceremos&quot; la ruta protegida &lt;code&gt;v2&lt;/code&gt; y añadiremos unas
líneas de código en el lenguaje Lua que es el que usa Apisix:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/jagedn/apisix-example/blob/main/k3d/05-jwt-user-id/apisix-secure-whoami.yml&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example/blob/main/k3d/05-jwt-user-id/apisix-secure-whoami.yml&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: apisix-whoami-route
  namespace: default
spec:
  http:
    - name: route-1
      match:
        paths:
          - /v2/*
      backends:
        - serviceName: whoami-service
          servicePort: http
      authentication:
        enable: true
        type: jwtAuth
      plugins:
        - name: &quot;serverless-post-function&quot;
          enable: true
          config:
            functions:
              - |
                -- probably this function can be placed in another file
                function parseJWTPayload(conf, ctx)
                    -- Import neccessary libraries
                    local core  = require(&quot;apisix.core&quot;)
                    local jwt      = require(&quot;resty.jwt&quot;)
                    -- Parse jwt
                    local sub_str  = string.sub
                    local jwt_token = core.request.header(ctx, &quot;authorization&quot;)
                    local prefix = sub_str(jwt_token, 1, 7)
                    if prefix == &apos;Bearer &apos; or prefix == &apos;bearer &apos; then
                        jwt_token = sub_str(jwt_token, 8)
                    end
                    local jwt_obj = jwt:load_jwt(jwt_token)
                    -- Set x-user-id header
                    core.request.set_header(ctx, &quot;X-USER-ID&quot;, jwt_obj.payload.sub)
                end
                -- this is the function to call
                return function(conf, ctx)
                  return parseJWTPayload(conf, ctx)
                end&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Todavía tengo que investigar cómo/dónde ubicar la funcion parseJWTPayload
para poderla reutilizar en otras rutas&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente lo que hacemos es decirle a Apisix que una vez ejecutado el plugin
jwtAuth nos ejecute otro de sus plugins, &lt;code&gt;serverless-post-function&lt;/code&gt;, el cual
puede acceder a los datos de la petición y modificarlos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En nuestro caso por ejemplo &quot;enriquecemos&quot; el request añadiendo una nueva
cabecera que el microservicio puede usar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;core.request.set_header(ctx, &quot;X-USER-ID&quot;, jwt_obj.payload.sub)&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez aplicado en el cluster los cambios podemos observar que nuestro whoami
servicio recibe una cabecera X-USER-ID diferente segun el usuario con el que generemos el token:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;username=jorgepayaso&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ TOKEN=$(http localhost:8881/login username=jorgepayaso password=password | jq -r .access_token)

$ http localhost:8881/v2/soy/yo Authorization:&quot;Bearer $TOKEN&quot;
HTTP/1.1 200 OK
Content-Length: 729
Content-Type: text/plain; charset=utf-8
Date: Fri, 07 Apr 2023 16:21:22 GMT
Server: APISIX/3.2.0

Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.195
IP: fe80::d4f6:4dff:fe1f:75af
RemoteAddr: 10.42.0.191:59322
GET /v2/soy/yo HTTP/1.1
Host: localhost:8881
User-Agent: HTTPie/2.6.0
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb3JnZXBheWFzbyIsIm5iZiI6MTY4MDg4NDQ4MCwicm9sZXMiOltdLCJpc3MiOiJtaWNyb25hdXRndWlkZSIsImV4cCI6MTY4MDg4ODA4MCwiaWF0IjoxNjgwODg0NDgwLCJrZXkiOiJtYWluIn0.o-yU9BSYyw314oPN_KZFLxgmqXOT2IQ9smDlfmC28Ss
X-Forwarded-For: 10.42.0.1, 10.42.0.187
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.187
X-User-Id: jorgepayaso&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;username=pepitopalotes&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ TOKEN=$(http localhost:8881/login username=pepitopalotes password=password | jq -r .access_token)

$ http localhost:8881/v2/soy/yo Authorization:&quot;Bearer $TOKEN&quot;

HTTP/1.1 200 OK
Content-Length: 734
Content-Type: text/plain; charset=utf-8
Date: Fri, 07 Apr 2023 16:22:06 GMT
Server: APISIX/3.2.0

Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.195
IP: fe80::d4f6:4dff:fe1f:75af
RemoteAddr: 10.42.0.191:59322
GET /v2/soy/yo HTTP/1.1
Host: localhost:8881
User-Agent: HTTPie/2.6.0
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwZXBpdG9wYWxvdGVzIiwibmJmIjoxNjgwODg0NTI0LCJyb2xlcyI6W10sImlzcyI6Im1pY3JvbmF1dGd1aWRlIiwiZXhwIjoxNjgwODg4MTI0LCJpYXQiOjE2ODA4ODQ1MjQsImtleSI6Im1haW4ifQ.-uFlClK5kmjMYWK4F-jZFwRTwvM8vXQEuKl5OzkhRdY
X-Forwarded-For: 10.42.0.1, 10.42.0.187
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.187
X-User-Id: pepitopalotes&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Securizando rutas Apisix con JWT</summary>
    </entry>
    <entry>
        <title>Primeros pasos con Apisix en Kubernetes</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/apisix-1.html"/>
        <updated>2023-03-28T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/apisix-1.html</id>
        <category term="apisix"/>
        <category term="kubernetes"/>
        <category term="apigateway"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A poco que la arquitectura de tu producto (no hablo de la de tu aplicación, sino de la del producto) se base en
microservicios, bien porque así la diseñasteis desde el principio o bien porque estáis en proceso de migración,
seguramente alguien de tu equipo se estará peleando con un Api Gateway&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;An API Gateway is the traffic manager that interfaces with the actual backend service or data, and applies policies,
authentication, and general access control for API calls to protect valuable data.
An API gateway is the way you control access to your back-end systems and services, and it was designed to optimize
communication between external clients and your backend services, giving your clients a seamless experience&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&amp;#8201;&amp;#8212;&amp;#8201;&lt;a href=&quot;https://www.tibco.com/reference-center/what-is-an-api-gateway&quot; class=&quot;bare&quot;&gt;https://www.tibco.com/reference-center/what-is-an-api-gateway&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un apigw es algo más que un simple proxy hacia los servicios de backend, pues también lo podemos usar para agregar
varias llamadas a los servicios en una única respuesta, así como también se encarga de la autentificación, cache, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otro lado, Kubernetes se ha convertido en el orquestador por excelencia en las soluciones de hoy en día sin
importar el tamaño de la misma (aunque algunos verán una barbaridad usar kubernetes en una aplicación pequeña yo
personalmente sí lo veo porque hoy en día existen implementaciones muy ligeras que te permitirán correr la aplicación
y podrás aprovechar un montón de herramientas disponibles para este entorno)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues en este post vamos a ver cómo &quot;desplegar&quot; una aplicación con microservicios en kubernetes &lt;strong&gt;en nuestro local&lt;/strong&gt;,
es decir, lo que pretendo es jugar con las herramientas disponibles para comprender cómo funcionan los diferentes
componentes de la solución sin necesidad de invertir mucho tiempo (ni dinero). Más adelante, según disponibilidad, iré
ampliando esta serie de post usando otras plataformas y explorando otras herramientas, &lt;strong&gt;siempre desde el punto de vista
de aprender&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Ignoro si todo lo que vamos a ver funcionaría en un Windows. En un Linux debería funcionar sin
problema y probablemente en un Mac también&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;docker (a estas alturas quién no tiene instalado docker en su máquina)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;kubectl, es una aplicacion de consola que se instala facilmente siguiendo &lt;a href=&quot;https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/&quot; class=&quot;bare&quot;&gt;https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;k9s, un kubectl supervitaminado, yo no puedo vivir ya sin él&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;k3d es una implementación muy ligera de kubernetes que se instala siguiendo &lt;a href=&quot;https://k3d.io/v5.4.9/&quot; class=&quot;bare&quot;&gt;https://k3d.io/v5.4.9/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;helm, es una herramienta que &quot;recubre&quot; kubectl y mediante plantillas podemos personalizar y desplegar en el cluster
aplicaciones muy complejas con un sólo comando&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;crear_el_cluster&quot;&gt;Crear el cluster&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;K3d (hay otras opciones igual de ligeras) nos va a servir para crear nuestro cluster en local. Sería como tener el
gestor de contenedores de Amazon de EKS en tu local así que lo primero va a ser crear un cluster:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;k3d cluster create example  -p &quot;8881:80@loadbalancer&quot;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Voy a usar el puerto 8881 de &lt;strong&gt;mi maquina&lt;/strong&gt; como puerto de acceso al balanceador que se encuentra &lt;strong&gt;dentro&lt;/strong&gt; del
cluster y acceder así a los servicios que despliegue en él. Si usas ese puerto para tus cosas usa otro que esté libre&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;INFO[0000] portmapping &apos;8881:80&apos; targets the loadbalancer: defaulting to [servers:*:proxy agents:*:proxy]
INFO[0000] Prep: Network
INFO[0000] Created network &apos;k3d-example&apos;
INFO[0000] Created image volume k3d-example-images
INFO[0000] Starting new tools node...
INFO[0000] Starting Node &apos;k3d-example-tools&apos;
INFO[0001] Creating node &apos;k3d-example-server-0&apos;
INFO[0001] Creating LoadBalancer &apos;k3d-example-serverlb&apos;
INFO[0001] Using the k3d-tools node to gather environment information
INFO[0001] HostIP: using network gateway 172.20.0.1 address
INFO[0001] Starting cluster &apos;example&apos;
INFO[0001] Starting servers...
INFO[0001] Starting Node &apos;k3d-example-server-0&apos;
INFO[0005] All agents already running.
INFO[0005] Starting helpers...
INFO[0005] Starting Node &apos;k3d-example-serverlb&apos;
INFO[0011] Injecting records for hostAliases (incl. host.k3d.internal) and for 2 network members into CoreDNS configmap...
INFO[0013] Cluster &apos;example&apos; created successfully!
INFO[0013] You can now use it like this:
kubectl cluster-info&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Comprobamos que el cluster efectivamente se ha creado y vemos el puerto que nos ha asignado (que seguramente
NO coincidirá con el tuyo)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl cluster-info&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Kubernetes control plane is running at https://0.0.0.0:39137
CoreDNS is running at https://0.0.0.0:39137/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://0.0.0.0:39137/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy

To further debug and diagnose cluster problems, use &apos;kubectl cluster-info dump&apos;.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a comprobar que accedemos al cluster:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;curl -v localhost:8881&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;*   Trying 127.0.0.1:8881...
* Connected to localhost (127.0.0.1) port 8881 (#0)
&amp;gt; GET / HTTP/1.1
&amp;gt; Host: localhost:8881
&amp;gt; User-Agent: curl/7.81.0
&amp;gt; Accept: */*
&amp;gt;
* Mark bundle as not supporting multiuse
&amp;lt; HTTP/1.1 404 Not Found
&amp;lt; Content-Type: text/plain; charset=utf-8
&amp;lt; X-Content-Type-Options: nosniff
&amp;lt; Date: Tue, 28 Mar 2023 14:49:46 GMT
&amp;lt; Content-Length: 19
&amp;lt;
404 page not found
* Connection #0 to host localhost left intact&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Digamos que si has llegado hasta aquí es como si te hubieras creado una cuenta en AWS y le hubieras dado al botón
de crear un EKS&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;repositorio&quot;&gt;Repositorio&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El código de este post lo puedes encontrar en &lt;a href=&quot;https://github.com/jagedn/apisix-example/&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/apisix-example/&lt;/a&gt; aunque también puedes ir copiando
y pegando desde el post&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;monolito&quot;&gt;Monolito&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que tenemos nuestro monolito en una imagen docker tal que podemos desplegarla en nuestro cluster. Para este
ejemplo nuestro monolito va a ser una imagen pública que simplemente nos devuelve información sobre el container donde
se está ejecutando.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Creamos un fichero con la definición de nuestra aplicacion:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;whoami-deployment.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
      - name: whoami-container
        image: containous/whoami&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y la desplegamos en nuestro cluster:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f whoami-deployment.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;deployment.apps/whoami-deployment created&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Comprobamos que esta desplegada&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl get pods&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;NAME                                 READY   STATUS    RESTARTS   AGE
whoami-deployment-5d4fc76b57-2h8pl   1/1     Running   0          45s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nuestra aplicación está &quot;viva&quot; y ejecutandose en el cluster pero NO tenemos forma de pedirle que haga nada (sin tener
que hacer trucos) así que tenemos que crear y desplegar un servicio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;whoami-service.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  ports:
  - name: http
    targetPort: 80
    port: 80
  selector:
    app: whoami&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f whoami-service.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Básicamente hemos creado un objeto en el cluster que se encargará de recibir peticiones en un &lt;code&gt;port&lt;/code&gt; 80 y los reenviará
al container al &lt;code&gt;targetPort&lt;/code&gt; 80. Ahora mismo tenemos 1 pod y 1 service pero k8s nos permite que creemos
más réplicas del deployment por lo que podriamos tener n pod y 1 service de tal forma que el service se encargaría
de ir repartiendo las peticiones a cada pod&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, seguimos sin poder acceder a nuestra aplicación &quot;desde fuera&quot;. Para ello necesitamos desplegar un &lt;em&gt;Ingress&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;whoami-ingress.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-ingress
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: whoami-service
            port:
              name: http&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f whoami-ingress.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y ahora &lt;strong&gt;por fín&lt;/strong&gt; ya podemos acceder a nuestro monolito&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;curl  localhost:8881/mira/mama/un/monolito

Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.9
IP: fe80::6cc5:9aff:fe51:9a19
RemoteAddr: 10.42.0.8:40276
GET /mira/mama/un/monolito HTTP/1.1
Host: localhost:8881
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;apisix&quot;&gt;APISIX&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Perfecto, ya tenemos nuestro producto en producción pero el nuevo arquitecto viene con otras ideas y preconiza que
el monolito nos va a ralentizar por lo que debemos evolucionar a una arquitectura microservicios. Perfecto (estoy
de acuerdo con él) así que mientras los programadores empiezan a crear nuevos servicios nosotros vamos a ir
implementando un API Gateway que sirva para consumir sus endpoints (como ya hemos dicho sabemos que un simple
proxy a los servicios no será suficiente)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Existen varias formas de instalar Apisix en nuestro cluster pero nosotros vamos a usar la más fácil (no por ello
incompleta) usando el chart de Helm disponible&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si seguimos la página oficial, &lt;a href=&quot;https://apisix.apache.org/docs/apisix/installation-guide/&quot; class=&quot;bare&quot;&gt;https://apisix.apache.org/docs/apisix/installation-guide/&lt;/a&gt;, veremos que consiste en
ejecutar 3 comandos, pero primero vamos a preparar un fichero de configuración adecuado para nuestro caso&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;values.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;gateway:
  type: NodePort

ingress-controller:
  enabled: true
  config:
    apisix:
      serviceNamespace: apisix&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con este fichero le vamos a decir a Helm que nos instale Apisix &quot;en modo NodePort&quot; (necesario para que k3d luego
pueda acceder a él sin problemas) y que también nos instale su &lt;em&gt;controller&lt;/em&gt; para ingress. Este será el encargado
de que podamos crear rutas y reglas en apisix usando ficheros de kubernetes de forma fácil (y versionada)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;voy a usar el namespace &lt;strong&gt;apisix&lt;/strong&gt; donde desplegar todos los artefactos relacionados con apisix, pero le puedes
dar otro nombre o incluso usar el &lt;em&gt;default&lt;/em&gt;, aunque lo veo un poco &quot;guarro&quot;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente ejecutaremos estos 3 comandos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;helm repo add apisix https://charts.apiseven.com
helm repo update
helm install apisix apisix/apisix --create-namespace  --namespace apisix -f values.yml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(Fíjate que indico el fichero &quot;-f values.yml&quot; que es como he llamado al fichero de configuración de helm para
ajustar la ínstalación de Apisix a mi medida)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo va bien, Apisix va a necesitar un minuto aprox para inicializar todas sus cosas. Vamos a utilizar &lt;em&gt;k9s&lt;/em&gt; para
comprobarlo. Ejecutamos en un terminal &lt;code&gt;k9s&lt;/code&gt; y deberíamos ver todos los pods del cluster:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2023/k9s_1.png&quot; alt=&quot;k9s 1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Según vayan terminando de inicializarse cada componente iremos viendo que pasan de rojo a azul (en mi terminal al menos)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez comprobado que todos han inicializado bien saldremos con &amp;#8230;&amp;#8203;. síiiiii &quot;dos puntos q&quot;, al más puro estilo de vi&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Lo que ha hecho helm ha sido crear un montón de recursos nuevos en nuestro cluster, además de crearnos el
namespace y desplegar en él los diferentes componentes.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A grandes rasgos la arquitectura de Apisix se divide:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;componente stateful que guarda la configuración (etcd)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;componente admin que gestiona esta configuración&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;componente gateway el encargado de gestionar las peticiones&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ingress controller, un operador que &quot;convierte&quot; nuestros recursos k8s en configuración apisix&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Lo bueno de Apisix es que con esta arquitectura NO necesitas ninguna base de datos&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Comprobamos que nuestro &quot;monolito&quot; sigue funcionando (pues en principio no hemos hecho nada que le afecte)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;curl  localhost:8881/sigo/vivo

Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.9
IP: fe80::6cc5:9aff:fe51:9a19
RemoteAddr: 10.42.0.8:40276
GET /sigo/vivo HTTP/1.1
Host: localhost:8881
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;desplegando_rutas_en_nuestro_apisix&quot;&gt;Desplegando rutas en nuestro Apisix&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a indicarle a Apisix que queremos enrutar todas las peticiones que lleguen a una determinada ruta (por ejemplo
todas /*) hasta nuestro monolito:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;apisix-whoami.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: apisix-whoami-route
  namespace: default
spec:
  http:
    - name: route-1
      match:
        paths:
          - /*
      backends:
        - serviceName: whoami-service
          servicePort: http&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f apisix-whoami.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este recurso es detectado por el controller de Apisix (al ser un &lt;code&gt;kind ApisixRoute&lt;/code&gt;) y lo aplica en su configuración
de tal forma que &lt;strong&gt;todas&lt;/strong&gt; las peticiones que cumplan la condición &quot;match&quot; se envíen al servicio &lt;code&gt;whoami-service&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como Apisix todavía no está recibiendo peticiones del exterior podemos aplicar esta configuración sin ningún problema.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;activando_nuestro_apigw&quot;&gt;Activando nuestro APIGW&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ha llegado el momento de &quot;intercambiar&quot; nuestro monolito por nuestro ApiGW. Como estamos en un cluster de ejemplo
y nos podemos permitir un pequeño downtime simplemente quitaremos un ingress y lo sustituiremos por otros&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;apisix-ingress.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: apisix-ingress
  namespace: apisix
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: apisix-gateway
            port:
              name: apisix-gateway&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;helm nos creó, en el namespace que le dijimos, un servicio llamado &lt;code&gt;apisix-gateway&lt;/code&gt; el cual está
escuchando en un puerto llamado &lt;code&gt;apisix-gateway&lt;/code&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quitamos el enrutado hacia el monolito&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl delete -f whoami-ingress.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Comprobamos que lo &quot;hemos roto&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;curl  localhost:8881/sigo/vivo
404 page not found&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y corremos a activar el del apigw&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f apisix-ingress.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Comprobamos que lo &quot;hemos arreglado&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;curl  localhost:8881/sigo/vivo
Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.9
IP: fe80::6cc5:9aff:fe51:9a19
RemoteAddr: 10.42.0.19:58848
GET /sigo/vivo HTTP/1.1
Host: localhost:8881
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1, 10.42.0.8
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.8&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien hemos &quot;insertado&quot; por delante del monolito una nueva pieza que será la que se encargue ahora
de las peticiones desde el exterior y decidirá en base a su configuración cómo responder&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;configurando_rutas&quot;&gt;Configurando rutas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;¿Cómo podemos estar seguros de que todo esto es cierto? Supongamos que en la nueva arquitectura queremos que la
vieja aplicación siga ofreciendo servicios pero bajo una nueva ruta &lt;code&gt;/v1&lt;/code&gt; por ejemplo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Reconfiguramos el ApisixRoute&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;apisix-whoami.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: apisix-whoami-route
  namespace: default
spec:
  http:
    - name: route-1
      match:
        paths:
          - /v1/*
      backends:
        - serviceName: whoami-service
          servicePort: http&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(Fijate que el paths lo hemos cambiado a /v1)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y lo aplicamos &lt;code&gt;kubectl apply -f apisix-whoami.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si ahora intentamos acceder a la aplicación veremos que no está disponible&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;curl  localhost:8881/sigo/vivo
{&quot;error_msg&quot;:&quot;404 Route Not Found&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(Fíjate que el body de respuesta ha cambiado porque ahora es el Apisix el que lo está devolviendo)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero sin embargo tenemos a monolito escuchando en /v1&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;curl  localhost:8881/v1/sigo/vivo
Hostname: whoami-deployment-5d4fc76b57-2h8pl
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.9
IP: fe80::6cc5:9aff:fe51:9a19
RemoteAddr: 10.42.0.19:42206
GET /v1/sigo/vivo HTTP/1.1
Host: localhost:8881
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.0.1, 10.42.0.8
X-Forwarded-Host: localhost:8881
X-Forwarded-Port: 8881
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-7cd4fcff68-9ksw7
X-Real-Ip: 10.42.0.8&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;disclaimer&quot;&gt;Disclaimer&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente este post es a modo de primeros pasos con Apisix y no pretende cubrir todos los aspectos del mismo. De
hecho me faltan todavía muchos conocimientos y cosas a probar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, a diferencia de otros post que encontrarás en Internet, está enfocado totalmente a kubernetes mientras
que casi todos los que he encontrado lo hacen usando Docker&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Probando a desplegar apisix en kubernetes</summary>
    </entry>
    <entry>
        <title>Creando mapas con Notion y Github</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/notion-maps.html"/>
        <updated>2023-03-12T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/notion-maps.html</id>
        <category term="notion"/>
        <category term="maps"/>
        <category term="github"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Notion es un software de gestión de proyectos y para tomar notas&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Continuando con la &quot;serie&quot; de post sobre Notion y aprovechando que un amigo ha publicado un componente web para
crear maps usando LeafLet de forma realmente fácil voy a contar en este post cómo podemos usar
Notion como backend para geolocalizar puntos y hacer que Github publique un mapa con ellos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post no voy a volver a contar cómo crearse cuenta o cómo crear una integración. Para ello te remito
a los otros post del blog donde (espero) se explica. Así que para este post partimos de que tenemos
una cuenta &lt;strong&gt;free&lt;/strong&gt; en Notion y que hemos creado una integración&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Al final del post pongo un enlace al proyecto ejemplo que te puedes clonar para no tener que
picarte el código&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aprovechando las capacidades de Notion de que cada página es una &quot;base de datos&quot; vamos a crear una donde
vamos a crear las siguientes columnas:&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Name&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;String&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Presentación&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;String&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;GoogleMap&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;URL&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y vamos a añadir algunas entradas:&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Madrid&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1969&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Donde todo empezó&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://www.google.com/maps/place/C.+Pescara,+28032+Madrid/@40.4253096,-3.6319141,14z/data=!4m5!3m4!1s0xd4225809d6376ef:0xbd6238a7e387bba!8m2!3d40.4152972!4d-3.6246032&quot; class=&quot;bare&quot;&gt;https://www.google.com/maps/place/C.+Pescara,+28032+Madrid/@40.4253096,-3.6319141,14z/data=!4m5!3m4!1s0xd4225809d6376ef:0xbd6238a7e387bba!8m2!3d40.4152972!4d-3.6246032&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Costa Rica&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1999&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Primer viaje al extranjero&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://www.google.com/maps/place/Tortuguero,+Lim%C3%B3n+Province,+Costa+Rica/@10.5445739,-83.5075092,16z/data=!3m1!4b1!4m5!3m4!1s0x8fa7560928a2e741:0x7b7e58e66658b6eb!8m2!3d10.5424838!4d-83.5023552&quot; class=&quot;bare&quot;&gt;https://www.google.com/maps/place/Tortuguero,+Lim%C3%B3n+Province,+Costa+Rica/@10.5445739,-83.5075092,16z/data=!3m1!4b1!4m5!3m4!1s0x8fa7560928a2e741:0x7b7e58e66658b6eb!8m2!3d10.5424838!4d-83.5023552&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Nepal&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2004&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Nepal y Tibet, un viaje a otro universo&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://www.google.com/maps/place/Kathmandu+44600,+Nepal/@27.7090319,85.2911134,13z/data=!3m1!4b1!4m13!1m7!3m6!1s0x3995e8c77d2e68cf:0x34a29abcd0cc86de!2sNepal!3b1!8m2!3d28.394857!4d84.124008!3m4!1s0x39eb198a307baabf:0xb5137c1bf18db1ea!8m2!3d27.7169653!4d85.3239441&quot; class=&quot;bare&quot;&gt;https://www.google.com/maps/place/Kathmandu+44600,+Nepal/@27.7090319,85.2911134,13z/data=!3m1!4b1!4m13!1m7!3m6!1s0x3995e8c77d2e68cf:0x34a29abcd0cc86de!2sNepal!3b1!8m2!3d28.394857!4d84.124008!3m4!1s0x39eb198a307baabf:0xb5137c1bf18db1ea!8m2!3d27.7169653!4d85.3239441&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como puedes ver la idea es añadir tantos lugares como queramos, poner una descripcion y una url copiada directamente
desde GoogleMaps la cual parsearemos para extraer las coordenadas. Esta es una de las &lt;strong&gt;gracias&lt;/strong&gt; de este
script, facilitar el añadir entradas sin tener que estar buscando las posiciones, sino usar una url donde
aparezcan&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya expliqué en los post relativos a Notion, esta base de datos tiene un identificador que puedes ver
en la propia URL de Notion y que usaremos para extraer la información desde Github&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;github&quot;&gt;Github&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a usar un repositorio de Github para ejecutar un script que extraerá la información desde Notion
y generará una página HTML+JS con un mapa mundi. Aprovecharemos la funcionalidad de Github pages para
publicarlo en Internet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Crearemos un repositorio, por ejemplo, &quot;mis-andanzas&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;En settings añadiremos dos secrets:&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DATABASE, con el ID de nuestra página de Notion&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NOTION_TOKEN, con el token que nos proporció Notion a la hora de crear nuestra integration&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;En settings configuraremos &quot;Pages&quot; indicando que el source será un Github Actions (no lo crees,
ya te paso el código yo)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con esto tenemos preparado el repositorio Github (recuerda que al final del post te paso un enlace
para que te lo clones si no quieres hacerlo desde cero)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script en sí es muy sencillo. Simplemente usará las librerías JS de notion para conectarse a la página
de Notion y volcar en un fichero &quot;.js&quot; las filas como si fueran puntos de un mapa usando el formato
GeoJSON. Como ya he comentado, la gracia del script es que parsea las URLs de GoogleMap para hacerlo
más cómodo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;notion2map.js&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const {
    Client
} = require(&quot;@notionhq/client&quot;);

const {
    env
} = require(&apos;process&apos;);

require(&apos;dotenv&apos;).config();

// Initializing a client
const notion = new Client({
    auth: process.env.NOTION_TOKEN,
})
const myPage = await notion.databases.query({
        database_id: process.env.DATABASE
    });
...
 myPage.results.forEach(i =&amp;gt; {
...
        const title = i.properties[&apos;Name&apos;];
        if (title.title.length == 0)
            return;

        const name = title.title[0].plain_text;
        const url = i.properties[&apos;GoogleMap&apos;].url;
        const presentation = i.properties[&apos;Presentation&apos;].rich_text.map(m =&amp;gt; m.plain_text).join(&apos; &apos;);
        const coordinates = googlemapExpr.exec(url);
        const lat = coordinates[1];
        const lng = coordinates[2];
...
        fs.appendFileSync(GEO_FILE, `{
            type: &quot;Feature&quot;,
            geometry: {
                type: &quot;Point&quot;, coordinates: [${lng}, ${lat}]
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script completo lo puedes ver en &lt;a href=&quot;https://github.com/jagedn/mis-andanzas/blob/main/notion2map.js&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/mis-andanzas/blob/main/notion2map.js&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente generamos un fichero Javascript &quot;creado al vuelo&quot; que contiene un array con los puntos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;web_component&quot;&gt;Web Component&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Miguel Pagola, &lt;a href=&quot;https://github.com/migupl&quot; class=&quot;bare&quot;&gt;https://github.com/migupl&lt;/a&gt;, se ha creado un componente web para generar mapas de forma muy simple,
&lt;a href=&quot;https://github.com/migupl/vanilla-js-web-component-leaflet-geojson&quot; class=&quot;bare&quot;&gt;https://github.com/migupl/vanilla-js-web-component-leaflet-geojson&lt;/a&gt;, y muy bien documentado así que lo vamos a usarlo
como &quot;plantilla&quot; para nuestro mapa, en lugar de complicarnos con los objetos de LeafLeft&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
...
    &amp;lt;script type=&quot;module&quot; src=&quot;https://migupl.github.io/vanilla-js-web-component-leaflet-geojson/components/leaflet-map-component/leaflet-map.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;data.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
...
&amp;lt;body&amp;gt;
    &amp;lt;header&amp;gt;
        &amp;lt;div class=&quot;container&quot;&amp;gt;
            &amp;lt;h1&amp;gt;Mis Andanzas&amp;lt;/h1&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/header&amp;gt;
    &amp;lt;section id=&quot;setting-up&quot;&amp;gt;
        &amp;lt;leaflet-map id=&quot;notion-map&quot; fitToBounds=&quot;true&quot; flyToBounds=&quot;true&quot;&amp;gt;&amp;lt;/leaflet-map&amp;gt;
    &amp;lt;/section&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script&amp;gt;
    window.onload = (event) =&amp;gt; {
        const map = document.getElementById(&apos;notion-map&apos;);

        const eventBus = map.eventBus;

        eventBus.dispatch(&apos;x-leaflet-map-geojson-add&apos;, {
            leafletMap: map,
            geojson: features
        });
    }
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente cargamos su javascript junto con el generado por el script y una vez que se carga la página, le indicamos
que nos muestre los puntos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;No te tienes que preocupar por centrar el mapa, indicar zoom por defecto, ni nada parecido!!!&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;github_action&quot;&gt;Github Action&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para terminar crearemos un github action (&quot;static.yml&quot; en mi proyecto) que ejecute el script y genere las páginas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para publicarlas usaremos un action disponible al que le dices la ruta que quieres publicar y Github las publica&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jobs:
  # Single deploy job since we&apos;re just deploying
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    env:
      NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
      DATABASE: ${{ secrets.DATABASE }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Pages
        uses: actions/configure-pages@v3
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 14.21.3
      - run: npm install
      - run: npm run generate
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          # Upload entire repository
          path: &apos;docs&apos;
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Recuerda NO usar los tokens en claro en tus actions, sino usar secrets&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez ejecutado el pipeline Github publicará una página accesible desde Internet como&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://jagedn.github.io/mis-andanzas/&quot; class=&quot;bare&quot;&gt;https://jagedn.github.io/mis-andanzas/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;todo&quot;&gt;TODO!!!&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Queda pendiente para próximas versiones algunas ideas que me han planteado/surgido:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Añadir imágenes en el tooltip&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Poder crear rutas en lugar de puntos sueltos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;proyecto&quot;&gt;Proyecto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proyecto está publicado en &lt;a href=&quot;https://github.com/jagedn/mis-andanzas&quot; class=&quot;bare&quot;&gt;https://github.com/jagedn/mis-andanzas&lt;/a&gt; y te lo puedes &quot;forkear&quot; o clonar
y modificar a tu gusto. Si te fijas, los tokens y secretos están ocultos con lo que tu información
está protegida&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>creando un mapa mundicon Notion y Github</summary>
    </entry>
    <entry>
        <title>Publicar en Linkedin con Groovy</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/groovy-linkedin.html"/>
        <updated>2023-02-14T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/groovy-linkedin.html</id>
        <category term="linkedin"/>
        <category term="groovy"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Si no has leído el articulo anterior sobre publicar en Linkedin usando Gmail,
te animo a que lo hagas para tener más contexto&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Linkedin, la plataforma del postureo, tiene un API que puedes usar, entre otras cosas, para publicar textos, artículos,
imágenes, etc de tal forma que puedes usar herramientas externas o incluso hacerte
tu propia integración. En este post vamos a investigar cómo podemos publicar en esta red usando un script en Groovy
que podemos planificar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En concreto vamos a ver cómo publicar diariamente la efeméride del CalendarioCientificoEscolar (del cual ya he hablado
en alguna otra entrada del blog)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cuenta en Linkedin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Groovy (y Java) instalados&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Un token de Linkedin (ver el post &quot;Publicar en Linkedin desde Gmail&quot; para saber cómo obtenerlo)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El CalendarioCientificoEscolar es un proyecto open source que mantiene en una serie de ficheros CSV una efemeride
por día, con textos en diferentes idiomas y una imagen para cada una. La idea es parsear este fichero, buscar la
efemeride del día asi como la imagen asociada y publicarlo en Linkedin usando un script un Groovy&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;En el blog ya he comentado en alguna entrada sobre este proyecto por si quieres conocer más de él&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este es el script a ejecutar. A continuación comentaré las partes más importantes&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;@Grab(&apos;io.github.http-builder-ng:http-builder-ng-core:1.0.4&apos;)
@Grab(group=&apos;javax.mail&apos;, module=&apos;mail&apos;, version=&apos;1.4.7&apos;)

import static groovyx.net.http.MultipartContent.multipart
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import groovyx.net.http.CoreEncoders
import groovyx.net.http.*
import static java.util.Calendar.*

year = args.length &amp;gt; 0 ? args[0] as int : new Date()[YEAR]
month = args.length &amp;gt; 1 ? args[1] as int : new Date()[MONTH]+1
day = args.length &amp;gt; 2 ? args[2] as int : new Date()[DAY_OF_MONTH]

println &quot;Processing $year/$month/$day&quot;

author=System.getenv(&quot;LINKEDIN_USER&quot;)
accessToken=System.getenv(&quot;LINKEDIN_TOKEN&quot;)

if( !author || !accessToken ){
    println &quot;Necesito la configuracion de telegram&quot;
    return
}

http = configure{
    request.uri = &quot;https://api.linkedin.com&quot;
    request.contentType = JSON[0]
    request.headers[&apos;Authorization&apos;] = &quot;Bearer $accessToken&quot;
    request.headers[&apos;X-Restli-Protocol-Version&apos;] = &apos;2.0.0&apos;
}

html = &quot;&quot;
img = &quot;&quot;

[&apos;es&apos;:&apos;🇪🇸&apos;,
 &apos;en&apos;:&apos;🏴󠁧󠁢󠁥󠁮󠁧󠁿&apos;,
 &apos;fra&apos;:&apos;☫&apos;,
 &apos;arab&apos;:&apos;🇸🇦&apos;,
 &apos;pt&apos;:&apos;🇵🇹&apos;,
 &apos;gal&apos;:&apos;🐙&apos;,
 &apos;astu&apos;:&apos;🐮&apos;,
 &apos;eus&apos;:&apos;🪨&apos;,
 &apos;cat&apos;:&apos;🌊&apos;,
 &apos;arag&apos;:&apos;⛰️&apos;,
// &apos;epo&apos;:&apos;🌍&apos;,
 ].each{ kv -&amp;gt;
    String lang = kv.key
    String emoji = kv.value
    String[]found

    if( new File(&quot;source/csv/${year}/${lang}.tsv&quot;).exists() ){
        new File(&quot;source/csv/${year}/${lang}.tsv&quot;).withReader{ reader -&amp;gt;
            reader.readLine()
            String line
            while( (line=reader.readLine()) != null){
                def fields = line.split(&apos;\t&apos;)
                if( fields.length != 5)
                    continue
                if( fields[0] as int == day &amp;amp;&amp;amp; fields[1] as int == month &amp;amp;&amp;amp; fields[2] as int == year){
                    found = fields
                    break
                }
            }
        }
    }

    if(!found){
        println &quot;not found $year/$month/$day&quot;
        return
    }

    String title=  found[4].split(&apos;\\.&apos;).first()
	String body=  found[4].split(&apos;\\.&apos;).drop(1).join(&apos; &apos;)

    if( !img ){
        img = &quot;https://calendario-cientifico-escolar.gitlab.io/_/images/${year}/${found[3]}.png&quot;
    }

    html +=&quot;&quot;&quot;
$emoji $title

$body

&quot;&quot;&quot;
}


html += &quot;&quot;&quot;
Fuente: Calendario Cientifico Escolar, https://t.me/CalendarioCientifico
&quot;&quot;&quot;

json = http.post{
    request.uri.path = &quot;/v2/assets&quot;
    request.uri.query = [&quot;action&quot;:&quot;registerUpload&quot;]
    request.body = [
        &quot;registerUploadRequest&quot;: [
            &quot;recipes&quot;: [
                &quot;urn:li:digitalmediaRecipe:feedshare-image&quot;
            ],
            &quot;owner&quot;: &quot;urn:li:person:&quot;+author,
            &quot;serviceRelationships&quot;: [
                [
                    &quot;relationshipType&quot;: &quot;OWNER&quot;,
                    &quot;identifier&quot;: &quot;urn:li:userGeneratedContent&quot;
                ]
            ],
            &quot;supportedUploadMechanism&quot;:[
                &quot;SYNCHRONOUS_UPLOAD&quot;
            ]
        ]
    ]
}
uploadUrl = json.value.uploadMechanism[&quot;com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest&quot;].uploadUrl

someFile = File.createTempFile(&quot;img&quot;,&quot;png&quot;)
someFile.bytes = img.toURL().bytes

configure{
    request.uri = uploadUrl
    request.contentType = &apos;image/png&apos;
    request.headers[&apos;Authorization&apos;] = &quot;Bearer $accessToken&quot;
    request.headers[&apos;X-Restli-Protocol-Version&apos;] = &apos;2.0.0&apos;
    request.headers[&apos;Accept&apos;]=&apos;*/*&apos;
    request.body = someFile
    request.encoder(&apos;image/png&apos;){ ChainedHttpConfig config, ToServer req-&amp;gt;
        req.toServer(new ByteArrayInputStream(someFile.bytes))
    }
}.put()


post = [
    &quot;author&quot;: &quot;urn:li:person:&quot;+author,
    &quot;lifecycleState&quot;:&apos;PUBLISHED&apos;,
    &quot;visibility&quot;: [
        &quot;com.linkedin.ugc.MemberNetworkVisibility&quot;:&apos;PUBLIC&apos;
    ],
    &quot;specificContent&quot;:[
        &quot;com.linkedin.ugc.ShareContent&quot;:[
            &quot;shareCommentary&quot;:[
                &quot;text&quot;: html
            ],
            &quot;shareMediaCategory&quot; : &quot;IMAGE&quot;,
            &quot;media&quot;:[
                [status:&apos;READY&apos;,media: json.value.asset]
            ]
        ]
    ]
]

http.post{
    request.uri.path = &quot;/v2/ugcPosts&quot;
    request.body = post
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;calendario&quot;&gt;Calendario&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como he comentado el calendario se compone de varios ficheros cada uno con una efemeride en un idioma. Lo que hacemos
es crear un mapa con los idiomas disponibles e ir concatenando en una cadena los diferentes textos. Cada idioma
será un párrafo con un emoji al inicio:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[&apos;es&apos;:&apos;🇪🇸&apos;,
 &apos;en&apos;:&apos;🏴󠁧󠁢󠁥󠁮󠁧󠁿&apos;,
 &apos;fra&apos;:&apos;☫&apos;,
 &apos;arab&apos;:&apos;🇸🇦&apos;,
 &apos;pt&apos;:&apos;🇵🇹&apos;,
 &apos;gal&apos;:&apos;🐙&apos;,
 &apos;astu&apos;:&apos;🐮&apos;,
 &apos;eus&apos;:&apos;🪨&apos;,
 &apos;cat&apos;:&apos;🌊&apos;,
 &apos;arag&apos;:&apos;⛰️&apos;,
// &apos;epo&apos;:&apos;🌍&apos;,
 ].each{ kv -&amp;gt;
    String lang = kv.key
    String emoji = kv.value
    String[]found

    if( new File(&quot;source/csv/${year}/${lang}.tsv&quot;).exists() ){
        new File(&quot;source/csv/${year}/${lang}.tsv&quot;).withReader{ reader -&amp;gt;
...
    html +=&quot;&quot;&quot;
$emoji $title

$body

&quot;&quot;&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente una vez ejecutado el bucle tenemos una cadena con el texto que queremos publicar.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;librerías&quot;&gt;Librerías&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Las dos primeras lineas del script nos sirven para indicar las dependencias que tiene el mismo. En este caso
uso un proyecto &lt;code&gt;http-ng-builder&lt;/code&gt; que me encanta aunque ya está abandonado (creo) y la librería standard de javax mail
para poder subir la imagen.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;linkedin&quot;&gt;Linkedin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este proyecto nos proporciona un DSL muy intuitivo para interactuar con servicios remotos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Preparamos un objeto &lt;code&gt;http&lt;/code&gt; con el token de acceso:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;http = configure{
    request.uri = &quot;https://api.linkedin.com&quot;
    request.contentType = JSON[0]
    request.headers[&apos;Authorization&apos;] = &quot;Bearer $accessToken&quot;
    request.headers[&apos;X-Restli-Protocol-Version&apos;] = &apos;2.0.0&apos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Le decimos a Linkedin que queremos subir una imagen:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;json = http.post{
    request.uri.path = &quot;/v2/assets&quot;
    request.uri.query = [&quot;action&quot;:&quot;registerUpload&quot;]
    request.body = [
        &quot;registerUploadRequest&quot;: [
            &quot;recipes&quot;: [
                &quot;urn:li:digitalmediaRecipe:feedshare-image&quot;
            ],
            &quot;owner&quot;: &quot;urn:li:person:&quot;+author,
            &quot;serviceRelationships&quot;: [
                [
                    &quot;relationshipType&quot;: &quot;OWNER&quot;,
                    &quot;identifier&quot;: &quot;urn:li:userGeneratedContent&quot;
                ]
            ],
            &quot;supportedUploadMechanism&quot;:[
                &quot;SYNCHRONOUS_UPLOAD&quot;
            ]
        ]
    ]
}
uploadUrl = json.value.uploadMechanism[&quot;com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest&quot;].uploadUrl&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Linkedin nos proporciona una URL (uploadURL) donde subir nuestra imagen mediante el método &lt;code&gt;put&lt;/code&gt;:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;configure{
    request.uri = uploadUrl
    request.contentType = &apos;image/png&apos;
    request.headers[&apos;Authorization&apos;] = &quot;Bearer $accessToken&quot;
    request.headers[&apos;X-Restli-Protocol-Version&apos;] = &apos;2.0.0&apos;
    request.headers[&apos;Accept&apos;]=&apos;*/*&apos;
    request.body = someFile
    request.encoder(&apos;image/png&apos;){ ChainedHttpConfig config, ToServer req-&amp;gt;
        req.toServer(new ByteArrayInputStream(someFile.bytes))
    }
}.put()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Publicamos el artículo indicando como atributo el ID de la imagen a adjuntar&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;post = [
    &quot;author&quot;: &quot;urn:li:person:&quot;+author,
    &quot;lifecycleState&quot;:&apos;PUBLISHED&apos;,
    &quot;visibility&quot;: [
        &quot;com.linkedin.ugc.MemberNetworkVisibility&quot;:&apos;PUBLIC&apos;
    ],
    &quot;specificContent&quot;:[
        &quot;com.linkedin.ugc.ShareContent&quot;:[
            &quot;shareCommentary&quot;:[
                &quot;text&quot;: html
            ],
            &quot;shareMediaCategory&quot; : &quot;IMAGE&quot;,
            &quot;media&quot;:[
                [status:&apos;READY&apos;,media: json.value.asset]
            ]
        ]
    ]
]

http.post{
    request.uri.path = &quot;/v2/ugcPosts&quot;
    request.body = post
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente este script es muy específico para el caso del Calendario pero es muy fácil extraer las partes necesarias
para poder crear uno que se adapte a nuestras necesidades y poder publicar en Linkedin de forma desatendida&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Siguiendo la saga de publicar automáticamente en Linkedin hoy vemos cómo hacerlo con un GroovyScript</summary>
    </entry>
    <entry>
        <title>Onboarding</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/onboarding.html"/>
        <updated>2023-01-05T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/onboarding.html</id>
        <category term="personal"/>
        <category term="onboarding"/>
        <content type="html">
            &lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hoy, 5 de enero de hace justo un montón de años (pero que muchos), el vecino me
consiguió un trabajillo de un día para poner las frutas escarchadas en los roscones
de la pastelería donde él trabajaba.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Obviamente, eran otros tiempos y era en &quot;negro&quot;, claro está.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como buen JASP, (si eres de mi quita recordarás la referencia a joven aunque sobradamente preparado),
me levanté superpronto y acudí dispuesto a darlo todo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Aquí coges los roscones, aquí está la fruta y los metes en estas cajas. Los vas apilando aquí.
No te entretengas que hay mucho currele&quot; fue mi onboarding. Tal cual me dio las instrucciones, tal
cual se fué a despachar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pinpan pinpan fui sacando roscones a toda mecha como buen becario aplicado
(admito que alguna frutilla caía en mi boca durante el proceso)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A media mañana, el pastelero con cara satisfecha y clientela casi despachada entro a supervisar la faena&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quiero recordar su cara enrojecida, pero sería mentir, mis recuerdos no llegan a tanto, pero sí
recuerdo cuando me gritó&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;pero qué estás haciendo????? En las de cuarto solo tienes que poner 4 frutillas y en las de medio 6!!!!!!&quot; (Cifras inventadas porque no me acuerdo ya)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin niguna instrucción previa y simplemente usando mi intuición de lo que a mí me gustaría ver en un roscón había puesto frutitas a discreción.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Su cabreo y desesperación fue en aumento según abría cajas y veía que me había aplicado a conciencia.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Creo recordar que aún así, al terminar el día, me pagó lo que le había dicho a mi madre que me daría.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;#onboarding&lt;/p&gt;
&lt;/div&gt;
        </content><summary>Una anecdota #AbueloCebolleta acerca del onboarding</summary>
    </entry>
    <entry>
        <title>Rutas por Madrid</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/rutas-por-madrid.html"/>
        <updated>2022-12-30T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/rutas-por-madrid.html</id>
        <category term="personal"/>
        <category term="groovy"/>
        <category term="javascript"/>
        <category term="asciidoctor"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;RutasPorMadrid es una publicación cuasi-semanal de rutas aleatorias visitando edificios y monumentos de la ciudad
(&lt;a href=&quot;https://blog.rutas-por-madrid.es&quot; class=&quot;bare&quot;&gt;https://blog.rutas-por-madrid.es&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A principios de este año (o así) estuve jugando un poco con librerías para trabajar con Mapas (sí, en
javascript) y aunque feas, publiqué algunas aplicaciones.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo subí TocamelaMadrid, una aplicación que con base en una lista de canciones geo posicionadas
que hablan de Madrid las muestra en un mapa creando áreas de Voronoy (áreas que cubren una superficie,
usando los puntos como vertices. Yo tampoco sabía que se llamaban así)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;También jugué un poco con la idea de hacerme un StoryMap: mostrar en una lista los lugares por
donde había estado de vacaciones y que según hicieras scroll por ellos te fuera mostrando en el mapa
donde estaban, incluso enseñando algún carrusel de fotos tomadas en ese sitio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Trasteando con el tema descubrí que es todo un universo. Por ejemplo, existen muchas librerías JS, y probablemente la
más conocida sea Leaflet, pero la librería solo manipula un mapa que tienes que
&quot;descargar&quot; de algún lado, por ejemplo de OpenStreetMap, pero quieres añadirle capas y así sucesivamente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;También aprendí, por ejemplo, que existe un formato GeoJSON definido por el W3C donde puedes especificar elementos
(lineas, puntos, polígonos, &amp;#8230;&amp;#8203;) geo posicionados, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con todo este chapurreo de librerías e ideas se me ocurrió echar un ojo al OpenData del Ayuntamiento de
Madrid, fuente de inspiración para muchas de mis tonterías y uno de ellos me dio la idea de RutasPorMadrid&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El catálogo en cuestión es un dataset con edificios (luego descubrí que hay uno similar de monumentos)
geo localizados, con descripciones y url a imágenes y se me encendió la bombilla: elegiría un punto
aleatorio entre ellos y buscaría los N elementos más cercanos para crear una ruta.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El buscar los puntos más cercanos a una posición es algo que había hecho en algún otro proyecto (
el bot de las gasolineras por ejemplo, donde me mandas tu position y busco la más cercana, o el de
las fuentes públicas) así que el algoritmo de ordenar, que sería lo más difícil, ya lo tenía. Todo lo
demás sería mi buen saber hacer&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este punto sabía que técnicamente no iba a presentar ninguna dificultad (tampoco estoy mandando un satélite a la luna)
así que lo que de verdad quería trabajar era el &quot;formato de entrega&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi cabeza veía una ficha en PDF donde poner la descripción del punto de partida, una foto, un mapa estático con los
puntos de la ruta así como el resto de la ruta (realmente en mi cabeza veía la plantilla de asciidoctor que usé para montar mi
CV cambiando algunos elementos y a correr)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para no complicarme mucho, lo enviaría como newsletter a gente que se suscribiera y así echaba un ojo al mundillo de
las newsletter y que quería probar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente, el crear el script que parsee los datos, elija un punto y cree la ruta me llevó 10 minutos, pero
enmaquetar el PDF me llevó media vida, pero bueno, quien hace lo que puede no está obligado a más&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El siguiente tema a pelear fue elegir una plataforma de newsletter. Soy consciente de que no esto no va a llegar a
una masa de lectores que me haga decantarme por plataformas que hacen de todo. Tampoco tiene ningún tema comercial
ni marketiniano así que MailChimp y similares las descarté. En realidad el requisito que le ponía a la plataforma
es que dispusiera de un API tal que pudiera automatizar el envío al máximo (así que olvídate de Substack)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al final me decanté por la más simple, TinyLetter con la cual simplemente mando un correo con lo que quiero publicar
y cuando está listo me envían un correo de aviso. Simplemente respondiéndolo tal cual se envía la newsletter&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero &amp;#8230;&amp;#8203; para crear la cuenta me piden un correo electrónico (obvio) y hombre, no era cuestión de usar el mío, así que
&amp;#8230;&amp;#8203; efectivamente, registré el &lt;code&gt;dominio rutas-por-madrid.es&lt;/code&gt;. Lo bueno es que era una oferta por unos pocos euros y
encima con cuenta de correo incluida&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ya tenía &quot;todo&quot; cuando haciendo las primeras pruebas me enfrenté a la realidad de que una ficha pdf puede estar muy
bien, pero el público quiere una página donde consultar toda la info más cómodamente. Así que me decidí por crear
un static site con Hugo (JBake me parecía demasiado para algo tan sencillo como tenía en mente)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así que bueno, más o menos todo automatizado desmenuzo las partes a continuación&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;stack_tecnológico&quot;&gt;Stack tecnológico&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El stack es un batiburrillo de tecnologías. Uso Groovy para parsear y generar rutas, pero también uso JavaScript
para los mapas. Uso Hugo, que es Go, para el blog y también uso Asciidoctor para generar el pdf. Y un poco de bash
para concatenar llamadas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para generar el mapa estático he usado un proyecto (&lt;a href=&quot;https://github.com/flopp/go-staticmaps.git&quot; class=&quot;bare&quot;&gt;https://github.com/flopp/go-staticmaps.git&lt;/a&gt;) que mediante
comando de consola le puedes indicar todos los elementos y te crea un PNG&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para generar el pdf uso una utilidad de asciidoctor, &lt;code&gt;asciidoctor-web-pdf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Iba a poner el código de los diferentes scripts pero como ya me está quedando aburrido el post y tampoco
aportan nada, pongo mejor el enlace al final del articulo y quien quiera que lo vea allí&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;choose&quot;&gt;Choose&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;choose.groovy es un script que simplemente carga todos los elementos posibles, quita los que ya han sido elegidos
y elige uno aleatorio de los restantes&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El id elegido se guarda en un fichero &lt;code&gt;current.txt&lt;/code&gt; y así es más fácil pasarselo al resto de procesos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;build&quot;&gt;Build&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La etapa de build es la que construye los diferentes artefactos a desplegar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;build_pdf&quot;&gt;Build Pdf&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Construir el Pdf lee la ruta elegida anteriormente, y usando un template construye el fichero asciidoctor que
espera asciidoctor-web-pdf&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;build_blog&quot;&gt;Build Blog&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usando la misma técnica que en el Pdf creo una entrada markdown de Hugo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;build_map&quot;&gt;Build Map&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo mismo para el mapa. Usando una plantilla genero un HTML con el mapa de la ruta. He intentando hacerlo ameno y que
los elementos de la ruta vayan apareciendo poco a poco pero creo que no tengo dotes para el front&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;build_letter&quot;&gt;Build Letter&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo preparo un html formateado para ser enviado por correo electrónico. Como TimyLetter no deja muchas
florituras, el html es muy espartano&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;build_telegram&quot;&gt;Build Telegram&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último un groovy script crea otro groovy script personalizado con la ruta listo para postear en Telegram&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;deploy&quot;&gt;Deploy&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proceso de deploy es simplemente copiar los elementos del blog, como el markdown o el html con el mapa y ejecutar
Hugo para que cree el static site&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;publish&quot;&gt;Publish&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Publish por su parte es subir el contenido al repositorio web (un rsync a una carpeta que tengo en DreamHost pero
podria ser a un Netlify o similar)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;notify&quot;&gt;Notify&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En realidad, este no existe todavia y es el paso más manual que tengo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un script envia el html a TinyLetter y a la vez que confirmo que se publique, ejecuto el script que postea en Telegram&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;repo&quot;&gt;Repo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Todo el codigo así como el contenido del blog puedes consultarlo en &lt;a href=&quot;https://gitlab.com/jorge-aguilera/rutas-por-madrid&quot; class=&quot;bare&quot;&gt;https://gitlab.com/jorge-aguilera/rutas-por-madrid&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Una ruta aleatoria por Madrid cada semana</summary>
    </entry>
    <entry>
        <title>Changelog con Notion</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/notion-changelog.html"/>
        <updated>2022-12-12T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/notion-changelog.html</id>
        <category term="notion"/>
        <category term="documentation"/>
        <category term="git"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Notion es un software de gestión de proyectos y para tomar notas&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En realidad Notion es un poco más que lo que dice su descripción oficial y se ha vuelto muy popular entre las empresas
(y a nivel personal) para mantener la documentación corporativa.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes crear páginas, organizarlas en árbol, tiene muchos emojis (demasiados) disponibles, bases de datos por páginas
donde puedes estructurar tus datos como quieras y la página asociada los renderiza, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post lo que voy a utilizar es la posibilidad de poder alimentar una base de datos de Notion para que muestre
el changelog (el historial de commits) de un repo git&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El concepto de base de datos de Notion es un poco diferente del tradicional (hasta donde he entendido). La idea
es que puedes crear &quot;bases de datos&quot; y renderizar páginas usando los datos de la misma. De esta forma lo que tienes son
muchas bases de datos diferentes, cada una asociada a una &quot;sección&quot;, y en la que puedes guardar diferente contenido
(textos, fechas, enlaces, etc) y crear páginas hijas&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación&quot;&gt;Preparación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como en el post &lt;a href=&quot;notion-krokio.html&quot;&gt;anterior&lt;/a&gt; en primer lugar necesitamos crear una connection (obtener un token)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para este caso además crearemos una base de datos en lugar de una página y le daremos permisos al connection.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Notion crea la base de datos con un par de campos por defecto. Nosotros vamos a usar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Name (title)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Created (date)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Description (string)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/notion-bbdd-1.png&quot; alt=&quot;notion bbdd 1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;repositorio_git&quot;&gt;Repositorio Git&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es que tendremos un repositorio git en el que iremos subiendo cambios y cada cierto tiempo generaremos una
release. Las buenas prácticas lo que nos dicen es que al generar la release creemos un &lt;code&gt;tag&lt;/code&gt; de tal forma que se pueda
saber qué código corresponde con cada release.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así por ejemplo en un momento dado el historial de nuestro repo sería:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ git log --oneline
f82c632 (HEAD -&amp;gt; master) tres
270b4c9 dos
cd4ed0b (tag: v0.0.1) commit 1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este log vemos que tenemos 3 commits (ya, ya, pero es un ejemplo). El primero lo etiquetamos como release &lt;code&gt;v0.0.1&lt;/code&gt;
y ahora tenemos 2 cambios más y vamos a generar release &lt;code&gt;v0.0.2&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El nombre de los tags para este script le da igual pero es buena práctica seguir una nomenclatura semver&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ git tag -a v0.0.2 -m &quot;esta es la release v0.0.2 con dos cambios&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con lo que ahora tenemos master etiquetado como &lt;code&gt;v0.0.2&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ git log --oneline
f82c632 (HEAD -&amp;gt; master, tag: v0.0.2) tres
270b4c9 dos
cd4ed0b (tag: v0.0.1) commit 1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea sería tener una página de Notion asociada a nuestro repositorio donde poder ver de un vistazo que
&lt;code&gt;v0.0.2&lt;/code&gt; fue creada en una fecha, la descripcion con la que se creó así como una lista con los commits que se incluyeron
(en nuestro ejemplo serían &lt;code&gt;270b4c9 dos&lt;/code&gt; y &lt;code&gt;f82c632 tres&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;notion&quot;&gt;Notion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que ejecutemos el script nuestra página se actualizará automaticamente como en las imágenes siguientes&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(La presentación se puede/debe mejorar pero se deja al lector como ejercicio ;) )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/notion-bbdd-2.png&quot; alt=&quot;notion bbdd 2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/notion-bbdd-3.png&quot; alt=&quot;notion bbdd 3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script ejecuta un par de comandos de git para obtener el historial y detalles de los commits realizados en el repo
y simplemente usa el api de Notion para crear páginas hijas asociadas a la bbdd que se le configure&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);
const {Client} = require(&quot;@notionhq/client&quot;)

const {execSync} = require(&quot;child_process&quot;);
const process = require(&quot;process&quot;);


// Initializing a client
require(&apos;dotenv&apos;).config();

;(async () =&amp;gt; {
    // tag list
    const tagsStr = execSync(
        &quot;git tag -l --sort=-creatordate --format=&apos;%(refname:short)&apos;&quot;,{
            cwd: process.env.REPO_DIR
        }).toString();
    const tags = tagsStr.split(&apos;\n&apos;)
    const last = tags[0]
    const prev = tags[1]

    // last tag history
    const history = execSync(
        `git log &apos;${last}&apos;...&apos;${prev}&apos; --pretty=format:&quot;%cs%x09%s%x09%h&quot;`,{
            cwd: process.env.REPO_DIR
        }).toString().split(&apos;\n&apos;).slice(0, 30);

    // tag message
    const messageStr = execSync(
        `git tag -l --format=&apos;%(contents) | %(taggerdate:short) | %(creatordate:short)&apos; ${last}`,{
            cwd: process.env.REPO_DIR
        }).toString().replace(/(\r\n|\n|\r)/gm,&apos;&apos;).split(&apos;|&apos;)

    const tagMsg = messageStr[0];
    const tagDate = (messageStr[1].trim() != 0 ? messageStr[1].trim() : messageStr[2].trim() != 0 ? messageStr[2].trim() : &apos;2001-01-01&apos;);


    const blocks = []
    for(h of history){
        blocks.push({
            &quot;type&quot;: &quot;paragraph&quot;,
            &quot;paragraph&quot;: {
                &quot;rich_text&quot;: [{
                    &quot;type&quot;: &quot;text&quot;,
                    &quot;text&quot;: {
                        &quot;content&quot;: `\n- ${h}\n`,
                        &quot;link&quot;: null
                    }
                }]
            }
        })
    }

    const payload = {
        parent: {type: &quot;database_id&quot;, database_id: process.env.DB_ID},
        properties: {
            Name: {type: &quot;title&quot;, title: [{type: &quot;text&quot;, text: {content: tags[0]}}]},
            Created: {type: &quot;date&quot;, date: {start: tagDate}},
            Description: {rich_text: [{text: {content: tagMsg,},},],},
        },
        children: blocks
    }

    const notion = new Client({
        auth: process.env.NOTION_TOKEN,
    })

    const newpage = await notion.pages.create(payload);
    console.log(newpage)

})()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La presentación de las páginas se pueden mejorar para incluir enlaces, negritas, etc y hacer más legible el changelog
pero es un ejercicio interesante como prueba de integración de procesos externos y Notion para generar contenido de
forma automática&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>publicando el changelog de un repo en Notion</summary>
    </entry>
    <entry>
        <title>DocAsCode con Notion</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/notion-krokio.html"/>
        <updated>2022-12-03T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/notion-krokio.html</id>
        <category term="notion"/>
        <category term="documentation"/>
        <category term="diagram"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Notion es un software de gestión de proyectos y para tomar notas&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En realidad Notion es un poco más que lo que dice su descripción oficial y se ha vuelto muy popular entre las empresas
(y a nivel personal) para mantener la documentación corporativa.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes crear páginas, organizarlas en árbol, tiene muchos emojis (demasiados) disponibles, bases de datos por páginas
donde puedes estructurar tus datos como quieras y la página asociada los renderiza, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, lo mismo que pasaba con Confluence, la mayoría de nosotros lo usa a través de la interfaz web y acabamos
teniendo mil páginas desactualizadas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Siguiendo la propuesta de DocAsCode, donde la documentación deberíamos tratarla como el código (versionado, con
revisiones y despliegues automatizados) y aprovechando que Notion tiene una API sencillo he desarrollado un pequeño
script para mantener mi repositorio git, donde tengo los diagramas de arquitectura del proyecto, actualizado en Notion.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Lo he desarrollado en javascript, porque sí, porque puedo&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo típico en todos los proyectos (con suerte) es crear una página en Notion (antes se hacía en Confluence, y antes en
un Wiki, y previamente en una carpeta compartida) donde describimos la arquitectura del proyecto, incluimos algún
diagrama generado con alguna herramienta como Draw.io y con mucha suerte incluimos el enlace al código del diagrama
en lugar de insertar el PNG generado, (pero esto es con mucha suerte. El 99.9999% de los casos incluimos el PNG porque
claro, &quot;la arquitectura no va a cambiar&quot;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Por otra parte, no niego la facilidad de uso y potencia que ofrece Notion para otro tipo de comunicaciones&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así la idea es tener una página de Notion &quot;Arquitectura&quot; y un repositorio git donde tendré mis diagramas C4 junto
con explicaciones a los mismos. Cada vez que actualize el repo (tal vez mergeando en master, tal vez cada commit) un
pipeline ejecutará mi script y reconstruirá la página con la última versión extraída del repo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quiero poder tener una estructura de carpetas y sub carpetas en mi repo de tal forma que se replique en Notion colgando
de la página &quot;Arquitectura&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación&quot;&gt;Preparación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar, debemos tener un espacio ya creado en Notion (obvio) así como una página &quot;master&quot; de donde colgaremos
las generadas. Esta página puede ser una principal o una hija. Así mismo, esta página puede contener el texto explicativo
que queramos, pues el script lo que va a hacer es eliminar las hijas y recrearlas (así que no deberías crear hijas,
pues el script las va a borrar)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En segundo lugar, necesitamos crear una &quot;conection&quot;. Esto lo tiene que hacer el owner del espacio y básicamente es
generar una aplicación privada y Notion nos devolverá un token a usar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tienes la &quot;connection&quot; hay que darle permisos para poder acceder a la página &quot;master&quot; (usando los tres
puntitos arriba a la derecha)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;repositorio&quot;&gt;Repositorio&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nuestro repositorio es un repositorio normal, donde puedes tener el código o lo que quieras, y simplemente vamos a
usar una carpeta del mismo para tener nuestra documentación y diagramas (como código)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Esta versión es muy simple y la documentación la va a subir como texto. No negritas, no italica, no titulos,
, quién sabe, tal vez en otra version lo mejore a un parseador de markdown a notion&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;|_ src
  |_ main
    |_ java
|_ docs
  |_ 1.- Context
    |_ content.txt
    |_ content.puml
  |_ 2.- Applications
    |_ 1.- API
        |_ content.txt
        |_ content.puml
    |_ 2.- Cron
        |_ content.txt
        |_ content.puml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En principio el nombre de los ficheros da igual, solo importa que haya uno con una extensión .txt y otro con extension .puml,
(pero puedes cambiarlo a tu gusto)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los ficheros .puml contienen los diagramas en formato texto y casi todos los IDEs tienen un plugin para editarlos
y renderizarlos. Por ejemplo Intellij o VsCode los tienen, por lo que puedes ir viendo el diagrama a la vez que los editas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un ejemplo de un puml&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@startuml
!include https://raw.githubusercontent.com/adrianvlupu/C4-PlantUML/latest/C4_Context.puml

LAYOUT_WITH_LEGEND()

Person(pbc, &quot;Customer&quot;, &quot;A customer of our application.&quot;)
System(api, &quot;MyCompany&quot;, &quot;Place orders.&quot;)
System_Ext(es, &quot;Exchange system&quot;, &quot;Exchange systems.&quot;)
System_Ext(nf, &quot;Notifications system&quot;, &quot;Notifications system&quot;)

Rel(pbc, api, &quot;Uses&quot;, &quot;REST&quot;)

Rel(api, es, &quot;Uses&quot;)

Rel(api, nf, &quot;Uses&quot;)

Rel(nf, pbc, &quot;Sends notificaions&quot;)
@enduml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al ser texto plano es fácil versionarlo, ver las diferencias, crear PR, etc&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último añadiremos a nuestro repo el siguiente script (en javascript, toma ya)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);

const { Client } = require(&quot;@notionhq/client&quot;)

const pako = require(&quot;pako&quot;)

require(&apos;dotenv&apos;).config();

// Initializing a client
const notion = new Client({
    auth: process.env.NOTION_TOKEN,
})

async function removeSubPages( id ){
    const page = await notion.blocks.children.list({
        block_id: id
    })
    const blocks = page.results.filter( (c)=&amp;gt; c.type ==&apos;child_page&apos; ).map( (c)=&amp;gt; c.id)
    for(b of blocks){
        console.log(`deleting ${b}`)
        const deleted = await notion.blocks.delete({
            block_id: b
        })
    }
}

async function processFolder(directory, parentId, title){
    console.log(`Processing ${directory}`)
    const blocks = []

    const files = fs.readdirSync(directory)
    const fcontent = files.filter( (f)=&amp;gt;path.extname(f) ==&quot;.txt&quot;)
    const fuml = files.filter( (f)=&amp;gt;path.extname(f)==&quot;.puml&quot;)

    if( fcontent.length == 1) {
        const content = fs.readFileSync(`${directory}/${fcontent[0]}`, {
            encoding: &apos;utf8&apos;,
            flag: &apos;r&apos;
        });
        blocks.push({
            &quot;type&quot;: &quot;paragraph&quot;,
            &quot;paragraph&quot;: {
                &quot;rich_text&quot;: [{
                    &quot;type&quot;: &quot;text&quot;,
                    &quot;text&quot;: {
                        &quot;content&quot;: content,
                        &quot;link&quot;: null
                    }
                }]
            }
        })
    }
    if( fuml.length == 1) {
        const diagramSource = fs.readFileSync(`${directory}/${fuml[0]}`, {
            encoding: &apos;utf8&apos;,
            flag: &apos;r&apos;
        });
        const data = Buffer.from(diagramSource, &apos;utf8&apos;)
        const compressed = pako.deflate(data, {level: 9})
        const result = Buffer.from(compressed)
            .toString(&apos;base64&apos;)
            .replace(/\+/g, &apos;-&apos;).replace(/\//g, &apos;_&apos;)

        blocks.push({
            &quot;type&quot;: &quot;embed&quot;,
            &quot;embed&quot;: {
                &quot;url&quot;: &quot;https://kroki.io/plantuml/png/&quot; + result
            }
        })
    }
    blocks.push({
        &quot;type&quot;: &quot;paragraph&quot;,
        &quot;paragraph&quot;: {
            &quot;rich_text&quot;: [{
                &quot;type&quot;: &quot;text&quot;,
                &quot;text&quot;: {
                    &quot;content&quot;: `\n(No modify this page, it&apos;s generated automatically)\n`,
                    &quot;link&quot;: null
                }
            }]
        }
    })
    const newpage = await notion.pages.create({
        parent: {
            page_id: parentId
        },
        properties: {
            title: [{
                &quot;text&quot;: {
                    &quot;content&quot;: title
                }
            }]
        },
        children: blocks
    })
    console.log(`Page ${newpage.id} created`)
    const folders = fs.readdirSync(directory);
    for( subdir of folders) {
        if( fs.statSync(`${directory}/${subdir}`).isDirectory() )
            await processFolder( `${directory}/${subdir}`, newpage.id, subdir)
    }
}

;(async () =&amp;gt; {
    // clean page
    await removeSubPages(process.env.PAGE);

    const folders = fs.readdirSync(&apos;docs&apos;)
    const parentId = process.env.PAGE
    for( subdir of folders) {
        if( fs.statSync(`docs/${subdir}`).isDirectory() )
            await processFolder( `docs/${subdir}`, parentId, subdir)
    }
})()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para no incluir el token en el código y así poder versional el script junto con nuestro código, usaremos un fichero
&lt;code&gt;.env&lt;/code&gt; donde pondremos los siguientes valores:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.env&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;NOTION_TOKEN=secret_nN8Zxxxxxxxxxxxx
PAGE=2f188xxxxxxxxxxxxxxxxx&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Donde PAGE es la parte final de la url de notion de nuestra página master (los numeros despues del titulo)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejecución&quot;&gt;Ejecución&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente, necesitamos hacer un &lt;code&gt;npm install&lt;/code&gt; una vez para que se bajen las dependencias y una vez bajadas ejecutaremos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;node upload2notion.js&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo va bien verás como las páginas hijas de la master desaparecen (si había) y se van creando las nuevas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada página hija contendrá un texto y/o una imagen (en realidad un enlace al servicio de kroki.io)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;disclaimer&quot;&gt;Disclaimer&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;NO tengo ni idea de JavaScript asi que el código seguramente sea un atentado a ojos de gente con más experiencia&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>publicando diagramas UML en Notion (gracias a Krokio)</summary>
    </entry>
    <entry>
        <title>Publicar en Linkedin desde Gmail</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/gmail-linkedin.html"/>
        <updated>2022-11-20T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/gmail-linkedin.html</id>
        <category term="linkedin"/>
        <category term="google"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;23-11-2022. He actualizado el script de Google para mejorar el parseo del body&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Para poder publicar en Linkedin necesitarás autentificar tu usuario y obtener
un &lt;code&gt;acccess token&lt;/code&gt;, una cadena de caracteres que te identifica y que NO debes compartir.
NO es tu usuario y passsword (pero casi) y se necesita realizar un flujo &quot;OAuth&quot; para obtenerlo.
En este post comparto tanto el código como una aplicación ya desplegada que podrás usar para ello.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Linkedin, la plataforma del postureo, tiene un API que puedes usar, entre otras cosas, para publicar textos, artículos, imágenes, etc de tal forma que puedes usar herramientas externas o incluso hacerte
tu propia integración. En este post vamos a investigar cómo podemos publicar en esta red desde nuestra
cuenta de Google de tal forma que al enviarnos un correo con un subject determinado, Google lo detecte
y ejecute un script que publique el cuerpo del correo automáticamente&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De esta forma un usuario no necesita entrar en la página de Linkedin y usar el interface que ofrece,
sino que puede usar el editor del correo, programarlos, etc y de esta forma no perder el foco en lo
que está realizando en ese momento.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cuenta en Linkedin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cuenta Gmail&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;linkedin&quot;&gt;Linkedin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya se ha comentado, Linkedin implementa una flujo OAuth para la autentificación del usuario. Esto
quiere decir, por una parte, que se requiere de su intervención para generar un token que
lo identifique y por otra parte, una aplicación que realize el flujo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;crear_la_aplicación&quot;&gt;Crear la aplicación&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar crearemos una aplicación en &lt;a href=&quot;https://developer.linkedin.com/&quot; class=&quot;bare&quot;&gt;https://developer.linkedin.com/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este enlace puedes encontrar los pasos a realizar de forma detallada&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://guides.micronaut.io/latest/micronaut-oauth2-linkedin-maven-java.html#create-a-linkedin-app&quot; class=&quot;bare&quot;&gt;https://guides.micronaut.io/latest/micronaut-oauth2-linkedin-maven-java.html#create-a-linkedin-app&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Esta guía de Micronaut explica cómo usar Linkedin como sistema de autentificación por lo que
en Products sólo se configura la parte de &quot;Sign In with LinkedIn&quot; pero nosotros queremos también poder publicar
por lo que añadiremos &quot;Share on LinkedIn&quot;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;identificarse_y_obtener_el_token&quot;&gt;Identificarse y obtener el Token&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez realizados los pasos indicados obtendremos un par de claves (ClientId y ClientSecret) que
servirán para identificar a nuestra aplicación. Esta pareja de claves hay que mantenerlas guardadas
y &lt;strong&gt;no compartirlas&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si sigues la guía al completo (no es necesario) al final tendrías una aplicación que te permitiría
hacer login en la misma con tu cuenta de Linkedin &amp;#8230;&amp;#8203; y nada más. Así que he preparado esta aplicación
, basada en este tutorial, que una vez te identificas en ella obtienes tu &lt;strong&gt;userId&lt;/strong&gt; y un &lt;strong&gt;accessToken&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/puravida-software/linkedin-accesstoken&quot; class=&quot;bare&quot;&gt;https://gitlab.com/puravida-software/linkedin-accesstoken&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente esta aplicación permite al usuario autentificarse en Linkedin y obtener los datos necesarios para publicar
un post en la red. Esta aplicación se ejecutaría en &lt;strong&gt;tu local&lt;/strong&gt; por lo que si has seguido los pasos de la guia
anterior y ejecutado esta aplicación, obtendrás estos datos &lt;strong&gt;de forma segura&lt;/strong&gt; (el código de la aplicación es abierto
así que puedes revisarlo para comprobarlo)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo esto te resulta complicado (no es fácil pero es lo que hay) y confías en mí, he desplegado esta misma aplicación
en&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://linkedin-pvidasoftware.cloud.okteto.net/&quot; class=&quot;bare&quot;&gt;https://linkedin-pvidasoftware.cloud.okteto.net/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;la cual te solicitará acceso a tu cuenta y que le des permisos para publicar en tu nombre. Obviamente aquí podría
usar tus datos para liartela así que te corresponde a tí decidir qué hacer (La otra opción es que te pongas en
contacto conmigo y hablemos 😉 )&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gmail&quot;&gt;Gmail&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La parte &quot;fácil&quot; es crear el script de Google. Para ello iremos a&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://script.google.com/home&quot; class=&quot;bare&quot;&gt;https://script.google.com/home&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y crearemos un  nuevo proyecto (yo lo he llamado &quot;Publicar en Linkedin&quot;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nos aparecerá un pequeño código que reemplazaremos por este:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const author=&apos;NQxxxxxxx&apos;;  // Tu userId
const accessToken=&apos;AQUg-xxxxxxx&apos;; // El accessToken, una cadena superlarga

const url = &apos;https://api.linkedin.com&apos;
const label = &apos;Publicar en Linkedin&apos;;
const lifecycleState=&apos;PUBLISHED&apos;;
const visibility = &apos;PUBLIC&apos;;

function checkEmail() {
    try{
        unreadEmail()
    }catch(error){
      Logger.log(error)
    }
}

function unreadEmail() {
  const ts = GmailApp.search(`subject:${label} is:unread to:me from:me`);
  ts.forEach(t =&amp;gt; {
    t.getMessages().forEach(m =&amp;gt; {
      processEmail(m);
      m.markRead();
    });
  });
}

function processEmail(m) {

  const images = m.getAttachments();
  let medias = [];
  if( images ){
    images.forEach( (a)=&amp;gt;{ medias.push(uploadImage(a))} )
  }

  const body=html2text(m.getBody())

  var payload = {
    &quot;author&quot;: &quot;urn:li:person:&quot;+author,
    &quot;lifecycleState&quot;:lifecycleState,
    &quot;visibility&quot;: {
      &quot;com.linkedin.ugc.MemberNetworkVisibility&quot;:visibility
    },
    &quot;specificContent&quot;:{
      &quot;com.linkedin.ugc.ShareContent&quot;:{
        &quot;shareCommentary&quot;:{
          &quot;text&quot;: body
        },
        &quot;shareMediaCategory&quot; : medias.length ? &quot;IMAGE&quot; : &quot;NONE&quot;,
        &quot;media&quot;:medias
      }
    }
  };

  var options = {
    &apos;method&apos; : &apos;post&apos;,
    &apos;contentType&apos;: &apos;application/json&apos;,
    &apos;payload&apos;: JSON.stringify(payload),
    &apos;headers&apos;: {
      &apos;Authorization&apos;: `Bearer ${accessToken}`,
      &apos;X-Restli-Protocol-Version&apos;: &apos;2.0.0&apos;
    }
  };
  Logger.log(options)
  const resp = UrlFetchApp.fetch(`${url}/v2/ugcPosts`, options);
  Logger.log(resp)
}

function html2text(body){
  const ret = `${body}`.replaceAll(&apos;&amp;lt;br&amp;gt;&apos;,&apos;\n&apos;)
          .replaceAll(&apos;&amp;lt;/div&amp;gt;&apos;,&apos;\n&apos;)
          .replaceAll(/&amp;lt;[^&amp;gt;]+&amp;gt;/g, &apos;&apos;)
          .replaceAll(&quot;&amp;amp;quot;&quot;,&apos;&quot;&apos;)
          .replaceAll(&quot;&amp;amp;quota&quot;,&apos;á&apos;)
          .replaceAll(&quot;&amp;amp;quote&quot;,&apos;é&apos;)
          .replaceAll(&quot;&amp;amp;quoti&quot;,&apos;í&apos;)
          .replaceAll(&quot;&amp;amp;quoto&quot;,&apos;ó&apos;)
          .replaceAll(&quot;&amp;amp;quotu&quot;,&apos;ú&apos;)
  return ret;
}

function uploadImage(attachment) {

  var imgPayload = {
    &quot;registerUploadRequest&quot;: {
        &quot;recipes&quot;: [
            &quot;urn:li:digitalmediaRecipe:feedshare-image&quot;
        ],
        &quot;owner&quot;: &quot;urn:li:person:&quot;+author,
        &quot;serviceRelationships&quot;: [
            {
                &quot;relationshipType&quot;: &quot;OWNER&quot;,
                &quot;identifier&quot;: &quot;urn:li:userGeneratedContent&quot;
            }
        ],
        &quot;supportedUploadMechanism&quot;:[
          &quot;SYNCHRONOUS_UPLOAD&quot;
        ]
    }
  }
  var imgOptions = {
    &apos;method&apos; : &apos;post&apos;,
    &apos;contentType&apos;: &apos;application/json&apos;,
    &apos;payload&apos;: JSON.stringify(imgPayload),
    &apos;headers&apos;: {
      &apos;Authorization&apos;: `Bearer ${accessToken}`,
      &apos;X-Restli-Protocol-Version&apos;: &apos;2.0.0&apos;
    }
  };
  const resp = UrlFetchApp.fetch(`${url}/v2/assets?action=registerUpload`, imgOptions);
  Logger.log(resp);

  const json = JSON.parse(resp)
  const uploadUrl = json.value.uploadMechanism[&quot;com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest&quot;].uploadUrl;
  const mediaId = json.value.asset;

  var optionsImage = {
    method : &quot;put&quot;,
    payload : attachment.copyBlob(),
    headers:{
      &apos;Authorization&apos;: `Bearer ${accessToken}`,
      &apos;Accept&apos;:&apos;*/*&apos;
    }
  };
  Logger.log(uploadUrl)
  var respImage = UrlFetchApp.fetch(uploadUrl, optionsImage).getContentText();
  Logger.log(respImage)


  return {
    status:&apos;READY&apos;,
    media: mediaId
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez salvado el código vamos a probar que funciona correctamente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Envíate un correo a tí mismo con el subject &quot;Publicar en Linkedin&quot; (las mayusculas da igual) &lt;strong&gt;en formato texto plano&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NO&lt;/strong&gt; lo abras&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;En la parte superior del editor del script verás un listbox donde puedes seleccionar qué método ejecutar. Asegurate
que se encuentra seleccionado &lt;code&gt;checkEmail&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Al lado de este listbox tienes el botón &lt;code&gt;Ejecutar&lt;/code&gt;. Al pulsarlo aparecerán logs en la parte inferior de la pantalla
y si no aparece ninguno de error tu correo habrá sido publicado en Linkedin. Puedes ir a tu cuenta y comprobarlo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Comprueba que el email se ha marcado como leído&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;automatización&quot;&gt;Automatización&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez comprobado que el proceso funciona podemos decirle a Google que cada cierto ejecute este proceso:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;En el menú de la izquierda pulsa en el reloj y seleciona &quot;Activadores&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Añadir un Activador (botón abajo a la derecha)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Seleccionamos &lt;code&gt;checkEmail&lt;/code&gt; como método a ejecutar y configuramos el temporizador a usar. Yo por ejemplo estoy usando
cada hora&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Guardamos el activador&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente, cada hora Google ejecuta mi script en mi cuenta de gmail y comprueba si hay correos sin enviar con
el subject &quot;Publicar en Linkedin&quot; y lo publica.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;tips&quot;&gt;Tips&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este API permite publicar texto acompañado de fotos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El problema es que si te envías el email en formato enriquecido
hay que transformarlo (quitar los divs, span, &amp;#8230;&amp;#8203;.) por lo que lo más sencillo es mandarlo como texto plano.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes adjuntar imágenes al email y el script lo detecta y las sube a Linkedin junto al texto. NO he probado
el máximo que puedes adjuntar, sólo 3 ó 4. Todo es probar&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Sabías que puedes publicar automáticamente en Linkedin desde tu correo electrónico (gmail en este caso). Te cuento cómo</summary>
    </entry>
    <entry>
        <title>ApacheConf New Orleans</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/apacheconf-nola.html"/>
        <updated>2022-10-03T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/apacheconf-nola.html</id>
        <category term="apache"/>
        <category term="conferencias"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La primera semana de Octubre he estado en la ApacheConf (Conferencias de la fundación Apache
sobre Open Source) en New Orleans. En este post, voy a intentar resumir esta semana de experiencias&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cómo_porqué&quot;&gt;¿Cómo? ¿Porqué?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar, cómo he llegado a estar una semana en una conferencia tan lejos de casa (y no me refiero
a que he venido en avión, sino a cómo lo he hecho)?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Buena pregunta. Como casi todo lo que hago, ha sido gracias a un &quot;impulso&quot;. Allá por febrero, después del
año y medio tan complicado que hemos pasado todos, ví en Twitter que la cuenta ApacheGroovy mencionaba que se abría el
CFP (envío de propuestas de charlas) de la Apache Conf y buscaban propuestas relacionadas con Groovy,
así que sin pensarlo mucho decidí enviar una.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La verdad que cada vez que veo a ciertas personas que sigo el que van a dar una charla en eventos por ahí pues
como que me dá mucha envidia, así que esta vez no me lo pensé y desde el mismo móvil envié una propuesta.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya he dado &quot;algunas&quot; de Groovy dudé un par de segundos cúal podría ser más interesante, pero en seguida
me decanté por hablar de &lt;strong&gt;Groogle&lt;/strong&gt; un proyecto que empecé para jugar un poco y que había dejado aparcado porque
no despertó mucha curiosidad en mi &quot;circulo de influencia&quot; (vamos, que todos me preguntan que para qué
sirve).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Groogle, es un DSL que te facilita el uso de las APIs de Google, pero más allá de lo técnico, es un proyecto que
se me ocurrió &quot;desde cero&quot; y al que le he dedicado mucho rato, así que en cierta forma se merecía que le escogiera&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sorprendentemente, unos meses después, recibí un email de que mi charla había sido aceptada y me acordé de lo de
&quot;cuidado con lo que deseas que se puede cumplir&quot;. Es cuando comprendí que debía enfrentarme a uno de mis mayores
enemigos y dar una charla en inglés y no te miento si te digo que a ratos pensé en buscar alguna excusa para decir
que no podría ir.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La excusa más fácil era que costaba mucho dinero y blablabla pero la verdad que por suerte no me puedo quejar en
ese aspecto y podría costearlo &amp;#8230;&amp;#8203; y entoncés Iván, un perro viejo en esto, me recordó que puedes solicitar ayuda
para costear el viaje. Lo hicé y al poco recibí la contestación que ningún problema, que iba a gastos pagados.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;sé que esto es tema de debate y que mucha gente piensa que si vas a dar una charla por la que cobran, tú
deberías cobrar. Yo tengo mi opinión y si quieres saberla tendrás que pagar las cervezas para que te las cuente.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En resumen, envié la propuesta en febrero, recibí la aceptación por junio y la confirmación de los gastos pagados
sobre julio. Ya no había vuelta atrás y en octubre tenía una cita con la Apache Conf&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;He &quot;descubierto&quot; que existe un programa de voluntariado en el que, si te aceptan, puedes asistir a la
conferencia a gastos pagados (billete y hotel e incluso manutención). La labor que hay que hacer es básicamente
el primer día estar en recepción para dar las camisetas y luego ayudar en las charlas al ponente avisandole
del tiempo que le queda etc. No sé si lo entendí bien pero suelen pillar a gente que no lo haya solicitado
anteriormente. Creo que es una oportunidad para poder asistir, conocer gente y vivir una experiencia bastante
enriquecedora.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A pesar de que estaba apuntado al voluntariado he podido asistir a casi todas las charlas que me interesaban
(no sé si será siempre así pero habían unas 20 personas así que tenía tiempo para mí, aunque también es
verdad que me escaqueé un poco)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Las Keynotes, alguna sobre Avro, Kasandra y sobre todo las de Groovy. Como mi charla era el último día he tenido
la ventaja de ir conociendo a la gente, ganando un poco de confianza, viendo las demás, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;el_f_inglés&quot;&gt;El f*** inglés&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Seguramente en este punto dirás &quot;claro, es que eres muy extrovertido y te relacionas con todo el mundo, siendo
tan alto y guapo lo tienes muy fácil. En mi caso es diferente porque esto y lo otro&quot;. Pues siento contradecirte&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No soy extrovertido. Sí intento ser amable e ir con una sonrisa (eso abre muchas puertas) pero como casi todos,
necesito tiempo para ir ganando confianza e ir soltándome. Más de una mañana, en el cafe inicial, deambulaba
como haciendo que buscaba a alguien porque yo no voy a entrarle a nadie que no conozca &amp;#8230;&amp;#8203; y encima en un idioma
que medio chapurreo y mal.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De hecho, los speakers estábamos invitados a una cena (en un sitio muy chulo) y en un alarde de valentía en
lugar de sentarme en una mesa que no había nadie, ví una donde había un tío de mi edad más menos y me dije
&quot;amos allá&quot;. Como pude le dije si le molestaba que me sentara y nos pusimos a &quot;hablar&quot;. Sinceramente, en este
ambiente y con gente de todos los sitios, el inglés pasa a segundo plano y la peña hace por entenderte y que les
entiendas. El problema me vino cuando llegaron sus amigos y empezaron a tomar velocidad. Me costó 10 minutos
darme cuenta que estaban hablando del cambio de licencia de ElasticSearch y cuando me preparaba para poder aportar
algo (preguntar alguna cosa, porque del tema no tengo ni papa) vino la vicepresidenta ejecutiva de Apache
(cágate lorito) y se puso a contarnos historias de su retoño y ya me sentí fuera de lugar e hice mutis como pude.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al día siguiente, 3 de las personas que estaban en esa mesa, al pasar al lado mío me saludaron e intercambiamos
alguna frase hecha. A donde voy con todo esto? que mira, nadie te va a comer por no saber expresarte, que te vas a
sentir en algun momento fuera de lugar, vale, y? en peores plazas hemos toreado&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como contrapartida, los assistant, al habernos conocido antes y tener &quot;un objetivo común&quot;, empezabamos a contarnos
nuestras movidas: como es nuestro país, nuestro trabajo, etc. He conocido a un turco que no sabía (literalmente)
ni una palabra de inglés, 3 indios, 1 chica de Kazahistan, 1 argentino, 1 uruguallo &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/cena.jpg&quot; alt=&quot;cena&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. Cenando con la cúpula de Apache sin saberlo&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;la_conferencia&quot;&gt;La conferencia&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sobre la conferencia y las charlas pues poca cosa que contar. Me ha sorprendido que esta no es la típica movida
americana, a pesar de haber sido en el Serathon Hotel. Las charlas eran en salitas no muy grandes, como para 20-25
personas, salvo alguna que usaban la sala más grande.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por lo demás muy normal. La comida era asunto tuyo (no había, vamos) pero las pausas del café muy bien, los tiempos
entre charlas bien medidos, un ambiente muy ameno y buen rollo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como en todos estos tipos de eventos donde hay 6-7 charlas a la vez más de una te planteas si has acertado con la
correcta (he estado en 2 que me hubiera cargado al ponente si no fuera por el sueño que estaba dando). Obviamente
las de Groovy, las mejores, pero no quiero dejar de pasar una en concreto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El ponente explicaba cómo empezó con una idea de crear un protocolo para la industria de IoT, lo propuso a la
fundación, las peleas con los mentores, crear comunidad, ganar atracción en el proyecto, etc que me dieron ganas
de ponerme en pie para aplaudir al finalizar su charla.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://plc4x.apache.org/&quot; class=&quot;bare&quot;&gt;https://plc4x.apache.org/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo que sí he hecho ha sido, cual Paco Martínez Soria, arrasar con todos los stands cogiendo pegatinas, camisetas,
botellas, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo, esta foto era del segundo día&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/apacheconf.jpg&quot; alt=&quot;apacheconf&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;groogle&quot;&gt;Groogle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No te voy a mentir. En mi opinión &quot;bordé la charla&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No, mi inglés seguía siendo justito, de hecho el temido turno de preguntas, como esperaba, no supe qué me estaba
preguntando la chica aquella (pongo como excusa que llevaba la mascarilla) y me tuvieron que echar un capote.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero la bordé. Primero porque el proyecto &quot;es mío&quot; y aunque es técnica lo que cuento es vivido en primera persona,
no es un framework que trabajas con él ni nada de eso (sin querer desmerecer esas charlas, que yo también las hago)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Segundo, porque había buscado unas semanas antes unos entornos amigables donde practicarla: primero en remoto
para los compas de la empresa, casi todos de habla inglesa, y luego en un meetup en MadridGUG donde la gente
pudo darme su feedback al finalizar (y menudo feedback!! me hizo cambiar casi toda la charla a falta de 2 semanas)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En tercer lugar, la suerte de que fuera los últimos días y hubiera tenido tiempo de hacer amistades y relajarme.
Ver que las salas eran pequeñas, que a nadie le importa Groovy e ibamos a ir los de siempre (al final se apuntó
más gente), crearon un ambiente más relajado y me sentí como en casa.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y bueno, el que fuera tan bien pues me ha animado para seguir mejorando el proyecto e incluirle más cositas
que tenía en el tintero y que iremos sacando poco a poco.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/nola1.jpeg&quot; alt=&quot;nola1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/nola2.jpeg&quot; alt=&quot;nola2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;new_orleans&quot;&gt;New Orleans&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como &quot;premio&quot; pues te puedes imaginar. Conseguimos que un familiar se quedara la semana al cargo del heredero
y mi pareja y yo nos pillamos un apartamento cerca del hotel (tranqui, mi habitación la usó otro ponente) así que
por las tardes, una vez terminadas las charlas, nos ibamos a recorrer las calles del barrio francés, cementerio,
etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como este no es un blog de viajes, sólo diré que, en mi opinión, te tiene que gustar mucho la música y el salir
de copeo por las noches para sacarle todo el partido a #NOLA&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/nola3.jpg&quot; alt=&quot;nola3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2022/nola4.jpg&quot; alt=&quot;nola4&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pues obviamente, ahora mismo estoy de subidón. No sólo he sobrevivido sino que he conocido gente y lugares
(bueno NOLA ya la habíamos visitado pero hacía mucho y sólo un par de días), así que a cualquiera que me pregunte
le diría sin dudar que intente hacerlo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que el no controlar inglés puede ser tu barrera pero también tu aliado echándole un poco de morro&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que seguro tienes algo que contar que a otros les puede interesar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que las ayudas económicas se buscan y seguro encuentras alguien que te eche una mano&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que seguro que para el año que viene, si todo sigue igual, envío propuestas hasta para darlas en Marte&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>una semana en NewOrleans en la ApacheConf</summary>
    </entry>
    <entry>
        <title>Nextflow: Mezclando multiples lenguajes</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/python-java.html"/>
        <updated>2022-09-27T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/python-java.html</id>
        <category term="groovy"/>
        <category term="nextflow"/>
        <category term="python"/>
        <category term="java"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez hecha la introducción a Nextflow (ver artículo &lt;a href=&quot;process.html&quot;&gt;anterior&lt;/a&gt;) vamos a empezar a explorar
algunas de las funcionalides que nos ofrece.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que tenemos un flujo de trabajo que requiere que ciertas partes se realicen usando un programa (o lenguaje
de programación) y otras partes otros.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo existe una librería Python que realiza unos cálculos y tenemos otra
en Java que realiza otros.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probablemente usaríamos un bash donde ejecutaríamos cada fase, usando las salidas de unos como entradas de otros.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El problema es que esto nos obliga a tener instalados los diferentes programas (o runtimes) con el engorro de controlar
versiones, instalaciones, etc. Una forma de mitigar este problema sería usando, por ejemplo, Docker, montando volumenes,
compartiendo ficheros, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;caso_de_uso&quot;&gt;Caso de uso&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo vamos a desarrollar un pipeline tal que un Python va a crear un número indeterminado de ficheros (
le pasaremos por párametro cuántos queremos generar), cada uno de ello va a ser leído y &quot;modificado&quot; por un comando bash
y por último un proceso Java va a pasar a mayúsculas cada fichero (como ves, un ejemplo sin mucha utilidad)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La &quot;gracia&quot; va a ser que:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;cada fichero se va a tratar de forma paralela e independiente&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vamos a usar Docker &quot;sin manos&quot; y hacer que cada proceso se ejecute en una imagen de Docker diferente (Python y Java)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;procesos&quot;&gt;Procesos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para usar la capacidad de ejecutar containers necesitamos activar docker. Para ello crearemos el fichero de configuración
&lt;code&gt;nextflow.config&lt;/code&gt; con el siguiente contenido:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;nextflow.config&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;docker{
    enabled = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;generar_ficheros_python&quot;&gt;Generar ficheros (Python)&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process generarFicheros {
   container &apos;python:latest&apos;
   input:
      val size
   output:
      path &apos;example.*&apos;

   &quot;&quot;&quot;
   #!/usr/bin/env python
   import sys
   total = int($size)

   for x in range(0, total):
      with open(&apos;example.&apos;+str(x), &apos;w&apos;) as f:
         f.write(&apos;line 1 &apos;+str(x)+&apos;\\n&apos;)
         f.write(&apos;line 2 &apos;+str(x)+&apos;\\n&apos;)
         f.write(&apos;line 3 &apos;+str(x)+&apos;\\n&apos;)
   &quot;&quot;&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proceso &lt;code&gt;generarFicheros&lt;/code&gt; se va a ejecutar en un container usando la imagen &lt;code&gt;python:latest&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Va a recibir un párametro de entrada &lt;code&gt;size&lt;/code&gt; que usará en el programa a ejecutar (&lt;code&gt;total=int($size)&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada fichero consistirá en 3 lineas que contendrán entre otras cosas, el índice del fichero simplemente a modo de
demostrar que se están procesando en paralelo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por cada fichero que se cree con el nombre &lt;code&gt;example.XXX&lt;/code&gt; el proceso emitirá un output&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;limpieza_de_fichero_bash&quot;&gt;Limpieza de fichero (bash)&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process eachFile{
   input:
     path file
   output:
     stdout
   &quot;&quot;&quot;
   head -n 1 $file
   &quot;&quot;&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proceso &lt;code&gt;eachFile&lt;/code&gt; recibe un fichero y simplemente muestra su contenido por consola.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que este proceso fuera por ejemplo una limpieza del fichero, quitar líneas mal formateadas, etc. En nuestro
ejemplo simplemente va a mostrar la primera línea de cada uno (es decir mostrará &quot;line 1 XX&quot; donde XX es el índice del
fichero que generó el proceso Python)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;procesado_java&quot;&gt;Procesado (Java)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último vamos a ejecutar por cada fichero un Java que lo único que hace es pasar a mayusculas el argumento proporcionado:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;import java.util.stream.Collectors;
import java.util.stream.Stream;

class MyJava{

    public static void main(String[]args){
        System.out.println( Stream.of(args).collect(Collectors.joining(&quot; &quot;)).toUpperCase());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En un proyecto típico, este código lo compilaríamos y generaríamos un Jar para ejecutarlo con &lt;code&gt;java -jar myproject.jar&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para este ejemplo lo que vamos a usar es JBang, una funcionalidad de Java que permite que le pasemos el código y él
se encarga de compilarlo y ejecutarlo directamente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues el proceso a definir en nuestro pipeline quedaría como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process toUpperString{
   container &apos;jbangdev/jbang-action&apos;
   containerOptions &quot;--volume $projectDir:/ws&quot;
   input:
      val str
   output:
      stdout
   &quot;&quot;&quot;
      jbang /ws/src/MyJava.java $str
   &quot;&quot;&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este process usamos una característica más de Nextflow. Además de indicarle la imagen que queremos usar en el
process, podemos proporcionar una cadena de configuración con &lt;code&gt;containerOptions&lt;/code&gt; que usaremos para montar el directorio de trabajo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow se encarga de descargar, montar volumenes y ejecutar en el container creado el comando&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;jbang /ws/src/MyJava.java $str&lt;/code&gt; donde &lt;code&gt;$str&lt;/code&gt; va a ser una cadena que se reciba como input&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;workflow&quot;&gt;Workflow&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último nos queda definir el workflow que une a estos tres process&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;workflow{
   generarFicheros(params.size).flatMap() | eachFile | toUpperString | view
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El workflow llama en primer lugar a &lt;code&gt;generarFicheros&lt;/code&gt; y mediante el operador &lt;code&gt;flatMap&lt;/code&gt; convertimos la
salida (una lista de ficheros) en una emisión en paralelo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada fichero será procesado por eachFile y cada salida de eachFile se enviará a una instancia de toUpperString&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;pipeline&quot;&gt;Pipeline&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El pipeline queda pues de esta forma:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process generarFicheros {
   container &apos;python&apos;
   input:
      val size
   output:
      path &apos;example.*&apos;

   &quot;&quot;&quot;
   #!/usr/bin/env python
   import sys
   size = int($size)

   for x in range(0, size):
      with open(&apos;example.&apos;+str(x), &apos;w&apos;) as f:
         f.write(&apos;line 1 &apos;+str(x)+&apos;\\n&apos;)
         f.write(&apos;line 2 &apos;+str(x)+&apos;\\n&apos;)
         f.write(&apos;line 3 &apos;+str(x)+&apos;\\n&apos;)
   &quot;&quot;&quot;
}


process eachFile{
   input:
     path file
   output:
     stdout
   &quot;&quot;&quot;
   head -n 1 $file
   &quot;&quot;&quot;
}

process toUpperString{
   container &apos;jbangdev/jbang-action&apos;
   containerOptions &quot;--volume $projectDir:/ws&quot;
   input:
      val str
   output:
      stdout
   &quot;&quot;&quot;
      jbang /ws/src/MyJava.java $str
   &quot;&quot;&quot;
}

workflow{
   generarFicheros(params.size).flatMap() | eachFile | toUpperString | view
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y lo ejecutaremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow run ./multilang.nf --size=5&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo va bien iremos viendo por consola las primeras lineas de los ficheros convertidas a mayúsculas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;N E X T F L O W  ~  version 22.04.5
Launching `./multilang.nf` [hungry_laplace] DSL2 - revision: bd507594b6
executor &amp;gt;  local (11)
[2a/65dd40] process &amp;gt; generarFicheros   [100%] 1 of 1 ✔
[79/0c4662] process &amp;gt; eachFile (2)      [100%] 5 of 5 ✔
[94/01790c] process &amp;gt; toUpperString (2) [100%] 5 of 5 ✔
LINE 1 0

LINE 1 4

LINE 1 3

LINE 1 1

LINE 1 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es interesante observar en esta salida cómo generarFicheros se ha ejecutado &lt;code&gt;1 of 1&lt;/code&gt; mientras que las otras
tareas se han ejecutado &lt;code&gt;5 of 5&lt;/code&gt; (ya que indicamos size=5 como parámetro de ejecución)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post hemos visto cómo usar los process para ejecutar aplicaciones diversas, como en este caso Python
y Java, así como el procesado en paralelo de las tareas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo hemos visto cómo podemos usar Docker incluso con imágenes diferentes en cada proceso junto con una
pequeña introducción al fichero de configuración &lt;code&gt;nextflow.config&lt;/code&gt; el cual explicaré en un futuro post en más detalle&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Explorando cómo usar diferentes lenguajes en los procesos</summary>
    </entry>
    <entry>
        <title>Groogle: preparacion entorno</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2023/inicio.html"/>
        <updated>2022-09-23T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2023/inicio.html</id>
        <category term="groovy"/>
        <category term="groogle"/>
        <category term="google"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Groogle es un conjunto de DSL (domain specific language) para trabajar con las APIs
 de Google&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo que quiere decir, es que siguiendo una sintáxis sencilla puedes programar pequeños
 (al principio) programas que interactúen con tu cuenta de Google.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo puedes enviar un correo usando tu cuenta de Gmail con sólo unas pocas lineas,
 o acceder a una hoja de cálculo y leer/escribir en ella usando una sintaxis muy fácil.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;groovy&quot;&gt;Groovy&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Groogle está implementado en Groovy por lo que necesitarás instalarlo. Groovy puede correr
 en máquinas Windows, Linux y Mac.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sigue los pasos de la página oficial e instala la versión &lt;strong&gt;3.0.10&lt;/strong&gt; :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://groovy-lang.org/install.html&quot; class=&quot;bare&quot;&gt;https://groovy-lang.org/install.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;También puedes usar las imágenes de Docker que se encuentran disponibles pero para
este tutorial vamos a usar la versión instalable.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;google&quot;&gt;Google&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tengas instalado Groovy tienes que crear un proyecto en la consola de Google&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://console.cloud.google.com/&quot; class=&quot;bare&quot;&gt;https://console.cloud.google.com/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Te va a solicitar que proporciones una tarjeta de crédito, pero tienes unos meses de
prueba y puedes cancelarlo cuando quieras. De todas formas la capa gratuita es suficiente,
al menos para el uso que yo hago, y yo no he incurrido todavía en ningún cargo.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El nombre del proyecto no importa, yo he elegido Groogle.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/google_cloud.png&quot; alt=&quot;google cloud&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;credenciales&quot;&gt;Credenciales&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ahora los scripts que vamos a ejecutar usarán tu cuenta de Google por lo que tenemos que
crear unas credenciales &quot;OAuth&quot;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;en el menu principal (la hamburgesa de arriba a la izquierda)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;selecciona &quot;API y servicios&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;submenu &quot;Pantalla de consentimiendo OAuth&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a crear una pantalla donde se le informará al usuario (a tí mismo) que vas a otorgar
acceso a una aplicación (hecha por tí):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Nombre, &lt;strong&gt;NO&lt;/strong&gt; pongas Google, Drive, o algo similar a productos de Google o dará un error que no dice
nada y te volverás loco. Yo la he llamado &quot;Quickstart&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;email, el mismo de la cuenta&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;no necesitas autorizar dominios&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez guardada crearemos las credenciales OAuth:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;en el menu de la izquierda&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;selecciona &quot;Credenciales&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;en la parte superior &quot;+ Crear credenciales&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Id de cliente OAuth&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En tipo de aplicacion vamos a seleccionar &quot;App de escritorio&quot; pues en este tutorial nos vamos a
centrar en desarrollar scripts de consola. El nombre de las credenciales no importa mucho pero
si es descriptivo mejor, por ejemplo &quot;Pruebas Groogle&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OJO: Descargar fichero del dialogo que aparece. Si lo cierras sin guardar tendrás que volver a empezar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El nombre con el que se guarda es un tanto enfarragoso, lo puedes renombrar por el que quieras,
por ejemplo &quot;cliente_groogle.json&quot;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este fichero habrá que copiarlo/usarlo en los scripts, tenlo a mano en alguna ruta fácil de recordar&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;habilitar_apis&quot;&gt;Habilitar APIs&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script que vamos a programar va a ser para enviar un correo electrónico usando nuestra cuenta de Gmail
por lo que lo primero que hay que hacer es habilitar esta Api en el proyecto:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;En el menu de la izquierda &quot;Biblioteca&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Buscar Gmail&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Habilitar&lt;/p&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Si quieres, en este punto puedes habilitar otras APIs con las que trabajaremos y así no tienes que
volver a esta pantalla. Por ejemplo puedes probar a activar Drive y Sheet&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;primer_script&quot;&gt;Primer script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En una carpeta de tu máquina crea un directorio, por ejemplo, &quot;groogle&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crea un fichero &lt;code&gt;test.groovy&lt;/code&gt; y copia este contenido&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;test.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Grab(&quot;com.puravida-software.groogle:groogle-gmail:3.1.1&quot;)

import com.google.api.services.gmail.GmailScopes

import com.puravida.groogle.GmailService
import com.puravida.groogle.GmailServiceBuilder

import static com.puravida.groogle.GroogleBuilder.build

build {
      withOAuthCredentials {
              applicationName &apos;test&apos;
              withScopes GmailScopes.GMAIL_SEND
              usingCredentials &quot;client_secret.json&quot;
              storeCredentials true
      }
      register GmailServiceBuilder.build(), GmailService

} with {
      service GmailService sendEmail{
          from &quot;me&quot;
          to &quot;jorge.aguilera@puravida-software.com&quot;
          subject &quot;Hi&quot;
          body &quot;&quot;&quot;
              Hi
              Oye, estoy probando Groogle y mola mucho
              a seguir dandole
          &quot;&quot;&quot;.stripIndent()
      }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Copia en este directorio el fichero con las credenciales y llamalo &quot;client_secret.json&quot; (o
como hayas puesto en el &lt;code&gt;usingCredentials&lt;/code&gt;)&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente este script:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;declara un artefacto para bajarse de Internet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;declara unos cuantos imports necesarios para decir qué vamos a usar&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;creamos una instancia de Groogle (llamando a &lt;code&gt;build&lt;/code&gt; ) configurando qué credenciales y scopes va a usar,
así como qué servicios va a manejar (Gmail en este caso)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una vez creada la instancia, buscamos el servicio GmailService y componemos un email&amp;#8201;&amp;#8212;&amp;#8201;from , es una direccion de correo. &quot;me&quot; es una palabra reservada por Google para indicar la cuenta de usuario&amp;#8201;&amp;#8212;&amp;#8201;to , la cuenta de correo a la que estamos escribiendo&amp;#8201;&amp;#8212;&amp;#8201;etc&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Body es una cadena de texto. En Groovy las cadenas de texto se pueden escribir de varias formas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si vas a escribir una sola linea, puedes usar una comilla simple: &apos;Hola pepe&apos;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;O una doble: &quot;Hola pepe&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así puedes meter una de ellas dentro de la otra: &quot;Hola pepe, &apos;bicho&apos; &quot;  (o a la inversa &apos;Hola pepe, &quot;bicho&quot; &apos;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si quieres escribir varias lineas, puedes hacer lo mismo pero usando 3 comillas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;&quot;&quot;
una
linea
otra
&quot;&quot;&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Groovy tiene una funcion muy util en las cadenas llamada &lt;code&gt;stripIndent&lt;/code&gt; que lo que hace es quitar los tabulados de cada
linea de tal forma, que al escribirla en el script podemos meter tabulados para hacerlo legible y al enviarlo se quitan&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejecutando_el_script&quot;&gt;Ejecutando el script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente ejecutaremos en una consola:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;groovy test.groovy&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien, nos mostrará por consola una URL para abrir en un navegador (o tal vez se te abra automáticamente, depende
del sistema y del navegador) y autorizar a la aplicación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta es la pantalla de autorización que vería un usuario al usar tu aplicación si fuera publica. Puedes aceptar la petición
puesto que es una aplicacion desarrollada por tí mismo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez aceptada, si no ha habido errores, yo debería recibir un correo a los pocos segundos (o al usuario que hayas puesto
como destinatario)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este es un primer script muy simple, pero si has llegado hasta aqui, tienes ya montado casi todo el proyecto para siguientes scripts&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En proximos articulos veremos cómo acceder a nuestro Drive y crear carpetas, subir documentos (o bajarlos), acceder a una hoja de
cálculo, etc y con pocas lineas de programación les iremos dotando de funcionalidades &amp;#8230;&amp;#8203;. o eso espero&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Preparación del entorno para ejecutar scripts de Groogle</summary>
    </entry>
    <entry>
        <title>Introduccion a Nextflow: Process</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/process.html"/>
        <updated>2022-09-17T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/process.html</id>
        <category term="groovy"/>
        <category term="nextflow"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el artículo &lt;a href=&quot;channels.html&quot;&gt;anterior&lt;/a&gt; vimos una introducción al concepto de Channel. En este, vamos a profundizar un poco más
en el concepto de process&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como ya dijimos un process es la unidad básica en el DSL de Nextflow y usamos los canales para conectarlos, por lo que
parecería lógico empezar explicandolos primero. Sin embargo he optado por explicar primero los canales porque así ahora podemos
hacer cosas más interesantes con ellos&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El process es la parte del DSL mediante la que definimos una entrada, una salida y una ejecución de sistema. Es decir, Nextflow es
agnóstico de lo que quieras procesar (sólo requiere que se pueda ejecutar en Linux). Tú compones el/los comando(s) a ejecutar y él se encarga
de orquestar las ejecuciones&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De esta forma tu pipeline puede ser tan simple como buscar una cadena en un fichero (básicamente ejecutar un grep) hasta leer un fichero
de gigas y procesarlos de forma paralela, ejecutando un Python por cada línea, etc y todo ello con la ventaja de que el mismo pipeline puedes
ejecutarlo en tu local, en un contenedor docker, usando AWS Batch, Google, etc&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;contando_lineas_de_un_fichero&quot;&gt;Contando lineas de un fichero&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a empezar definiendo un proceso simple:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;uno.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process example {
   input:
      path &apos;entrada&apos;

   output:
      stdout

   &quot;wc -l $entrada&quot;
}

workflow{
    example( params.param1 ) | view
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ejecutamos el pipeline&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow run uno.nf --param1 $(pwd)/uno.nf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y veremos que la salida nos dice el número de líneas del fichero &lt;code&gt;uno.nf&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;N E X T F L O W  ~  version 22.04.5
Launching `uno.nf` [exotic_jones] DSL2 - revision: c79a922618
executor &amp;gt;  local (1)
[28/a1eb27] process &amp;gt; example [100%] 1 of 1 ✔
12 input&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como vemos, el proceso &lt;code&gt;example&lt;/code&gt; requiere un fichero de entrada (dentro de su ejecución va a crear una variable &lt;code&gt;entrada&lt;/code&gt; que podremos referenciarla dentro de
la misma), y que va a emitir una salida tomándola del stdout del comando que va a ejecutar. Más adelante veremos qué otras posibilidades podemos usar&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;comando&quot;&gt;Comando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El comando a ejecutar se puede definir de varias formas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;La más simple es con un string al final de la definición del proceso&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process example{
  .. ... ...

  &apos;echo hola&apos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El comando que ejecutará &lt;code&gt;example&lt;/code&gt; será un simple &lt;code&gt;echo hola&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Un string multilinea, usando triple comillas&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process example{
  .. .. ...

  &apos;&apos;&apos;
  echo hola &amp;gt; f
  echo hi &amp;gt;&amp;gt; f
  echo ciao &amp;gt;&amp;gt; f
  cat f
  &apos;&apos;&apos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo el comando a ejecutar puede ser &quot;personalizado&quot; usando los inputs del process (atención al uso de comillas dobles en lugar de simples!!)
como en el caso de &lt;code&gt;uno.nf&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;&quot;wc -l $entrada&quot;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este caso Nextflow ejecutará un &lt;code&gt;wc&lt;/code&gt; sobre un fichero cuya ruta no está predefinida, sino que se usará la que se le proporcione al process.
Esto es así por el uso de comillas dobles que lo que hace es que la cadena sea &quot;compilada&quot; y se use el resultado&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;uniendo_procesos&quot;&gt;Uniendo procesos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes imaginar, podemos &quot;concatenar&quot; process uniendo las salidas de uno con las entradas de otro (en realidad usando canales)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;dos.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;process lines {

   input:
      path &apos;input&apos;

   output:
      stdout

   // wc es un programa de la shell
   &quot;wc -l $input&quot;
}

process cut {

   input:
      val lines

   output:
      stdout

   // cut es un programa de la shell
   // indicamos que use el espacio como delimitador y extraiga el primer campo
   // le pasamos una cadena con espacios como entrada, por eso la entrecomillamos
   &quot;cut -d &apos; &apos; -f 1 &amp;lt;&amp;lt;&amp;lt; &apos;$lines&apos;&quot;
}


workflow{
    lines( params.param1 )
    | cut
    | view
}&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este caso el workflow creará dos procesos, &lt;code&gt;lines&lt;/code&gt; y &lt;code&gt;cut&lt;/code&gt;. Este último permanecerá a la espera de recibir valores por su canal de entrada
(espera recibir un simple objeto &lt;code&gt;val&lt;/code&gt; y lo llamará &apos;line&apos; ). Por su parte &lt;code&gt;lines&lt;/code&gt; (el proceso, no confundir con la variable del proceso cut)
permanecerá a la espera de recibir un fichero de entrada
(al declarar un &lt;code&gt;path&lt;/code&gt; como entrada)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mediante el caracter pipe &quot;|&quot; indicamos al workflow cómo unir estos procesos de tal forma que la salida de uno sea la entrada de otro&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que se inicia el worflow, Nextflow alimenta el canal &lt;code&gt;params&lt;/code&gt; lo que hace que &lt;code&gt;lines&lt;/code&gt; se ejecute y su salida alimente a &lt;code&gt;cut&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;paralelización_divide_y_vencerás&quot;&gt;Paralelización. Divide y vencerás&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El ejemplo anterior muestra cómo definimos los procesos así como el &quot;flujo de trabajo&quot;. Como lines emite un sólo valor (el stdout generado por wc)
el worflow es lineal.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el ejemplo siguiente vamos a ver la capacidad de definir ejecuciones de procesos en paralelo. El ejemplo va a consistir en generar un fichero
de N líneas, y por cada línea ejecutaremos un process que &quot;haga algo&quot; con ella (simplemente imprimirla por su stdout). Para poder comprobar la
paralelización cada ejecución realizará primero una espera aleatoria mediante un random&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;tres.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;process generarFicheroEntrada {
   input:
      val size
   output:
      path &apos;origin.txt&apos;

   // el script puede ser multilinea
   // y podemos &quot;mezclar&quot; variables del proceso con las del script a ejecutar
   // si es variable del proceso no la escapamos con una barra: $size
   // si es variable del script la escapamos con la barra: \$x
   &quot;&quot;&quot;
   for x in {0..$size}
   do
       echo \$x &amp;gt;&amp;gt; origin.txt
   done
   &quot;&quot;&quot;
}


process simulate{
   input:
     val value
   output:
     stdout
   &quot;&quot;&quot;
     sleep \$((RANDOM % 10))
     echo  $value
   &quot;&quot;&quot;
}

workflow{
   simulate(
      generarFicheroEntrada(params.size).splitText()
    ) | view
}&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si por ejemplo ejecutas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nextflow run tres.nf --size=10&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;repetidas veces verás que la salida es diferente en cada ejecución. Esto es debido a que el proceso &lt;code&gt;simulate&lt;/code&gt; es ejecutado
en paralelo (en realidad todos los valores en paralelo a la vez no, sino en bloques que puedes definir como parametro de splitText)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El workflow de este ejemplo es sencillo de leer pero por detrás se están ejecutando conceptos bastante importantes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;se crea un process &lt;code&gt;simulate&lt;/code&gt; que permanecerá a la espera de que se vayan generando valores en su canal de entrada. Mientras no
reciba un EOF en el canal, el proceso se ejecutará repetidamente por cada valor en el mismo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;generarFicheroEntrada leerá de su canal de entrada un valor y enviará por el de salida un Path (no el contenido, sino un objeto
Path que apunta al fichero).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;splitText es un operador que actua sobre los valores emitidos por un canal. Lee sobre el canal de entrada (en este caso un Path)
y emite valores, uno por cada linea&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La definición del workflow es en realidad una Closure, un bloque de código, lo que permite definir el worflow de muchas formas.
Por ejemplo el mismo workflow puede escribirse:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;workflow{
   def fichero = generarFicheroEntrada(params.size)

   def split = fichero.splitText()

   simulate( split ).view()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post hemos visto cómo usar los process para ejecutar comandos de sistema dentro de un workflow. Básicamente su función
es generar y ejecutar un fichero .sh con el comando que definas, integrando dicha ejecución en el flujo de trabajo permitiendo
unir y controlar todas las ejecuciones.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;También hemos visto una pequeña introducción al concepto de workflow el cual trataremos más adelante&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Introducción al concepto de Procesos en Nextflow</summary>
    </entry>
    <entry>
        <title>Introduccion a Nextflow: Channels</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/channels.html"/>
        <updated>2022-09-12T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/channels.html</id>
        <category term="groovy"/>
        <category term="nextflow"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el artículo &lt;a href=&quot;basic.html&quot;&gt;anterior&lt;/a&gt; vimos los conceptos básicos de Nextflow (process, channel y workflow). En este, vamos a profundizar un poco más
en el concepto de canales (channel)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El Channel es el medio por el que &quot;hacemos llegar&quot; datos a los procesos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Existen múltiples formas de crear un canal. Algunos ejemplos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;ch1 = Channel.create()
ch1 &amp;lt;&amp;lt; 1
ch1 &amp;lt;&amp;lt; &apos;hola&apos;

ch2 = Channel.of( 1, 3, 5, 7 )

ch3 = Channel.fromPath( &apos;/data/some/*.txt&apos; )

ch4 = Channel.watchPath( &apos;/path/*.fa&apos; )&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y el canal irá emitiendo los elementos disponibles según se vayan consumiendo (es decir, el canal es una cola no bloqueante que conecta consumidores)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;También existe el tipo de Channel &lt;code&gt;value&lt;/code&gt; que emite siempre el mismo valor y que puede ser leído mútiples veces. Por ejemplo &lt;code&gt;chn = Channel.value(&apos;hi&apos;)&lt;/code&gt;
crea un canal que siempre emitirá el string &lt;code&gt;hi&lt;/code&gt; cuando se lea de él.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;operadores&quot;&gt;Operadores&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los canales disponen de un conjunto de operadores (extensibles, puedes añadir los tuyos mediante el mecanismo de plugins que hablaremos en otro post) que
permiten modificar los valores emitidos por estos (por ejemplo, un canal emite todos los ficheros de un directorio pero queremos filtrar por un
criterio diferente al nombre)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algunos de los operadores disponibles:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;filter (filtra por una expresion regular o por una lógica tuya, por ejemplo &lt;code&gt;channel.of(1,2,3).filter{ it % 2 == 1 }.view()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unique&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;first, last&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;until&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;map&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;collect&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;toSortedList&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;#8230;&amp;#8203; (la lista completa de los operadores por defecto está en &lt;a href=&quot;https://www.nextflow.io/docs/latest/operator.html#&quot; class=&quot;bare&quot;&gt;https://www.nextflow.io/docs/latest/operator.html#&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejemplo&quot;&gt;Ejemplo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como caso &quot;práctico&quot; vamos a crear un pipeline que descargue un CSV del Ayuntamiento de Madrid, filtre algunos registros y los muestre&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El csv en concreto es la relacion de accidentes de trafico ocurridos durante el último año, donde se especifican, entre otros, el
distrito y si hubo rastros de alcohol (campos 6 y 17 respectivamente) y el tipo de accidente (campo 7)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://datos.madrid.es/portal/site/egob/menuitem.c05c1f754a33a9fbe4b2e4b284f1a5a0/?vgnextoid=7c2843010d9c3610VgnVCM2000001f4a900aRCRD&amp;amp;vgnextchannel=374512b9ace9f310VgnVCM100000171f5a0aRCRD&amp;amp;vgnextfmt=default&quot;&gt;La página del catálogo de datos&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;parsecsv.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;workflow {

  Channel.fromPath( &quot;https://datos.madrid.es/egob/catalogo/300228-24-accidentes-trafico-detalle.csv&quot; )

    | splitCsv(sep:&apos;;&apos;)

    | filter{ row -&amp;gt;
        // distrito &amp;amp;&amp;amp; alchohol
	    row[6] == &apos;MORATALAZ&apos; &amp;amp;&amp;amp; row[17] == &apos;S&apos;
    }

    | map { row -&amp;gt;
        // tipo
	    row[7]
    }

    | view
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si ejecutas este pipeline obtendras algo como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;nextflow run ./parsecsv.nf
N E X T F L O W  ~  version 22.04.5
Launching `./parsecsv.nf` [shrivelled_meninsky] DSL2 - revision: 93c84a05a0
Atropello a persona
Vuelco
Colisión múltiple
Colisión múltiple
Choque contra obstáculo fijo
Vuelco
Choque contra obstáculo fijo
Choque contra obstáculo fijo
Colisión fronto-lateral
Alcance
Choque contra obstáculo fijo
Vuelco
Colisión fronto-lateral
Alcance
Choque contra obstáculo fijo
Vuelco
Choque contra obstáculo fijo
Choque contra obstáculo fijo
Choque contra obstáculo fijo&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;la utilidad real de los canales, y en realidad de Nextflow, va mucho más allá de este ejemplo pero creo que estos pequeños
ejercicios nos sirven para ir aproximandonos al lenguaje e ir intuyendo su potencial&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como puedes observar el DSL permite concatenar operadores mediante el uso del pipe &quot;|&quot;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Al ejecutar este pipeline, Nextflow creará un canal (anónimo) que emitirá un elemento &lt;code&gt;path&lt;/code&gt; (Nextflow es capaz de manejar URLs como
si fueran ficheros locales, como en este caso)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Este &lt;code&gt;path&lt;/code&gt; será consumido por el operador &lt;code&gt;splitCSV&lt;/code&gt; el cual a su ve emitirá las líneas del CSV parseadas como una lista&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cada línea será filtrada por la ejecución de una closure que devolverá &lt;code&gt;true&lt;/code&gt; o &lt;code&gt;false&lt;/code&gt; para indicar si el elemento tiene que ser emitido o no&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;El operador &lt;code&gt;map&lt;/code&gt; recibe una lista de elementos (por cada linea filtrada) y emite un sólo campo de esta lista&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Por último usamos un operador &lt;code&gt;view&lt;/code&gt; que simplemente muestra por consola lo que lee por su canal&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se puede intuir, los canales son una herramienta imprescindible para diseñar nuestros pipelines. Su naturaleza asíncrona y capacidad de
paralelizar el envío de los mensajes nos ofrecen una potencia para realizar tareas complejas muy interesante.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Introducción al concepto de Channel en Nextflow</summary>
    </entry>
    <entry>
        <title>Conceptos básicos</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/basic.html"/>
        <updated>2022-08-23T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/basic.html</id>
        <category term="groovy"/>
        <category term="nextflow"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Nextflow enables scalable and reproducible scientific workflows using software containers. It allows the adaptation of pipelines written in the most common scripting languages.
Its fluent DSL simplifies the implementation and the deployment of complex parallel and reactive workflows on clouds and clusters.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nextflow.io/&quot; class=&quot;bare&quot;&gt;https://www.nextflow.io/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el artículo &lt;a href=&quot;intro.html&quot;&gt;anterior&lt;/a&gt; vimos una introducción a Nextflow. En este vamos a profundizar un poco más
en los conceptos básicos de este lenguaje&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;pipeline&quot;&gt;Pipeline&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podemos definir el pipeline como el conjunto de tareas que tiene que realizar Nextflow. Básicamente es asociarlo con
el fichero (y sus includes) a ejecutar&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;process&quot;&gt;Process&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proceso, process, es la unidad básica a ejecutar. Podemos asociarlo con el comando de sistema a ejecutar para
completar una tarea. Dicho comando tendrá unos parámetros de entrada y generará una salida.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo los procesos son independientes y no comparten un estado común sino que se comunican entre sí mediante
colas (o canales)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;channel&quot;&gt;Channel&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un canal es el mecanismo por el cual los procesos se intercambian datos. Cuando definimos un proceso declaramos
su input pero no especificamos de donde leer este input. Es mediante los canales (y sus operadores) donde realizamos
esta interconexión o dependencia&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;flujo_de_trabajo&quot;&gt;Flujo de trabajo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya he mencionado, los procesos son unidades &quot;atómicas&quot; e independientes que lo único que necesitan para su
ejecución es disponer de los datos de entrada que definan (y producir unos datos de salida también definidos
por el propio proceso).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues, podemos pensar que al inicio de nuestro pipeline todos los procesos se encuentran
creados y a la espera de &quot;ser alimentados&quot;. Dicha alimentación se realiza a través de los diferentes operadores
que ofrece un Channel:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;hola.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process goodbye {
  input:
    path name
  output:
    stdout

    &quot;&quot;&quot;
    cat $name
    &quot;&quot;&quot;
}

process hi {
  input:
    val name

  output:
    path &quot;name.txt&quot;

    &quot;&quot;&quot;
    echo Hi $name &amp;gt; name.txt
    &quot;&quot;&quot;
}

workflow {
   Channel.of( &apos;Jorge&apos; ) | hi | goodbye | view
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo podemos ver dos procesos definidos (goodbye y hi)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;goodbye recibe un fichero como entrada (&lt;code&gt;path&lt;/code&gt;) y en su contexto lo vamos a referenciar como &lt;code&gt;name.
Su ejecución va a consistir en volcar (`cat&lt;/code&gt;) dicho fichero por la consola, emitiendo la salida&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;hi por su parte recibe una cadena (&lt;code&gt;val&lt;/code&gt;) y en su contexto la vamos a referenciar como &lt;code&gt;name&lt;/code&gt; (no confundir
con el de goodbye). Dicho proceso va a concatenar una cadena fija con el parámetro de entrada para generar
un fichero &lt;strong&gt;de trabajo&lt;/strong&gt; &lt;code&gt;name.txt&lt;/code&gt; y emitirá la ruta de este fichero&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último definimos el flujo de trabajo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Un canal emitirá una cadena fija, que será enviada al proceso &lt;code&gt;hi&lt;/code&gt;. La salida de este será enviada por el canal
al proceso &lt;code&gt;goodbye&lt;/code&gt; y por ultimo la salida de este será volcada por consola (&lt;code&gt;view&lt;/code&gt;)&lt;/p&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Este ejemplo, aunque no hace nada de utilidad, nos muestra los diferentes componentes de un pipeline.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como podemos intuir, el orden de ejecución de los procesos no viene determinado por su posición en el fichero
sino por la composición del flujo de trabajo. De esta manera Nextflow nos permitirá definir (y reutilizar)
procesos en diferentes ficheros y componerlos a nuestro antojo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;En los siguientes artículos iremos viendo, entre otras cosas, cómo ejecutar procesos en paralelo&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;executor&quot;&gt;Executor&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mientras que el proceso define el comando a ejecutar, el executor es el que define cómo se ejecutará dicho comando.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El executor por defecto es la máquina donde se está ejecutando el pipeline pero, sin modificar este,
podemos indicarle a Nextflow qué executor usar como por ejemplo un cluster SLURM, un container de Amazon o de Google,
etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así por ejemplo, cuando le indicamos a Nextflow que un pipeline lo ejecute, por ejemplo, en AWS él se encargará de aprovisionar la instancia, esperar a que se encuentre lista y ejecutar el script en esta (en realidad usando
las APIs del executor en concreto)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;programación&quot;&gt;Programación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea principal de Nextflow es que con la sintaxis que ofrece se disponga prácticamente de todas las herramientas
para definir pipelines complejos, pero aun así permite incluir scripts con toda la complejidad que se desee.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al estar desarrollado en Groovy es posible incluir scripts en el propio pipeline que nos ayuden a ampliar el lenguaje.
Así mismo permite la inclusión de librerías Java (jar) simplemente colocándolas en la carpeta &lt;code&gt;lib&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;random.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def cadenaRandom(){
   new Random().with {(1..9).collect {((&apos;a&apos;..&apos;z&apos;)).join()[ nextInt(((&apos;a&apos;..&apos;z&apos;)).join().length())]}.join()}
}


process hi {
  input:
    val name

  output:
    stdout

    &quot;&quot;&quot;
    echo $name is a random string
    &quot;&quot;&quot;
}

workflow {
   Channel.of( cadenaRandom() ) | hi | view
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;nextflow run ./random.nf
N E X T F L O W  ~  version 22.04.5
Launching `./dos.nf` [friendly_fourier] DSL2 - revision: d0b1349f31
executor &amp;gt;  local (1)
[65/ee7786] process &amp;gt; hi (1) [100%] 1 of 1 ✔
lswwkzafv is a random string&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este artículo hemos visto las piezas básicas que componen un flujo de trabajo así como una breve introducción
a la definición y ejecución de los procesos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Conceptos básicos en Nextflow</summary>
    </entry>
    <entry>
        <title>Introduccion a Nextflow</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/intro.html"/>
        <updated>2022-08-17T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/intro.html</id>
        <category term="groovy"/>
        <category term="nextflow"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Nextflow enables scalable and reproducible scientific workflows using software containers. It allows the adaptation of pipelines written in the most common scripting languages.
Its fluent DSL simplifies the implementation and the deployment of complex parallel and reactive workflows on clouds and clusters.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nextflow.io/&quot; class=&quot;bare&quot;&gt;https://www.nextflow.io/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow es un proyecto OpenSource muy usado sobre todo por la comunidad bioinformática (debido a que sus creadores
trabajaban en esta área,) pero que puede ser usado en otros ámbitos y que básicamente permite definir procesos que
puedan ser ejecutados de forma paralela tanto en local como en cloud sin tener que cambiarlos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello se ha creado un DSL (un lenguaje específico) en donde defines los procesos, con sus entradas, salidas, etc,
y el flujo de llamadas entre los mismos.  Este &quot;pipeline&quot; es ejecutado por Nextflow en el entorno que le digas y él
se encarga de aprovisionar los recursos necesarios para ejecutarlo y vigilar su ejecución.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El DSL es agnóstico a lo que tú quieras ejecutar. Es decir, puedes definir un proceso que recibe la ruta de un
fichero como parámetro de entrada y tú decides qué hacer con él: que Python lo ejecute, volcarlo a la salida con un &lt;code&gt;cat&lt;/code&gt;, ejecutar un programa de procesado de imágenes, &amp;#8230;&amp;#8203; Lo único que &quot;necesita&quot; Nextflow es que el &lt;code&gt;exitStatus&lt;/code&gt;
del programa le diga si ha ido bien o no para poder continuar con el resto de tareas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a verlo con un &lt;code&gt;hello-world&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;main.nf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process sayHello {
  input:
    val x
  output:
    stdout
  script:
    &quot;&quot;&quot;
    echo &apos;$x world!&apos;
    &quot;&quot;&quot;
}

workflow {
   sayHello(&apos;hola&apos;) | view
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este pipeline creamos un proceso &lt;code&gt;sayHello&lt;/code&gt; que recibe un &lt;code&gt;String&lt;/code&gt;, su retorno va a ser un volcado de la ejecución,
y que lo que va a ejecutar es un comando de bash &lt;code&gt;echo&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otra parte, se define el workflow del proceso, el qué ejecutar y en qué orden. En este ejemplo simplemente
vamos a ejecutar el proceso anterior y vamos a volcar su salida por pantalla&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;nextflow run ./hola.nf
N E X T F L O W  ~  version 22.04.5
Launching `./hola.nf` [jolly_shaw] DSL2 - revision: 3df369f295
executor &amp;gt;  local (1)
[9a/eed166] process &amp;gt; sayHello [100%] 1 of 1 ✔
hola world!&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podríamos ejecutar el mismo proceso vía Docker:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;nextflow run ./hola.nf -with-docker -process.container=ubuntu
N E X T F L O W  ~  version 22.04.5
Launching `./hola.nf` [fervent_wescoff] DSL2 - revision: 3df369f295
executor &amp;gt;  local (1)
[bf/f66b2d] process &amp;gt; sayHello [100%] 1 of 1 ✔
hola world!&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente al ser un simple echo no hay diferencia, pero la idea es que podrías ejecutar el comando usando la imagen
que te venga bien sin tener que instalar nada en tu equipo (más que Docker)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además de Docker, puedes ejecutar el proceso contra numerosos servicios de cloud como AWS, Google, Azure, Kubernetes
, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;instalación&quot;&gt;Instalación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nextflow requiere tener instalada una versión de Java moderna, aunque también corre con la versión 11.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para instalarlo simplemente ejecutaremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;curl -s &lt;a href=&quot;https://get.nextflow.io&quot; class=&quot;bare&quot;&gt;https://get.nextflow.io&lt;/a&gt; | bash&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(para más detalle consultar la página oficial &lt;a href=&quot;https://www.nextflow.io/&quot; class=&quot;bare&quot;&gt;https://www.nextflow.io/&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien y creas el fichero anterior deberías poder ejecutar el comando tal como se muestra en el
apartado anterior.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;canales&quot;&gt;Canales&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hasta ahora solamente hemos visto cómo ejecutar un proceso, pero una de las capacidades de Nextflow es poder definir
varios procesos y expresar cómo queremos que se ejecuten (paralelo, dependientes entre sí).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello usa el concepto de &lt;code&gt;Channel&lt;/code&gt; y &lt;code&gt;operators&lt;/code&gt;. Un channel podemos verlo como el &lt;code&gt;main&lt;/code&gt; de un flujo de procesos
y donde &quot;concatenamos&quot; la ejecución de los mismos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a modificar el ejemplo anterior:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;process sayHello {
  input:
    val x
  output:
    stdout
  script:
    &quot;&quot;&quot;
    echo &apos;$x world!&apos;
    &quot;&quot;&quot;
}

workflow {
  Channel.of(&apos;Bonjour&apos;, &apos;Ciao&apos;, &apos;Hello&apos;, &apos;Hola&apos;) | sayHello | view
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Channel.of&lt;/code&gt; recibe una lista de objetos y por cada uno de ellos invoca al proceso &lt;code&gt;sayHello&lt;/code&gt; mostrando la salida
de cada uno&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;N E X T F L O W  ~  version 22.04.5
Launching `./hola.nf` [golden_panini] DSL2 - revision: 68e7da3913
executor &amp;gt;  local (4)
[ea/bb9b4b] process &amp;gt; sayHello (2) [100%] 4 of 4 ✔
Bonjour world!

Hola world!

Hello world!

Ciao world!&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;of&lt;/code&gt; es sólo uno de los muchos operadores que ofrece &lt;code&gt;Channel&lt;/code&gt; (con el añadido de que puedes crear los tuyos si
echas en falta alguno). Hay operadores para filtrar, transformar, combinar, paralelizar, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Channel
    .from( &apos;a&apos;, &apos;b&apos;, &apos;aa&apos;, &apos;bc&apos;, 3, 4.5 )
    .filter( Number )
    .view()

Channel
    .from( 1, 2, 3, 4, 5 )
    .filter { it % 2 == 1 }
    .view()

Channel
    .from(1,2,3,40,50)
    .branch {
        small: it &amp;lt; 10
        large: it &amp;gt; 10
    }
    .set { result }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En próximos post espero poder ir explicando más funcionalidades y casos de uso. Por ahora basta deecir que &amp;#8230;&amp;#8203;
está implementado en Groovy y que formo parte del equipo de desarrollo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Introducción a Nextflow</summary>
    </entry>
    <entry>
        <title>Tootear desde Google Sheet</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/google-sheet-mastodon.html"/>
        <updated>2022-05-17T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/google-sheet-mastodon.html</id>
        <category term="google"/>
        <category term="mastodon"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A estas alturas no creo que haga mucha falta explicar qué es Mastodon pero por si acaso simplemente
decir que es una aplicación para el microblogging similar a Twitter aunque con algunas (muchas)
diferencias.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A diferencia de Twitter un usuario de Mastodon lo primero que tiene que elegir es a qué instancia
quiere pertenecer, entiendo instancia como una &quot;comunidad&quot;, &quot;grupo de usuarios con algo en común&quot;,
y es esa instancia la que se federa con otras instancias tejiendo una red de instancias de tal forma
que el toot (equivalente a tweet) se puede propagar entre la red.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El API de Mastodon es bastante simple (aunque totalmente funcinoal) y con un simple POST podemos enviar
toots.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a ver cómo mantener en una hoja de Google Sheet una lista de refranes preparados que
se enviarán de forma aleatoria cada día a una hora determinada por nosotros&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;mastodon&quot;&gt;Mastodon&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que estás dado de alta en una instancia puedes crear un API Token desde tu perfil, en
opciones de Desarrollo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crear una nueva aplicación con el nombre que quieras y marcando la opción de &lt;code&gt;write&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Te generará una token (una cadena de caracteres y simbolos raros).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;No compartas este token pues sirve para tootear en tu nombre&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;google_sheet&quot;&gt;Google Sheet&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La estructura de la hoja va a ser supersimple:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;en la primera fila tendremos una cabecera con dos columnnas, Estado y Refrán&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;en las siguientes filas vamos a escribir tantos refranes como queremos, uno por fila dejando la primera
columna vacía si queremos que el refrán se envíe o con el texto &quot;NO ENVIAR&quot; si por alguna razón no queremos
que sea enviado&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Estado&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Refrán&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Perro ladrador, poco mordedor&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Sabe más el diablo por viejo que por diablo&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;NO ENVIAR&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;No me mires de reojo &amp;#8230;&amp;#8203; buscar como continuaba&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el menú de Google Sheet seleccionamos &lt;code&gt;Extensiones&lt;/code&gt;, &lt;code&gt;Apps Script&lt;/code&gt; y se nos abrirá una ventana nueva&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De forma predeterminada te habrá escrito un par de líneas de código a modo de ejemplo que no nos interesa.
Las borramos y las sustituimos por este código:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;var instancia = &apos;mastodon.madrid&apos;;
var bootToken = &apos;1d04U1Wnc0h-xxxxxxXXXX-_YYYY&apos;;

function sendMastodon() {

  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();

  const rows = sheet.getRange(2,1,100,2).getValues()
  var candidates = []
  for( var r=0; r&amp;lt;rows.length; r++){
    const row = rows[r];
    if( row[0].toString() !== &quot;&quot;){
      continue;
    }
    if( row[1].toString().trim().length == 0){
      continue;
    }
    candidates.push(row[1])
  }

  const toot = candidates[Math.floor(Math.random() * candidates.length)];

  var payload = {
    &apos;status&apos;: toot,
  }
  var options = {
    &apos;method&apos; : &apos;post&apos;,
    &apos;contentType&apos;: &apos;application/json&apos;,
    &apos;payload&apos;: JSON.stringify(payload),
    &apos;headers&apos;: {
      &apos;Authorization&apos;: `Bearer ${bootToken}`
    },
    &apos;muteHttpExceptions&apos;:true
  };
  Logger.log(payload)

  var url = `https://${instancia}/api/v1/statuses`;
  const resp = UrlFetchApp.fetch(url, options);
  Logger.log(resp)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En las dos primeras tienes que personalizar con los datos de tu usuario:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;la instancia es el nombre
del servidor (por ejemplo mi servidor es &lt;a href=&quot;https://mastodon.madrid/&quot; class=&quot;bare&quot;&gt;https://mastodon.madrid/&lt;/a&gt; así que la variable es &lt;code&gt;mastodon.madrid&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el token que generamos al principio&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente el script lo que hace es recuperar las 100 primeras filas y filtrar aquellas que la primera columna está &quot;limpia&quot;
(si tuviera texto no se envia) y la segunda columna tiene algo (el refran a enviar)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez filtrados los refranes posibles se elige una de forma aleatoria y se prepara un POST para enviar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;el payload es un json con un solo campo &lt;code&gt;status&lt;/code&gt; que contiene el toot&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;headers contiene una autorizacion con el token&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;probando&quot;&gt;Probando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para probar que todo está bien ejecutaremos el script (que nos pedirá permisos para poder ejecutar en nuestra cuenta)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente nos aseguraremos que la funcion &lt;code&gt;sendMastodon&lt;/code&gt; se encuentra en el listbox del menú superior, junto al botón
&lt;code&gt;Ejecutar&lt;/code&gt;. Pulsaremos el botón &lt;code&gt;Ejecutar&lt;/code&gt; y si todo ha ido bien habremos envíado un toot a nuestra instancia&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;scheduler&quot;&gt;Scheduler&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podemos pulsar el botón ejecutar tantas veces como queramos pero para hacerlo de forma desatendida usaremos la capacidad
de planificar una llamada que nos ofrece Google Sheet:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el menú vertical de la izquierda buscaremos &lt;code&gt;Activadores&lt;/code&gt; (icono de un reloj despertador) y crearemos un nuevo activador&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;seleccionamos la funcion &lt;code&gt;sendMastodon&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;fuente de evento &lt;code&gt;Según tiempo&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;temporizador por dias&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a las 09:00&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(obviamente tú seleccionarás la frecuencia y hora a la que quieres ejecutarlo, esto es un ejemplo)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>cómo tootear en Mastodon de forma desatendida desde Google Sheet</summary>
    </entry>
    <entry>
        <title>Docker-compose con Gradle</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/gradle-docker-compose.html"/>
        <updated>2022-04-13T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/gradle-docker-compose.html</id>
        <category term="java"/>
        <category term="gradle"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probablemente este post a los puristas del TDD les resulte una aberración porque
lo que deberías es fomentar el uso de los test para guiar el desarrollo de tu aplicación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta idea una de las batallas que tienes que lidiar suele ser el tema de cómo preparar
una infraestructura para estos test. Por ejemplo, tu aplicación requiere una base de datos
y te toca usar un H2, pero puede ser más complicado y requerir además un Redis, un RabbitMQ
etc por lo que terminas usando &lt;code&gt;test-containers&lt;/code&gt; (todo un acierto)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Todo eso está muy bien pero a veces el desarrollo no puede, o no quieres, que sea guiado por los
test. A veces tienes que montar todo el entorno y tirar de depurador para reproducir un comportamiento
extraño, por ejemplo. Y casi siempre esto lo resuelves montando un docker-compose donde configuras
toda la infra y/o servicios requeridos por tu aplicación, donde tienes que lidiar con puertos, persistencia
, levantar los servicios etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a ver cómo podemos &quot;integrar&quot; este docker-compose con nuestro &lt;code&gt;build.gradle&lt;/code&gt; de tal
forma que podamos levantar esos servicios e integrarlos en nuestra app de forma cómoda integrandola en
nuestras task. Para ello vamos a usar el plugin &lt;code&gt;com.avast.gradle.docker-compose&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;docker_compose&quot;&gt;Docker-compose&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que nuestra arquitectura requiere de una base de datos MySQL, un Redis y un servicio propio
tuyo (distinto del que estás trabajando). Probablemente
nuestro docker-compose de desarrollo sería algo como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;version: &quot;3.5&quot;
services:

  mysql:
    image: mysql:5.6
    ports:
      - 3306
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: &quot;yes&quot;

  cache:
    image: redis:6.2-alpine
    restart: always
    ports:
      - 6379
    command: redis-server --save 20 1 --loglevel warning --requirepass tupwd

  business:
    image: pvidasoftware/greatapp
    ports:
      - 8080&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probablmente tu docker-compose tenga una configuracion distinta para los puertos y mapees un puerto
del contenedor con un puerto fijo de tu maquina para poder así acceder al servicio. No es mala idea pero
al final es un infierno de configuración cuando el proyecto es compartido entre varios miembros y
hay que ponerse de acuerdo en qué puertos usamos para qué cosas&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gradle&quot;&gt;Gradle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar vamos a incluir el plugin de &lt;code&gt;avast&lt;/code&gt; en nuestro build:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins {
    id &quot;java&quot; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
    ....
    id &quot;com.avast.gradle.docker-compose&quot; version &quot;0.15.2&quot; &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Si es un proyecto java sueles tener este plugin, pero no es necesario para este post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Añadimos el pluign de Avast, la ultima version a día de hoy&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esto nos va añadir una serie de &lt;code&gt;tasks&lt;/code&gt; en el grupo &lt;code&gt;docker&lt;/code&gt; como por ejemplo &lt;code&gt;composeUp&lt;/code&gt;
y &lt;code&gt;composeDown&lt;/code&gt; que obviamente sirven para gestionar un docker-compose que le indiquemos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En segundo lugar vamos a indicarle al plugin donde reside nuestro docker-compose&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;dockerCompose {
    useComposeFiles.add(&quot;infra/docker-compose.yml&quot;)
    isRequiredBy(tasks.run)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo le estamos diciendo que tenemos un docker-compose en el directorio &lt;code&gt;infra&lt;/code&gt; y
que la tarea que está interesada en controlar el estado del docker-compose es &lt;code&gt;run&lt;/code&gt; (tipica tarea
para correr el java)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último vamos a &quot;enlazar&quot; los servicios del docker con nuestra tarea que ejecuta la aplicación mediante variables de entorno:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;run.doFirst {
    dockerCompose.exposeAsEnvironment(run)

    def mysqlInfo = dockerCompose.servicesInfos.mysql.firstContainer &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
    def redisInfo = dockerCompose.servicesInfos.cache.firstContainer &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
    def businessInfo = dockerCompose.servicesInfos.business.firstContainer &lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;

    environment &quot;JDBC_URL&quot;, &quot;mysql://${mysqlInfo.host}:${mysqlInfo.ports[3306]}/&quot;

    environment &quot;REDIS_HOST&quot;, redisInfo.host
    environment &quot;REDIS_PORT&quot;, redisInfo.ports[6379]

    environment &quot;SERVICE_API_URL&quot;, &quot;http://${businessInfo.host}:${businessInfo.ports[8080]}/api/&quot;

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;servicesInfos.mysql obtiene info del servicio llamado mysql en el docker-compose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;servicesInfos.cache obtiene info del servicio llamado cache en el docker-compose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;servicesInfos.business obtiene info del servicio llamado business en el docker-compose&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El plugin se encarga de levantar los servicios y proporcionarnos información sobre los puertos
donde está escuchando cada uno, información que usamos para configurar nuestra tarea. En este
ejemplo configuramos un jdbc, un host y un puerto y una url a un api que son las variables
que se supone requiere nuestra aplicacion para poder ejecutarse correctamente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando ejecutemos la tarea &lt;code&gt;run&lt;/code&gt;, los servicios se levantan, se ejecuta nuestra aplicación y cuando
paramos la tarea, el plugin se encarga de parar el docker-compose (alguna vez me ha fallado y me ha tocado
pararlos a mano usando la tarea &lt;code&gt;composeDown&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Este plugin tiene unas cuantas configuraciones interesantes adicionales que iré investigando&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>cómo gestionar un docker-compose desde Gradle</summary>
    </entry>
    <entry>
        <title>Gradle Configuration</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2022/gradle-new-configuration.html"/>
        <updated>2022-03-14T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2022/gradle-new-configuration.html</id>
        <category term="java"/>
        <category term="gradle"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Seguro que ya lo sabes, pero por si acaso, Gradle es una herramienta para construir artefactos de software.
Es una alternativa al archifamoso Maven que en lugar de basarse en una definición de las tareas en XML,
com hace este, ha desarrollado su propio DSL siendo el fichero &lt;code&gt;build.gradle&lt;/code&gt; el &quot;punto de entrada&quot; donde
se definen las tareas, dependencias, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En Gradle una &lt;code&gt;configuration&lt;/code&gt; es un conjunto de artefactos junto con sus dependencias. Así por ejemplo,
en un proyecto típico de Java, Gradle ofrece de por sí dos configuraciones, &lt;code&gt;main&lt;/code&gt; y &lt;code&gt;test&lt;/code&gt;, representadas
cada una por sus respectivos directorios.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así cuando ubicamos un fichero .java en el directorio &lt;code&gt;src/main/java&lt;/code&gt; estamos asignandolo a la configuración &lt;code&gt;main&lt;/code&gt;,
por lo que se usarán las dependencias configuradas en &lt;code&gt;main&lt;/code&gt; sobre él, mientras que si ubicamos el fichero en
el directorio &lt;code&gt;test&lt;/code&gt; se aplicarán otras dependencias.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esto es útil para poder indicar de forma sencilla qué dependencias van en cada &lt;code&gt;configuration&lt;/code&gt;. Por ejemplo,
las dependencias anotadas con &lt;code&gt;testImplementation&lt;/code&gt; no se utilizan en construir los artefactos ubicados en &lt;code&gt;main&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo bueno de Gradle es que podemos extender este modelo para incluir nuevas configuraciones de una forma sencilla.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;objetivo&quot;&gt;Objetivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Queremos incluir en nuestro proyecto un nuevo tipo de Tests con unas necesidades específicas y queremos que se integren
dentro del resto de tareas pudiendo definir sus depencias, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por necesidades específicas puedes entender cualquier cosa: unos tests que sólo se pueden ejecutar en un entorno
dockerizado concreto, con unas variables de entorno particulares, requieren un software instalado previamente, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La solución habitual es etiquetarlos con algún tipo de condición de tal forma que si no se cumplen los requisitos,
el test se ignore, pero al final terminamos teniendo una mezcla de tests que dificulta su ejecución e incluso depuración.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo lo que vamos a crear es una configuración específica para testear una imagen nativa creada por el proyecto
usando GraalVM. &lt;strong&gt;Estos tests van a ejecutar la aplicación nativa y comprobar su funcionamiento como si fueran un usuario
ejecutandola desde consola&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nativeclitest&quot;&gt;nativeCliTest&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya hemos dicho, vamos a ubicar los test en un nuevo directorio, a la misma altura que el directorio &lt;code&gt;test&lt;/code&gt; (y &lt;code&gt;main&lt;/code&gt;)
. Una vez terminada la configuración, nuestro editor lo va a reconocer como uno más.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;span class=&quot;alt&quot;&gt;Diagram&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;build_gradle&quot;&gt;Build.gradle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para incluir este nuevo directorio como una nueva configuración en gradle, modificaremos el &lt;code&gt;build.gradle&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def nativeCliTest = sourceSets.create(&apos;nativeCliTest&apos;)

configurations[nativeCliTest.implementationConfigurationName].extendsFrom(configurations.testImplementation)
configurations[nativeCliTest.runtimeOnlyConfigurationName].extendsFrom(configurations.testRuntimeOnly)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con estas líneas estamos creando una nueva configuración igual a la de &lt;code&gt;test&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;dependencies{

    api &apos;....&apos;
    implementation &apos;....&apos;
    testImplementation &apos;.....&apos;

    nativeCliTestImplementation project
    nativeCliTestImplementation &quot;org.codehaus.groovy:groovy-sql:3.0.9&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Le indicamos a gradle que los artefactos bajo la nueva configuración requieren de las dependencias del proyecto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si para estos tipos de test requerimos alguna dependencia extra podemos indicarla como en el ejemplo (groovy-sql) y así
no estaremos &quot;contaminando&quot; a las otras configuraciones (es decir, en este ejemplo los artefactos de
&lt;code&gt;main&lt;/code&gt; ni de &lt;code&gt;test&lt;/code&gt; no ven esta nueva dependencia)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;def nativeCliTestTask = tasks.register(&apos;nativeCliTest&apos;, Test) {
    description = &apos;Runs nativeCli tests.&apos;
    group = &apos;verification&apos;
    useJUnitPlatform()

    testClassesDirs = nativeCliTest.output.classesDirs
    classpath = configurations[nativeCliTest.runtimeClasspathConfigurationName] + nativeCliTest.output

    dependsOn nativeCompile
    environment &apos;NATIVE_BINARY_PATH&apos;, &quot;$buildDir/native/nativeCompile/miaplicacion&quot;
}

tasks.named(&apos;check&apos;) {
    dependsOn(nativeCliTestTask)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Creamos una nueva tarea &lt;code&gt;nativeCliTestTask&lt;/code&gt; (o como quieras llamarla), que extiende &lt;code&gt;Test&lt;/code&gt;
y la configuramos para que use las dependencias de &lt;code&gt;nativeCliTest&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo la incluimos dentro de las tareas necesarias para completar la tarea &lt;code&gt;check&lt;/code&gt;, es decir, cuando
le indiquemos a Gradle que queremos ejecutar la tarea &lt;code&gt;check&lt;/code&gt;, gradle ejecutará :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;compileXXXX&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;build&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;test&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;integrationTest (si lo hubiera)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nativeCompile&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nativeCliTestTask&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;check&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;test&quot;&gt;Test&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con esta configuración cualquier test que creemos en la carpeta &lt;code&gt;nativeCliTest&lt;/code&gt; dispondrá de una variable
de entorno &lt;code&gt;NATIVE_BINARY_PATH&lt;/code&gt; con la ruta a la imagen nativa generada previamente&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;TIP&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Obviamente este es mi caso particular. En el tuyo lo que hacen particulares a estos test puede ser
cualquier otra variable de entorno, ejecutable, etc&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;import groovy.sql.Sql
import org.testcontainers.containers.MySQLContainer
import spock.lang.Specification
import spock.lang.Timeout

class MysqlTest extends Specification {

    static MySQLContainer container

    static {
        container = new MySQLContainer(&quot;mysql:5.6&quot;)
        container.start()
    }

    @Timeout(30)
    def &apos;should run native binary&apos; () {
        given:
        def BIN = System.getenv(&apos;NATIVE_BINARY_PATH&apos;)
        def CLI = [BIN,
                &apos;-u&apos;, container.username,
                &apos;-p&apos;, container.password,
                &apos;--url&apos;, container.jdbcUrl,
                &apos;--driver&apos;, &apos;com.mysql.cj.jdbc.Driver&apos;,
                &apos;--dialect&apos;, &apos;mysql&apos;
                ]

        when:
        def proc = new ProcessBuilder()
                .command(CLI)
                .redirectErrorStream(true)
                .start()
        and:
        def result = proc.waitFor()

        then:
        result == 0

        and:
        Sql.newInstance(container.jdbcUrl, container.username, container.password)
                .rows(&quot;SELECT * FROM mytable&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ide&quot;&gt;IDE&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(Al menos con Intellij) Al configurar un nuevo sourceSet el IDE es capaz de reconecer qué tipo es y qué
dependencias necesita, etc y nos permite ejecutar y depurar cada test de forma independiente si lo desamos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post me he centrado en añadir un nuevo tipo de test al estilo de &lt;code&gt;integrationTest&lt;/code&gt; o &lt;code&gt;functionalTest&lt;/code&gt;
pero puedes extenderlo a cualquier otro tipo de artefactos que requieran unas dependencias especiales&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>cómo crear una nueva configuracion en Gradle</summary>
    </entry>
    <entry>
        <title>Builders</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/effective-java/2-builders.html"/>
        <updated>2022-03-03T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/effective-java/2-builders.html</id>
        <category term="java"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el &lt;a href=&quot;1-constructors.html&quot;&gt;post&lt;/a&gt; anterior vimos cómo usar Factorias y las ventajas que estas
pueden aportar sobre los constructores&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a ver otra forma de construir objetos, los Builders.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;consider_a_builder_when_faced_with_many_constructor_parameters&quot;&gt;&quot;Consider a builder when faced with many constructor parameters&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tanto el uso de factorías como el de constructores tienen un problema cuando el número de parámetros para
construir una clase empieza a ser elevado&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Y cúando es elevado? pues cada cual tendrá su opinión pero en mi opinión más de tres parámetros ya es una
señal de que esa clase va a necesitar más bien pronto que tarde otro parámetro más y sería buena idea aplicar
un Builder&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Cuando te encuentras creando métodos similares a los que le vas añadiendo un parámetro cada vez
se le llama &quot;telescoping constructor&quot;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;telescoping constructor&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class Person{
    public static Person newInstance(){
        return new Person();
    }
    public static Person newInstance(String nombre){
        return new Person(nombre);
    }
    public static Person newInstance(String nombre, int edad){
        return new Person(nombre, edad...);
    }
    public static Person newInstance(String nombre, int edad, Date nacimiento){
        return new Person(nombre, edad...);
    }
    public static Person newInstance(String nombre, int edad, Date nacimiento, Ciudad ciudad){
        return new Person(nombre, edad...);
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Otra forma típica de inicializar objetos cuando el número de parámetros es numeroso es aplicando la aproximación de &lt;code&gt;JavaBeans&lt;/code&gt;
creando métodos get/set para cada propiedad de la instancia:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;java beans&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class Person{
    String nombre;
    int edad;
    Ciudad ciudad;
    ... getters/setters
}

Person p = new Person();
p.setNombre(&quot;nombre&quot;);
p.setEdad(12);
p.setXXX&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Sin embargo esta forma tiene muchos problemas&lt;/strong&gt; como por ejemplo
la validación de que todos los parámetros han sido proporcionados, es muy &quot;verbose&quot;, dificil de seguir y &lt;strong&gt;no puede crear objetos inmutables&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;builder&quot;&gt;Builder&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por todo ello el otro patrón que se aplica es creando un &lt;code&gt;Builder&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class Person{

    private final String nombre;

    public int getNombre() { return nombre;}
    // NO tenemos metodo setNombre

    public static class Builder{
        private final String nombre;

        public Builder(){}

        public Builder nombre(String nombre){ nombre = nombre; return this;}
        ...

        public Person build(){
            // validar que tenemos todos los parametros necesarios
            return new Person(this);
        }
    }
    private Person(Builder builder){
        nombre = builder.nombre;
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y el código para crear una instancia Person sería:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Person a = new Person.Builder().nombre(&quot;pepe&quot;).build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;TIP&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Si por ejemplo la clase requiere de unos parámetros requeridos es fácil pedirlos
en el constructor del Builder y declarar así explicitamente que son necesarios.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the_builder_pattern_is_well_suited_to_class_hierarchies&quot;&gt;&quot;The Builder pattern is well suited to class hierarchies&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta capacidad del patrón Builder no es fácil de ver de primeras (al menos a mí) pero es
muy potente&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando tenemos una jerarquía de objetos tipo clase base &quot;A&quot; y otras clases que extienden de ella,
&quot;B&quot; y &quot;C&quot;, podemos aplicar el patrón Builder usando genéricos de tal forma que reutilizemos al
máximo los builders&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public abstract class Person{

    abstract static class Builder&amp;lt;T extends Builder&amp;lt;T&amp;gt;&amp;gt;{

        public Builder(){
        }

        public T nombre(int nombre){
            this.nombre = nombre;
            return self();
        }

        abstract Person build();

        protected abstract T self();
    }

    private Person(Builder&amp;lt;?&amp;gt; builder){
        this.nombre = builder.nombre;
    }
    String nombre;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class Cliente extends Person{

    public static class Builder extends Person.Builder&amp;lt;Builder&amp;gt;{

        public Builder(){
        }

        @Override public CLiente build(){
            return new Cliente(this)
        }

        int codigo;

        public T codigo(int codigo){
            this.codigo=codigo;
            return self();
        }

        @Override protected Builder self(){ return this;}
    }

    int codigo;

    private Cliente(Builder builder){
        this.codigo = builder.codigo;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El &quot;truco&quot; se encuentra en el Generic de la clase A: &lt;code&gt;Builder&amp;lt;T extends Builder&amp;lt;T&amp;gt; &amp;gt;&lt;/code&gt; así como en el método
&lt;code&gt;self&lt;/code&gt;. Este es usado internamente por el builder para permitir al compilador poder concatenar las llamadas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Person person = new Cliente.Builder().nombre(&quot;pepe&quot;).codigo(2).build()&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Apuntes sobre el libro Effective Java. Capitulo dedicado a usar builders para construir objetos</summary>
    </entry>
    <entry>
        <title>Private Constructor</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/effective-java/3-private-constructor.html"/>
        <updated>2022-03-03T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/effective-java/3-private-constructor.html</id>
        <category term="java"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el &lt;a href=&quot;1-constructors.html&quot;&gt;post&lt;/a&gt; anterior vimos cómo usar Factorias y las ventajas que estas
pueden aportar sobre los constructores mientras que en el segundo &lt;a href=&quot;2-builders.html&quot;&gt;post&lt;/a&gt;
veíamos cómo usar el patrón Builder a la hora de construir objetos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post (corto) vamos a ver las ventajas que tiene y cuándo aplicar un constructor privado.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;enforce_noninstantiability_with_a_private_constructor&quot;&gt;&quot;Enforce noninstantiability with a private constructor&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Muchas veces nos encontramos con la típica clase &lt;code&gt;Util&lt;/code&gt; que lo único que contiene son un buen puñado de funciones
y campos privados, &quot;atentando&quot; contra todo diseño orientado a objetos. Sin entrar en tantas exquisitices estas
clases sí son útiles cuando consiguen agrupar un número de funciones comunes (el caso más común podría ser la
clase &lt;code&gt;java.util.Math&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La característica principal de estas clases es el que no tienen por objetivo ser instanciable. No tienen sentido
como objetos tal cual y lo que pretendemos es usar los métodos static directamente. &lt;strong&gt;Sin embargo&lt;/strong&gt;, si una clase
no tiene un constructor Java crea uno por defecto sin parámetro y &lt;strong&gt;público&lt;/strong&gt; con lo que cualquiera que vaya
a usar esta clase puede estar tentado a instanciar uno objeto de esta clase.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En muchos casos, para evitar esto, se define la clase como &lt;strong&gt;abstract&lt;/strong&gt; para evitar su instanciación pero el problema
es que se podría crear una clase que la heredera y esta ofrecer el constructor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por eso, cuando lo que queremos es tener una clase &lt;code&gt;Util&lt;/code&gt; es aconsejable añadirle un constructor sin parámetros
con visibiilidad &lt;code&gt;private&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class CalculosHacienda{

    private CalculosHacienda(){
        throw new AssertionError(); // no es necesario pero así reforzamos que no se puede crear el objeto
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como bola extra, el crear el método privado hace que la clase no pueda ser extendidas por otras pues el constructor
que estas implementaran NO podría llamar al de la clase padre al ser private&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Apuntes sobre el libro Effective Java. Capitulo dedicado a usar builders para construir objetos</summary>
    </entry>
    <entry>
        <title>Factorias vs Constructores</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/effective-java/1-constructors.html"/>
        <updated>2022-02-23T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/effective-java/1-constructors.html</id>
        <category term="java"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El tema de los constructores de objetos es prácticamente lo primero que se aprende cuando
empiezas a usar el paradigma OOP (programación orientada a objetos).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Yo lo tuve que aprender en el paso de C a C plus plus (en el cual supongo que se inspiró luego Java y otros) y recuerdo que me explotó la cabeza cuando por fín más o menos lo entendí, teniendo en cuenta que además
C plus plus permite la herencia múltiple, por lo que las llamadas entre constructores de la clase instanciada y
las heredadas era todo un laberinto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En Java el método que se llame como la clase es el constructor, y puedes tener más de uno simplemente
definiendo diferentes parámetros de entrada:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class Person{
    Person(){}
    Person(String nombre)
    Person(String nombre, int edad)
    Person(String nombre, int edad, String pais)
    Person(String nombre, int edad, String pais, String moneda)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Todos hemos hecho estos constructores (y de hecho me consta que seguimos haciendolos, no me escondo) y sufrido por ello.
Effective Java propone una serie de consejos para mejorar la construcción de objetos:&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;consider_static_factory_methods_instead_of_constructors&quot;&gt;&quot;Consider static factory methods instead of constructors&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además de los constructores típicos (o en lugar de), el autor propone usar métodos estáticos que sirvan para construir
los objetos. Por ejemplo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class Person{
    public static Person newInstance(){
        return new A();
    }
    public static Person newInstanceWithName(String nombre){
        return new A(nombre);
    }
    public static Person newInstanceWithNameAndAge(String nombre, int edad){
        return new A(nombre, edad);
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque nada te obliga a seguir haciendo los constructores públicos, la propuesta es hacerlos &quot;invisibles&quot;
al resto de clases en favor de los métodos factory&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;TIP&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;a diferencia del constructor (que tiene que llamarse como la clase), los métodos pueden tener nombre.
No es lo mismo &quot;new A(arg0, arg1)&quot; que &quot;A.newInstanceWithNameAndAge( name, age)&quot;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando el número de parámetros en el constructor no ayuda en elegir cual de ellos usar, pero un factory método sí pude hacerlo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Si tienes un objeto con uno o varios constructores, o si el número de parámetros para invocarlo es
&quot;grande&quot; o &quot;confuso&quot;, hazlos privados o mejor aún, eliminalos, y conviertélos en métodos static factory&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;TIP&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;otra ventaja es que a diferencia del constructor, que siempre crea un objeto nuevo, el método no tiene
porqué.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes entonces jugar con Caches o instancias construidas como static, etc que te permitan reutilizar
objetos ya construidos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;TIP&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;no estás sujeto a devolver una instancia de esa clase&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes devolver objetos que la extiendan, etc sin necesidad de que el que llame al método tenga que hacer casteos ni preocuparse por conversiones&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;TIP&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;flexibilidad para cambios futuros en las clases devueltas.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Derivado del comentario anterior, si el objeto sólo se puede construir a través de metódos estáticos,
puedes cambiar en una versión futura el objeto devuelto sin afectar a quien lo llama. Por ejemplo,
clases que se deprecan pueden dejar de ser utilizadas cambiando simplemente el factory&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;TIP&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;permite construir clases &quot;al vuelo&quot;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque parece algo &quot;para nota&quot;, el método factory puede usar la potencia de Java y obtener la implementación a devolver en el momento en el que se le llame (por ejemplo usando &lt;code&gt;service provider framework&lt;/code&gt;, descargando el jar de un repositorio remoto, etc)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;si sólo tienes métodos static factory, la clase no puede contener subclases&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;tienes que usar nombres de métodos claros que indiquen que son factoria&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Todos los programadores, y el IDE; saben buscar el constructor de una clase pero al usar ahora métodos &quot;no oficiales&quot; hay que
facilitar al que los va a usar el encontrarlos por lo que es bueno usar nombres de métodos reconocidos por las buenas prácticas
y/o descriptivos ( como por ejemplo &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;valueOf&lt;/code&gt;, &lt;code&gt;newInstance&lt;/code&gt;, &amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Apuntes sobre el libro Effective Java. Capitulo dedicado a usar factoria para construir objetos</summary>
    </entry>
    <entry>
        <title>Effective Java, apuntes</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/effective-java/intro.html"/>
        <updated>2022-02-22T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/effective-java/intro.html</id>
        <category term="java"/>
        <content type="html">
            &lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/effective-java/portada.jpg&quot; alt=&quot;portada&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hace mucho tiempo (demasiado) me leí la primera edición de este libro, imprescindible para todo programador Java y tengo que admitir que cayó en el olvido, pero más por mi culpa que por el libro.
Muchos de los consejos que se recogen en él los vas aprendiendo después de enfrentarte a situaciones
y no saberlas resolver e ir viendo cómo otros las han resuelto de forma elegante, cuando en realidad
podía haberlas aplicado yo mismo si hubiera leido con antención en su momento.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Creo que parte de olvidar muchas de las cosas que leemos (técnicas) es que no las ponemos en práctica
inmediatamente, así que esta vez que ha caído la tercera edición en mis manos voy a intentar ir
&quot;tomando apuntes&quot; y escribiendo por aquí los resúmenes&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así que este primer post es más un compromiso de subir futuros artículos que uno en sí mismo, lo admito.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A modo de guía, y fusilando por completo el índice del libro, iré desmenuzando:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Capitulo 1, creando y destruyendo objetos&amp;#8201;&amp;#8212;&amp;#8201;&lt;a href=&quot;1-constructors.html&quot;&gt;Factorias vs Constructores&lt;/a&gt;&amp;#8201;&amp;#8212;&amp;#8201;&lt;a href=&quot;2-builders.html&quot;&gt;Builders vs Constructores&lt;/a&gt;&amp;#8201;&amp;#8212;&amp;#8201;&lt;a href=&quot;3-private-constructor.html&quot;&gt;Private constructor&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 2, Métodos comunes&amp;#8201;&amp;#8212;&amp;#8201;los dichosos equals, hashCode y toString&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 3, Clases e Interfaces&amp;#8201;&amp;#8212;&amp;#8201;Interfaces (aquí hay tela marinera)&amp;#8201;&amp;#8212;&amp;#8201;Composicion vs herencia&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 4, Genéricos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 5, Enums&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 6, Lambdas y Streams,&amp;#8201;&amp;#8212;&amp;#8201;aquí usaré probablemente otro libraco que viene muy bien explicado&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 7, Metodos&amp;#8201;&amp;#8212;&amp;#8201;parametros, varargs, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 8, Programación en general&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 9, Excepciones. Nadie sabemos usarlas de verdad&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 10, Concurrencia. Nota mental, desmenuzar GPars&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capítulo 11, Serializacion, me da que esto ya casi nadie habla de ello&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
        </content><summary>Apuntes sobre el libro Effective Java</summary>
    </entry>
    <entry>
        <title>Cerrando la cuenta personal de Twitter</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/chapo-twitter.html"/>
        <updated>2021-12-01T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/chapo-twitter.html</id>
        <category term="personal"/>
        <content type="html">
            &lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pues después de unos cuantos años (no muchos, no era de los más veteranos) usando Twitter al final he cerrado la cuenta
personal aunque sigo manteniendo, por eso de quien no está en Twitter no está, la de @pvidasoftware.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras haber desinstalado la app en el móvil hace ya muchos meses y usar sólo el navegador, después de haberme hecho extensiones
de Chrome para auto-reducirme el consumo y las funcionalidades (Chrome extension no-more-twitter) e incluso hacerme firmes
propósitos de usar la cuenta personal para cosas positivas, al final me es imposible y la he cerrado.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que podía ser un cierre temporal ? pues claro, pero mira, muerto el perro se acabo la rabia.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La pega es que ya no puedo hacer
uso de esta plataforma para notificar cuando hay nuevos post así que te pediría que si por un casual lees este post te pases
el canal de Telegram y mandes un saludo (no hace falta que te quedes si no quieres)&lt;/p&gt;
&lt;/div&gt;
        </content><summary>he cerrado la cuenta personal de twitter y aqui me explico los motivos</summary>
    </entry>
    <entry>
        <title>Amigo invisible con GoogleSheet</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/googlesheet-amigo-invisible.html"/>
        <updated>2021-11-07T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/googlesheet-amigo-invisible.html</id>
        <category term="google"/>
        <category term="sheet"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Siguiendo los post sobre GoogleSheet en este vamos a usarlo para enviar correos electrónicos a usuarios
creando una hoja de cálculo para el amigo invisible&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Todos los años en nuestra familia nos hacemos el amigo invisible. Ya sabes, nos reunimos, escribimos el nombre de cada uno
en un papelito, los juntamos todos y vamos repartiendolos en secreto. Si a alguien le toca así mismo tiene que decirlo
y volvemos a empezar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a hacer un script simple que lo haga por nosotros y envie un correo a cada uno con un mensaje
indicando quién le ha tocado (con un poco más de trabajo podríamos hacer que cada uno pudiera incluso una lista de cosas
que le gustaría que le regalaran, etc)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación&quot;&gt;Preparación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para el sorteo vamos a necesitar los nombres y correos electrónicos de cada participante, los cuales los pondremos en
filas en el rango A2:B20, poniendo en la columna A los nombres y en la B los correos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como los otros script, simplemente necesitaremos una hoja de Google y acceder al editor de secuencias de comandos.
Reemplazaremos el código que nos propone por este otro:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;function shuffleArray(array) {
    for (var i = array.length - 1; i &amp;gt; 0; i--) {
        var j = Math.floor(Math.random() * (i + 1));
        var temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

function equalList(a, b){
  for(var i=0; i&amp;lt;a.length; i++){
    if( a[i]===b[i])
      return true;
  }
  return false;
}

const subject = &quot;Tu amigo invisible&quot;

function repartir() {
  const sheet = SpreadsheetApp.getActiveSheet()
  const range = sheet.getRange(2,1,20,3).getValues()
  const friends = [];
  let availables = []
  for( var r = 0; r &amp;lt; range.length; r++){
    if( !range[r][0] )
      continue;
    friends.push( range[r][0])
    availables.push( range[r][0])
  }
  for( var retry=0; retry&amp;lt;10; retry++){
    shuffleArray(availables)
    Logger.log(availables)
    if( equalList(friends, availables) == false)
      break
  }
  for( var r=0; r&amp;lt;availables.length; r++){
    range[r][2] = availables[r];
    const message = `Hola ${range[r][0]} \n En el sorteo del amigo invisible te ha tocado: ${range[r][2]}`
    MailApp.sendEmail(range[r][1], subject, message);
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente vamos a tener dos funciones de apoyo (una para randomizar una lista de nombres y otra para comparar
dos listas y si algun elemento se encuentra repetido en la misma posición marcarlas como &quot;iguales&quot;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La lógica principal reside en la funcion repartir, que simplemente leerá la hoja de Google, randomizará la lista
de participantes y comprobará que ninguno se ha tocado así mismo (si así fuera, randomizamos de nuevo)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez asignados iremos enviando un correo a cada uno de los participantes indicandoles quién le ha tocado como
amigo invisible.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Lógicamente, el usuario que ejecute el script podrá saber a quién le ha tocado cada uno simplemente mirando
la bandeja de salida, así que pídete tú ser quien lo organizará este año&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente falta añadir un botón o disparador de la función al estilo de los otros post sobre GoogleSheet y ejecutarlo
para que cada miembro de la lista reciba su correo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>un ejemplo sencillo de cómo usar GoogleSheet para enviar correos</summary>
    </entry>
    <entry>
        <title>Primeros pasos con k3s</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/k3s.html"/>
        <updated>2021-11-03T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/k3s.html</id>
        <category term="k8s"/>
        <category term="k3s"/>
        <category term="kubernetes"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;En Internet hay ahora mismo muchos post mejores que este sobre cómo
instalar y desplegar apliaciones en k3s, pero pocos en español y sobre todo
actualizados a la última versión &lt;code&gt;v1.21.5+k3s2&lt;/code&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;K3s es la distribución ligera de kubernetes de Rancher (&lt;a href=&quot;https://rancher.com/docs/k3s/latest/en/&quot; class=&quot;bare&quot;&gt;https://rancher.com/docs/k3s/latest/en/&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es super fácil de instalar (al menos en Linux):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;curl -sfL &lt;a href=&quot;https://get.k3s.io&quot; class=&quot;bare&quot;&gt;https://get.k3s.io&lt;/a&gt; | sh -&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y si tienes ya instalado &lt;code&gt;kubectl&lt;/code&gt;, el comando docker de kubernetes, puedes desplegar aplicaciones
simplemente configurando tu variable de entorno KUBECONFIG=/etc/rancher/k3s/k3s.yaml o importando el
fichero a tu $HOME/.kube/config&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;puedes tener múltiples clusters, usuarios, etc en tu fichero .kube/config, por ejemplo apuntando
a tu recien instlado k3s y además a tu cluster en Okteto y/o diferentes namespaces. Simplemente cuando
ejecutes el comando &lt;code&gt;kubectl&lt;/code&gt; tienes que indicarle el contexto contra el que ejecutarse&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;objetivo&quot;&gt;Objetivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desplegar un simple &quot;hello-world&quot; en el cluster (de una sóla máquina) y poder acceder a ella mediante
algo parecido a &quot;http://localhost:xxxx/hello-world&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez conseguido podremos continuar desplegando otras aplicaciones en diferentes rutas de tal forma
que podamos ofrecer a los usuarios de nuestra red un único sitio en el que encontrar las aplicaciones
si tener que andar preocupandonos por puertos, etc y a la vez tener un sitio centralizado donde
desplegarlas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Una vez instalado k3s en mi local estuve leyendo multitud de post sobre cómo hacer que la
aplicación tuviera su ruta y prácticamente todos hablaban de desinstalar &lt;code&gt;traefik&lt;/code&gt; e instalar una versión
nueva que lo permitía. El caso es que la versión actual de k3s YA LA INCLUYE y no hay que hacer nada
, sólo &quot;atinar&quot; con la configuración correcta&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para esta prueba vamos a desplegar una aplicacion &lt;code&gt;WhoAmI&lt;/code&gt; que simplemente devuelve información sobre
el container donde está corriendo, para lo que usaremos la imagen &lt;code&gt;containous/whoami:latest&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ir paso a paso voy a usar diferentes ficheros de despliegue pero puedes &quot;juntarlos&quot; todos en uno
sólo y desplegarlo en un sólo paso&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;deployment&quot;&gt;Deployment&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este fichero describimos la aplicacion que queremos desplegar (en nuestro caso la imagen mencionada
anteriormente)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;deployment.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami-deployment
  labels:
    app: whoami
spec:
  replicas: 3
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
        - name: whoami
          image: containous/whoami:latest
          ports:
            - containerPort: 80&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo vamos a correr 3 replicas (por probar)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f deployment.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl get pods&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;NAME                                 READY   STATUS    RESTARTS   AGE
whoami-deployment-67446995f4-ccfjv   1/1     Running   1          2m&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;service&quot;&gt;Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien, nuestros pods están corriendo en el cluster pero no
se pueden acceder a ellos, para lo que desplegamos un servicio:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;service.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  selector:
    app: whoami
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Fijate cómo enlazamos el nombre del deployment &lt;code&gt;whoami&lt;/code&gt; con el selector, así
como mapeamos los puertos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;kubectl apply -f service.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;kubectl get services
NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes       ClusterIP   10.43.0.1       &amp;lt;none&amp;gt;        443/TCP   37h
whoami-service   ClusterIP   10.43.110.224   &amp;lt;none&amp;gt;        80/TCP    2m&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con este servicio k3s nos crea una IP y un puerto con el que poder acceder a
nuestros pods, pero lo que queremos es no tener que manejar tantas IPs y puertos,
así que lo que necesitamos es definir una ruta en nuestro cluster que nos permita
crear una ruta hacia nuestro servicio&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ingress&quot;&gt;Ingress&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Precisamente, mediante este fichero, le podemos decir a k3s que haga ese routeo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Esta configuración usa la última versión que es precisamente lo que no encontraba
en todos los post que trataban el tema&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ingress.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-ingress
spec:
  rules:
  - http:
      paths:
      - path: /quiensoy
        pathType: Prefix
        backend:
          service:
            name: whoami-service
            port:
              number: 80&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aplicando este fichero le estamos diciendo a k3s (en realidad a traefik que es el
encargado de realizar el routeo) que las peticiones a &lt;code&gt;/quiensoy&lt;/code&gt; las rediriga al servicio
&lt;code&gt;whoami-service&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez aplicado el fichero podemos navegar a&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://localhost/quiensoy&quot; class=&quot;bare&quot;&gt;http://localhost/quiensoy&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y la página nos contestará algo como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Hostname: whoami-deployment2-746cf888d5-ss2f9
IP: 127.0.0.1
IP: ::1
IP: 10.42.0.58
IP: fe80::80af:5dff:fe12:6e48
RemoteAddr: 10.42.0.43:51310
GET /quiensoy HTTP/1.1
Host: 192.168.1.126
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 10.42.0.1
X-Forwarded-Host: 192.168.1.126
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: traefik-97b44b794-8xfp7
X-Real-Ip: 10.42.0.1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Fijate, que como desplegamos varias réplicas, puedes refrescar la página y cada vez dará
información diferente del container (cambia Hostname)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>instalando y desplegando una primera app en k3s</summary>
    </entry>
    <entry>
        <title>Enviar imágenes y texto a Telegram desde GoogleSheet</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/googlesheet-telegram2.html"/>
        <updated>2021-10-24T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/googlesheet-telegram2.html</id>
        <category term="google"/>
        <category term="telegram"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Un lector ha pedido si sería posible modificar el script para enviar primero la imagen
y luego el texto, así que he aprovechado para crear otro post y engordar el blog.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este post es simplemente otra forma de enviar mensajes a Telegram desde GoogleSheet parecido
al del post &lt;a href=&quot;googlesheet-telegram.html&quot; class=&quot;bare&quot;&gt;googlesheet-telegram.html&lt;/a&gt;, así que básicamente copiare el codigo nuevo sin muchas
explicaciones&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como este post se basa exactamente en el post comentado los requisitos son los mismos así como la
preparación y entorno de ejecución. Simplemente vamos a usar otra disposición de las celdas y en
lugar de enviar a muchos grupos vamos a suponer que queremos mandar a uno sólo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;hoja_excel&quot;&gt;Hoja Excel&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script se va a basar en la disposición de celdas reflejadas en la imagen siguiente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/telegramgoogle/sheet2.png&quot; alt=&quot;sheet2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver la idea ahora es poner en filas los mensajes que queremos enviar y asociar una imagen a
cada uno (así mismo fíjate que podemos formatear el texto con negritas, subrayados, etc usando un subset de Markdown)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La &quot;clave&quot; para que el script se ejecute correctamente es la primera columna &quot;Estado&quot; donde el script
irá mirando si queremos enviar esa fila. &lt;strong&gt;Aquellas filas que estado sea &lt;code&gt;enviar&lt;/code&gt; serán enviadas al grupo&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo, a modo de control, el script usará esta columna para indicar si ha podido enviar el mensaje y cuando lo ha hecho
y de esta forma facilitar el poder ejecutar el script sin repetir mensajes&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;El script sólo leerá las primeras 100 filas&lt;/strong&gt;, así que de vez en cuando haz limpieza de estas borrando/moviendo las que ya
no te interesen&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script es muy parecido al anterior así que simplemente comentar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ahora recorremos filas de un rango (A5:C100)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;enviamos un primer mensaje a Telegram llamando al endpoint &lt;code&gt;/sendPhoto&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;enviamos un segundo mensaje con el texto a modo de explicación de la foto&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;var ui = SpreadsheetApp.getUi();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var rangeData = sheet.getDataRange();
var lastColumn = rangeData.getLastColumn();
var lastRow = rangeData.getLastRow();

function sendTelegrams() {

  const bootToken = sheet.getRange(1,3).getValue()
  const group = sheet.getRange(2,3).getValue()

  const rows = sheet.getRange(5,1,100,5).getValues()
  for( var r=0; r&amp;lt;rows.length; r++){
    const row = rows[r];
    if( row[0].toString().toLowerCase() !== &quot;enviar&quot;){
      continue;
    }
    const caption = (row[2]||&apos;&apos;)+&apos;\n&apos;+(row[3]||&apos;&apos;)+&apos;\n&apos;+(row[4]||&apos;&apos;);
    var payload = {
      &apos;chat_id&apos;:group,
      &apos;photo&apos;: row[1],
      &apos;caption&apos;: caption,
      &apos;parse_mode&apos;:&apos;MarkdownV2&apos;,
      &apos;disable_web_page_preview&apos;:false
    }
    var options = {
      &apos;method&apos; : &apos;post&apos;,
      &apos;contentType&apos;: &apos;application/json&apos;,
      &apos;payload&apos;: JSON.stringify(payload),
      &apos;muteHttpExceptions&apos;:true
    };

    var url = &apos;https://api.telegram.org/bot&apos;+bootToken+&apos;/sendPhoto&apos;;
    const respImage = UrlFetchApp.fetch(url, options);
    Logger.log(payload)
    Logger.log(respImage)
    sheet.getRange(5+r,1).setValue(respImage.getResponseCode()==200?&quot;Enviado &quot;+new Date():respImage.getContentText())
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y ya estaría. Cada vez que pulsemos el botón &quot;Enviar&quot;, el script se ejecutará (si seguiste los pasos del post anterior)
recorriendo las filas y enviando aquellas que estén marcadas como &quot;enviar&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>un ejemplo práctico de cómo enviar mensajes de Telegram usando GoogleSheet</summary>
    </entry>
    <entry>
        <title>Primeros pasos en programación reactiva, IV</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/reactivex/reactivex-4.html"/>
        <updated>2021-10-11T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/reactivex/reactivex-4.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <category term="rxjava2"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta serie de post acerca de programación reactiva voy a ir contando los pasos que estoy dando para ir practicando con esta forma de programación y más en concreto su uso en aplicaciones HTTP con Micronaut&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el post anterior (&lt;a href=&quot;reactivex-3.html&quot; class=&quot;bare&quot;&gt;reactivex-3.html&lt;/a&gt;) nos centramos en la parte consumidora viendo cómo podemos usar el modelo reactivo en la construcción
de un API que consuma endpoints remotos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post lo que vamos a tratar es el caso de una llamada a una función de negocio que puede fallar y cómo realizar reintentos dentro de un contexto asíncrono.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;servicio&quot;&gt;Servicio&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que tenemos un microservicio que dada una cadena la convierte a mayusculas, ordenas los caracteres por orden alfabético y devuelve la cadena
resultante y que queremos llamar de forma asíncrona&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Get(&quot;/{word}&quot;) Single&amp;lt;String&amp;gt; reverse(final String word) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usando Java Stream la &quot;lógica de negocio&quot; podría ser algo así&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;List&amp;lt;Character&amp;gt; list = word.chars().mapToObj(c -&amp;gt; (char) c)
    .map(Character::toUpperCase)
    .collect(Collectors.toList())
    .stream()
    .sorted()
    .collect(Collectors.toList());

list
    .stream()
    .map(c -&amp;gt; c.toString())
    .collect(Collectors.joining())&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para simular que la llamada a nuestra función de negocio puede fallar, vamos a incluir un random usando el instante de ejecución y si los
milisegundos son pares lanzamos una exception, con lo que nuestra función de negocio completa sería algo así:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;private String logicaNegocio(String word) {
    if ((System.currentTimeMillis() % 2) == 0) {
        System.out.println(&quot;Vamos a simular una exception de negocio&quot;);
        throw new RuntimeException(&quot;Milis es par&quot;);
    }

    List&amp;lt;Character&amp;gt; list = word.chars().mapToObj(c -&amp;gt; (char) c)
            .map(Character::toUpperCase)
            .collect(Collectors.toList())
            .stream()
            .sorted()
            .collect(Collectors.toList());

    return
            list
            .stream()
            .map(c -&amp;gt; c.toString())
            .collect(Collectors.joining());
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;problema&quot;&gt;Problema&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La primera implementación que haríamos probablemente de nuestro microservicio haría algo parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Controller(&quot;/&quot;)
public class EchoReverseController {

    @Get(&quot;/{word}&quot;)
    Single&amp;lt;String&amp;gt; reverse(final String word) {
        return Single.create(emitter -&amp;gt; {
            emitter.onSuccess( logicaNegocio(word) )
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y si la lógica de negocio falla devolveríamos un error a quien nos ha llamado para que vuelva a reintentarlo o lo que estime
oportuno.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, digamos que lo que queremos es que el servicio sea lo más &quot;robusto&quot; posible y pueda contemplar estas excepciones
y actuar en consecuencia siendo el caso que queremos ver hacer un reintento sin salir del contexto de la llamada.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;solución&quot;&gt;Solución&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para resolver esta situación lo primero que se nos ocurrirá (a mí al menos), es hacer el típico bucle &lt;code&gt;for&lt;/code&gt; con &lt;code&gt;n&lt;/code&gt; reintentos
y capturar las excepciones dentro de él. Obviamente esto no es la solución más elegante cuando las excepciones son debidas a
problemas con recursos puesto que las estás invocando de forma cuasi instantáneas en cada vuelta del bucle.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo que queremos hacer es que mientras el servicio que nos ha llamado está esperando que se resuelva la llamada, nosotros poder
invocar a la función con un delay de milisegundos-segundos-minutos-horas &amp;#8230;&amp;#8203; y todo de forma asíncrona. Para ello usaremos
el método &lt;code&gt;defer&lt;/code&gt; de un Observable junto con &lt;code&gt;retryWhen&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Get(&quot;/{word}&quot;)
Single&amp;lt;String&amp;gt; reverse(final String word) {
    return Single.create(emitter -&amp;gt; {
        Observable
                .defer( () -&amp;gt; Observable.just(logicaNegocio(word)) )
                .retryWhen(
                        f -&amp;gt; f.take(3).delay(new Random().nextInt(8), TimeUnit.SECONDS))
                .subscribe(
                        str -&amp;gt; emitter.onSuccess(str),
                        err -&amp;gt; emitter.onError(err)
                );
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Utilizando los métodos de reactive lo que estamos haciendo es:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;devolver a Micronaut un Single para que lo ejecute de forma asíncrona&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Crear un observable sobre nuestra lógica de negocio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Se ejecutará hasta 3 veces (&lt;code&gt;f.take(3)&lt;/code&gt;) con un delay de entre 0 y 8 segundos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a la primera ejecución que vaya bien emitiremos el resultado&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;si no va bien despues de intentarlo n veces, emitiremos un error&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;resumen&quot;&gt;Resumen&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con un poco de esfuerzo podemos extender la programación reactiva no sólo a las capas de &quot;entrada&quot;
sino a las propias llamadas de negocio&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>primeros pasos con reactivex</summary>
    </entry>
    <entry>
        <title>TypeConverter en Micronaut</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/micronaut-typeconverter.html"/>
        <updated>2021-10-05T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/micronaut-typeconverter.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Recientemente he tenido que implementar una movida para uno de mis proyectos
en la que había que persistir en base de datos no sólo los datos típicos del
cliente-usuario, sino que también tenía que guardar el &lt;code&gt;Plan&lt;/code&gt; de subscripción
en el que se encontraba.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La solución por la que optado está bien explicada
en las guías de Micronaut pero me he decidido a escribirla aquí por el
matiz de &quot;negocio&quot; que creo que puede ser interesante para otras situaciones.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;modelo&quot;&gt;Modelo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Digamos que tenemos una tabla donde persistimos los datos de un &lt;code&gt;Customer&lt;/code&gt; tipo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@MappedEntity
public class Customer {

	@Id
	private Long id;

    private String nombre;

    private String plan;

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Donde &lt;code&gt;plan&lt;/code&gt; era un string donde se iba a almacenar algun tipo de constante. Como
no había nada en concreto para ello (ni se le esperaba) la idea era un mantenimiento
a mano en la base de datos y en lugar de optar por valores numéricos que no dicen
mucho se optó por un String que era más representativo, con lo que en la bbdd
encontramos registros tipo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;1, jorge, free
2, pepe, basic
3, manolito, free&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;problema&quot;&gt;&quot;Problema&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente una vez que lo tienes funcionando y empiezas a guardar esos valores llega un
momento en el que tienes que hacer algo con ese campo que no sea meramente guardarlo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así que lo primero que uno piensa es &quot;codificar&quot; los posibles valores de una forma más
decente en lugar de los típicos &lt;code&gt;static final String FREE=&quot;free&quot;;&lt;/code&gt; y cambiar el tipo
del atributo a un enum&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;solucion&quot;&gt;&quot;Solucion&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;public enum Plan{
    FREE(&quot;free&quot;)
    BASIC(&quot;basic&quot;),
    PREMIUM(&quot;premium&quot;);

    private final String name;

    Plan(final String name) {
        this.name = name;
    }

    public String getName(){
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@MappedEntity
public class Customer {

	@Id
	private Long id;

    private String nombre;

    private Plan plan;

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente con esto Micronaut ya es capaz de guardar y recuperar registros de la bbdd
asignando al atributo &lt;code&gt;plan&lt;/code&gt; el enum correspondiente (y dando error si en la bbdd algún
valor no está en el enum) SIN TENER QUE TOCAR LA BASE DE DATOS&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;problema_2&quot;&gt;&quot;Problema&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Digamos que el &quot;problema&quot; al que me enfrentaba ahora era que, una vez recuperado un customer
de la base de datos, debía conocer en qué plan se encontraba para poder sugerirle los
siguientes planes (por ejemplo). En mi caso era tan &quot;simple&quot; como una serie de ifes pero
el añadir nuevos planes, por ejemplo &lt;code&gt;SUPER&lt;/code&gt;, haría que tuviera que revisar esos ifes&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;solucion_2&quot;&gt;&quot;Solucion&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una de las posibles soluciones, tal vez la más sencilla, sería convertir al enum en un
enum complejo tipo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;public enum Plan {
    FREE(&quot;free&quot;, 1)
    BASIC(&quot;basic&quot;, 2),
    PREMIUM(&quot;premium&quot;, 3);

	private final String name;
	private final int level;

	Plan(String name, int level) {
		this.name = name;
		this.level = level;
	}

	public String getName() {
		return name;
	}

	public int getLevel() {
		return level;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De esta forma ordenar planes en base a su &quot;prioridad&quot; es tan sencillo como ordenar por &lt;code&gt;level&lt;/code&gt; y
así puedes saber qué funcionalidades puede optar, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;problema_3&quot;&gt;&quot;Problema&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Micronaut (y supongo que otros frameworks) NO saben (sin ayuda) convertir estos valores y al intentar
recuperar de la base de datos algún Customer tendrás errores tipo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;io.micronaut.data.exceptions.DataAccessException: Cannot convert type [class java.lang.String] with value [] to target type: Plan plan. Consider defining a TypeConverter bean to handle this case.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;solucion_3&quot;&gt;&quot;Solucion&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como bien nos dice Micronaut, tenemos que añadir un algo que le diga cómo convertir de un String a un Plan y viceversa
(que es el objetivo de este post) para lo que hay que seguir estos pasos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Anotaremos al enum &lt;code&gt;Plan&lt;/code&gt; con un TypeDef&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@TypeDef(type= DataType.STRING) &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
public enum Plan {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Puesto que partimos de un string en el modelo inicial
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Crearemos un Factory que cree dos ayudantes Singleton, uno para cada direccion de conversion&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Factory
public class PlanConverter {

	@Singleton
	TypeConverter&amp;lt;Plan, String&amp;gt; planStringTypeConverter(){
		return ((object, targetType, context) -&amp;gt; Optional.of(object.name()));
	}

	@Singleton
	TypeConverter&amp;lt;String, Plan&amp;gt; stringPlanTypeConverter(){
		return ((object, targetType, context) -&amp;gt; Arrays.stream(Plan.values())
			.filter(p-&amp;gt;p.getName().equals(object))
			.findFirst()
		);
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente el primero dado un Plan devuelve un String y el segundo a la inversa,
busca en el array de Plan aquel que coincida su nombre con el String proporcionado&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Todo esto viene explicado en la guía de Micronaut Data pero el caso de uso que emplean para explicarlo
no me decía mucho pero una vez leído y aplicado a mi problema me ha parecido interesante compartirlo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Cómo persistir un enum "complejo" con Micronaut Data</summary>
    </entry>
    <entry>
        <title>Proteger con password un site estático</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/antora-password.html"/>
        <updated>2021-09-27T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/antora-password.html</id>
        <category term="asciidoc"/>
        <category term="documentation"/>
        <category term="write"/>
        <category term="antora"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
En este post voy a centrarme en un site generado con Antora pero puedes extenderlo
a cualquier otro site tipo Gatsby, Hugo, etc o incluso a un simple html
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Puedes descargar un repo de ejemplo en &lt;a href=&quot;https://gitlab.com/puravida-asciidoctor/antora-skeleton&quot; class=&quot;bare&quot;&gt;https://gitlab.com/puravida-asciidoctor/antora-skeleton&lt;/a&gt;
y/o acceder al ejemplo en &lt;a href=&quot;https://antora-password-protected.herokuapp.com/&quot; class=&quot;bare&quot;&gt;https://antora-password-protected.herokuapp.com/&lt;/a&gt; con el usuario test/test
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tienes tu contenido generado en HTML, una de las preguntas que te surge es cómo
puedo restringuir su acceso a sólo algunos usuarios.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a ver alguna de las
alternativas que podrías usar dependiende de si los lectores serían internos (por ejemplo
otros equipos en la intranet de la empresa) o externos y en este caso si cuentas con infraestructura
propia o quieres usar un proveedor externo. Así mismo puedes plantearte si lo que quieres es proteger una simple página
o todo un site&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;simple_página_html&quot;&gt;Simple página HTML&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Preparando este post encontré un script en node, staticrypt, que encripta una página html con una password que le
proporcionas generando así un html con un interface para pedir la password y poder desencriptar la
página&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Digamos que tienes un Asciidoctor donde tienes documentado un API y quieres publicarlo pero que sólo
lo puedan leer los que tengan la clave:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;= My API

this is my api
blablablab&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Generamos el html:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$asciidoctor api.adoc&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y obtenemos &lt;code&gt;api.html&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo encriptamos con staticrypt&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$staticrypt api.html la_clave_para_desencriptar -o api.html&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y ya tenemos un fichero &lt;code&gt;api.html&lt;/code&gt; protegido que se puede publicar en Gitlab Pages, Github, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo esta solución no funciona cuando tu documentación contiene varios ficheros porque, aunque puedes
encriptar todos los ficheros, el usuario tendrá que proporcionar la página en cada una. Así que necesitamos
algo que mantenga la sesión&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;site_alojado_en_s3&quot;&gt;Site alojado en S3&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como existe mucha documentación tanto de AWS como tutoriales sobre cómo crear un Bucket de S3 y proteger
su contenido no voy a explicarlo aquí. Simplemente lo menciono por si no lo habías tenido en cuenta&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;site_alojado_en_bucket_ggp&quot;&gt;Site alojado en Bucket GGP&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo mismo para un bucket de Google Cloud Platform, y en realidad por extensión a todos los compatibles con
estos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;site_alojado_por_nuestros_medios&quot;&gt;Site alojado por nuestros medios&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una de las formas de proteger un site web es usando un fichero &lt;code&gt;.htpasswd&lt;/code&gt; que puede ser manejado por Apache
o por Nginx. En este post vamos a usar este último pues es mucho más fácil y ligero que Apache&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para crear el fichero seguiremos cualquier tutorial de Internet pero básicamente es ejecutar un comando
tipo &lt;code&gt;httpass&lt;/code&gt; o usar una de las muchas páginas online que lo generan&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Siguiendo cualquiera de las dos opciones, vamos a crear el usuario &lt;code&gt;test&lt;/code&gt; con password &lt;code&gt;test&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;httpasswd&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;test:$apr1$yofaxwjl$xw.u6dmKy2qYiayuEnWW1. &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Obviamente puedes crear tantos usuarios como quieras, uno por cada linea
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El fichero esperado es con un punto al inicio pero yo lo uso sin punto para que me aparezca cuando
listo el directorio y luego lo copio al contenedor con el nombre esperado&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;site_interno_con_nginx&quot;&gt;Site interno con NGINX&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que en nuestra empresa contamos con una infraestructura y disponemos de al menos un servidor con espacio
y con Docker instalado (obviamente Docker es opcional pero así podemos aislar aplicaciones unas de otras).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que hemos creado nuestro site Antora pero no queremos que cualquier usuario pueda acceder a ello
(yo que sé porqué) sino sólo aquellos que dispongan del usuario y password creados anteriormente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En nuestro proyecto Antora, crearemos un directorio &lt;code&gt;nginx&lt;/code&gt; y ubicaremos en él el fichero &lt;code&gt;htpasswd&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo crearemos en este directorio un fichero mínimo de configuración de Nginx&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;default.conf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        auth_basic &quot;Restricted Content&quot;;
        auth_basic_user_file /etc/nginx/.htpasswd;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y en el root de nuestro proyecto crearemos un docker-compose similar a&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;docker-compose.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;version: &quot;3&quot;

services:

  antora:
    image: &quot;ggrossetie/antora-lunr:2.3.4&quot;
    volumes:
      - .:/antora
    entrypoint: /bin/ash
    command: /antora/custom-antora.sh &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

  nginx:
    image: &quot;nginx&quot;
    volumes:
     - ./nginx/.htpasswd:/etc/nginx/.htpasswd
     - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
     - ./public:/usr/share/nginx/html
    ports:
     - &quot;9080:80&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;El contenido de este fichero escapa a este post pero basicamente construye un site antora&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con este docker-compose podemos generar el site mediante &lt;code&gt;docker-compose run antora&lt;/code&gt; lo cual
nos genera la documentaacion en &lt;code&gt;public&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez generada y revisada en local podemos compartirla vía nginx con &lt;code&gt;docker-compose up -d nginx&lt;/code&gt;
y nuestros usuarios internos podrán acceder a ella vía el puerto &lt;code&gt;9080&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;site_publico_con_heroku&quot;&gt;Site publico con Heroku&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usando la capa gratuita de Heroku podemos desplegar nuestro site junto con el servicio Nginx anterior
de tal forma que podríamos publicarlo en Internet y protegido mediante usuario/password de la misma
forma&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Para este paso necesitas tener una cuenta gratuita en Heroku y el comando de consola instalado
, así como haber hecho login con el mismo.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es muy similar y lo que vamos a aprovechar es la capacidad de poder subir una imagen a Heroku
construida por nosotros y correr una instancia de la misma. En la capa gratuita, Heroku para los containers
transcurrido un tiempo (creo que 30 minutos) y lo vuelve a levantar ante una petición. Como nuestro servicio
es realmente ligero, este tiempo de espera es casi inmediato para el usuario&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Uno de los requisitos de Heroku es que él nos indica en tiempo de arranque el puerto en el que va a escuchar el contenedor
por lo que el fichero nginx anterior no nos sirve. Usaremos para ello este otro&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;change.default.conf&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;server {
    listen       $PORT;
    listen  [::]:$PORT;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        auth_basic &quot;Restricted Content&quot;;
        auth_basic_user_file /etc/nginx/.htpasswd;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Junto con este &lt;code&gt;Dockerfile&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Dockerfile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;FROM nginx

COPY nginx/htpasswd /etc/nginx/.htpasswd
COPY nginx/change.default.conf /etc/nginx/conf.d/default.conf
COPY public /usr/share/nginx/html

CMD /bin/bash -c &quot;envsubst &apos;\$PORT&apos; &amp;lt; /etc/nginx/conf.d/default.conf &amp;gt; /etc/nginx/conf.d/default.conf&quot; &amp;amp;&amp;amp; cat /etc/nginx/conf.d/default.conf &amp;amp;&amp;amp; nginx -g &apos;daemon off;&apos; &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&amp;lt;1&amp;gt;Es una sóla linea muy larga, supongo que se puede partir en varias pero no lo he investigado&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente vamos a construir una imagen de nginx cambiando en el arranque la configuracion del puerto antes de ejecutar el servicio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con todo ello preparamos nuestra aplicación en Heroku&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;heroku container:push web -a TU-APPLICATION&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y si todo ha ido bien, la desplegamos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;heroku container:release web -a TU-APPLICATION&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y eso es todo. Ahora tu documentación estaría expuesta en Internet pero protegida por usuario/password&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente no es la forma más robusta y completa de desplegar una documentación, pero es sencilla, barata y si lo automatizas
puedes incluso ir rotando los usuarios y/o sus claves de una forma sencilla&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Métodos para proteger un site estático con password</summary>
    </entry>
    <entry>
        <title>Microservicios con Okteto stack</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/okteto-stack.html"/>
        <updated>2021-09-10T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/okteto-stack.html</id>
        <category term="java"/>
        <category term="kubernetes"/>
        <category term="k8s"/>
        <category term="okteto"/>
        <category term="microservices"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este blog ya he escrito algunas entradas sobre la plataforma de Okteto (&lt;a href=&quot;http://okteto.com&quot; class=&quot;bare&quot;&gt;http://okteto.com&lt;/a&gt;) y cómo utilizarla
para desplegar pequeños proyectos (de forma gratuita) en Kubernetes de una forma simple.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente Okteto son dos productos diferentes pero relacionados:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;una herramienta a &quot;instalar&quot; en tu Kubernetes que permite al desarrollador sincronizar
su proyecto en un pod del cluster así como poder depurar la aplicación directamente en el mismo
(entre otras cosas)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un servicio kubernetes con diferentes planes de precios incluida una capa gratuita con suficientes
recursos como para desplegar servicios básicos en real (ideal en mi opinión para el aprendizaje de k8s)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En estos post he descrito cómo puedes desplegar desde un static site de fotos (sí, soy culpable, pero
a cambio aprendí el concepto de volumenes y cómo definirlos y gestionarlos en k8s), una aplicación
Grails (o SpringBoot) etc. todos ellos usando el descriptor propio de k8s (que es bastante verbose)
.Así mismo, en todos estos ejemplos siempre ha sido desplegar una aplicación simple y en
algunos casos usar una instancia Postgre desplegada mediante el interface gráfico.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post voy a contar otra de las funcionalidades con las que cuenta este servicio, okteto stack, y que es propia de
él, es decir, no es válida para otro cluster k8s que no sea Okteto, al menos hasta donde yo se.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Okteto stack es muy parecido a un docker-compose (de hecho salvo algunas particularidades podrías usar un docker-compose)
donde podemos definir las características principales de nuestro(s) servicio(s) e incluso generar la imagen al estilo
de este. La ventaja que tiene es que nos permite desplegar en el cluster de kubernetes servicios sin necesidad de
la verbosidad de este (ficheros de cientos de líneas de kubernetes se convierten en unas pocas con Okteto stack).&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;objetivo&quot;&gt;Objetivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para verlo en su conjunto vamos a hacer el ejercicio de desplegar 2 microservicios en el mismo namespace de tal forma
que:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;uno de ellos, API, va a realizar la labor de hacer de ApiGateway, enrutando las llamadas al otro servicio.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el otro servicio, Customer-Service, va a persistir entidades en una base de datos &quot;bajo su control&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a usar lo menos posible características propias de Kubernetes, aunque se podría usar. La idea es no añadir más
complejidad en este ejemplo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;He creado un repositorio donde puedes bajarte el código e intentar desplegarlo en tu cluster. Simplemente
descargarlo de &lt;a href=&quot;https://gitlab.com/jorge-aguilera/okteto-stack&quot; class=&quot;bare&quot;&gt;https://gitlab.com/jorge-aguilera/okteto-stack&lt;/a&gt; y sigue las instrucciones del README&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Para poder ejecutar y desplegar este ejemplo vas a necesitar una cuenta en Okteto así como la herramienta de
consola &lt;code&gt;okteto-cli&lt;/code&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;customer_service&quot;&gt;Customer Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Customer service es un microservicio destinado a guardar y devolver entidades de Customer para lo que usará una base
de datos PostgreSQL.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En un proyecto típico de Docker tendriamos un docker-compose similar a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;okteto-stack.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;services:

  dbcustomers:
    image: okteto.dev/dbcustomers
    build:
      context: .
      dockerfile: DockerDatabase
      args:
        - DATABASE_NAME
        - DATABASE_USERNAME
        - DATABASE_PASSWORD
    ports:
      - 5432
    volumes:
      - data_customers:/var/lib/postgresql/data/


  customer-service:
    image: okteto.dev/customer-service
    build:
      context: .
      dockerfile: DockerApp
    ports:
      - 8080
    environment:
      - DATABASE_HOST
      - DATABASE_NAME
      - DATABASE_USERNAME
      - DATABASE_PASSWORD
    depends_on:
      dbcustomers:
        condition: service_healthy

volumes:
  data_customers:&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y lo podríamos desplegar mediante &lt;code&gt;docker-compose build &amp;amp;&amp;amp; docker-compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes observar el build requiere de dos ficheros de docker: DockerDatabase y DockerApp&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;DockerDatabase&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;FROM postgres:latest

ARG DATABASE_NAME
ARG DATABASE_USERNAME
ARG DATABASE_PASSWORD

ENV POSTGRES_HOST_AUTH_METHOD=trust
ENV POSTGRES_PASSWORD=${DATABASE_PASSWORD}
ENV POSTGRES_DB=${DATABASE_NAME}
ENV POSTGRES_USER=${DATABASE_USERNAME}

ENTRYPOINT [&quot;docker-entrypoint.sh&quot;]
EXPOSE 5432
CMD [&quot;postgres&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;DockerApp&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;FROM gradle:7.2.0-jdk11 AS build
COPY . /home/gradle
RUN gradle build -x check

FROM openjdk:16-alpine
WORKDIR /home/app
COPY --from=build /home/gradle/build/docker/layers/libs /home/app/libs
COPY --from=build /home/gradle/build/docker/layers/resources /home/app/resources
COPY --from=build /home/gradle/build/docker/layers/application.jar /home/app/application.jar
EXPOSE 8080
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/home/app/application.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Obviamente todo esto puede ser más simplificado, por ejemplo no necesitarias construir una imagen para Postgre y
podrías usar la standard. Así mismo tampoco necesitarías el DockerApp si construyes y subes la imagen a tu repo
mediante las herramientas que uses habitualmente.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si en el directorio del servicio customer ejecutamos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;okteto deploy stack&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y todo va bien, habrás desplegado un stack &lt;code&gt;customers&lt;/code&gt; donde se está ejecutando dos pods: un postgresql y un servicio&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;api&quot;&gt;API&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Api sigue el mismo principio pero su fichero es más simple puesto que no necesita base de datos. Sin embargo su
okteto-stack.yml ya no es compatible con docker-compose porque vamos a incluir una funcionalidad propia de esta
plataforma:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;okteto-stack.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;services:
  api:
    image: okteto.dev/api
    build:
      context: .
      dockerfile: DockerApp
    ports:
      - 8080

endpoints:
  - path: /
    service: api
    port: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si en el proyecto &lt;code&gt;api&lt;/code&gt; ejecutamos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;okteto stack deploy&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;veremos que aparece un nuevo stack &lt;code&gt;api`en nuestro namespace con un sólo pod el cual además ofrece un endpoint
abierto a internet en `https://api-TUNAMESPACE.cloud.okteto.net/&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;probando&quot;&gt;Probando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si hemos conseguido desplegar los dos stacks ahora podriamos ejecutar peticiones REST a API el cual las enrutará a
Customer (yo utilizo &lt;code&gt;httpie&lt;/code&gt; pero puedes usar curl, postman o la herramienta que uses habitualmente)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http &lt;a href=&quot;https://api-TUNAMESPACE.cloud.okteto.net/api&quot; class=&quot;bare&quot;&gt;https://api-TUNAMESPACE.cloud.okteto.net/api&lt;/a&gt; name=test&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http &lt;a href=&quot;https://api-TUNAMESPACE.cloud.okteto.net/api&quot; class=&quot;bare&quot;&gt;https://api-TUNAMESPACE.cloud.okteto.net/api&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si no hay ningún error el primer comando habrá creado un customer con el name igual a test y la segunda llamada nos
devolverá una lista de un sólo elemento&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;actualizando&quot;&gt;Actualizando&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez validado que nuestros dos stacks se encuentran funcionando y hablan entre sí, vamos a realizar un cambio
en uno de ellos y redesplegarlo sin necesidad de actualizar el otro.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo, vamos a editar &lt;code&gt;CustomerController.java&lt;/code&gt; en el proyecto de &lt;code&gt;customer-service&lt;/code&gt; y vamos a cambiar la línea
27&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;return customerRepository.save(customerEntity);&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;por&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;return customerEntity;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(Simplemente vamos a hacer que ya no se puedan añadir más customers pero sin devolver un error)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para redesplegar la actualización ejecutaremos desde el proyecto de &lt;code&gt;customer-service&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;okteto stack deploy --build&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo cual va a volver a generar y subir la imagen actualizada para después redesplegar el servicio. Una vez desplegado
podremos repetir los pasos de intentar añadir un nuevo customer&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http &lt;a href=&quot;https://api-TUNAMESPACE.cloud.okteto.net/api&quot; class=&quot;bare&quot;&gt;https://api-TUNAMESPACE.cloud.okteto.net/api&lt;/a&gt; name=otro&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;http &lt;a href=&quot;https://api-TUNAMESPACE.cloud.okteto.net/api&quot; class=&quot;bare&quot;&gt;https://api-TUNAMESPACE.cloud.okteto.net/api&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y deberíamos ver que aunque el post para crear el customer &quot;otro&quot; no ha dado error en realidad no ha sido guardado
y el get nos sigue devolviendo una lista con un sólo elemento.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo interesante de este cambio es ver que API no ha sido afectado y seguía ejecutándose.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;microservicios&quot;&gt;Microservicios&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si abrimos la configuración de API (application.yml) podemos ver que este está enrutando las llamadas a un servicio
del que sólo sabe el nombre:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;micronaut:
  application:
    name: api
  http:
    services:
      customers:
        url: http://customer-service:8080&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es decir, API hará de proxy hacia un host que responda a &lt;code&gt;customer-service&lt;/code&gt; el cual corresponde con el del stack
customer. En una solución más compleja se podría usar sistemas de descubrimiento de servicios pero para este ejemplo
es suficiente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Otra de las ventajas de esta aproximación es que podemos agrupar los servicios por dependencias (por ejemplo
customer-service y su base de datos) pudiendo actualizar un stack sin tener que actuar en los otros.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En un petproject en el que estoy trabajando he empezado a usar esta funcionalidad y me está siendo muy valiosa para
poder tener separados los diferentes servicios a la vez que defino en cada uno las dependencias. Así por ejemplo
tengo unos stacks de infraestructura (Kafka y Databases) y otros para cada servicio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;kafka&quot;&gt;Kafka&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: kafka

services:

  kafdrop:
    image: obsidiandynamics/kafdrop:3.28.0-SNAPSHOT
    ports:
      - 9000
    environment:
      - KAFKA_BROKERCONNECT=kafka:9092
      - JVM_OPTS=-Xms16M -Xmx48M -Xss180K -XX:-TieredCompilation -XX:+UseStringDeduplication -noverify

  zookeeper:
    image: docker.io/bitnami/zookeeper:3-debian-10
    ports:
      - 2181
    volumes:
      - data_zookeeper:/bitnami
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes

  kafka:
    image: docker.io/bitnami/kafka:2-debian-10
    ports:
      - 9092
    volumes:
      - data_kafka:/bitnami
    environment:
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
      - ALLOW_PLAINTEXT_LISTENER=yes
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092

volumes:
  data_zookeeper:
    driver_opts:
      size: 1Gi
  data_kafka:
    driver_opts:
      size: 2Gi&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;databases&quot;&gt;Databases&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: databases

services:

  dbcustomer:
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
    image: okteto.dev/dbcustomer
    build:
      context: .
      dockerfile: DockerfileCustomer
      args:
        - USERNAME=$USERNAME
        - PASSWORD=$PASSWORD
    ports:
      - 5432
    volumes:
      - data_customer:/var/lib/postgresql/data/

  dbcore:
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
    image: okteto.dev/dbcore
    build:
      context: .
      dockerfile: DockerfileCore
      args:
        - USERNAME=$USERNAME
        - PASSWORD=$PASSWORD
    ports:
      - 5432
    volumes:
      - data_core:/var/lib/postgresql/data/


volumes:
  data_customer:
    driver_opts:
      size: 1Gi
  data_core:
    driver_opts:
      size: 1Gi&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>desplegando microservicios con Okteto Stack</summary>
    </entry>
    <entry>
        <title>Primeros pasos en programación reactiva, III</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/reactivex/reactivex-3.html"/>
        <updated>2021-07-12T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/reactivex/reactivex-3.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <category term="rxjava2"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta serie de post acerca de programación reactiva voy a ir contando los pasos que estoy dando para ir practicando con esta forma de programación y más en concreto su uso en aplicaciones HTTP con Micronaut&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el post anterior (&lt;a href=&quot;reactivex-2.html&quot; class=&quot;bare&quot;&gt;reactivex-2.html&lt;/a&gt;) nos centramos en la parte servidora y vimos cómo crear un &lt;code&gt;Single&lt;/code&gt;
(y devolverlo como retorno de la función) para así delegar en el
subsistema la ejecución de nuestro código de forma asíncrona.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post nos vamos a centrar en la parte consumidora viendo cómo podemos usar el modelo reactivo en la construcción
de un API que consuma endpoints remotos. Para ello vamos a usar los siguientes recursos públicos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://api.open-notify.org/astros.json&quot; class=&quot;bare&quot;&gt;http://api.open-notify.org/astros.json&lt;/a&gt; que devuelve un detalle de los astronautas que hay ahora mismo en el espacio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://api.open-notify.org/iss-now.json&quot; class=&quot;bare&quot;&gt;http://api.open-notify.org/iss-now.json&lt;/a&gt; que devuelve la posición de la estación ISS en el momento de la petición&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;modelos&quot;&gt;Modelos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a construir el modelo que representa a cada uno de los endpoints&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;public class Astronaut {
    public String name;
    public String craft;

    @Override
    public String toString(){
        return name+&quot; &quot;+craft;
    }
}

public class Astros {

    public String message;
    public int number;
    public List&amp;lt;Astronaut&amp;gt;people;

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;public class IssPosition {

    public double latitude;
    public double longitude;

    public String toString(){
        return &quot;lat:&quot;+latitude+&quot; log:&quot;+longitude;
    }
}

public class Iss {

    public IssPosition iss_position;

    public String toString(){
        return &quot;current position &quot;+iss_position;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así como el modelo que va a devolver nuestra API, que no es más que la unión de ambos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;public class Nasa {
    public Astros astros;
    public Iss iss;

    public Nasa astros(Astros astros){
        this.astros=astros;
        return this;
    }

    public Nasa iss(Iss iss){
        this.iss=iss;
        return this;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;clients&quot;&gt;ClientS&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otra parte vamos a definir nuestros interfaces Client Micronaut que nos permiten recuperar dichos modelos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Client(&quot;http://api.open-notify.org/&quot;)
public interface AstrosClient {

    @Get(&quot;/astros.json&quot;)
    Single&amp;lt;Astros&amp;gt;getAstros();

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Client(&quot;http://api.open-notify.org/&quot;)
public interface IssClient {

    @Get(&quot;iss-now.json&quot;)
    Single&amp;lt;Iss&amp;gt; getIss();

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;service&quot;&gt;Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para recuperar ambos modelos (ISS y Astros) y devolverlos como uno sólo (Nasa) vamos a crear un servicio &lt;code&gt;NasaService&lt;/code&gt; el
cual va a ofrecer 3 formas diferentes de hacerlo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;block&lt;/code&gt;, primero va a llamar a uno de los endpoints de forma bloqueante y después al otro de la misma forma&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;chain&lt;/code&gt;, primero va a llamar a uno de lso endpoints de forma reactiva y cuando se complete la llamada se llamará
al segundo endpoint de la misma forma&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;zip&lt;/code&gt;, vamos a llamar a ambos endpoints a la vez de forma reactiva y cuando se completen ambos devolveremos el
resultado&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Singleton
public class NasaService {

    @Inject
    IssClient issClient;

    @Inject
    AstrosClient astrosClient;

    public Single&amp;lt;Nasa&amp;gt; blockingGet() {
        ...
    }

    public Single&amp;lt;Nasa&amp;gt;chain() {
        ...
    }

    public Single&amp;lt;Nasa&amp;gt;zip(){
        ...
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;blocking&quot;&gt;Blocking&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    public Single&amp;lt;Nasa&amp;gt; blockingGet() {
        return Single.create(emitter -&amp;gt; {
            Nasa nasa = new Nasa();
            nasa.astros = astrosClient.getAstros().blockingGet();
            nasa.iss = issClient.getIss().blockingGet();
            emitter.onSuccess(nasa);
        });
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este modo, el servicio construye un objeto Nasa de respuesta y lo va completando según se van obteniendo las
respuestas de los endpoints remotos. Una vez terminados se emite el objeto resultante&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como podemos intuir, este método será el más lento y propenso a errores pues en el mejor de los casos el total del
tiempo necesario será la suma de ambas llamadas. Además, al esperar la respuesta de ambas llamadas estamos bloqueando
el hilo por lo que otras solicitudes se quedarán encoladas.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;chain&quot;&gt;Chain&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    BiFunction&amp;lt;Nasa, SingleEmitter&amp;lt;Nasa&amp;gt;, Disposable&amp;gt; issFunc = (nasa, emitter)-&amp;gt;
            issClient.getIss().subscribe(
                    iss -&amp;gt;
                            emitter.onSuccess(nasa.iss(iss))
                    ,
                    err-&amp;gt;
                            emitter.onError(err)
            );

    BiFunction&amp;lt;Nasa, SingleEmitter&amp;lt;Nasa&amp;gt;, Disposable&amp;gt; astrosFunc = (nasa, emitter)-&amp;gt;
            astrosClient.getAstros().subscribe(
                    astros -&amp;gt;
                            issFunc.apply(nasa.astros(astros), emitter)
                    ,
                    err-&amp;gt;
                            emitter.onError(err)
            );

    public Single&amp;lt;Nasa&amp;gt;chain() {
        return Single.create(
                emitter -&amp;gt; astrosFunc.apply(new Nasa(), emitter)
        );
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Para ayudar en la legibilidad de este método vamos a usar &lt;code&gt;BiFunction&lt;/code&gt; aunque podríamos seguir usando clases
anónimas lambdas&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente definimos un método &lt;code&gt;chain&lt;/code&gt; que crea un &lt;code&gt;Single&lt;/code&gt; (como en caso block) y lo que hace es aplicar una función
&lt;code&gt;astrosFunc&lt;/code&gt;. Esta función simplemente se subscribe a la llamada asíncrona que recupera los astronautas del espacio
y cuando se complete encadena a su vez otra subscripción al endpoint de la ISS. Cuando este segundo endpoint se completa
se devuelve el resultado al &lt;code&gt;emitter&lt;/code&gt; &quot;original&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es muy parecida al método anterior (aunque el código parece un poco más complicado) en el sentido de que vamos
a llamar a dos funciones de forma seguida, por lo que el tiempo total será aproximadamente igual. Sin embargo, al no
bloquear el hilo podremos aceptar más solicitudes.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;zip&quot;&gt;Zip&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    public Single&amp;lt;Nasa&amp;gt;zip(){
        return Single.zip(astrosClient.getAstros(), issClient.getIss(),
                (astros, iss) -&amp;gt;
                        new Nasa().astros(astros).iss(iss));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mediante &lt;code&gt;zip&lt;/code&gt; un &lt;code&gt;Single&lt;/code&gt; es capaz de poder llamar de forma simultánea a un número de observables y ejecutar una
función con el resultado de todos ellos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;La lista de observables a ejecutar puede ser &quot;fija&quot;, como en este caso, o
una lista de ellos. En caso de fija podemos especificar hasta 10 (creo) observables y en la función a ejecutar recibiremos
los resultados de forma explícita. En el caso de una lista, lo que obtendremos en la función es un array de Object y
nos toca a nosotros interpretarlos&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;controller&quot;&gt;Controller&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente el controller es un simple punto de entrada para cada tipo que llama directamente al servicio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Controller(&quot;/nasa&quot;)
public class NasaController {

    @Inject
    NasaService service;

    @Get(&quot;/blocking&quot;)
    Single&amp;lt;Nasa&amp;gt;blockingGet() {
        return service.blockingGet();
    }

    @Get(&quot;/chain&quot;)
    Single&amp;lt;Nasa&amp;gt;chain() {
        return service.chain();
    }

    @Get(&quot;/zip&quot;)
    Single&amp;lt;Nasa&amp;gt;zip(){
        return service.zip();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jmeter&quot;&gt;JMeter&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con estos 3 endpoints he preparado una prueba de carga usando JMeter y he ejecutado 100 peticiones simultáneas contra
cada uno.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;blocking_2&quot;&gt;Blocking&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obtenemos una tasa de error muy elevada&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/reactivex/SummaryReportBlock.png&quot; alt=&quot;SummaryReportBlock&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;chain_2&quot;&gt;Chain&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Conseguimos no bloquear el hilo de llamada reduciendo los errores a cero&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/reactivex/SummaryReportChain.png&quot; alt=&quot;SummaryReportChain&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;zip_2&quot;&gt;Zip&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No sólo conseguimos reducir los errores a cero sino que la diferencia de tiempo respecto de chain es significativa
(casi la mitad)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/reactivex/SummaryReportZip.png&quot; alt=&quot;SummaryReportZip&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;resumen&quot;&gt;Resumen&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por el lado del consumidor vemos que utilizar programación reactiva nos ayuda a optimizar los recursos y mejorar los
tiempos de respuesta&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>primeros pasos con reactivex</summary>
    </entry>
    <entry>
        <title>Primeros pasos en programación reactiva, II</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/reactivex/reactivex-2.html"/>
        <updated>2021-07-12T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/reactivex/reactivex-2.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <category term="rxjava2"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta serie de post acerca de programación reactiva voy a ir contando los pasos que estoy dando para ir practicando con esta forma de programación y más en concreto su uso en aplicaciones HTTP con Micronaut&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el post anterior (&lt;a href=&quot;reactivex-1.html&quot; class=&quot;bare&quot;&gt;reactivex-1.html&lt;/a&gt;) vimos mediante un par de ejemplos sencillos cómo un Controller puede ofrecer recursos de forma
reactiva en lugar de bloqueante. En este segundo post simplemente vamos a estructurar un poco mejor el código usando un
Service y dejar así el Controller más &quot;limpio&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Controller(&quot;/numbers&quot;)
public class NumberGeneratorController {

    @Inject
    NumberGeneratorService service;

    @Get(&quot;/list{?size}&quot;)
    Single&amp;lt;int[]&amp;gt; list(Optional&amp;lt;Integer&amp;gt; size){
        return service.list( size.orElse(99+1) );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Singleton
public class NumberGeneratorService {

    Single&amp;lt;int[]&amp;gt; list( Integer size) {
        Random rnd = new Random();
        return Single.just(
                IntStream.
                        range(1, size).
                        map(i -&amp;gt; Math.abs(rnd.nextInt())).
                        toArray()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como puedes ver, simplemente hemos movido el código al servicio y hacemos que el controlador lo invoque directamente.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hasta ahora el código era devolver una lista de números que generamos con una línea de código (partida en varias para
mejorar su lectura, pero una línea de código al fín y al cabo). Sin embargo muchas veces la lógica a implementar
requerirá de algo más complejo, por lo que un simple &lt;code&gt;just&lt;/code&gt; no nos servirá. Crearemos entonces un Single que emitirá
el resultado (o fallo) cuando se complete dicha lógica&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Recuerda que además de Single, existen otros tipos reactivos interesantes como FLowable o Maybe. Por ahora
seguiremos trabajando con Single el cual nos permite notificar un resultado o un error.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    Single&amp;lt;int[]&amp;gt; listWithBoundControl( Integer size){
        return Single.create( emitter -&amp;gt;{

            if( size &amp;gt; 100){
                emitter.onError(new ArrayIndexOutOfBoundsException());
                return;
            }

            Random rnd = new Random();
            int[] ret = new int[size];
            for(int i=0; i&amp;lt;size; i++){
                ret[i] = Math.abs(rnd.nextInt());
            }
            emitter.onSuccess(ret);
        });
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo vemos que podemos notificar un error (si por ejemplo el tamaño que nos piden a generar excede un
límite), ejecutar código en una lambda (o llamando a una función privada, etc) y notificar que hemos terminado
proporcionando un resultado.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La &quot;magia&quot; en este código es que tanto el controller como el servicio lo que devuelven en las llamadas es un &lt;code&gt;Single&lt;/code&gt;
el cual el framework, micronaut en este caso, va a manejar para ejecutarlo y devolver el resultado cuando se ejecute.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La segunda &quot;magia&quot; (que a mí particularmente me fascina) es la capacidad del compilador de comprobar que realmente
estamos devolviendo lo que dice la firma del método que devolvemos. Es decir, si pruebas a cambiar el objeto que
devolvemos en &lt;code&gt;onSuccess&lt;/code&gt; por un String, por ejemplo, el compilador lo detecta y no compila.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;resumen&quot;&gt;Resumen&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este segundo, y corto, post hemos visto cómo ejecutar una lógica de negocio más compleja y notificar el resultado
mediante el uso de &lt;code&gt;emitter&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>primeros pasos con reactivex</summary>
    </entry>
    <entry>
        <title>Primeros pasos en programación reactiva, I</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/reactivex/reactivex-1.html"/>
        <updated>2021-07-11T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/reactivex/reactivex-1.html</id>
        <category term="java"/>
        <category term="micronaut"/>
        <category term="rxjava2"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta serie de post acerca de programación reactiva voy a ir contando los pasos que estoy dando para ir practicando con esta forma de programación y más en concreto su uso en aplicaciones HTTP con Micronaut&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente mediante la programación reactiva lo que conseguimos es evitar el bloqueo de los hilos de ejecución mientras se espera a que se complete una tarea.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta forma de programación se puede realizar en ambos lados del diálogo, es decir:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Podemos hacer que el cliente invoque llamadas al servidor y continue
con sus cosas hasta recibir una respuesta de este.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Podemos usarlo en el servidor de tal forma que este pueda aceptar múltiples peticiones
sin bloquear el hilo http, ejecutar la solicitud en otro hilo y responder al cliente cuando se complete.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Casi todos los lenguajes tienen una forma u otra de permitir esta programación, así como diferentes librerías o frameworks.
En estos post vamos a usar Micronaut el cual usa RxJava2 (hasta la versión 3 en la que se ha migrado a Reactor)
y lo vamos a ver tanto en el cliente como en el servidor&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;servicio_bloqueante&quot;&gt;Servicio bloqueante&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación podemos ver un &lt;code&gt;controller&lt;/code&gt; típico:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Controller(&quot;/numbers&quot;)
public class NumberGeneratorController {

    @Get(&quot;/list{?size}&quot;)
    int[] list(Optional&amp;lt;Integer&amp;gt; size){
        Random rnd = new Random();
        return IntStream.
                range(1,size.orElse(99)+1).
                map( i-&amp;gt; Math.abs(rnd.nextInt())).
                toArray();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente definimos un Controller con un método &lt;code&gt;/list&lt;/code&gt; al que se le puede pasar un parámetro opcional, &lt;code&gt;size&lt;/code&gt;, y
que genera una lista de números de forma aleatoria&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este endoint probablemente no bloquee demasiado el thread y no veamos la diferencia con un reactivo pero es un buen
punto de inicio.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;servicio_reactivo&quot;&gt;Servicio reactivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La versión reactiva más simple y parecida, sería algo como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Controller(&quot;/numbers&quot;)
public class NumberGeneratorController {

    @Get(&quot;/list{?size}&quot;)
    Single&amp;lt;int[]&amp;gt; list(Optional&amp;lt;Integer&amp;gt; size){
        Random rnd = new Random();
        return Single.just(
                IntStream.
                range(1,size.orElse(99)+1).
                map( i-&amp;gt; Math.abs(rnd.nextInt())).
                toArray();
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente hemos cambiado el tipo de retorno (recubriéndolo con un &lt;code&gt;Single&lt;/code&gt;) por lo que devolvemos un objeto
de ese tipo que lo único que hace es ejecutar el mismo código. La diferencia entre ambos es que Micronaut, cuando
devolvemos un objeto de este tipo, lo que hace es utilizar la librería reactiva para que sea ella la que lo complete.
En el caso simple, por el contrario, se está ejecutando en el mismo hilo que hace la petición y si recibimos muchas
peticiones, a este u otro endpoint, se terminará bloqueando.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Lo mejor de este ejemplo es que para el cliente que invoca la petición ambas implementaciones son iguales
en cuanto a la forma de invocarlas se refiere.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;descargar_imagen_de_forma_reactiva&quot;&gt;Descargar Imagen de forma reactiva&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando la tarea a ejecutar por el servidor es más pesada, como servir una imagen por ejemplo, la programación
reactiva nos permite optimizar su ejecución. Por ejemplo, a continuación se muestra un Controller que permite descargar
un Gif. En lugar de leer todo el fichero y enviarlo como un array de bytes en la misma petición lo que va a usar es
un &lt;code&gt;Flowable&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un objeto del tipo &lt;code&gt;Flowable&lt;/code&gt; nos va a permitir ir enviando bloques de información al cliente según vayamos obteniéndola.
Una vez completada la tarea notificaremos al &lt;code&gt;Flowable&lt;/code&gt; que ya se ha completado y esto terminará el diálogo con el
cliente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Controller(&quot;/test&quot;)
class FileController{

    @Get(value = &quot;/{file}&quot;, produces = MediaType.IMAGE_GIF)
    Flowable&amp;lt;byte[]&amp;gt; image(@PathVariable String file) {

      Flowable.create({ emitter -&amp;gt;
          new File(file).withInputStream{ inputStream -&amp;gt;
                int size=1024
              byte[]buff = inputStream.readNBytes(size)
              while( buff.length == size){
                  emitter.onNext(buff)
                  buff = inputStream.readNBytes(size)
              }
              emitter.onNext(buff)
              emitter.onComplete()
          }
      }, BackpressureStrategy.BUFFER)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo, el cliente solicita un fichero y lo que devuelve el controller es un objeto &lt;code&gt;Flowable&lt;/code&gt;. Cuando el subsistema
se encuentre preparado se subscribirá a este y le proporcionará un objeto &lt;code&gt;emitter&lt;/code&gt; el cual nos va a servir para ir
enviando &quot;chunks&quot; de información:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;abrimos el fichero y vamos leyendo bloques de 1024 bytes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;se los vamos pasando al emitter mediante &lt;code&gt;onNext&lt;/code&gt;. El subsistema es el encargado de ir enviándolos a su vez usando
la estrategia que hayamos especificado (BUFFER en este caso)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una vez enviados los últimos bytes notificamos que hemos terminado con &lt;code&gt;onComplete&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mediante esta técnica podemos servir ficheros pesados a multitud de clientes sin bloquear las llamadas. (He empleado
este bot en una página donde podías descargar las imágenes de tráfico de la DGT y una simple aplicación era capaz de
servir cientos de imágenes de forma fluida)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;resumen&quot;&gt;Resumen&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El resumen de este primer post es:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;la programación reactiva nos ayuda a optimizar los recursos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a un nivel básico no añaden más complejidad&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nuevos jugadores como Single, Flowable y Emitter&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>primeros pasos con reactivex</summary>
    </entry>
    <entry>
        <title>Publicar artefactos en MavenCentral</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/publicar-maven.html"/>
        <updated>2021-05-09T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/publicar-maven.html</id>
        <category term="gradle"/>
        <category term="maven"/>
        <category term="java"/>
        <category term="open source"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algunos de mis proyectos (librerías Java) los he publicado usando Bintray, un servicio que tenía una capa
gratuíta que te permitía crear tus repositorios y subir tus &quot;artefactos&quot; de una forma fácil. Además gestionaban
por tí el publicarlos y sincronizarlos tanto en JCenter como en MavenCentral&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo hace poco, por las razones que fueran, la empresa ha dejado de ofrecer este servicio dejando JCenter
como sólo lectura (
&lt;a href=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot; class=&quot;bare&quot;&gt;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&lt;/a&gt;) y siendo imposible ya crear
nuevos artefactos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De las alternativas que se ofrecen me he decantado por publicar &quot;directamente&quot; en MavenCentral a través de Sonatype
que al fín y al cabo son los creadores.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Si no lo había hecho antes es porque recordaba que cuando lo miré hace años el proceso era bastante complicado
y se podía tardar varios días en que te aprobaran la cuenta, etc. Sin embargo siguiendo varios tutoriales he podido
completar el proceso y (re)-publicar mis dos primeras extensiones en una tarde&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como uso principalmente Gradle, en este post voy a explicar los pasos y configuración que he usado para publicarlo en MavenCentral&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente el único requisito que vas a tener es que seas el &quot;propietario&quot; del dominio con el que pretendes publicar tus artefactos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi caso, con Bintray este requisito no era necesario y yo publicaba bajo &lt;code&gt;com.puravida&lt;/code&gt; (me pareció más sencillo que &lt;code&gt;com.puravida-software&lt;/code&gt;)
pero como no soy el dueño de ese dominio ahora me toca migrar todos los proyectos a &lt;code&gt;com.puravida-software&lt;/code&gt; el cual sí es mío.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo vas a necesitar acceso a la gestión de DNS que básicamente todos los proveedores de dominio te ofrecen de una forma u otra.
Como contaré más adelante, una de las formas más rápidas y cómodas de validar que eres el propietario del dominio es creando un registro TXT
a través de la consola de tu proveedor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El segundo requisito es tener una cuenta de correo, que en principio no tiene porqué ser de ese dominio.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cuenta_sonatype&quot;&gt;Cuenta SonaType&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar hay que crearse una cuenta en Sonatype (&lt;a href=&quot;https://issues.sonatype.org/secure/Signup!default.jspa&quot; class=&quot;bare&quot;&gt;https://issues.sonatype.org/secure/Signup!default.jspa&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que te hayas registrado crearemos un ticket (Botón &quot;Crear&quot; en el menú superior) y nos aseguraremos que es del tipo &quot;New Project&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el cuerpo del ticket yo he puesto:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Hi

I want to create the `com.puravida-software` groupId where host several OSS I&apos;ve developed&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y a los pocos minutos (5-8) alguien de soporte ha contestado en el ticket:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Do you own the domain puravida-software.com? If so, please verify ownership via one of the following methods:

* Add a TXT record to your DNS referencing this JIRA ticket: OSSRH-XXXXX (Fastest)

* Setup a redirect to your Github page (if it does not already exist)

You can find more information here: https://central.sonatype.org/publish/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como comentaba al principio, crear una entrada TXT en el DNS es muy fácil en el panel de administración de mi proveedor. Simplemente creo
una entrada tipo TXT con la clave el código del ticket y el valor la URL al tocket:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;TXT OSSRH-XXXXX &lt;a href=&quot;https://issues.sonatype.org/browse/OSSRH-XXXX&quot; class=&quot;bare&quot;&gt;https://issues.sonatype.org/browse/OSSRH-XXXX&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y una vez creada he comentado en el ticket que ya estaba creada. Si es necesario o no, ni idea, pero a los pocos minutos el ticket se marca
como resuelto con un texto:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Central OSSRH resuelta OSSRH-XXXXX.
-----------------------------------

    Resolución: Solucionada

com.puravida-software has been prepared, now user(s) jorge.aguilera can:

Publish snapshot and release artifacts to https://s01.oss.sonatype.org

Have a look at this section of our official guide for deployment instructions:

https://central.sonatype.org/publish/publish-guide/#deployment

Please comment on this ticket when you&apos;ve released your first component(s), so we can activate the sync to Maven Central.

Depending on your build configuration, this might happen automatically. If not, you can follow the steps in this section of our guide:

https://central.sonatype.org/publish/release/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ahora podremos ir a la consola Nexus de SonaType &lt;a href=&quot;https://s01.oss.sonatype.org/#welcome&quot; class=&quot;bare&quot;&gt;https://s01.oss.sonatype.org/#welcome&lt;/a&gt; y crear un API token el cual copiaremos en lugar
seguro antes de cerrar el diálogo. (Aquí &lt;a href=&quot;https://blog.solidsoft.pl/2015/09/08/deploy-to-maven-central-using-api-key-aka-auth-token/&quot; class=&quot;bare&quot;&gt;https://blog.solidsoft.pl/2015/09/08/deploy-to-maven-central-using-api-key-aka-auth-token/&lt;/a&gt; puedes
ver cómo crear el token)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tanto el username como el token lo guarderemos en nuestro fichero de configuración global &lt;code&gt;gradle.properites&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;$HOME/.gradle/gradle.properties&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sonatypeUsername=XXXXX
sonatypePassword=XXXXXXXXXXXXXXXXXXXXXXXXXXXxx&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Perfecto. Ahora ya tenemos un sitio donde subir nuestros artefactos y si pasan todas las validaciones se sincronizarán con Maven Central.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;firma_de_artefactos&quot;&gt;Firma de artefactos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Uno de los requisitos de Maven Central es que todos los artefactos que quieras subir tienen que ir firmados asi que ahora lo que toca
es crear y publicar una firma gpg (también podía ser el primero puesto que son independientes).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Yo he seguido los pasos de está página &lt;a href=&quot;https://weibeld.net/java/publish-to-maven-central.html&quot; class=&quot;bare&quot;&gt;https://weibeld.net/java/publish-to-maven-central.html&lt;/a&gt; que se resumen en:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Crearemos una clave si no la tenemos&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ gpg --gen-key&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;e introduciremos los detalles que nos pregunta, tipo nombre, organizacion, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Publicaremos la clave&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ gpg --keyserver hkp://pool.sks-keyservers.net --send-keys XXXXXXXXXXXXXXX&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Yo también la he publicado en este otro servidor porque durante una de las ejecuciones me dió un error que no lo encontraba ahí:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ gpg --keyserver hkp://keyserver.ubuntu.com --send-keys XXXXXXXXXXXXXXX&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Configuramos Gradle con los datos de la firma, siguiendo los pasos que recomienda la web mencionada.&lt;/p&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Esto quiere decir que cuando quiera publicar un artefacto tendré que hacerlo desde un ordenador que tenga la clave (?)&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;$HOME/.gradle/gradle.properties&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;signing.keyId=XXXXXX
signing.password=&amp;lt;YOUR-PASSWORD&amp;gt;
signing.secretKeyRingFile=/home/jorge/.gnupg/secring.gpg&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;proyecto_java&quot;&gt;Proyecto Java&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que lo que queremos publicar es una simple librería. En mi caso una librería java standard para crear Gif animados a
partir de un array de imágenes (en fichero o en memoria).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En sí el proyecto Gradle es algo como&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins{
    id &apos;java-library&apos;
}

repositories {
    mavenCentral()
}

group &apos;com.puravida-software.gif&apos;

sourceCompatibility = &apos;1.8&apos;
targetCompatibility = &apos;1.8&apos;

dependencies {
    // Use the latest Groovy version for Spock testing
    testCompile &apos;org.codehaus.groovy:groovy-all:2.5.4&apos;
    // Use the awesome Spock testing and specification framework even with Java
    testCompile &apos;org.spockframework:spock-core:1.2-groovy-2.5&apos;
    testCompile &apos;junit:junit:4.12&apos;
}

javadoc {
    title = &quot;$project.description&quot;
}

java {
    withJavadocJar()
    withSourcesJar()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente podemos construir la libreria con &lt;code&gt;./gradlew build&lt;/code&gt; y la tendremos disponible en el directorio &lt;code&gt;build/libs&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea ahora es poder publicarla en Maven Central por lo que añadiremos una serie de plugins que nos facilitarán la labor&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;plugins_de_gradle&quot;&gt;Plugins de Gradle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los plugins que vamos a incluir en nuestro &lt;code&gt;gradle.build&lt;/code&gt; son tres:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;maven-publish&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;io.github.gradle-nexus.publish-plugin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;signing&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El primero va a preparar nuestro proyecto para poder generar ficheros requeridos por maven (pom.xml, etc), mientras que el segundo
es el encargado de subir al Nexus de SonaType los binarios y preparar la release. Por último signing se va a encargar de utilizar
la clave gpg que hemos configurado para firmarlos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues nuestro &lt;code&gt;gradle.build&lt;/code&gt; se queda como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins{
    id &apos;java-library&apos;
    id &apos;maven-publish&apos;
    id &quot;io.github.gradle-nexus.publish-plugin&quot; version &apos;1.1.0&apos;
    id &apos;signing&apos;
}

repositories {
    mavenCentral()
}

group &apos;com.puravida-software&apos;
archivesBaseName =&apos;gif-generator&apos;

sourceCompatibility = &apos;1.8&apos;
targetCompatibility = &apos;1.8&apos;

dependencies {
    // Use the latest Groovy version for Spock testing
    testCompile &apos;org.codehaus.groovy:groovy-all:2.5.4&apos;
    // Use the awesome Spock testing and specification framework even with Java
    testCompile &apos;org.spockframework:spock-core:1.2-groovy-2.5&apos;
    testCompile &apos;junit:junit:4.12&apos;
}

javadoc {
    title = &quot;$project.description&quot;
}

java {
    withJavadocJar()
    withSourcesJar()
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            from(components.java)
            pom {
                name = &apos;PuraVida Software GifGenerator&apos;
                description = &apos;A gif generator from an array of images&apos;
                url = &apos;https://gitlab.com/puravida-software/gif-generator&apos;
                properties = [:]
                licenses {
                    license {
                        name = &apos;The Apache License, Version 2.0&apos;
                        url = &apos;http://www.apache.org/licenses/LICENSE-2.0.txt&apos;
                    }
                }
                developers {
                    developer {
                        id = &apos;jorge&apos;
                        name = &apos;Jorge Aguilera&apos;
                        email = &apos;jorge.aguilera@puravida-software.com&apos;
                    }
                }
                scm {
                    connection = &apos;scm:git:git://puravida-software/gif-generator.git&apos;
                    developerConnection = &apos;scm:git:ssh://puravida-software/gif-generator.git&apos;
                    url = &apos;https://gitlab.com/puravida-software/gif-generator&apos;
                }
            }
        }
    }
}

signing {
    sign publishing.publications.mavenJava
}


nexusPublishing {
    repositories {
        sonatype {  //only for users registered in Sonatype after 24 Feb 2021
            nexusUrl.set(uri(&quot;https://s01.oss.sonatype.org/service/local/&quot;))
            snapshotRepositoryUrl.set(uri(&quot;https://s01.oss.sonatype.org/content/repositories/snapshots/&quot;))
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;proceso&quot;&gt;Proceso&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para comprobar que todo es correcto y que podemos subir la libreria al Nexus ejecutaremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ ./gradlew build publishToSonatype&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo cual, si todo va bien, nos firmará la libreria y demás archivos y las subirá a nuestra área de &lt;strong&gt;staging&lt;/strong&gt; (es decir, no hemos publicado nada
en Maven, tranquilidad) donde podremos revisar si todo está bien.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la salida del comando anterior te muestra la url, que en mi caso es
&lt;a href=&quot;https://s01.oss.sonatype.org/service/local/repositories/compuravida-software-XXXXXX/content/&quot; class=&quot;bare&quot;&gt;https://s01.oss.sonatype.org/service/local/repositories/compuravida-software-XXXXXX/content/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Cada vez que ejecutas una subida el identificador XXXXX irá incrementándose&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes acceder a tu repositorio para verificar las subidas en &lt;a href=&quot;https://s01.oss.sonatype.org/#stagingRepositories&quot; class=&quot;bare&quot;&gt;https://s01.oss.sonatype.org/#stagingRepositories&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Seleccionando el elemento subido, puedes revisar la actividad del mismo y si presenta algún error que te impida publicarlo en Maven. Como
ahora mismo sólo hemos hecho una subida, simplemente nos dirá que la actividad de subida ha ido bien.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación lo que pretendemos es subir el artefacto y que se ejecuten todas las acciones que validen si nuestro artefacto puede ser
desplegado en Maven Central:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ ./gradlew publishToSonatype closeSonatypeStagingRepository&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y revisamos en la consola de Nexus la actividad de la última subida:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/publicar-maven/nexus-gif-generator.png&quot; alt=&quot;nexus gif generator&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si no has puesto bien una descripcion para el pom, autores, la licencia, etc puede que este paso te muestre errores. Por eso es recomendado
ejecutarlo la primera vez de esta forma, para poder revisarlo y repetirlo hasta tener el ok de las validaciones. A mí me ha fallado la firma
(tuve que publicar la firma en el servidor de ubuntu), mal puestas las urls al repo git, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tengamos la subida y sus validaciones pasadas podemos liberar la primera versión simplemente pulando el icono de &quot;Release&quot; lo cual
enviará nuestra librería a Maven Central en unos minutos (en el search aparecerá más tarde)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTA&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Si recuerdas, en los pasos de creación de tickets nos decían que una vez hayamos hecho la primera release lo comentaramos en el ticket
para ejecutar la sync con Maven. A mí no me dió tiempo!!! en cuestión de unos minutos había recibido el correo de que ya estaba publicada la
release.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;desplegando_sin_manos&quot;&gt;Desplegando sin manos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que hemos liberado nuestra primera versión de una forma cuasi-manual ya podremos liberar las siguientes ejecutando simplemente&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo cual ejecutará los dos pasos anteriores de forma automática&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;control_de_versiones&quot;&gt;Control de versiones&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como tema tangencial voy a incluir un apartado sobre el control de versiones.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mis proyectos personales intento aplicar (dentro de mi caos mental) &lt;code&gt;semver&lt;/code&gt; , ya sabes, pasar de versión 1.2.0 a 1.3.0 cuando añado
alguna funcionalidad nueva, y de versión 1.3.0 a 1.3.1 cuando arreglo algún defecto, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si eres una persona cuidadosa o sigues una metodología probablemente no tengas muchos problemas. Yo personalmente uso un plugin (de los cientos que hay)
que utiliza el estado de git para determinar la versión en la que te encuentras, de tal forma que no necesitas codificarla en ningún fichero&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi caso uso el plugin &lt;code&gt;reckon&lt;/code&gt; (&lt;a href=&quot;https://github.com/ajoberstar/reckon&quot; class=&quot;bare&quot;&gt;https://github.com/ajoberstar/reckon&lt;/a&gt;) de tal forma que cuando estoy en disposición de liberar una versión
(o parche) a Maven primero &quot;cierro&quot; la versión ejecutando:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$./gradlew build -Preckon.scope=minor -Preckon.stage=final reckonTagCreate&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este plugin comprueba que mi repositorio git se encuentra &quot;limpio&quot;, con todos los commits hechos, y en base a los tags que existan y al
&lt;code&gt;stage&lt;/code&gt; que le indico determina la versión. Así mismo, si todo está correcto, me crea el tag correspondiente en el repo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;bolas_extras&quot;&gt;Bolas extras&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Andrés Almiray ha publicado JReleaser (&lt;a href=&quot;https://jreleaser.org/&quot; class=&quot;bare&quot;&gt;https://jreleaser.org/&lt;/a&gt;) que promete publicar artefactos Java sin esfuerzo y al que hay que seguir
de cerca porque viendo la documentación el proceso de liberar versiones suena muy fácil&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;referencias&quot;&gt;Referencias&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación algunas páginas que me ha ayudado durante el proceso:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://andresalmiray.com/publishing-to-maven-central-using-apache-maven/&quot; class=&quot;bare&quot;&gt;https://andresalmiray.com/publishing-to-maven-central-using-apache-maven/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gradle-nexus/publish-plugin&quot; class=&quot;bare&quot;&gt;https://github.com/gradle-nexus/publish-plugin&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://weibeld.net/java/publish-to-maven-central.html&quot; class=&quot;bare&quot;&gt;https://weibeld.net/java/publish-to-maven-central.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.solidsoft.pl/2015/09/08/deploy-to-maven-central-using-api-key-aka-auth-token/&quot; class=&quot;bare&quot;&gt;https://blog.solidsoft.pl/2015/09/08/deploy-to-maven-central-using-api-key-aka-auth-token/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>cómo publicar en MavenCentral usando gradle</summary>
    </entry>
    <entry>
        <title>Enviar a grupos de Telegram desde GoogleSheet</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/googlesheet-telegram.html"/>
        <updated>2021-01-18T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/googlesheet-telegram.html</id>
        <category term="google"/>
        <category term="telegram"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Enviar mensajes a un grupo de Telegram es realmente fácil pues con una simple llamada HTTP puedes enviarlos.
En este post vamos a utilizar la capacidad de GoogleSheet de ejecutar scripts para hacer esta llamada a una
serie de grupos de Telegram con un simple click.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación&quot;&gt;Preparación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a necesitar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Una hoja de cálculo de GoogleSheet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;El token de un bot de Telegram que obtendremos desde @BotFather&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Los IDs de los grupos a los que queremos enviar el mensaje y en los que el admin de cada grupo habrá dado permisos
para ello al bot&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;bot_de_telegram&quot;&gt;Bot de Telegram&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crear un bot de Telegram es realmente fácil y no lleva más de un minuto y unos pocos clicks (obviamente necesitas tener
una cuenta de Telegram). Bien sea desde la aplicación del móvil, la versión web o la versión de escritorio (sí, Telegram
tiene 3 formas de usarlo) buscaremos en los contactos a @BotFather&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/telegramgoogle/botfather.png&quot; alt=&quot;botfather&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez empecemos el diálogo con BotFather seleccionaremos /newbot y seguiremos los pasos que nos va guiando el bot&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/telegramgoogle/newbot.png&quot; alt=&quot;newbot&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al final del proceso tendremos nuestro bot listo y podremos consultar su API token (siempre puedes consultarlo así que
no necesitas guardarlo pero allá donde lo vayas a usar ten cuidado de no compartirlo o versionarlo en algún repo donde
pueda acceder gente que no quieras que lo vean)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/telegramgoogle/token.png&quot; alt=&quot;token&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;grupos_de_telegram&quot;&gt;Grupos de telegram&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tengas el bot creado el admin del grupo al que quieres enviar mensajes le tiene que añadir como admin para que
pueda escribir en el grupo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo tienes que saber el ID del grupo. Si es público es simplemente su nombre (con la arroba) pero si es un grupo
privado tienes que &quot;adivinar&quot; su id. La forma más fácil de buscarlo es con el cliente web (&lt;a href=&quot;https://web.telegram.org&quot; class=&quot;bare&quot;&gt;https://web.telegram.org&lt;/a&gt;)
fijándote en la url una vez que seleccionas el grupo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/telegramgoogle/groupid.png&quot; alt=&quot;groupid&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El id del grupo privado es &quot;-100&quot; más la parte subrayada de la imagen (a mí no me mires, es cosa de Telegram)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;sheet&quot;&gt;Sheet&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con todo ello vamos a preparar en una hoja de GoogleSheet una serie de celdas para que nos sea cómodo poder especificar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;el token&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el titulo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el mensaje&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una imagen (opcional)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una lista de grupos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una lista de celdas donde ir poniendo si el envío ha sido correcto o no.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El diseño que he hecho corresponde al de la imagen siguiente y el código adjunto será en base a estas posiciones:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/telegramgoogle/sheet.png&quot; alt=&quot;sheet&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A1: un dibujo de un boton y que tiene asociado el script &lt;code&gt;sendTelegrams&lt;/code&gt; cuando se le pincha (usar el boton derecho sobre la imagen)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B2: el Token&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B3: el titulo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B4: una url a una imagen en internet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B5-N: recorreremos todas las celdas y las concatenaremos para hacer le mensaje&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C3-N: una lista de ids de grupos privados o públicos&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde Herramientas seleccionaremos &quot;Editor de secuencias de comandos&quot; y en el editor que nos aparece sustituieremos lo que
nos crea por defecto por el siguiente código:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;var ui = SpreadsheetApp.getUi();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var rangeData = sheet.getDataRange();
var lastColumn = rangeData.getLastColumn();
var lastRow = rangeData.getLastRow();

function sendTelegrams() {

  const title = sheet.getRange(3,2).getValue()
  const image = sheet.getRange(4,2).getValue()
  let message = sheet.getRange(5,2,10,1).getValues().join(&apos;\n&apos;)
  const bootToken = sheet.getRange(1,3).getValue()
  if( image != &quot;&quot;){
    message+=&quot;\n[​​​​​​​​​​​](&quot;+image+&quot;)&quot;
  }
  const groups = sheet.getRange(3,3,100,1).getValues()
  for( var g=0; g&amp;lt;groups.length; g++){
    if(!groups[g][0]){
      continue
    }
    var payload = {
      &apos;chat_id&apos;:groups[g][0],
      &apos;text&apos;: title+&apos;\n&apos;+message,
      &apos;parse_mode&apos;:&apos;MarkdownV2&apos;,
      &apos;disable_web_page_preview&apos;:false
    }
    var options = {
      &apos;method&apos; : &apos;post&apos;,
      &apos;contentType&apos;: &apos;application/json&apos;,
      &apos;payload&apos;: JSON.stringify(payload),
      &apos;muteHttpExceptions&apos;:true
    };
    var url = &apos;https://api.telegram.org/bot&apos;+bootToken+&apos;/sendMessage&apos;;
    const resp = UrlFetchApp.fetch(url, options);
    sheet.getRange(3+g,4).setValue(resp.getResponseCode()==200)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script es realmente simple:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;accedemos a los objetos de GoogleSheet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;leemos los valores de las celdas de interés&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;concatenamos todas las celdas de mensajes con retornos de carro (\n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;si queremos ḿandar una imagen la incluimos en formato markdown&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;recorremos el rango destinado a los ids de grupo y para cada uno
construimos un payload según lo quiere Telegram y un options según lo
quiere Google&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;enviamos el mensaje con &lt;code&gt;UrlFetchApp&lt;/code&gt; y vamos escribiendo si ha ido bien o mal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;chimpun&lt;/p&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como ves para no complicar el ejemplo he optado por usar MarkdownV2 aunque también
se puede usar HTML (en realidad un subconjunto del mismo) simplemente cambiando &lt;code&gt;parse_mode&lt;/code&gt;
a &lt;code&gt;html&lt;/code&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Guardamos el script y volvemos a la hoja de cálculo donde podemos añadir un dibujo (Insertar/Dibujo)
de un botón por ejemplo y con el botón derecho sobre él asignarle que ejecute la función:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/telegramgoogle/boton1.png&quot; alt=&quot;boton1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/telegramgoogle/boton2.png&quot; alt=&quot;boton2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;escribir_mensajes&quot;&gt;Escribir mensajes&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último simplemente nos resta redactar nuestro mensaje usando formato markdown&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es que puedas escribir de forma cómoda usando las celdas de Google de tal forma que no hace falta
que lo escribas todo en una única celda, sino aprovechar que, si le das al enter, Google te lleva a la celda
inferior. El scritp concatenará todas estas celdas en una cadena con retornos de carro.
De esta forma, si dejas una celda en blanco podrás formatear mejor los mensajes&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;He puesto un límite de 100 celdas, más que suficientes para un mensaje&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tengas preparado el mensaje puedes, por ejemplo, quitar (o mover) todos los ids salvo uno
de pruebas y pulsar el botón para que se ejecute el script. Si todo ha ido bien y el mensaje es de tu
gusto, puedes volver a poner en las celdas de los ids los mismos y volver a pulsar el botón.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para cada grupo indicado el script escribirá un TRUE o FALSE si ha podido enviar el mensaje en ese canal o no&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>un ejemplo práctico de cómo enviar mensajes de Telegram usando GoogleSheet</summary>
    </entry>
    <entry>
        <title>Colaboración Calendario Científico Escolar</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2021/calendario-cientifico.html"/>
        <updated>2021-01-18T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2021/calendario-cientifico.html</id>
        <category term="personal"/>
        <category term="git"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El Calendario Científico Escolar (&lt;a href=&quot;https://twitter.com/CalCientifico&quot; class=&quot;bare&quot;&gt;https://twitter.com/CalCientifico&lt;/a&gt;) es un proyecto dedicado
a publicar una efeméride científica cada día del año. Actualmente van por su segunda edición.
Además de publicar la efeméride proponen una serie de actividades para ser realizadas en las aulas
.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque creo recordar que el proyecto me sonaba de algo de la edición pasada, la verdad es que no habría
reparado en este proyecto sino fuera porque un amigo de Twitter (@ArbolIdeas) me mencionó por si
había alguna manera de pasar un fichero plano a formato calendario ics&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/calendario/1.png&quot; alt=&quot;1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y evidentemente piqué el anzuelo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/calendario/2.png&quot; alt=&quot;2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así que tras unos mensajitos por Twitter y otros por Telegram llegamos a &quot;concretar&quot; una lista de funcionalidades que
se podrían hacer de forma fácil:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;tuitear todos los días la efeméride que tocara. Preferiblemente un tweet por cada idioma y si el texto era más largo
de lo que permite Twitter enviarlo en forma de hilo. Obviamente adjuntando la imagen de la efeméride&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un canal de Telegram donde enviar todos los días un mensaje con todos los idiomas (en lugar de varios canales por
nombre)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unos ficheros iCal para ser importados en un calendario. Se crearía además un calendario público en Google por cada
idioma&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una web mínima donde tener las imágenes de forma aislada para poder ser adjuntada en cada mensaje accesible desde
internet.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello la lista de requisitos en realidad no era muy exigente: los ficheros con los textos y
las imágenes de los personajes de forma individual. Ninguna de las dos cosas era problema pues
los ficheros estaban publicados de forma abierta (ya les había echado un ojo antes para ver dónde
me metía) y las imágenes las proporcionaría la diseñadora.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otra parte indagué sobre los sistemas que se estaban empleando en ese momento siendo en realidad
bastante simples: sólo se utilizaba la web del organismo patrocinador y cualquier enlace a descargas
debería apuntar a esta web pues es donde se encuentra el sistema de estadística. Por lo demás
teníamos barra libre.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;aunque pueda parecer a priori una lista con bastante cosas a hacer en realidad casi todas ellas ya las tenía
de una forma u otra hechas de otros proyectos como enviar la calidad del aire de Madrid todos los días a Twitter,
publicar actividades de las bibliotecas en Telegram y cosas así.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;origen_de_datos&quot;&gt;Origen de datos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como documentos origen contábamos principalmente con unos PDFs (uno por cada 5 idiomas) enmaquedaos con la imagen de
la efeméride y el texto del día, así como una serie de Docx con los textos de cada día en párrafos. Existen ficheros
planos pero como no los tenía al alcance y los documentos Docx estaban bien estructurados decidí usarlos como
los textos origen a utilizar.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ficheros_csv&quot;&gt;Ficheros CSV&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar deberíamos convertir los textos a un formato más manejable tipo CSV. Como quería incluir la imagen
del día y los ficheros planos tenían otro formato, decidí crear unos nuevos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mediante GoogleSheet definí un documento que tuviera las columnas:&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;dia&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;mes&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;año&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;personaje&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;frase&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2021&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;-------&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2021&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;-------&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot; colspan=&quot;5&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;---&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;31&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;12&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2021&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;365&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;-------&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente asigné un identificador correlativo a cada personaje en función del día que le tocaba puesto que en principio cada día sería uno diferente (tal vez el personaje coincidiera pero no la
imagen)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Formatear los documentos de párrafos a lineas en el GoogleSheet fue una tarea simple que una vez
pillado el truco no me llevo más de una hora importar todos los idiomas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al comprobar que muchos de los textos contenían caracteres como la coma, o el pipe opté por volcar
cada tab en ficheros TSV (es decir usar el tabulador).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo, mientras esperaba que me pasaran las imágenes separadas, opté por hacer algunos recortes
de pantalla y generarme unas cuantas imágenes de personajes para probar la idea.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;primeros_scripts&quot;&gt;Primeros scripts&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como la motivación inicial de @ArbolIdeas era disponer de las efemérides en su calendario (supongo que para que su
agenda automática le avise) lo primero que hice fue un script que &quot;convirtiera&quot; estos csv a iCal (ficheros de eventos
de calendarios). Para ello existe una librería en Java muy fácil de usar y el script queda tan sencillo como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Grab(group=&apos;org.mnode.ical4j&apos;, module=&apos;ical4j&apos;, version=&apos;3.0.21&apos;)

import net.fortuna.ical4j.model.*
import net.fortuna.ical4j.model.property.DtStamp

if( args.length != 3){
    println &quot;necesito lang, entrada, salida&quot;
    return
}

new File(args[1]).withReader{ r-&amp;gt;
    r.readLine()

    def builder = new ContentBuilder()
    def calendar = builder.calendar() {
        prodid &apos;-//Ben Fortuna//iCal4j 1.0//EN&apos;
        version &apos;2.0&apos;
        def line
        while( (line=r.readLine())!= null){
            def fields = line.split(&apos;\t&apos;)
            def title = fields[4].split(&apos;\\.&apos;).first()
            vevent {
                uid String.format(&apos;%04d%02d%02d-%s&apos;, fields[2] as int, fields[1] as int, fields[0] as int, args[0])
                dtstamp new DtStamp()
                dtstart String.format(&apos;%04d%02d%02d&apos;, fields[2] as int, fields[1] as int, fields[0] as int), parameters: parameters {
                    value(&apos;DATE&apos;)
                }
                summary title
                description fields[4]
                action &apos;DISPLAY&apos;
            }
        }
    }
    new File(args[2]).text = calendar.toString()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente le das un lenguaje, un csv de entrada y un ical de salida y va leyendo línea a línea el fichero de entrada
generando al final el fichero ical&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ groovy scripts/ical.groovy es static/data/csv/2021_es.csv static/data/ical/2021_es.ical&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ejecutando este script por cada idioma dispuse al momento de un calendario por idioma. Importarlo a tu agenda es tan
fácil (al menos con Google Calendar que es con lo que lo he probado) como ir a tu cuenta, crear un calendario e
importar el fichero de tu interés.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así por ejemplo un colegio, organización o grupo de interés puede crear un calendario, o añadir a uno existente, con
estos eventos y todos tener su notificación.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;telegram&quot;&gt;Telegram&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para poder visualizar la idea que tenía me decanté en primer lugar por publicar un mensaje en
un canal privado de Telegram, principalmente porque es super sencillo y rápido. Puedes crear
el canal, tanto público como privado, en cuestión de segundos así como obtener un token para
publicar en él de forma desatendida.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El primer script con #groovy fue a grandes rasgos algo como leer uno de los csv y buscar
el día, mes y año en curso y realizar un http-post usando las claves de Telegram. Al final
el script ha quedado en algo como este:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Grab(&apos;io.github.http-builder-ng:http-builder-ng-core:1.0.4&apos;)

import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import groovyx.net.http.*
import static java.util.Calendar.*

year = args.length &amp;gt; 0 ? args[0] as int : new Date()[YEAR]
month = args.length &amp;gt; 1 ? args[1] as int : new Date()[MONTH]+1
day = args.length &amp;gt; 2 ? args[2] as int : new Date()[DAY_OF_MONTH]

println &quot;Processing $year/$month/$day&quot;

TELEGRAM_CHANNEL=System.getenv(&quot;TELEGRAM_CHANNEL&quot;)
TELEGRAM_TOKEN=System.getenv(&quot;TELEGRAM_TOKEN&quot;)

if( !TELEGRAM_CHANNEL || !TELEGRAM_TOKEN ){
    println &quot;Necesito la configuracion de telegram&quot;
    return
}

http = configure{
    request.uri = &quot;https://api.telegram.org&quot;
    request.contentType = JSON[0]
}

html = &apos;&apos;

[&apos;es&apos;,&apos;astu&apos;,&apos;cat&apos;,&apos;eus&apos;,&apos;gal&apos;,&apos;en&apos;].each{ lang -&amp;gt;

    String[]found

    new File(&quot;static/data/csv/${year}_${lang}.tsv&quot;).withReader{ reader -&amp;gt;
        reader.readLine()
        String line
        while( (line=reader.readLine()) != null){
            def fields = line.split(&apos;\t&apos;)
            if( fields.length != 5)
                continue
            if( fields[0] as int == day &amp;amp;&amp;amp; fields[1] as int == month &amp;amp;&amp;amp; fields[2] as int == year){
                found = fields
                break
            }
        }
    }

    if(!found){
        println &quot;not found $year/$month/$day&quot;
        return
    }

    if( !html ){
        html = &quot;&quot;&quot;Tal día como hoy

        &amp;lt;a href=&quot;https://calendario-cientifico-escolar.github.io/images/personajes-min/${found[3]}.png&quot;&amp;gt; &amp;lt;/a&amp;gt;
        &quot;&quot;&quot;
    }

    html +=&quot;&quot;&quot;${found[4]}
    -------
    &quot;&quot;&quot;
}

html += &quot;&quot;&quot;
&amp;lt;i&amp;gt;Proyecto FECYT FTC-2019-15288&amp;lt;/i&amp;gt;
&amp;lt;a href=&quot;http://www.igm.ule-csic.es/calendario-cientifico&quot;&amp;gt;Puedes descargar el calendario y la guía didáctica en nuestra web&amp;lt;/a&amp;gt;
&quot;&quot;&quot;

http.post{
    request.uri.path = &quot;/bot$TELEGRAM_TOKEN/sendMessage&quot;
    request.body = [
        chat_id: TELEGRAM_CHANNEL,
        text: html,
        parse_mode: &apos;HTML&apos;,
        disable_web_page_preview: false,
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que seguramente se pueda hacer más bonito y óptimo pero este sea sencillo y funciona. Simplemente para cada idioma, recorre
las líneas del fichero buscando las del dia que se le haya pasado por argumento y va concatenando
en un string las diferentes traducciones. Una vez completados todos los idiomas añade un pie
de mensaje y realiza un http post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La &quot;gracia&quot; aquí es adjuntar una imagen en un mensaje de telegram. En principio el api NO lo
permite pero existe un truco añadiendo un href con la imagen y habilitando el preview de las
páginas web&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El resultado final es algo como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/calendario/3.png&quot; alt=&quot;3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;em&gt;(Seguiré buscando un diseño más atractivo para el mensaje)&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como todavía no tenía un repositorio decente donde poner las imágenes utilicé un &lt;em&gt;ngrok&lt;/em&gt; mediante el
cual puedes servir páginas en tu local a través del túnel que te crea. Como solución temporal para hacer unas pruebas
no está mal y funciona.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como puedes ver en el script anterior ya contamos con un repositorio en Github donde poder acceder a las imágenes.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;repositorio_git&quot;&gt;Repositorio Git&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez validada la idea y mostrado el resultado procedimos a crear el repositorio público git. Gracias a la
predisposición por compartir el proyecto la verdad es que fue fácil el convencer de los beneficios que puede aportar
un repositorio así. Además las operaciones que íbamos a realizar sobre este no iban a ser tan contínuas como en un
proyecto típico de software y con el interface web que ofrecen estos servicios consideramos que sería suficiente para
los miembros menos técnicos del equipo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues la &quot;decisión&quot; se centraba entre usar Gitlab (el cual vengo usando desde hace años con muy buenos resultados)
o Github, más conocido y usado por la gente. Github me ofrecía la oportunidad de tener un caso real para probar el
tema de Github Actions, sobre los que había leído pero no probado, así que de forma totalmente egoísta decidí usarlo
como repositorio.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;github&quot;&gt;Github&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como ya he mencionado, para mí Gitlab no es sólo una alternativa mejor &quot;filosóficamente&quot; hablando, sino que tiene funcionalidades que
Github no ha tenido hasta hace poco, como Github Actions que viene siendo el Pipeline de Gitlab y que llevo usando ya
desde hace varios años.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Github es un servicio que permite el trabajo colaborativo con control de versiones git integrado muy popular y que ya
es usado por ambientes muy diferentes al del desarrollo de software. Así pues, y tras comentar las ventajas de disponer
de un repositorio en dicho servicio lo creé y añadí como owners a las personas interesadas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La intención inicial del repositorio era alojar en él tantos los csv como las imágenes (y así lo use al principio)
aunque luego hemos añadido un site estático basado en Hugo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente creé una organización &quot;calendario-cientifico-escolar.github&quot; y un repositorio
&quot;calendario-cientifico-escolar.github.io&quot; en dicha organización. Al coincidir el nombre del repositorio con la cuenta
y terminarlo con &quot;github.io&quot; Github te permite publicar un static site bajo ese nombre. De esta forma ya tenemos
un site en Internet en &lt;a href=&quot;https://calendario-cientifico-escolar.github.io/&quot; class=&quot;bare&quot;&gt;https://calendario-cientifico-escolar.github.io/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras crear la organización y repositorio, un miembro del proyecto se creó una cuenta y le añadí como owner del mismo.
De esta forma según fueran recibiendo nuevas actualizaciones de las imágenes podrían subirlas directamente sin
necesidad de mi intermediación (además de que el proyecto es suyo, yo sólo estoy colaborando)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con este repositorio en marcha pudimos empezar a &quot;jugar&quot; con la idea de ejecutar tareas de forma programada usando
Github Actions&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;github_actions&quot;&gt;Github ACtions&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya he comentado, Github Actions es la nueva propuesta de Github para ejecutar pipelines cuando subes un cambio
o también de forma planificada. Por ejemplo, si tienes un static site puedes planificar un action para que cuando
actualizas el contenido se ejecute una tarea que publique el site o que a cierta hora ejecute
un &quot;algo&quot;, en nuestro caso los scripts para enviar a las redes sociales la efeméride del día.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Gitlab tiene esta funcionalidad desde hace muchos años y en mi opinión mucho mejor integrada.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Más allá de los detalles técnicos que puedes encontrar en la docu, algunas notas mías:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;recuerda que las actions se definen en el directorio &lt;code&gt;.github/workflows&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;puedes tener muchas actions y las defines cada una en un fichero&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;en Gitlab puedes usar cualquier imágen de Docker, aquí hasta donde he visto sólo puedes usar las que tienen, por ejemplo &lt;code&gt;ubuntu-18.04&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;existen un monton de &lt;code&gt;actions&lt;/code&gt; (pasos a ejecutar en tu pipeline) predefinidas que te pueden simplificar la vida. Por aquí es por donde va el tema,
saber cúales hay cómo crearte la tuya.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;el schedule se define en UTC (vamos que NO planifiques según tu horario local) y en base a mi experiencia, no
programes una operación a corazón abierto con él (vamos, que más o menos se ejecuta a esa hora pero&amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además de forma desatendida puedes configurar un action para que se ejecute manualmente incluso pasándole parámetros
vía web, lo cual fue muy útil para hacer las primeras pruebas. Por ejemplo, el script de envío de Telegram (y luego
el de Twitter) permiten pasar por argumentos un día, mes y año y si no se pasan el script toma los del día actual.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así por ejemplo el action de Telegram se configuraría:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: telegram
on:
  schedule:
    # * is a special character in YAML so you have to quote this string
    - cron:  &apos;0 7 * * *&apos;
  workflow_dispatch:
    inputs:
      year:
        description: &apos;Year&apos;
        required: true
        default: &apos;2021&apos;
      month:
        description: &apos;Month&apos;
        required: true
      day:
        description: &apos;Day&apos;
        required: true
jobs:
  check-groovy:
    runs-on: ubuntu-latest
    env:
      GROOVY_VERSION: 3.0.5
      TELEGRAM_CHANNEL: ${{ secrets.TELEGRAM_CHANNEL }}
      TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
    steps:
      - uses: actions/checkout@v2
      - id: install
        shell: bash
        run: |
          curl -s &quot;https://get.sdkman.io&quot; | bash
          source &quot;$HOME/.sdkman/bin/sdkman-init.sh&quot;
          sdk install groovy $GROOVY_VERSION
          groovy scripts/telegram.groovy ${{ github.event.inputs.year }} ${{ github.event.inputs.month }} ${{ github.event.inputs.day }}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ves, defino dos formas de ejecutarlo, programada y manual (con parámetros introducidos por el usuario). Uso la
última imagen de ubuntu e instalo sobre ella Groovy para poder ejecutar el script.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;en Gitlab sería más simple pues puede indicar que use la imagen &lt;code&gt;groovy:3.0.5&lt;/code&gt; por ejemplo sin necesidad de
tener que hacer todos esos pasos. Hasta donde yo sé no existe (aún) un action adecuado para Groovy&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando queremos enviar el telegram de un día concreto simplemente es ir a la sección de &lt;code&gt;actions&lt;/code&gt; y proporcionar los
parámetros:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2021/calendario/4.png&quot; alt=&quot;4&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;github_secrets&quot;&gt;Github Secrets&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para poder autentificar a los scripts vamos a tener que proporcionar en la mayoría de los casos (siempre) las credenciales
correspondientes en cada servicio. En el caso de Telegram por ejemplo necesitamos el canal y el token del bot que va
a realizar la acción.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crear un canal de Telegram es instantáneo y sólo requiere que no exista uno con ese nombre. Así mismo el canal puede
ser privado o público (el primero se identifica con un número y el segundo con el nombre que le des). Por otra parte
crear un bot para publicar en este canal es igualmente fácil y utilizando el bot BotFather de Telegram puedes crearlo
y obtener su token al momento.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para las primeras pruebas de integración utilicé un canal privado así como un bot que tengo de Puravida Software por
lo que sólo falta hacerle &quot;llegar&quot; al script estas credenciales de forma segura para lo que se usa los &lt;em&gt;secrets&lt;/em&gt; de
Github&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes crear diferentes secretos por entornos (entorno de desarrollo, test, blablabbla) pero en nuestro caso no lo
necesitamos así que tenemos simplemente secretos de repositorio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Obviamente una vez probado y validado el script sobre el canal privado simplemente creamos un canal público
@CalendarioCientifico y actualizamos el secret correspondiente&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;twitter&quot;&gt;Twitter&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con toda esta infraestructura probada y funcionando introducir un nuevo script junto con su action y secrets para
Twitter es realmente fácil.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como queríamos tuitear desde la cuenta oficial, la persona que la lleva rellenó el formulario para crear una app y
obtener las ApiKey y Secrets de twitter (el proceso llevo sólo un par de horas de espera hasta obtenerlas)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este script va a ser muy similar al de Telegram pero con algunas diferencias:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;un tweet por idioma (en lugar de un sólo mensaje)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;adjuntar la imagen en el primer tweet de cada idioma&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;controlar si el texto excede el límite de un tweet y si es así enviarlo como hilo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un hashtag diferente por cada idioma&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por lo demás usará los mismos ficheros csv, imágenes, parámetros etc. quedando el script:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@Grab(group=&apos;org.twitter4j&apos;, module=&apos;twitter4j-core&apos;, version=&apos;4.0.6&apos;)

import twitter4j.TwitterFactory
import twitter4j.StatusUpdate
import static java.util.Calendar.*

year = args.length &amp;gt; 0 ? args[0] as int : new Date()[YEAR]
month = args.length &amp;gt; 1 ? args[1] as int : new Date()[MONTH]+1
day = args.length &amp;gt; 2 ? args[2] as int : new Date()[DAY_OF_MONTH]

println &quot;Processing $year/$month/$day&quot;

[
    &apos;es&apos;:&apos;#CalendarioCientifico&apos;,
    &apos;astu&apos;:&apos;#CalendariuCientificu&apos;,
    &apos;cat&apos;:&apos;#CalendariCientífic&apos;,
    &apos;eus&apos;:&apos;#ZientziaEskolaEgutegia&apos;,
    &apos;gal&apos;:&apos;#CalendarioCientifico&apos;,
    &apos;en&apos;:&apos;#ScientificCalendar&apos;,
].each{ kv -&amp;gt;
    String lang = kv.key
    String hashtag = kv.value

    String[]found

    new File(&quot;static/data/csv/${year}_${lang}.tsv&quot;).withReader{ reader -&amp;gt;
        reader.readLine()
        String line
        while( (line=reader.readLine()) != null){
            def fields = line.split(&apos;\t&apos;)
            if( fields.length != 5)
                continue
            if( fields[0] as int == day &amp;amp;&amp;amp; fields[1] as int == month &amp;amp;&amp;amp; fields[2] as int == year){
                found = fields
                break
            }
        }
    }

    if(!found){
        println &quot;not found $year/$month/$day&quot;
        return
    }


    String title=  found[4].split(&apos;\\.&apos;).first()
	String body=  found[4].split(&apos;\\.&apos;).drop(1).join(&apos; &apos;)
	String link = &quot;&quot;
	String hashtags = &quot;${hashtag}&quot;

    long inReply = 0
	def tweets = splitText(&quot;$title\n$body&quot;, &quot;$link\n$hashtags&quot;)
    tweets.eachWithIndex{ str, i -&amp;gt;
        String page = tweets.size() == 1 ? &quot;&quot; : &quot;${i+1}/${tweets.size()}&quot;
		StatusUpdate status = new StatusUpdate(&quot;$str\n$page&quot;).inReplyToStatusId(inReply)
        if( i == 0 ){
            def bytes = &quot;https://calendario-cientifico-escolar.github.io/images/personajes/${found[3]}.png&quot;.toURL().bytes
            println &quot;image con $bytes.length&quot;
            status.media &quot;${found[3]}&quot;, new ByteArrayInputStream(bytes)
        }
		inReply = TwitterFactory.singleton.updateStatus(status).id
		println status.status
    }

}


def splitText( String text, String suffix ){
	def ret = []
	def words = text.split(&apos; &apos;)
	def current = &apos;&apos;
	words.eachWithIndex{ w, i -&amp;gt;
		if( current.length() &amp;gt; 180 ){
			ret.add current
			current = &apos;&apos;
		}
		current+= &quot;$w &quot;
	}
	current += &quot;\n$suffix&quot;
	ret.add current
	ret
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y su action de forma muy parecida al de Telegram cambiando simplemente cúando ejecutarlo y los tokens que este
requiere. De esta forma podemos, de forma manual, enviar un tweet de un día cualquiera&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: twitter
on:
  schedule:
    # * is a special character in YAML so you have to quote this string
    - cron:  &apos;0 5 * * *&apos;
  workflow_dispatch:
    inputs:
      year:
        description: &apos;Year&apos;
        required: true
        default: &apos;2021&apos;
      month:
        description: &apos;Month&apos;
        required: true
      day:
        description: &apos;Day&apos;
        required: true
jobs:
  check-groovy:
    runs-on: ubuntu-latest
    env:
      GROOVY_VERSION: 3.0.5
      CONSUMERKEY: ${{ secrets.CONSUMERKEY }}
      CONSUMERSECRET: ${{ secrets.CONSUMERSECRET }}
      ACCESSTOKEN: ${{ secrets.ACCESSTOKEN }}
      ACCESSTOKENSECRET: ${{ secrets.ACCESSTOKENSECRET }}
    steps:
      - uses: actions/checkout@v2
      - id: install
        shell: bash
        run: |
          curl -s &quot;https://get.sdkman.io&quot; | bash
          source &quot;$HOME/.sdkman/bin/sdkman-init.sh&quot;
          sdk install groovy $GROOVY_VERSION
          groovy \
            -Dtwitter4j.oauth.consumerKey=${CONSUMERKEY} \
            -Dtwitter4j.oauth.consumerSecret=${CONSUMERSECRET} \
            -Dtwitter4j.oauth.accessToken=${ACCESSTOKEN} \
            -Dtwitter4j.oauth.accessTokenSecret=${ACCESSTOKENSECRET} \
            scripts/twitter.groovy ${{ github.event.inputs.year }} ${{ github.event.inputs.month }} ${{ github.event.inputs.day }}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;static_site&quot;&gt;Static Site&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último (por ahora) y teniendo todo funcionando decidímos crear un site sobre el mismo repositorio que ayude en la
localización de los ficheros.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente sobre el proyecto mismo creé un site con &lt;code&gt;Hugo&lt;/code&gt; el cual permite usar ficheros Markdown para editar el
contenido de las páginas y él se encarga de renderizarlo. La elección de este generador fue por su cuasi-simplicidad
y cómo no por tener una excusa para probarlo (yo uso para mi blog JBake pero el diseño por defecto requiere mucho
tuneo y así investigaba otros generadores).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez creado el site y editadas las páginas de inicio y enlaces, la labor fue conseguir un &lt;em&gt;action&lt;/em&gt; para generar y
publicar el site de forma desatendida.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El action al final ha quedado tal que:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: site
on:
  push:
    branches:
    - main
jobs:
  deploy:
    runs-on: ubuntu-18.04
    steps:
      - name: Git checkout
        uses: actions/checkout@v2

      - name: Update theme
        run: git submodule update --init --recursive

      - name: Setup hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: &quot;0.70.0&quot;

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          deploy_key: ${{secrets.ACTIONS_DEPLOY_KEY}}
          external_repository: calendario-cientifico-escolar/calendario-cientifico-escolar.github.io
          publish_dir: ./public
          user_name: Jorge Aguilera
          user_email: jagedn@gmail.com
          publish_branch: gh-pages&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo único de interés en este action es que hay que crear una clave pública/privada para poder autentificar a este
action en el proyecto y que pueda publicar el directorio &lt;code&gt;public&lt;/code&gt; generado por Hugo en el branch &lt;code&gt;gh-pages&lt;/code&gt; que es el
que hemos configurado en la consola de Github como rama donde reside el site a publicar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con esto, ante cualquier commit en main (anteriormente conocido como master) el action genera y publica el site en Internet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Utilizando el directorio de Hugo &lt;code&gt;static&lt;/code&gt; podemos ubicar los ficheros que los script (o cualquiera con otra idea) usaran
para su ejecución.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusiones&quot;&gt;Conclusiones&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque parezca mucho trabajo en realidad no es tanto, y más si tenemos en cuenta que ya tenía los scripts casi de otros
proyectos. Simplemente era tener clara la idea de lo que se pretendía, a saber:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;disponer de un repositorio donde ubicar ficheros a consumir&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;probar unos scripts en local&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jugar con la automatización de tareas de Github Actions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pelearse con la configuración de los tokens para no publicarlos y mantenerlos secretos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;como bola extra jugar con la idea de tener un site o blog para poder poner los enlaces sin más pretensiones (el
site actual es feo como él solo)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por mi parte la excusa, autoimpuesta, de tener que usar Github para automatizar tareas o probar Hugo (creo que
migraré el blog del chaval a Hugo) me ha sido muy gratificante. Si le añadimos el poder colaborar en proyectos
de divulgación científica (mira mamá, soy casi divulgador científico!!!) pues mayor placer aún.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>notas sobre mi colaboración con el proyecto Calendario Científico escolar</summary>
    </entry>
    <entry>
        <title>Resumen 2020</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/resumen.html"/>
        <updated>2020-12-21T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/resumen.html</id>
        <category term="personal"/>
        <category term="pensamientos"/>
        <category term="introspección"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Vaya por delante mis condolencias para todos aquellos que han perdido a algún ser querido así como mucho ánimo para
seguir con lo que nos espera, porque no nos engañemos, esto no termina el 31 de diciembre. En mi entorno familiar, por
suerte, aunque ha habido casos no han sido nada graves.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Termina un año tan complicado como 2020 y es tiempo de escribir el penúltimo post del año a modo de resumen y porqué no,
un poco de autobombo.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;blog&quot;&gt;Blog&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hace un año y poco que empecé el blog, por lo que podemos decir que este ha sido su primer año. Por si no lo sabes,
(aunque hay un post donde lo explico) en este blog me he creado un sistema de estadísticas totalmente manual de tal
forma que cuando lees un artículo del mismo se crea una entrada en una hoja GoogleSheet en la que simplemente guardo
la url y la fecha y con ello tengo todo el sistema de estadísticas que necesito, que viene siendo saber qué post es el
más leído y ya.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En cifras, este año he escrito 28 entradas (con esta) con unas 2680 visitas. Los post que han generado más interés
han sido:&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 1. Post 2020&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;URL&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Visitas&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2020/plantuml-c4.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;C4&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;464&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2020/telegram-bot-netlify.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Telegram Bot&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;343&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2020/intro-asciidoctor-1.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Asciidoctor 1&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;230&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2020/intro-asciidoctor-3.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Asciidoctor 3&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;212&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2020/intro-asciidoctor-4.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Asciidoctor 4&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;149&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2020/intro-asciidoctor-2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Asciidoctor 2&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;125&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2020/intro-antora.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Antora&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;122&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2020/intro-asciidoctor-5.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Asciidoctor 5&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;111&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque un par de post del 2019 siguen estando entre los más leídos&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 2. Post 2019&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2019/postureo.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Postureo&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;203&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2019/okteto-gallery.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Okteto Picture Gallery&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;176&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://blog.jagedn.dev/blog/2019/okteto-grails.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Okteto Grails&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;114&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A estas alturas, la mayoría de los que me leen saben por mi preferencia de Asciidoctor como herramienta no sólo para
documentar sino para escribir sea lo que sea, y eso se nota en los artículos que he escrito. Viendo el interés que
generó C4 (el primer sorprendido soy yo mismo) creo que debería sacar un rato para profundizar más sobre este y sobre
alguna de las herramientas que se están creando a su alrededor&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A mitad de año se me ocurrió crear un grupo de Telegram, así como un canal, ambos con el mismo nombre que el blog,
donde anunciar, algo así como a modo de exclusiva, la url del último post y que esté orientado a que la peña
suscribita comentar sobre lo que quieran, preferiblemente sobre qué les ha parecido
algún post, si no se entiende, etc. Por ahora somos pocos pero han surgido algunas cosas interesantes, espero que siga
así.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;salto_sin_red_otro&quot;&gt;Salto sin red (otro)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Echando la vista atrás me encontraba a principios de año trabajando para eDreams en un equipo pequeño (unas 6 personas)
desarrollando una aplicación interna de backoffice en Grails (mi framework favorito) donde estaba aprendiendo sobre
todo a aplicar Kanban y a trabajar en un entorno bastante Agile. Es un grupo que este tema ya llevaban un tiempo probando
qué era lo que mejor les funcionaba y aunque la aplicación tenía sus cosas la verdad es que en este tema aprendí
bastante.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo soy una persona que tiene que estar más en &quot;el barro&quot;, necesito tener la posibilidad de desarrollar y probar
ideas nuevas sin mucha ceremonia, tanto en herramientas como en arquitectura, y la verdad es que en este sitio no
veía la posibilidad de conseguirlo al menos en el corto plazo (no soy muy paciente para estas cosas, I know it) así que
en un meetup al que asistí tuve la oportunidad de retomar unas conversaciones con un asiduo a los MadridGUG y me
animó a unirme a Tymit, una startup FinTech con un producto actualmente funcionando en UK.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Me despedí de mis compas de eDreams tal que un 18 de Marzo para empezar el siguiente lunes 23, cuando ese mismo fin de
semana nos confinaron, por lo que he estado desde el primer día trabajando en remoto hasta ahora. En Tymit se tomó la decisión
de que a pesar de que ya se permitía volver a las oficinas sólo lo hiciera quién quisiera, y sinceramente en mi caso al
menos el remoto ha funcionado bien y por ahora no siento la necesidad de una oficina (salvo algún momento puntual)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como en toda buena startup, estos meses han sido muy intensos. Como muestra decir que nuestro proveedor de medios
de pago, Wirecard, de la noche a la mañana tuvo que cerrar porque 1.9 &lt;strong&gt;Billones&lt;/strong&gt; de euros habían desaparecido
(&lt;a href=&quot;https://en.wikipedia.org/wiki/Wirecard_scandal&quot; class=&quot;bare&quot;&gt;https://en.wikipedia.org/wiki/Wirecard_scandal&lt;/a&gt;), aunque no todas las movidas han sido por factores externos. El tener
un producto ya en marcha y la necesidad de consolidarlo requiere atender muchas áreas, desacoplar sistemas, aprender a
toda marcha, explicar las cosas como si hubieras crecido con ellas, &amp;#8230;&amp;#8203; En fín, un reto en el que no te aburres.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A modo de resumen el esfuerzo por mi parte creo que lo resumo en:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Trabajo en remoto. Hemos probado muchas herramientas, desde el típico Slack como herramienta oficial, a Mumble, Discord,
ahora Teams, alguna prueba con el plugin de Intellij CodeWithMe, mejorar los commits y las PR, empezamos a trabajar
orientado a la feature y no a la issue, forzarnos a escribir en los chats en inglés, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Trabajo en equipo. A mí me cuesta trabajar en equipo. Yo creo que tengo la capacidad de visualizar cómo debería quedar
lo que quiero pero no la de planificar paso a paso cómo llegar, sino que llego &quot;por aproximación&quot; lo que no le cuadra
a todo el mundo. En estos meses he tenido que ir trabajando en ello pero me queda mucho camino aún.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Trabajo de empresa. Después de muchos años llevando mi empresa (unipersonal) y siendo una especie de mercenario me
cuesta identificarme con los objetivos de la empresa siendo además en algunos casos muy difícil el hacerlo. Uno de los
esfuerzos que he hecho este año ha sido, en la medida de lo posible, identificarme con la empresa y ser parte de ella.
Ayuda el participar en los procesos de selección de los nuevos fichajes, tener canales distendidos donde poder expresarte
(el post sobre random-place lo pude practicar en nuestro canal #random)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente el mayor esfuerzo para mí sigue siendo el tema del inglés, en el que sé que nunca alcanzaré el nivel mínimo
que creo que debería tener y que hace que en ciertas propuestas al final tenga que ir al remolque por no poder &quot;liderar&quot;
mi punto de vista.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;charlas&quot;&gt;Charlas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si bien este año no ha sido el mejor para acudir a eventos tampoco tenía la intención de presentar charlas en eventos
&quot;multitudinarios&quot;. Tenía en mi radar dar la de #DocAsCode en @esLibre_ así como un taller sobre Telegram en el @Greachconf
y poco más, aunque sí que tenía intención de presentar
alguna en el grupo de MadridGUG para ir contando las tonterías que iba investigando, como la utilizar @Oktetohq como
plataforma para desplegar aplicaciones kubernetes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como al final el evento de @esLibre_ (sobre el que ya escribí mi resumen en este blog) se hizo de forma virtual pude
dar mi charla que a mí particularmente me gustó mucho.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y sin previo aviso un día recibo un mensaje de la peña de @hatthieves que si quiero hablar sobre mis movidas con #OpenData.
Claro, a eso uno no se puede resistir, así que preparé otra charla virtual, sobre la que también encontrarás su entrada
en este blog.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ideas&quot;&gt;Ideas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;tlgconf&quot;&gt;TlgConf&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Parece que fue hace milenios pero empezaba el año ilusionado con desarrollar un bot para manejar las agendas de los
eventos. Incluso llegó a funcionar en el WeCodeFest &amp;#8230;&amp;#8203; para luego ver cómo todos los eventos que tenía en el radar se
iban cancelando. Cierto que tanto la idea como el bot se pueden adaptar a los tiempos de charlas virtuales pero el
parón que hubo me hizo guardarlo en un cajón&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Aquí la documentación del proyecto &lt;a href=&quot;https://puravida-software.gitlab.io/tlg-conf/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tlg-Confg&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;telegram_workshop&quot;&gt;Telegram Workshop&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para el workshop de Telegram había desarrollado una librería que se integraba con Micronaut aunque ya ha quedado algo
obsoleta porque Sergio del Amo ha creado una mucho más potente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Aquí las notas para el workshop &lt;a href=&quot;https://puravida-software.gitlab.io/mn-telegram-workshop/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Workshop&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;googlesheet&quot;&gt;GoogleSheet&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algunos pinitos en Google Sheet como adaptar la aplicación para sortear regalos a esta plataforma (correcto, hay post sobre ello).
Alguien me comentó que lo probarían en su clase del colegio, pero no sé si llegaron a probarlo.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;bar_el_rambo&quot;&gt;Bar el Rambo&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al estilo de más gente que intentaba ayudar al pequeño comercio a adaptarse a la nueva situación, hice un planteamiento
(no muy currado la verdad sea dicha) para que un bar pueda publicar de forma gratis y cómoda el menú diario en un site&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;a href=&quot;https://gitlab.com/jorge-aguilera/bar-el-rambo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bar el rambo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;bots_para_slack&quot;&gt;Bots para Slack&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este blog encontrarás info sobre cómo usar Netlify para crearte bots para Slack con poco esfuerzo y sin coste.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tienes descrito algunos como /hastalapolla, /urls y /version&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;serversendevent_con_rust&quot;&gt;ServerSendEvent con Rust&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde hace tiempo tengo ganas de aprender Rust así que he aprovechado algunos días de vacaciones para practicar con él
y he creado un site donde se van publicando cada segundo un represaliado por la dictadura franquista&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
El código en &lt;a href=&quot;https://gitlab.com/jorge-aguilera/represaliados-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Represaliados&lt;/a&gt; y la app
funcionando en &lt;a href=&quot;https://sse-pvidasoftware.cloud.okteto.net/static/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Represaliados&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;easyfeedback&quot;&gt;EasyFeedback&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ahora mismo estoy madurando la idea de EasyFeedback, una app para que los conferenciantes puedan obtener feedback rápido
de los asistentes, tal vez durante su charla.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Está en fase de incubación pero puedes ver un vídeo en &lt;a href=&quot;https://twitter.com/jagedn/status/1340928959035416577&quot; class=&quot;bare&quot;&gt;https://twitter.com/jagedn/status/1340928959035416577&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;fracasos&quot;&gt;Fracaso(s)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Realmente no puedo quejarme del 2020. A nivel profesional el trabajo está siendo intenso e interesante y me deja tiempo
para seguir evolucionando y desarrollar mis tonterías.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, aunque llegué a montar un static site con la documentación usando #DocAsCode, no he conseguido hacer ver
al equipo las ventajas que tiene esta metodología y se ha terminado usando Confluence para toda la documentación. Desde
mi punto de vista es un retroceso (sin negar que Confluence tiene sus cosas buenas creo que es un engorro en ciertos
aspectos y que no promueve la colaboración) y me siento como un impostor hablando de #DocAsCode cuando no he sabido
&quot;venderlo&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si a esto le sumamos que mi chaval decidió al tercer post de su blog que le aburría y que ya no iba a
escribir más pues el sentimiento de impostor se acrecienta enormemente.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;planes_futuros&quot;&gt;Planes Futuros&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A grandes rasgos yo no hago planes para el siguiente año. Al igual que con el código, tengo una visión
de cómo me gustaría que fuera el producto final, pero no una ruta para llegar a él, porque años como el presente nos
demuestran que hacer planes a un año vista sirven de bien poco. Así que como mucho a un futuro inmediato,
creo que voy a dedicarle algo de esfuerzo a #EasyFeedback y ver hasta dónde puede llegar la idea sin perder
de vista cualquier otra cosa que me pueda emocionar por el camino.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Un resumen personal del 2020</summary>
    </entry>
    <entry>
        <title>Netlify</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/netlify.html"/>
        <updated>2020-12-04T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/netlify.html</id>
        <category term="javascript"/>
        <category term="node"/>
        <category term="netlify"/>
        <category term="asciidoctor"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Netlify es una plataforma que ofrece hosting y servicios de backend para aplicaciones web y
sitios &quot;estáticos&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Por sitios estáticos entendemos aquellas aplicaciones donde el código (si lo hay) se ejecuta
en el cliente, sin necesidad de una aplicación que genere la página tipo php, java, ruby, etc. Simplemente
se alojan páginas html, css, javascript y el navegador del cliente las descarga y muestra.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En lo relativo a Netlify, con &quot;servicios de backend para aplicaciones web&quot; me refiero
a que podemos desplegar en nuestro site funciones javascript y/o Go (por ahora) que se ejecutan en
el backend al ser invocadas por el front, así como algunas funcionalidades tipo tratamiento de
formularios enviados por el usuario, un sistema de autentificación, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta especie de tutorial voy a contar cómo lo utilizo yo, no queriendo decir esto que sea la forma correcta
(aunque a mí me funciona) ni que use todas las funcionalidades.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo, para lo que lo uso me sirve la capa gratuita por lo que no contaré nada que no esté incluida en ella.
Con esta capa puedes crear tantos sites como quieras e incluso si tienes un dominio podrás asociarlo a uno de ellos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que hayas creado una cuenta con tu correo o con tu cuenta Gitlab/Github podrás ir &quot;completando&quot; las
diferentes secciones que mostraré a continuación&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/netlify1.png&quot; alt=&quot;netlify1&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. dashboard&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;hello_world&quot;&gt;Hello World&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El primer ejemplo es desplegar un simple &quot;Hello World&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;crea un directorio &quot;helloworld&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crea un fichero &lt;code&gt;index.html&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;index.html&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    Hello World
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;selecciona en el dashboard el menú &quot;sites&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;arrastra la carpetea a la parte central de la página de Netlify (similar al de la imagen)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/netlify2.png&quot; alt=&quot;netlify2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eso es todo. A los pocos segundos estará disponible en Internet, bajo un nombre aleatorio generado por Netlify.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/netlify3.png&quot; alt=&quot;netlify3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la seccion de &quot;site details&quot; podremos cambiar el nombre por uno más adecuado, siempre que se encuentre libre.
Como puedes ver lo que no puedes cambiar, por ahora, es el dominio &lt;code&gt;netlify.app&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/netlify4.png&quot; alt=&quot;netlify4&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando se encuentre desplegado el site (con el único index.html) podremos actualizarlo repitiendo la misma operación pero sobre
el site creado.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;hello_vuejs&quot;&gt;Hello VueJS&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podemos complicar un poco más el &lt;code&gt;hello world&lt;/code&gt; y crear una aplicación VueJS (por ejemplo).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras instalar el CLI de Vue ejecutaremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ vue create hellowvue&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y una vez que tengamos la aplicación a nuestro gusto, generaremos la versión a distribuir&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ npm run build&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para desplegar el aplicativo seguiremos los mismos pasos que en el caso anterior seleccionando esta vez la carpeta
&lt;code&gt;dist&lt;/code&gt; que nos generó el build.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;netlify_cli&quot;&gt;Netlify CLI&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Netlifiy proporciona un cliente de consola con el que podemos desplegar desde la la terminal nuestra aplicación
sin necesidad de usar el drag&amp;amp;drop del explorador. Para ello necesitaremos tener instalado el &lt;code&gt;node&lt;/code&gt; en nuestro sistema&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ npm install netlify-cli -g&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y cuando se complete la instalación haremos login desde la consola&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ netlify login&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;lo cual nos abrirá un navegador para autentificarnos en Netlify&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez configurado y autentificado podremos usar el CLI para desplegar la aplicación desde la línea de consola
bien a un site existente o a uno nuevo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ netlify init&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;donde podremos indicarle que queremos un site nuevo, por ejemplo, o uno ya existente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo podremos correr la aplicación VueJS anterior como si se estuviera ejecutando en Netlify desde el directorio
donde hemos creado la aplicación Vue:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ netlify dev&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;O incluso crear una URL temporal que podremos compartir para que se pueda acceder a ella desde Internet&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ netlify dev --live&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;aws_lambda_en_nuestro_site&quot;&gt;AWS Lambda en nuestro site&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La capa gratuita nos permite desplegar también funciones javascript (o Go) que se ejecutan en el backend cloud asociado
a nuestro site, de tal forma que por ejemplo el front pueda hacer peticiones GET o POST a una URL del site para
que se ejecuten procesos que no se pueden correr en el cliente. Por ejemplo yo lo he usado para:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;cada vez que alguien visita una página se invoca una función backend que manda un mensaje a un canal de Telegram.
Como las credenciales no pueden ser públicas, esta ejecución corre en un entorno back donde las credenciales son
proporcionadas como variables de entorno&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;acceder a una hoja GoogleSheet y leer o escribir en ella ante ciertos eventos. Por ejemplo renderizar un json
con unas celdas determinadas que el front parsea y muestra formateado.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un bot de Slack que reacciona ante ciertos comandos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un bot de Telegram&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;etc&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
La capa gratuita te permite tener tantas functions como quieras con un máximo de 125K invocaciones al mes
(para mis cosas me sobran). OJO que no son tareas en background, para eso necesitarás la siguiente capa de pago, sino
funciones con un timeout definido.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar configuraremos nuestro site con un fichero &lt;code&gt;netlify.toml&lt;/code&gt; donde le indicaremos en qué directorio
residen nuestras funciones (además de otras configuraciones como por ejemplo qué directorio queremos publicar
como frontend)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;netlify.toml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[build]
  publish = &quot;dist&quot;
  functions = &quot;functions/&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crearemos el directorio &lt;code&gt;functions/hellovue&lt;/code&gt; y crearemos en él el fichero &lt;code&gt;hellowvue.js&lt;/code&gt; siguiente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;helloowvue.js&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;exports.handler = async function(event, context) {
    return {
        statusCode: 200,
        body: JSON.stringify({message: &quot;Hello World&quot;})
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y ahora podremos desplegar desde línea de comandos tanto nuestro frontend como nuestras funciones simplemente
ejecutando:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$netlify deploy&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez desplegado el site tendremos bajo el subdominio creado un URI al que podremos invocar, tal que
&lt;code&gt;&lt;a href=&quot;https:///XXXXXXXX.netlify.app/.netlify/functions/hellowvue&quot; class=&quot;bare&quot;&gt;https:///XXXXXXXX.netlify.app/.netlify/functions/hellowvue&lt;/a&gt;&lt;/code&gt; :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ curl https://5fcca9be43cd1713632cbf4d--xxxxxxxx.netlify.app/.netlify/functions/hellowvue
{&quot;message&quot;:&quot;Hello World&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación un ejemplo más elaborado de lo que podría ser una function de Netlify. En este ejemplo en cada invocación
se creará un nuevo registro en una hoja GooglSheet&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;var GoogleSpreadsheet = require(&apos;google-spreadsheet&apos;);
var async = require(&apos;async&apos;);

// spreadsheet key is the long id in the sheets URL
var doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEET);
var sheet;

exports.handler = function(event, context, callback) {

  var obj = JSON.parse(event.body);
  obj.when = new Date().toISOString();

  async.series([
    function setAuth(step) {
      var creds_json = {
        client_email: process.env.GOOGLE_CLIENT_EMAIL,
        private_key: process.env.GOOGLE_PRIVATE_KEY.replace(new RegExp(&quot;\\\\n&quot;, &quot;\g&quot;), &quot;\n&quot;)
      }
      doc.useServiceAccountAuth(creds_json, step);
    },
    function getInfoAndWorksheets(step) {
      doc.getInfo(function(err, info) {
        sheet = info.worksheets[0];
        step();
      });
    },
    function addRow(step) {
      sheet.addRow( obj, function( err, rows ){
        step();
      });
    }
  ], function(err){
      if( err ) {
        console.log(&apos;Error: &apos;+err);
      }
      callback(null, {
          statusCode: 200,
          body: &quot;OK&quot;
      });
  });

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente nos identificamos ante Google, recuperamos la primera hoja y añadimos una Row nueva con el json
recibido más la fecha de ejecución.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para que este ejemplo funcione deberemos haber creado unas variables de entorno a través de la consola de Netlify
y darle los valores adecuados a&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GOOGLE_SHEET&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GOOGLE_CLIENT_EMAIL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GOOGLE_PRIVATE_KEY&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ci_con_netlify&quot;&gt;CI con Netlify&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En lugar de desplegar desde tu local, puedes conectar tu cuenta de Github o Gitlab con Netlify y asociar proyectos
alojados en ellos con sites creados en este, de tal forma que ante un commit en tu repo se ejecute un build en
Netlify para publicar el site.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así por ejemplo, siguiendo con el caso anterior, puedes crear un repo en Gitlab &lt;code&gt;hellowvue&lt;/code&gt; y desde Netlify crear
un site nuevo desde él seleccionando &quot;New site from Git&quot; desde el dashbaord:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/netlify1.png&quot; alt=&quot;netlify1&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. gitlab&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez seleccionado el repo que queremos asociar se ejecutará el build en el cloud de Netlify y nos lo desplegará.
A partir de aquí, cada commit que realizemos en &lt;code&gt;master&lt;/code&gt; (configurable) ejecutará de nuevo el pipeline y el despliegue.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;pipeline_de_gitlab&quot;&gt;Pipeline de Gitlab&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En algunas ocasiones nuestro proyecto consistirá en algo más complejo que simplemente publicar un site, como por
ejemplo compilar una librería Java, publicarla en Maven y actualizar la documentación. Para estos casos yo uso el
pipeline de Gitlab el cual no sólo permite separar esos pasos en &lt;code&gt;stages&lt;/code&gt; sincronizados de una forma muy potente,
sino que también permite publicar la docu de la misma forma que Netlify, esta vez bajo el dominio &lt;code&gt;gitlab.io&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como Gitlab no ofrece las características de Netlify como la de functions (y otras como envío de formularios, etc)
podemos delegar la publicación del site a Netlify mientras el pipeline de Gitlab sigue realizando los otros pasos
(de forma resumida: Gitlab ejecuta la parte pesada y Netlify la publicación del site).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello simplemente deberemos crear un &lt;code&gt;step&lt;/code&gt; en nuestro pipeline de Gitlab que invoque la publicación en Netlify.
Este step va a requerir de dos variables extraídas de este proveedor: el ApiToken y el AppId del repo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El ApiToken lo obtenemos desde nuestro perfil en Netlify&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/netlify5.png&quot; alt=&quot;netlify5&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 3. ApiToken&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y el AppId del propio site&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/netlify4.png&quot; alt=&quot;netlify4&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 4. AppId&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estos dos valores los deberemos configurar en nuestro repo Gitlab. Como el ApiToken es el mismo para todos los sites
de Netlify, puesto que es el personal, y lo único que cambia en cada proyecto es el AppId yo tengo puesto el ApiToken a nivel
de grupo y así sólo configuro el de proyecto:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/gitlab1.png&quot; alt=&quot;gitlab1&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 5. gitlab&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez configurado, ya puedo ejecutar mi pipeline de Gitlab (bien de forma manual, en cada commit, o de forma
planificada) y publicar en Netlify automáticamente. Este sería un ejemplo de pipeline en mi proyecto Gitlab:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.gitlab-ci.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;stages:
  - build
  - staging
  - deploy

groovy:
  stage: build
  image:
    name: groovy:2.5.9
  script:
    - groovy dump.groovy
    - cp estaciones.json docs
  artifacts:
    paths:
      - docs

antora:
  stage: staging
  image:
    name: jagedn/antora-with-extensions
    entrypoint: [/bin/sh, -c]
  variables:
    ASCIIDOC_COPY_TO_CLIPBOARD: &quot;true&quot;
    DOCSEARCH_ENABLED: &quot;true&quot;
    DOCSEARCH_ENGINE: &quot;lunr&quot;
    NODE_PATH: &quot;$$(yarn global dir)/node_modules&quot;
  dependencies:
    - groovy
  script:
    - ls -lhat docs/modules/ROOT/pages
    - antora --generator antora-site-generator-lunr  estaciones.yml
    - mv docs/estaciones.json build/main/final
  artifacts:
    paths:
      - build

netlify:
  stage: deploy
  image: node:10.15.3
  script:
    - npm i -g netlify-cli
    - netlify deploy --site $NETLIFY_SITE_ID --auth $NETLIFY_AUTH_TOKEN --prod
  dependencies:
    - antora
  only:
    - master&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este pipeline corresponde al proyecto &quot;Estaciones de servicio&quot; que de forma diaria se descarga los precios de
todas las gasolineras de España, genera un site estático y lo publica en Netlify aplicando la técnica descrita
(&lt;a href=&quot;https://gitlab.com/jorge-aguilera/estaciones-de-servicio/&quot; class=&quot;bare&quot;&gt;https://gitlab.com/jorge-aguilera/estaciones-de-servicio/&lt;/a&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Otra cosa &quot;chula&quot; que puedes hacer es usar la funcionalidad de Gitlab para publicar la documentación de
tu proyecto mientras que despliegas el proyecto en sí mismo en Netlify&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aquí puedes ver el resultado final:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://estaciones-de-servicio.netlify.app/&quot; class=&quot;bare&quot;&gt;https://estaciones-de-servicio.netlify.app/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;blog&quot;&gt;Blog&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último, otra funcionalidad que he probado en Netlify es la de asociar un site con un dominio propio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo este blog que estás leyendo se encuentra alojado en Netlify bajo el nombre jorge-aguilera-blog.netlify.app
pero también está asociado al nombre blog.jagedn.dev&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello, en primer lugar tuve que comprar el dominio &lt;code&gt;aguilera.soy&lt;/code&gt; a través de un proveedor de servicios (hasta
donde sé puedes usar cualquiera, Good Dady, ConfigBox, Google, Arsys, &amp;#8230;&amp;#8203;) y simplemente configurar en él los servidores
DNS que Netlify me indicaba.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez configurado (y tras una espera que puede ser de varios minutos a algunas horas) Netlify detecta que sus
servidores están asociados a ese dominio y entonces ya puedes crear el subdominio que quieras en el site.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/netlify/netlify6.png&quot; alt=&quot;netlify6&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 6. domain&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;resumen&quot;&gt;Resumen&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aún quedan otras funcionalidades que no he investigado como probar el CMS (Content Management System, un Wordpress, vamos)
, recibir un correo cuando un usuario rellena un formulario y alguna otra cosa. A modo de resumen las cosas que
hago con la capa gratuita son:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;publicar un blog usando JBake con mi propio dominio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;enviar mensajes a un canal de Telegram cuando hay visitas en una página&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;escribir en una hoja Google&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;alojar Bots simples de Slack o Telegram&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;generar sites y publicarlos de forma diaria&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;divertirme&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Especie de tutorial de cómo uso Netlify para desplegar sites (y algún bot)</summary>
    </entry>
    <entry>
        <title>Slack Bot para Spring Info Actuator</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/slack-bot-info-actuator.html"/>
        <updated>2020-11-13T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/slack-bot-info-actuator.html</id>
        <category term="node"/>
        <category term="javascript"/>
        <category term="slack"/>
        <category term="netlify"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Este post se puede considerar una continuación a &lt;a href=&quot;mi-primer-slack-bot.html&quot; class=&quot;bare&quot;&gt;mi-primer-slack-bot.html&lt;/a&gt;, de hecho
sólo voy a contar la parte de código de interés porque todo lo demás ya se explica en ese post&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si tu aplicación usa el framework Spring o Grails entonces dispones de la funcionalidad Spring Actuator,
que te permite monitorizar y obtener información sobre tu aplicación en tiempo real a través de unos
endpoints, como el típico &lt;code&gt;/health&lt;/code&gt; , &lt;code&gt;/env&lt;/code&gt; o &lt;code&gt;/info&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En concreto &lt;code&gt;/info&lt;/code&gt; ofrece información como el nombre de la aplicacion, version de la misma, o del java, etc.
Además si encuentra un fichero &lt;code&gt;git.properties&lt;/code&gt; es capaz de adjuntar la información del mismo en la response&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Poder acceder a esta información de forma fácil puede ser una gran utilidad. Por ejemplo poder consultar
qué versión de API está corriendo en el entorno de desarrollo puede servirle al desarrollador de front para
comprobar si es la correcta contra la que quiere probar sus desarrollos.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;aplicación&quot;&gt;Aplicación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el caso en concreto que vamos a ver en este post, se trata de una aplicación Grails la cual incluye
por defecto tanto la dependencia a spring actuator
como una configuración mínima para exportar estos endpoints (más info en
&lt;a href=&quot;http://docs.grails.org/3.3.4/guide/spring.html#actuators&quot; class=&quot;bare&quot;&gt;http://docs.grails.org/3.3.4/guide/spring.html#actuators&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo, como norma básica, el acceso a dicho endpoint estará protegido con Spring Security por lo que
primero habrá que hacer un login para obtener tokens de acceso para poder acceder al endpoint indicado.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último, para poder ofrecer la máxima cantidad de información al usuario (por ejemplo el mensaje
del último commit), especificaremos que el endpoint use el formato &lt;code&gt;full&lt;/code&gt;
(&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-application-info-git&quot; class=&quot;bare&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-application-info-git&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conectividad&quot;&gt;Conectividad&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dependiendo de dónde se encuentre alojada la aplicación, así como el bot, tendrás que lidiar con diferentes
aspectos de conectividad.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este caso en concreto el bot se va a alojar en Netlify, el cual lo expone mediante una url pública en Internet.
Así mismo la aplicación es abierta (pero protegida) al público, por lo que también cuenta con una url pública.
Por último, para este ejemplo vamos a contar con que los 3 entornos, dev, staging y prod, están accesibles desde Internet&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si no fuera así, por ejemplo tu api de prod está abierta al público, pero los entornos de desarrollo están en una
red interna, tendrías que buscar una forma de unir los 3 &quot;mundos&quot;: slack &amp;#8594; bot &amp;#8594; aplicación&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/1781159839817.svg&quot; alt=&quot;&quot; width=&quot;800&quot; height=&quot;600&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;creación_del_proyecto&quot;&gt;Creación del proyecto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para la creación del proyecto y su publicación te remito al post &lt;a href=&quot;mi-primer-slack-bot.html&quot; class=&quot;bare&quot;&gt;mi-primer-slack-bot.html&lt;/a&gt;
donde se explica de forma detallada. De hecho si has conseguido seguirlo y publicar tu primer bot puedes
reutilizarlo pues lo que vamos a hacer es añadir una función nueva (y algunas variables de entorno)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;nueva_function&quot;&gt;Nueva function&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Siguiendo los pasos del post anterior crearemos una nueva &lt;code&gt;function&lt;/code&gt; git-status:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;netlify function:create&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(O simplemente ya que tenemos todo montado crearemos un nuevo subdirectorio dentro de &lt;code&gt;functions&lt;/code&gt; y añadiremos el código
siguiente)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;git-status.js&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const querystring = require(&quot;querystring&quot;);
const axios = require(&quot;axios&quot;);

const { SLACK_BOT_TOKEN } = process.env;
const { REPO_GIT } = process.env;

const handler = async (event) =&amp;gt; {
  try {

    if (event.httpMethod !== &quot;POST&quot;) {
      return { statusCode: 405, body: &quot;Method Not Allowed&quot; };
    }

    const params = querystring.parse(event.body);

    const environment = params.command.toUpperCase().substring(1)

    const REMOTE_API = process.env[`${environment}_API`];
    const REMOTE_USERNAME = process.env[`${environment}_USERNAME`];
    const REMOTE_PASSWORD = process.env[`${environment}_PASSWORD`];

    let login = await axios.post(`${REMOTE_API}/api/login`,{
      username:REMOTE_USERNAME,
      password:REMOTE_PASSWORD,
    })
    let access_token = login.data.access_token

    let version = await axios.get(`${REMOTE_API}/info`,{
      headers:{
        &apos;Authorization&apos;:`Bearer ${access_token}`
      }
    })

    const data = {
      text: `
*${environment}* *${version.data.git.commit.message.short}
Version *${version.data.app.version}*
Cooked at *${version.data.git.commit.time}*
last commit: *${version.data.git.commit.id}*
${REPO_GIT}/${version.data.git.commit.id}
`,
      channel: params.channel_id,
      as_user: true
    };

    const result = await axios.post(&apos;https://slack.com/api/chat.postMessage&apos;, data, {
      headers:{
        &apos;Authorization&apos;: `Bearer ${SLACK_BOT_TOKEN}`,
        &apos;X-Slack-User&apos;: params.user_id
      }
    })

    return {
      statusCode: 200
    }

  } catch (error) {
    console.log(error)
    return { statusCode: 500, body: error.toString() }
  }
}

module.exports = { handler }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver el código es muy similar al del comando anterior. Simplemente usaremos el comando que nos envía
el usuario para detectar que entorno es el que nos está pidiendo. Así por ejemplo si el usuario ejecuta el comando
/prod pasaremos a mayusculas esta cadena y quitaremos el primer caracter&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;const environment = params.command.toUpperCase().substring(1)&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y usaremos la constante &lt;code&gt;environment&lt;/code&gt; como prefijo para buscar las credenciales y url a invocar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const REMOTE_API = process.env[`${environment}_API`];
const REMOTE_USERNAME = process.env[`${environment}_USERNAME`];
const REMOTE_PASSWORD = process.env[`${environment}_PASSWORD`];&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente realizaremos un login y obtendremos el access_token para poder invocar al endpoint de Spring Actuator.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último renderizamos toda la información como una cadena Markdown:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const data = {
  text: `
*${environment}* *${version.data.git.commit.message.short}
Version *${version.data.app.version}*
Cooked at *${version.data.git.commit.time}*
last commit: *${version.data.git.commit.id}*
${REPO_GIT}/${version.data.git.commit.id}
`,&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;netlify&quot;&gt;Netlify&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por la parte del bot simplemente resta configurar todas las variables de entorno en Netlify:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;PROD_API=http://xxxxx.com/&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;STAGING_API=http://yyyyy.com/&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;DEV_API=http://zzzzz.com/&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;etc&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;slack&quot;&gt;Slack&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último habilitamos nuevos comandos desde la consola de nuestra App de Slack apuntando a la nueva
&lt;code&gt;function&lt;/code&gt; creada (los 3 comandos apuntan a la misma url pues en base a el comando la &lt;code&gt;function&lt;/code&gt; ya sabe
qué ejecutar)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>un bot de Slack para saver qué versión corre en cada entorno</summary>
    </entry>
    <entry>
        <title>Mi primer bot en Slack</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/mi-primer-slack-bot.html"/>
        <updated>2020-11-12T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/mi-primer-slack-bot.html</id>
        <category term="node"/>
        <category term="javascript"/>
        <category term="slack"/>
        <category term="netlify"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Slack es una herramienta de comunicación de equipos muy utilizada tanto en proyectos abiertos
como en entornos laborales. Aunque las comunidades que conozco lo han ido abandonando por otras
soluciones más ligeras, creo que la presencia en entornos labores es aún muy importante.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Entre las características típicas de mensajería de uno a uno, en grupo, canales, videollamadas, etc
se añade un ecosistema de aplicaciones muy amplio. Permite integrar procesos propios de la
empresa en él de tal forma que podamos crear nuestras propias aplicaciones que actúen como un miembro
más del equipo reaccionando ante eventos, escribiendo en los canales, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post voy a explicar de forma resumida cómo podemos hacer un bot que responda a un comando enviado
por los usuarios. En Internet hay tutoriales muy completos y la propia documentación de Slack
es bastante extensa con ejemplos y herramientas de testeo, por lo que aquí vamos a ver algo muy sencillo
pero que podrás instalar y adaptar en el workspace de vuestro slack (si tenéis permisos para ello)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otra parte, como el bot tiene que ejecutarse en algún sitio, voy a contar cómo podemos usar Netlify
para alojarlo. Netlify proporciona diferentes tipos de cuenta donde alojar tus proyectos, siendo la gratuita
muy interesante:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ilimitados proyectos de contenido estático&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;integración con los principales repositorios (Github, Gitlab,&amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;despliegues automáticos (o manuales con posibilidad de revisión previa)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ejecución de funciones Serverless (Lambdas de Amazon) 125.000 al mes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;formularios inteligentes (ni idea, no los he probado todavía)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues para este post vamos a necesitar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Una cuenta en Netlify, la gratuita nos sirve de sobra&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Un workspace en Slack (prueba primero en un workspace nuevo y si te mola instalas el bot en el de la empresa)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Node para desarrollar.&lt;/p&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como bola extra podríamos enlazar la cuenta de Netlify con nuestra cuenta en Gitlab/Github para desplegar
automáticamente cuando actualicemos el repo. Por ahora lo haremos manualmente)&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además para desarrollar en nuestro local vamos a tener instalado &lt;code&gt;npm&lt;/code&gt; y el cli de Netlify &lt;code&gt;netlify-cli&lt;/code&gt;
(&lt;a href=&quot;https://docs.netlify.com/cli/get-started/&quot; class=&quot;bare&quot;&gt;https://docs.netlify.com/cli/get-started/&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último &lt;code&gt;ngrok&lt;/code&gt; es una herramienta muy útil para probar en local nuestro bot antes de
subirlo a producción. La cuenta gratuita es suficiente, con el único inconveniente de que cada vez que mates
el proceso &lt;code&gt;ngrok&lt;/code&gt; te cambia la url y tienes que reconfigurar en slack donde está tu bot.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;politicamente_correcto_bot&quot;&gt;Politicamente Correcto Bot&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El bot a desarrollar es realmente sencillo y servirá para que ante solicitudes por parte
de algún compañero para realizar alguna tarea podamos soltar un exabrupto y que el bot nos lo cambie por una
frase políticamente correcta.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente cuando escribamos el comando &lt;code&gt;/hastalapolla&lt;/code&gt; (o el que quieras implementar) el bot se activará y en lugar
de este comando se mandará al canal una frase más adecuada seleccionada aleatoriamente de una lista de candidatas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La utilidad del bot es más bien poca salvo para servir como punto de partida de algunas funcionalidades más complejas
que se te puedan ocurrir&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo debido a la simplicidad del bot no requeriremos de ninguna librería ni framework extra de las existentes.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;arquitectura&quot;&gt;Arquitectura&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/1781159838209.svg&quot; alt=&quot;&quot; width=&quot;800&quot; height=&quot;600&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;entorno&quot;&gt;Entorno&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Debemos asegurarnos que tenemos npm instalado:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;npm -v&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;así como que hemos instalado el cliente de Netlify:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;netlify -v&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y hemos hecho login a nuestra cuenta con él&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;netlify login&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;creando_proyecto&quot;&gt;Creando proyecto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En un directorio limpio inicializaremos el proyecto:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;npm init&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;proporcionando un nombre, version, etc por defecto (si vas publicar el código tal vez sí te interese
rellenar esta parte más detenidamente)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;con lo que tendremos un fichero &lt;code&gt;package.json&lt;/code&gt; parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;package.json&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;test&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
  },
  &quot;author&quot;: &quot;&quot;,
  &quot;license&quot;: &quot;ISC&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;así mismo crearemos un subdirectorio &lt;code&gt;public&lt;/code&gt; donde crearemos un fichero &lt;code&gt;index.html&lt;/code&gt; con el contenido que
quieras (si quieres hacer un landing page para tu bot, este es tu directorio)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;public/index.html&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;HastaLaPolla Slack bot&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y por último crearemos un fichero &lt;code&gt;netlify.toml&lt;/code&gt; para configurar a netlify:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;netlify.toml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[build]
  functions = &quot;functions&quot;
  publish = &quot;public&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ejecutaremos &lt;code&gt;netlify&lt;/code&gt; para comprobar que tenemos la infra preparada&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;netlify dev&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;lo cual nos abrirá la página &lt;code&gt;index.html&lt;/code&gt; en un navegador&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Paramos el proceso y procedemos a crear la función para nuestro bot&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;function&quot;&gt;Function&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crearemos nuestra primera función&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;netlify function:create&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;seleccionamos hello-world (total, luego lo vamos a cambiar por nuestro código)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;como nombre especificaremos &lt;code&gt;hastalapolla&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;lo cual nos creará un subdirectorio y un fichero JS en él&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y procedemos a probar de nuevo que vamos bien&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;netlify dev&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y desde el navegador accederemos a &lt;code&gt;&lt;a href=&quot;http://localhost:8888/.netlify/functions/hastalapolla&quot; class=&quot;bare&quot;&gt;http://localhost:8888/.netlify/functions/hastalapolla&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ngrok&quot;&gt;Ngrok&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que te hayas creado una cuenta en ngrok y descargado el ejecutable podemos crear
un tunel entre el &lt;code&gt;netlify&lt;/code&gt; que está corriendo en tu máquina con el mundo exterior:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;ngrok http PUERTO_NETLIFY_FUNCTION&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Como no estoy seguro que Netlify siempre escuche en el mismo puerto para ejecutar las
funciones te toca revisar el que te muestre a tí. En mi caso es el 37947&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;ngrok&lt;/code&gt; te muestra dos URLs (http y https) que son diferentes cada vez que lo ejecutes. Copia
la direccion https y prueba a cambiarla por localhot:8888 en la prueba anterior, por ejempo
&lt;a href=&quot;https://a9123123xxxxx.ngrok.io/.netlify/functions/hastalapolla&quot; class=&quot;bare&quot;&gt;https://a9123123xxxxx.ngrok.io/.netlify/functions/hastalapolla&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;slack&quot;&gt;Slack&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ahora avanzaremos un poco por el lado de Slack.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Lo mejor es crearse un workspace en blanco donde poder instalar y depurar el bot antes de intentarlo en el
de la empresa.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;app&quot;&gt;App&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero que haremos será crear una App desde &lt;a href=&quot;https://api.slack.com/apps&quot; class=&quot;bare&quot;&gt;https://api.slack.com/apps&lt;/a&gt; donde deberemos indicar el nombre y
workspace donde queremos crearla.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Desconozco todas las funcionalidades que ofrece una App de Slack (espero irlas descubriendo) pero para este
bot vamos a necesitar lo mínimo&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;command&quot;&gt;Command&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nuestro bot es tan simple que lo único que va a hacer (por ahora) es reaccionar a un comando &lt;code&gt;/hastalapolla&lt;/code&gt; por lo que
lo daremos de alta en la seccion &lt;code&gt;Slash Commands&lt;/code&gt; y rellenaremos los campos que nos pide:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/slack/hastalapolla1.png&quot; alt=&quot;hastalapolla1&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. formulario.png&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En Request URL prestaremos especial atención para poner la URL que nos generó ngrok (&lt;strong&gt;la https&lt;/strong&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;workspace&quot;&gt;Workspace&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez configurado el comando podemos proceder a probarlo desde el workspace donde lo hemos instalado, por ejemplo
ejecutando el comando en el canal #random&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;/hastalapolla&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo está bien configurado, Slack nos debería ir completando el comando según lo escribes y se enviaría al canal tal cual&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;hastalapolla&quot;&gt;Hastalapolla&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es hora de añadir un poco de código a nuestra function&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;No cortes &lt;code&gt;ngrok&lt;/code&gt; o te tocará volver a lanzarlo y reconfigurar la URL en Slack&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sustituiermos la funcion &lt;code&gt;hastalapolla.js&lt;/code&gt; por esta:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const querystring = require(&quot;querystring&quot;);  &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
const axios = require(&quot;axios&quot;);

const { SLACK_BOT_TOKEN } = process.env; &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;

const list = [
  &apos;:thumbsup: no te preocupes, ahora mismo me pongo con ello&apos;,
  &apos;vale, termino una cosa :watch: y me pongo con ello asap&apos;,
  &apos;uff, bueno, lo miro y te digo algo&apos;,
  &apos;ahora mismo me pillas un poco ocupado, pero en cuanto pueda te cuento&apos;,
  &apos;Genial, no te preocupes, ya te cuento luego&apos;,
]

const handler = async (event) =&amp;gt; {

  if (event.httpMethod !== &quot;POST&quot;) {
    return { statusCode: 405, body: &quot;Method Not Allowed&quot; };
  }

  const params = querystring.parse(event.body);

  const selected = list[Math.floor(Math.random() * list.length)]; &lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;

  const data = {
    text: selected,
    channel: params.channel_id,
    as_user: true
  };

  const result = await axios.post(&apos;https://slack.com/api/chat.postMessage&apos;, data, { &lt;i class=&quot;conum&quot; data-value=&quot;4&quot;&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
    headers:{
      &apos;Authorization&apos;: `Bearer ${SLACK_BOT_TOKEN}`,
      &apos;X-Slack-User&apos;: params.user_id
    }
  })

  return {
    statusCode: 200 &lt;i class=&quot;conum&quot; data-value=&quot;5&quot;&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
  }
}

module.exports = { handler }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Las únicas dependencias que usaremos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;No hemos visto todavia el token ni para que sirve, lo haremos a continuación&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;La frase aleatoria a enviar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;4&quot;&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Con un &quot;simple&quot; post enviaremos la frase políticamente correcta al canal en nombre del usuario&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;5&quot;&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;con un 200 le decimos a Slack que hemos ejecutado su comando. Si añades texto lo leerá el sólo el usuario&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver son 15 líneas excasas de Javascript donde lo más interesante es que utilizamos un paquete
de node muy popular para enviar el post (podíamos haberlo hecho usando puro node)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente instalaremos las dependencias indicadas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;npm install --save axios&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;npm install --save querystring&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;lo cual nos modifica nuestro &lt;code&gt;package.json&lt;/code&gt; tal que:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;test&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;
  },
  &quot;author&quot;: &quot;&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;dependencies&quot;: {
    &quot;axios&quot;: &quot;^0.21.0&quot;,
    &quot;querystring&quot;: &quot;^0.2.0&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y podremos probar de nuevo a enviar el comando desde nuestro workspace de Slack. Si todo va bien NO veremos
NADA en el canal mientras que en la consola donde está corriendo Netlify veremos que ha llegado la petición.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente es que estamos intentando enviar un mensaje a un canal sin estar autentificados.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;token&quot;&gt;Token&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para que el bot pueda escribir en un canal hay que darle permisos. Para ello iremos a &quot;OAuth&amp;amp;Permissions&quot; y
añadiremos los scopes que muestra la imagen (como bot los scopes &lt;code&gt;chat:write&lt;/code&gt; y &lt;code&gt;commands&lt;/code&gt; y como user &lt;code&gt;chat:write&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/slack/hastalapolla2.png&quot; alt=&quot;hastalapolla2&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. scopes.png&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo copiaremos el token de bot que se nos muestra al principio &lt;code&gt;xoxb-XXXXXXXXXXXXXXXXXXXXXx&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a proceder a ejecutar de nuevo la consola de &lt;code&gt;netlify&lt;/code&gt; pero ahora proporcionandole el token copiado para que
el bot pueda escribir en el canal. (Desconozco cómo se hace en Window$)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;SLACK_BOT_TOKEN=xoxb-XXXXXXXXXXX netlify dev&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Seguramente tendrás que reconfigurar de nuevo &lt;code&gt;ngrok&lt;/code&gt; y la consola de la app con la nueva url generada.
Ten en cuenta que esto es así porque vamos paso a paso incrementando la aplicación, una vez lo tengamos todo no sería
necesario más que una vez.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y probamos de nuevo a enviar el comando /hastalapolla&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien ahora tendremos un mensaje políticamente correcto en el canal.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;deploy&quot;&gt;Deploy&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es hora de subir nuestra aplicación a Netlify y dejar que se ejecute en la capa gratuita (recuerda, tienes hasta 125K llamadas
al mes)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;netlify deploy&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y crearemos un nuevo site donde desplegar la aplicacion. Netlify elegirá un nombre aleatorio lo cual es bueno para nuestro bot,
aunque si lo prefieres luego lo puedes cambiar por otro que este libre.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente resta configurar el nuevo site creado con la variable SLACK_BOT_TOKEN para lo que desde la consola de Netlify iremos
a build, environment y crearemos la variable con el valor del token (de la misma forma que hicimos por consola en el paso anterior)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez configurado deberemos realizar un nuevo deploy para que tome la variable creada. Simplemente desde la consola de Netlify
iremos a build y seleccionamos redeploy.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ultimo sólo resta decirle a Slack dónde encontrar ahora el bot para el comando tal como hacíamos con &apos;ngrok&apos;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;TIP&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Yo lo que hago es tener un comando &quot;test&quot; que apunta al entorno local con &lt;code&gt;ngrok&lt;/code&gt; y el &quot;oficial&quot; que apunte a Netlify&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien, una vez configurado, cada vez que ejecutes el comando /hastalapolla estarás invocando a la función alojada
en Netlify por lo que no necesitas ya tu entorno de desarrollo levantado.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;bola_extra&quot;&gt;Bola extra&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes alojar tu proyecto en un repot git en Gitlab o Github y &quot;enlazarlo&quot; con Netlify de tal forma que cada vez que hagas un push
se despliegue automáticamente, pero eso es para otro post (Si estás interesado simplemente comentamelo y lo vemos)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>un bot de Slack simple corriendo en el AWS de Netlify</summary>
    </entry>
    <entry>
        <title>Cuando el diablo no sabe que hacer, programa en Node</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/random-place.html"/>
        <updated>2020-10-23T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/random-place.html</id>
        <category term="node"/>
        <category term="javascript"/>
        <category term="idea"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Detrás de ese título &quot;provocador&quot; sólo pretendo decir que YO no soy un programador
con experiencia en este lenguaje sin entrar a valorar sus ventajas o desventajas.
Simplemente se me ocurrió una tontería y quise ponerla en práctica usandolo y así practicar un poco
de paso.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una (si no la única) afición que tengo es la de viajar, y cuanto más lejos y menos visitado sea el sitio
mejor. Obviamente con la situación actual de la Covid19 esto es imposible así que lo único que me queda
es abrir de vez en cuando Google Maps y seleccionar un sitio al azar (vale, también reviso muchos sitios
donde he estado).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por una asociación de ideas recordé un juego que usa Google Street Maps para mostrarte un
sitio random y hay que adivinar donde es en un tiempo corto, simplemente moviendote por las calles sin
poder hacer zoom y buscándolo encontré &lt;code&gt;random.earth&lt;/code&gt; un site que me fascinó. Muestra luegares
random usando imágenes de Google Maps, con la posibilidad de que la gente las vote.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ni tengo el tiempo ni la capacidad de hacer algo parecido se me ocurrió hacerme mi versión sencilla
y en este post voy a contar cómo lo he hecho (y ya te digo que el código tendrá una calidad
regulinchi y ójala una horda de trolls puedan venir a explicarme cómo hacerlo mejor porque así
aprenderemos algo)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es simple:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;tener un comando que ejecutar al iniciar sesión que seleccione unas coordenadas random del planeta&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;construir la url en random.earth con esas coordenadas&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;descargar unos cuantos pantallazos de ese lugar aplicando diferente diferentes zoom desde el más
lejano hasta el de máxima resolución&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crear un gif animado con la secuencia de imágenes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;subirlo a Mastodon (donde también tengo cuenta como @jagedn)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El resultado final es tener algo parecido a&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;/images/2020/random-place.gif&quot; alt=&quot;random place&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al principio jugué con la idea de hacerlo sólo mediante herramientas disponibles en la shell (
&lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;httpie&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt;, etc ) Asi encontré algunas herramientas en Tk que permiten indicarle una URL
y hacen la captura del navegador, trasteé con la shell para generar números random, hice un bash
que lo unificaba todo&amp;#8230;&amp;#8203;. pero al final cambié de opinión y me decidí por hacerlo en un único
lenguaje y opté por Node (sí, podía haberlo hecho en Groovy pero no quiero que me acusen de encasillarme)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;instalar_node&quot;&gt;Instalar Node&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero es instalar Node. No sé si es dificil o no para Windows, pero para Linux no tiene mucha
historia.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://nodejs.org/es/download/package-manager/#debian-and-ubuntu-based-linux-distributions-enterprise-linux-fedora-and-snap-packages&quot; class=&quot;bare&quot;&gt;https://nodejs.org/es/download/package-manager/#debian-and-ubuntu-based-linux-distributions-enterprise-linux-fedora-and-snap-packages&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente instalar &lt;code&gt;Nvm&lt;/code&gt; y con él instalar la versión que nos interese de Node&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;proyecto&quot;&gt;Proyecto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En un directorio limpio crearemos el proyecto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;npm init&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como el proyecto no lo voy a publicar como paquete simplemnte he ido aceptando las opciones por defecto
que se ofrecen.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al final tienes un fichero &lt;code&gt;package.json&lt;/code&gt; que puedes luego ajustar a mano si te interesa&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;dependencias&quot;&gt;Dependencias&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para este proyecto voy a usar las siguientes dependencias:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;capture-website&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dotenv&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;get-image-colors&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;gif-creation-service&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;mastodon&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ npm install --save capture-website
$ npm install --save dotenv
$ npm install --save get-image-colors
$ npm install --save gif-creation-service
$ npm install --save mastodon&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;index_js&quot;&gt;Index.js&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script va a residir en un único fichero (luego he hecho algunas variantes) &lt;code&gt;index.js&lt;/code&gt; que iré
exponiendo a continuación por partes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;inicialización&quot;&gt;Inicialización&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;index.js&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const dotenv = require(&quot;dotenv&quot;);
dotenv.config();

const fs = require(&quot;fs&quot;);
const captureWebsite = require(&quot;capture-website&quot;);
const GifCreationService = require(&quot;gif-creation-service&quot;);
const path = require(&quot;path&quot;);
const getColors = require(&quot;get-image-colors&quot;);
const Masto = require(&quot;mastodon&quot;);

function getRndInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

(async()=&amp;gt;{
    // El código
})();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente cargamos las dependencias que vamos a usar así como creamos una función de utilidad
que parece que Javascript no tiene por defecto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a usar la capacidad de Javascript de llamar a funciones de forma asíncrona mediante &lt;code&gt;await&lt;/code&gt;
así que el código estará embebida en una función de &lt;code&gt;async&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;elegir_un_lugar_random&quot;&gt;Elegir un lugar random&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;const outputGifFile = &quot;output.gif&quot;;
let pngImages = [];

//vamos a buscar un sitio random pero puede ser agua que no interesa
//así que lo intentaremos varias veces hasta encontrar tierra
let doIt = false;
for (var j = 0; j &amp;lt; 20; j++) {

    let lat = getRndInteger(-89, 89);
    let latprec = getRndInteger(0, 9999);
    let log = getRndInteger(0, 179);
    let logprec = getRndInteger(0, 9999);
    let west = getRndInteger(0, 1) == 0 ? 1 : -1;
    log = log * west;

    // tendriamos por ejemplo 32.3430,-113.4350

    // añadimos al array una serie de urls con distinto zoom: ${i}z
    // asociado a un nombre de png
    pngImages = [];
    for (let i = 1; i &amp;lt;= 19; ) {
      pngImages.push([
        `https://random.earth/@${lat}.${latprec},${log}.${logprec},${i}z,2t`,
        `screenshot${i}.png`,
      ]);

      // borramos el fichero si existe
      try {
        fs.unlinkSync(`screenshot${i}.png`);
      } catch (e) {}

      // según el número de steps que usemos tendremos un gif con mayor peso
      i += 3;
    }

    // detectar si es tierra
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;detectar_si_es_tierra&quot;&gt;Detectar si es tierra&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez inicializado el array de urls a descargar vamos a descargar la penúltima foto que tiene un
un zoom elevado y ver si corresponde a tierra o mar analizando su paleta de colores.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Descargamos la imagen en &lt;code&gt;check.png&lt;/code&gt; quitando el elemento HTML &lt;code&gt;#map&lt;/code&gt; para que no meta ruido:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;await captureWebsite.file(pngImages[pngImages.length - 2][0], &quot;check.png&quot;, {element:&quot;#map&quot;});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El elemento #map es un objeto DOM que está en la página propia de random.earth, es decir, que si estás
descargando otra página ese elemento no lo tendrás seguramente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez descargada vamos a analizar su paleta:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;doIt = false;
await getColors(path.join(__dirname, &quot;check.png&quot;)).then((colors) =&amp;gt; {
  if ((&quot;&quot; + colors).startsWith(&quot;#e3e3dc&quot;) == false) {
    doIt = true;
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea es utilizar una librería que nos permite recuperar la paleta de colores de la imagen y
ver si nos interesa. A parte de que el código que he usado es muy feo lo que he hecho ha sido
comprobar que cuando es mar, la imagen descargada tiene una paleta de colores muy básica que siempre
empieza por #e3e3dc, así que básicamente si es diferente he encontrado una imagen interesante.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al cambiar el valor de doIt a true consigo que se salga del bucle inmediatamente marcando además
que hemos encontradao la imagen.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;descargar_todas_las_imagenes&quot;&gt;Descargar todas las imagenes&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para descargar las imágenes simplemente llamamos a la librería con el array que habiamos preparado
al principio, quitando algunos elementos del DOM para dejar las imágenes lo más limpias posibles&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;await Promise.all(
    pngImages.map(([url, filename]) =&amp;gt; {
      return captureWebsite.file(url, filename, {
		  scaleFactor: 0.75,
		  hideElements: [
            &apos;#address&apos;,&apos;#search-box&apos;,&apos;#top-menu&apos;,&apos;#controls-box&apos;,&apos;#prev&apos;,&apos;#next&apos;
        ]
	  });
    })
  );&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;generar_el_gif&quot;&gt;Generar el Gif&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De igual manera, generar el gif es tan sencillo como llamar a la libreria que lo hace:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;GifCreationService.createAnimatedGifFromPngImages(
    pngImages.map((obj) =&amp;gt; {
      return obj[1];
    }),
    outputGifFile,
    { repeat: true, fps: 1, quality: 10 }
  ).then((outputGifFile) =&amp;gt; {
    console.log(
      `Alright, GIF ${outputGifFile} created for ${pngImages[5][0]}!`
    );
  })&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez generado el gif muestro con una traza la URL usada para saber el lugar elegido.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;subir_a_mastodon&quot;&gt;Subir a Mastodon&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Subir la imagen con un mensaje a Mastodon es tan fácil como haber creado un token de aplicación
y apuntar a la instancia donde tienes la cuenta, en mi caso en &lt;a href=&quot;https://mastodon.madrid&quot; class=&quot;bare&quot;&gt;https://mastodon.madrid&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  var M = new Masto({
    access_token: process.env.MASTODON,
    timeout_ms: 60 * 1000,
    api_url: &quot;https://mastodon.madrid/api/v1/&quot;,
  });

  var id;
  M.post(&quot;media&quot;, { file: fs.createReadStream(outputGifFile) }).then(
    (resp) =&amp;gt; {
      id = resp.data.id;
      M.post(&quot;statuses&quot;, {
        status: &quot;Un sitio random cada día&quot;,
        media_ids: [id],
      });
    }
  );&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para no tener el token en el código simplemente se crea un fichero &lt;code&gt;.env&lt;/code&gt; y se añade como clave-valor&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este código, sube la imagen Gif y una vez subida crea un Toot con un texto y el id de la imagen subida&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejecución&quot;&gt;Ejecución&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez tenemos index.js simplemente invocamos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;node index.js&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y si todo va bien, tendremos un toot subido con un gif animado adjunto&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;variantes&quot;&gt;Variantes&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez he tenido el script completo he creado dos &quot;variantes&quot;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;norandom.js, es una copia de index.js pero en lugar de buscar un sitio random
utiliza los argumentos proporcionados en la linea de comandos como latitud y longitud&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;social.js, al final he separado en dos la lógica de crear el gif y la de subirlo a Mastodon, de tal
forma que primero genero la imagen y si me gusta llamo al social.js para que la suba&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;utilidad&quot;&gt;Utilidad&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ninguna, pero me he divertido subiendo algún sitio aleatorio y haciendo encuestas para intentar
adivinar de donde es, para una vez finalizada la encuesta subir el gif animado que muestre el lugar&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;link_al_código&quot;&gt;Link al código&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si quieres, puedes descargar el código completo de todos los scripts desde&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/-/snippets/2029648&quot; class=&quot;bare&quot;&gt;https://gitlab.com/-/snippets/2029648&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mi objetivo con este post NO es crear un tutorial de cómo usar Node, pues como se puede observar
ni tengo conocimientos ni el código tiene una calidad mínima.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Digamos que me gustaría sirviera para demostrar
que hay una gran cantidad de librerías que hacen las cosas más insospechadas, y que es bastante sencillo
crearnos utilidades con ellas.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Título clickbait para explicar un ejemplo sencillo de una aplicación en Node</summary>
    </entry>
    <entry>
        <title>Filtrando y respondiendo tweets "sin manos"</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/twitter-sin-manos.html"/>
        <updated>2020-10-13T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/twitter-sin-manos.html</id>
        <category term="twitter"/>
        <category term="bot"/>
        <category term="grovy"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para reproducir este post necesitarás:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;docker y docker-compose instalado en una máquina (puede ser una RaspberryPi)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Claves OAuth de aplicación de Twitter que tienes que obtener rellenando un formulario en &lt;a href=&quot;https://developer.twitter.com/en/apps&quot; class=&quot;bare&quot;&gt;https://developer.twitter.com/en/apps&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;objetivo&quot;&gt;Objetivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a lanzar un proceso desatendido que va a estar atento a todos los tweets que se vayan produciendo en la red social y que
contenga una o varias palabras claves de nuestro interés, por ejemplo &quot;PuraVida&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando se detecte un tweet con esta(s) palabras claves realizaremos un segundo filtrado para comprobar si la palabra está en un
&quot;contexto&quot; que nos interesa. Por ejemplo vamos a buscar que el texto del tweet contenga &quot;Costa Rica&quot; y cuando se encuentre
vamos a contestar con un tweet predefinido, por ejemplo &quot;PuraVida mae&quot;  (una expresión tica que
me enamoró cuando la escuché por primera vez)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;arquitectura&quot;&gt;Arquitectura&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para conseguir que la parte que lee eventos de Twitter no se demore con procesos de búsqueda de texto en el mensaje, envío de un tweet,
etc vamos a crear dos procesos independientes donde uno va a estar escuchando los eventos de Twitter y enviándolos a una cola de Rabbit
mientras que otro proceso va a leer de esta cola y enviará el tweet si procede&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-12a8c0611a9708a1c6fe4e6488778702.png&quot; alt=&quot;Diagram&quot; width=&quot;192&quot; height=&quot;300&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De esta forma aseguramos una lectura fluida de los eventos producidos en la red y los guardamos en una cola interna listos para ir siendo procesados&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(Esta misma arquitectura podemos ampliarla para escanear por más de un concepto y redirigir a diferentes colas según cada uno)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;oauth&quot;&gt;Oauth&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar tendremos que rellenar el formulario indicado al principio y obtener unas claves (4). Este proceso puede demorar
algunos días e incluso pueden rechazarte la petición si no lo rellenaste con la suficiente información.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que los tengas crearemos un fichero tal como este en el directorio que vayamos a usar para este proyecto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;twitterj4.properties&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;oauth.consumerKey=xxxxxxxxx
oauth.consumerSecret=xxxxxxxxxxxxxxxxxxxxxx
oauth.accessToken=yyyyyyy-zzzzzzzzzzzzz
oauth.accessTokenSecret=zzzzzzzzzzzzzzzzzzzzz&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;configuración&quot;&gt;Configuración&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La configuración va a consistir en un fichero &lt;code&gt;.env&lt;/code&gt; (ojo que
el nomre del fichero empieza en punto):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.env&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;SEARCH=puravida
FILTER=&apos;Costa Rica&apos;
ANSWER=&apos;PuraVida mae&apos;
IGNORE=jagedn&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es decir: vamos a buscar por la palabra &lt;code&gt;puravida&lt;/code&gt;, vamos a filtrar aquellos
que lleven la frase &lt;code&gt;Costa Rica&lt;/code&gt; y a los que cumplan las condiciones
les contestaremos con un RT &lt;code&gt;PuraVida mae&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para evitar &quot;una recursividad&quot; ignoraremos aquella cuenta cuyo autor sea
nuestra propia cuenta&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;docker&quot;&gt;Docker&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los tres componentes de la solución (rabbitmq, scan y reply) van a trabajar de forma conjunta mediante un &lt;code&gt;docker-compose&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;version: &apos;3&apos;

services:
  rabbitmq:
    image: &apos;rabbitmq:3-management&apos;
    ports:
      - &apos;5672:5672&apos;
      - &apos;15672:15672&apos;
    healthcheck:
      test: [ &quot;CMD&quot;, &quot;nc&quot;, &quot;-z&quot;, &quot;localhost&quot;, &quot;5672&quot; ]
      interval: 10s
      timeout: 10s
      retries: 5

  scan:
    image: groovy
    volumes:
     - .:/home/groovy
    environment:
     - SEARCH
    command: &quot;groovy scan.groovy rabbitmq $SEARCH&quot;
    depends_on:
      - rabbitmq

  reply:
    image: groovy
    volumes:
     - .:/home/groovy
    environment:
     - IGNORE
     - FILTER
     - ANSWER
    command: &quot;groovy react.groovy rabbitmq $IGNORE $FILTER $ANSWER&quot;
    depends_on:
      - rabbitmq&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como rabbit tarda unos segundos en arrancar la primera vez que se ejecuta, lo lanzaremos en primer lugar y le dejaremos unos 10 segundos hasta que este preparado para recibir mensajes. Existen técnicas más sofisticadas para ello, pero en este post no vamos a complicarnos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker-compose up -d rabbitmq&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si queremos ver los logs y comprobar que el servicio arranca sin problemas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker-compose logs -f&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando veamos que el servicio se encuentra levantado cortamos el proceso
con Ctrl+C&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;scan&quot;&gt;Scan&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que Rabbit está listo podríamos lanzar los otros dos componentes
simplemente ejecutando&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y ambos componentes empezarían a trabajar de forma coordinada.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo para explicar cada uno de ellos lo vamos a ejecutar por ahora
por separado:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker-compose up -d scan&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker-compose logs -f scan&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En consola deberemos ver cómo van apareciendo una serie de puntos según
se vayan produciendo en Twitter. Obviamente si tu palabra a buscar no es
muy popular a lo mejor tardas en verlo así que puedes empezar con algún
término de rabiosa actualidad del momento (por ejemplo Ayuso)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;@Grab(&apos;org.twitter4j:twitter4j-stream:4.0.7&apos;)
@Grab(group=&apos;com.rabbitmq&apos;, module=&apos;amqp-client&apos;, version=&apos;3.1.2&apos;)

import com.rabbitmq.client.*
import groovy.json.*
import twitter4j.*

exchangeName=&quot;groovy-script&quot;
queueName=&quot;grabbit&quot;
routingKey=&apos;#&apos;

factory = new ConnectionFactory()
	factory.username=&apos;guest&apos;
	factory.password=&apos;guest&apos;
	factory.virtualHost=&apos;/&apos;
	factory.host= args[0]
	factory.port=5672
conn = factory.newConnection() &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

channel = conn.createChannel()
channel.exchangeDeclare(exchangeName, &quot;direct&quot;, true)
channel.queueDeclare(queueName, true, false, false, null)
channel.queueBind(queueName, exchangeName, routingKey) &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;


tweetFilterQuery = new FilterQuery()
tweetFilterQuery.track(args[1].split(&apos;,&apos;))
tweetFilterQuery.language(&apos;es&apos;)  &lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;

stream = new TwitterStreamFactory().instance
  .addListener([
    onStatus:{ status-&amp;gt;
        String json = JsonOutput.toJson([
            statusId: status.id,
	        name: status.user.screenName,
            text: status.text
        ]) &lt;i class=&quot;conum&quot; data-value=&quot;4&quot;&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
        channel.basicPublish(exchangeName, routingKey, null, json.bytes)
        println &quot;.&quot;
    },
    onException:{ },
    onDeletionNotice:{},
    onTrackLimitationNotice:{},
    onScrubGeo:{},
    onStallWarning:{},
  ] as StatusListener)
  .filter(tweetFilterQuery)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Creamos una factoria de rabbit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Creamos y nos adjuntamos a una cola de rabbit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Creamos los criterios de búsqueda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;4&quot;&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Cada vez que recibimos un mensaje lo enviamos a la cola&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver casi todo el código es de infraestructura siendo lo más relevante cómo configuramos el &lt;code&gt;FilterQuery&lt;/code&gt; y cómo convertimos
un &lt;code&gt;status&lt;/code&gt; de twitter a un mensaje en la cola de rabbitmq&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente podemos indicar una lista de palabras (en nuestro caso si el
argumento SEARCH lo separamos con comas), por idioma, geoposición, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la cola simplemente guardaremos el &lt;code&gt;id&lt;/code&gt;, el usuario y el texto del tweet&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;react&quot;&gt;React&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tenemos el proceso escaneando Twitter y enviando mensajes a nuestro rabbit es hora de consumirlos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como rabbit va guardando los mensajes hasta que los consumamos podemos ejecutar
el proceso de react después del scan:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker-compose up -d react&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker-compose logs -f react&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;React, como scan, se conectará a la cola de rabbit e irá leyendo
json con el id, autor y texto del tweet. Simplemente buscará sin en dicho
texto aparece la cadena indica en FILTER y si es el caso comprobará
a su vez si la cuenta que originó el tweet es diferente a IGNORE.
Si es así enviará un &lt;code&gt;status&lt;/code&gt; como RT al original con el texto indicado&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;@Grab(&apos;org.twitter4j:twitter4j-stream:4.0.7&apos;)
@Grab(group=&apos;com.rabbitmq&apos;, module=&apos;amqp-client&apos;, version=&apos;3.1.2&apos;)

import com.rabbitmq.client.*
import groovy.json.*
import twitter4j.*

exchangeName=&quot;groovy-script&quot;
queueName=&quot;grabbit&quot;
routingKey=&apos;#&apos;

factory = new ConnectionFactory()
	factory.username=&apos;guest&apos;
	factory.password=&apos;guest&apos;
	factory.virtualHost=&apos;/&apos;
	factory.host= args[0]
	factory.port=5672
conn = factory.newConnection()

channel = conn.createChannel()
channel.exchangeDeclare(exchangeName, &quot;direct&quot;, true)
channel.queueDeclare(queueName, true, false, false, null)
channel.queueBind(queueName, exchangeName, routingKey)

twitter = TwitterFactory.singleton

jsonSlurper = new JsonSlurper()
autoAck = true;

ignore = args[1]
msg = args[2]
answer = args.drop(3).join(&apos; &apos;)

println msg
println answer

channel.basicConsume(queueName, autoAck, new DefaultConsumer(channel) {
	public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body) throws IOException{
		String line = new String(body)
		json = jsonSlurper.parseText(line)
        if( json.text.toLowerCase().indexOf(msg) != -1 ){
            println &quot;$json.name:\n$json.text\n-------&quot;
			if( answer &amp;amp;&amp;amp; json.name != ignore ){
				stat= new StatusUpdate(&quot;@$json.name $answer&quot;)
				stat.inReplyToStatusId = json.statusId as long
				twitter.updateStatus(stat)
			}
		}

	}
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El código es muy simpilar al de scan a la hora de conectarse a rabbitmq
y lo que hace es simplemente consumir eventos de la cola&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para cada mensaje que llegue se comprueba si contiene la cadena a filtar
y se envía un tweet con &lt;code&gt;inReplyToStatusId&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;bots&quot;&gt;Bots&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usando estos dos scripts puedes hacer bots que realicen acciones ante
ciertos tweets:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;hacer un retweet ante una palabra&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;guardar cuentas de usuario que mencionen una frase&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;etc&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo podrías &quot;tomar el pulso&quot; a Twitter filtrando por una serie de palabras y actualizando en tiempo real una aplicación web donde se
mostrara el número de veces que se están usando esas palabras&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Escanear Twitter y reaccionar a una frase determinada contestando con un tweet</summary>
    </entry>
    <entry>
        <title>Asciidoctor extension for JBake</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/asciidoctor-jbake.html"/>
        <updated>2020-10-06T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/asciidoctor-jbake.html</id>
        <category term="asciidoctor"/>
        <category term="jbake"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;English is not my mother language so if some part of this post is not clear
don&amp;#8217;t hesitate and let me know and I&amp;#8217;ll try to explain better&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JBake is an static site generator written in Java (there are a lot of similar static site generator
written in Python, Javascript, Ruby &amp;#8230;&amp;#8203;). As a Java/Groovy developer point of view it has a lot of
interesting features as:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run into the JVM machine, so you can integrate into your Java project&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;different template engines (freemarker, groovy, thymeleaf, jade&amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;markdown and asciidoctor support&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;open source&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;gradle plugins&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As an example, this full blog is written in Asciidoctor and rendered by JBake as a static site who
doesn&amp;#8217;t require database, php, etc so I can deploy it in a lot of free platform as
Github, Gitlab, Netlify, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;I use Gradle as build tool because it allows me to write code and customize the build
process. Not sure if this post can be or how to adapted to Maven for example.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;asciidoctor&quot;&gt;Asciidoctor&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I like write post in asciidoctor because I can maintain the focus in the content of the post an forget the
presentation.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For example, I can write in a post at some part of it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;img::image/path1/path2/image.png[]&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and Asciidoctor+Jbake will render the HTML similar to&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;&amp;lt;image src=&quot;image/path1/path2/image.png&quot;&amp;gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Img is part of asciidoctor&amp;#8217;s specification. Also you can use paragraphs, links, tables, etc but always
from a semantic point of view, I mean, you don&amp;#8217;t need to pay attention about &quot;visual&quot; details&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;asciidoctor_extension&quot;&gt;Asciidoctor extension&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;One of the best features of Asciidoctor is the capabilitie of write extension. You can write extension
in Ruby, Java or Javascript for example and &quot;attach&quot; these extensions to the Asciidoctor engine and use it
into your document.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ve written some simple extensions in Java as a Discuss block, a Google Analitycs preprocessor or a QRCode
generator. For example, with the QRCode extension you can write into your blog:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;barcode:qrcode[https://puravida-software.gitlab.io/asciidoctor-extensions/,300,300]&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and the extensión will generate the image and include the required code into the document to visualize them.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;You can find it at &lt;a href=&quot;https://puravida-asciidoctor.gitlab.io/asciidoctor-barcode/&quot; class=&quot;bare&quot;&gt;https://puravida-asciidoctor.gitlab.io/asciidoctor-barcode/&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;/images/barcode-afcfab40278c9cc9c29db6d0a0e3c702.png&quot; alt=&quot;https://puravida-software.gitlab.io/asciidoctor-extensions/&quot; width=&quot;300&quot; height=&quot;300&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;asciidoctor_extension_jbake&quot;&gt;Asciidoctor extension + Jbake&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you want to use some Asciidoctor extension, for example the previous barcode extension, you need to include it
into the classpath.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;&lt;strong&gt;Pay attention&lt;/strong&gt; you need to include them into the &lt;code&gt;buildScript&lt;/code&gt; dependencies section
because the &lt;code&gt;dependencies&lt;/code&gt; section in Gradle is dedicated to &lt;strong&gt;compile&lt;/strong&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As I use Gradle this is how my &lt;code&gt;build.gradle&lt;/code&gt; looks :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;buildscript {
    repositories {
        jcenter()
        maven{ url  &quot;http://dl.bintray.com/puravida-software/repo&quot; }
    }

    dependencies {
        classpath &apos;org.scilab.forge:jlatexmath:1.0.7&apos;
        classpath &apos;org.asciidoctor:asciidoctorj-diagram:2.0.5&apos;
        classpath &apos;org.asciidoctor:asciidoctorj:2.2.0&apos;
        classpath &apos;com.puravida.asciidoctor:asciidoctor-barcode:2.4.1&apos;
    }
}

plugins {
    id &apos;org.jbake.site&apos; version &apos;5.2.0&apos;
}

dependencies {

}

bakePreview {
    port = &apos;8090&apos;
}

bakePreview.dependsOn bake

jbake{
    version = &quot;2.6.5&quot;
    configuration[&apos;asciidoctor.option.requires&apos;] = &quot;asciidoctor-diagram&quot;
    configuration[&apos;asciidoctor.attributes&apos;] = [
            &quot;stem=&quot;, &quot;icons=font&quot;,
            &quot;imagesoutdir=&quot;+file(&apos;build/jbake/images&apos;).absolutePath,
            &quot;imagesdir=/images&quot;,
            &quot;source-highlighter=prettify&quot;,
    ].join(&apos;,&apos;)
}

build.dependsOn  bake&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;your_own_asciidoctor_extension&quot;&gt;Your own Asciidoctor extension&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;INFO&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Full code are available at &lt;a href=&quot;https://gitlab.com/jorge-aguilera/blog/-/tree/master/buildSrc/src/main/groovy/jorge/aguilera/soy&quot; class=&quot;bare&quot;&gt;https://gitlab.com/jorge-aguilera/blog/-/tree/master/buildSrc/src/main/groovy/jorge/aguilera/soy&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It&amp;#8217;s great when someone has been written and published an usefull extension and you can use it into your blog
without more effort that include it into the `build.gradle`file, but sometimes
you want to write your own extension without the complexity of create
a repo (maybe in Bintray), versioning, publishing, etc when all you want is to use the extension into the same blog&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For this situation I&amp;#8217;ll explain how I manage to write custom extensions only accesible in the blog and versioned together.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This extension will render a calendar for an specified month as an HTML table and looks as:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    .Agenda 2020/10
    [calendar,year=2020, month=10]
    ----
    1 Arrive
    2 Keynotes
    6 Visit to Madrid
    9 Close and party
    ----&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It&amp;#8217;s a block with an unique identifier (&lt;code&gt;calendar&lt;/code&gt;)  with two attributes (&lt;code&gt;year&lt;/code&gt; and &lt;code&gt;month&lt;/code&gt;) where every line
will start with a number followed by a String. The extension will parse this block and generate a table similar to&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 1. Agenda 2020/10&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2858%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;L&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;M&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;X&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;J&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;V&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;S&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;D&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;
&lt;p class=&quot;tableblock&quot;&gt; Arrive&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2&lt;/p&gt;
&lt;p class=&quot;tableblock&quot;&gt; Keynotes&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;3&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;4&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;5&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;6&lt;/p&gt;
&lt;p class=&quot;tableblock&quot;&gt; Visit to Madrid&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;7&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;8&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;9&lt;/p&gt;
&lt;p class=&quot;tableblock&quot;&gt; Close and party&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;10&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;11&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;12&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;13&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;14&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;15&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;16&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;17&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;18&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;19&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;20&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;21&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;22&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;23&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;24&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;25&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;26&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;27&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;28&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;29&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;30&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;31&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;buildsrc&quot;&gt;buildSrc&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Our extension will reside into the &lt;code&gt;buildSrc/src/main/groovy/jorge/aguilera/soy&lt;/code&gt; folder and we&amp;#8217;ll follow the steps
indicate into the guide &lt;a href=&quot;https://github.com/asciidoctor/asciidoctorj&quot; class=&quot;bare&quot;&gt;https://github.com/asciidoctor/asciidoctorj&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Briefly these steps are:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Create a CalendarBlock&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;CalendarBlock.groovy&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;package blog.jagedn.dev

// a lot of imports

@Name(&quot;calendar&quot;)
@Contexts([Contexts.LISTING])
public class CalendarBlock extends BlockProcessor {

    @Override
    public Object process(StructuralNode parent, Reader reader, Map&amp;lt;String, Object&amp;gt; attributes) {
        // main logic to parse and render
    }
]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically the extension will read the lines and parse all of them following the specified structure.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It will instantiate a Calendar object with the year and month set in the attributes (required) and
&lt;code&gt;navigate&lt;/code&gt; accross the month adding strings to an array in order to have an &quot;asciidoctor table&quot; as if
the user written it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;At the end the extension will call to the asciidoctor engine to render an array os strings similar to&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    |===
    | | | 30 | 1 | 2
    | 3 a line | 4 another line
    |===&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;using the method &lt;code&gt;parseContent&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    Block block = createBlock(parent, &quot;open&quot;, (String) null);
    parseContent(block, content);
    return block;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;register the extension using the &lt;code&gt;JorgeExtensionRegistry.groovy&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;package blog.jagedn.dev
// some imports
class JorgeExtensionRegistry implements ExtensionRegistry {
    void register(Asciidoctor asciidoctor) {
        asciidoctor.javaExtensionRegistry().with{
            block &apos;calendar&apos;, CalendarBlock
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;dependencies&quot;&gt;Dependencies&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We will include the dependencies required by our extension into the &lt;code&gt;buildSrc/build.gradle&lt;/code&gt; file. Pay
attention is not the &lt;code&gt;build.gradle&lt;/code&gt; file in the root of the project.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;repositories {
    jcenter()
}

dependencies {
    compile &apos;org.asciidoctor:asciidoctorj:2.2.0&apos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;build&quot;&gt;Build&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now when Gradle will run the &lt;code&gt;build&lt;/code&gt; task it will compile and use the buildSrc as classpath dependency so when
JBake instantiate the Asciidoctor engine will have all the artifacts required to understand your new
extension&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simple execute&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew build bakePreview&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and preview your blog at localhost:8090&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;sparql_extension&quot;&gt;SPARQL extension&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;m playing with a new idea for a SPARQL asciidoctor-extension how execute a sentence and dump the results as
a table (similar to the native capability of asciidoctor to render a CSV file for example)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The idea is to have a new block where you write the sentence and specify wich fields you want to dump.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can see it in action at &lt;a href=&quot;sparql.html&quot; class=&quot;bare&quot;&gt;sparql.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;others_extensions&quot;&gt;Others extensions&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this way you can have more extensions as:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;write some content in yellow to remark some ideas&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;encript some parts of your post and reveal it with a key&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;create a quizz with questions and answers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>How to create a custom extension for your JBake blog</summary>
    </entry>
    <entry>
        <title>Sparql</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/sparql.html"/>
        <updated>2020-10-05T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/sparql.html</id>
        <category term="asciidoctor"/>
        <category term="jbake"/>
        <category term="sparql"/>
        <category term="opendata"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Playing with a new #asciidoctor extension for the blog querying Wikidata (or Dbpedia) using SPARQL&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;wikidata&quot;&gt;Wikidata&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;.List of Pokemons
[sparql, fields=&quot;pokemon,pokemonLabel,pokedexNumber&quot;]
----
SELECT DISTINCT ?pokemon ?pokemonLabel ?pokedexNumber
WHERE
{
    ?pokemon wdt:P31/wdt:P279* wd:Q3966183 .
    ?pokemon p:P1685 ?statement.
    ?statement ps:P1685 ?pokedexNumber;
            pq:P972 wd:Q20005020.
    FILTER (! wikibase:isSomeValue(?pokedexNumber) )
    SERVICE wikibase:label { bd:serviceParam wikibase:language &quot;[AUTO_LANGUAGE],en&quot; }
}
ORDER BY (?pokedexNumber)
LIMIT 30
----&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;.Cats
[sparql, provider=&quot;wikidata&quot;, fields=&quot;item,itemLabel,pic&quot;]
----
SELECT ?item ?itemLabel ?pic
WHERE
{
?item wdt:P31 wd:Q146 .
?item wdt:P18 ?pic
SERVICE wikibase:label { bd:serviceParam wikibase:language &quot;[AUTO_LANGUAGE],en&quot; }
}
LIMIT 10
----&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;dbpedia&quot;&gt;Dbpedia&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;.People who were born in Berlin before 1900
[sparql, provider=&quot;dbpedia&quot;, fields=&quot;name,birth,death,person&quot;]
----
PREFIX dbo: &amp;lt;http://dbpedia.org/ontology/&amp;gt;
PREFIX xsd: &amp;lt;http://www.w3.org/2001/XMLSchema#&amp;gt;
PREFIX foaf: &amp;lt;http://xmlns.com/foaf/0.1/&amp;gt;
PREFIX : &amp;lt;http://dbpedia.org/resource/&amp;gt;
SELECT ?name ?birth ?death ?person WHERE {
    ?person dbo:birthPlace :Berlin .
    ?person dbo:birthDate ?birth .
    ?person foaf:name ?name .
    ?person dbo:deathDate ?death .
    FILTER (?birth &amp;lt; &quot;1900-01-01&quot;^^xsd:date) .
} ORDER BY ?name
LIMIT 30
----&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;.Jugadores de Futbol nacidos en Madrid
[sparql, provider=&quot;dbpedia&quot;, fields=&apos;athlete,number&apos;]
----
SELECT *
WHERE
{
?athlete a dbo:SoccerPlayer;
dbo:birthPlace [rdfs:label &quot;Madrid&quot;@en];
dbo:number ?number.
}
LIMIT 30
----&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;custom_endpoint&quot;&gt;Custom endpoint&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can specify the endpoint using the &lt;code&gt;provider&lt;/code&gt; attribute:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;[sparql, provider=&quot;https://es.dbpedia.org/sparql&quot;, fields=&quot;torero,cantante&quot;]
----
PREFIX dbpedia-owl: &amp;lt;http://dbpedia.org/ontology/&amp;gt;
PREFIX dcterms: &amp;lt;http://purl.org/dc/terms/&amp;gt;
SELECT ?torero ?cantante WHERE{
?torero rdf:type dbpedia-owl:BullFighter .
?torero dbpedia-owl:spouse ?cantante .
?cantante dcterms:subject &amp;lt;http://es.dbpedia.org/resource/Categoría:Cantantes_de_coplas&amp;gt;
}
----&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 1. Toreros y Tonadilleras&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;torero&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;cantante&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;http://es.dbpedia.org/resource/José_Ortega_Cano&quot; class=&quot;bare&quot;&gt;http://es.dbpedia.org/resource/José_Ortega_Cano&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;http://es.dbpedia.org/resource/Rocío_Jurado&quot; class=&quot;bare&quot;&gt;http://es.dbpedia.org/resource/Rocío_Jurado&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;http://es.dbpedia.org/resource/José_Ortega_Cano&quot; class=&quot;bare&quot;&gt;http://es.dbpedia.org/resource/José_Ortega_Cano&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;http://es.dbpedia.org/resource/Rocío_Jurado&quot; class=&quot;bare&quot;&gt;http://es.dbpedia.org/resource/Rocío_Jurado&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;http://es.dbpedia.org/resource/Curro_Romero&quot; class=&quot;bare&quot;&gt;http://es.dbpedia.org/resource/Curro_Romero&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;http://es.dbpedia.org/resource/Concha_Márquez_Piquer&quot; class=&quot;bare&quot;&gt;http://es.dbpedia.org/resource/Concha_Márquez_Piquer&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;http://es.dbpedia.org/resource/Curro_Romero&quot; class=&quot;bare&quot;&gt;http://es.dbpedia.org/resource/Curro_Romero&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;http://es.dbpedia.org/resource/Concha_Márquez_Piquer&quot; class=&quot;bare&quot;&gt;http://es.dbpedia.org/resource/Concha_Márquez_Piquer&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Jugando con sparql</summary>
    </entry>
    <entry>
        <title>Mi resumen de @esLibre_ 2020</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/eslibre.html"/>
        <updated>2020-09-22T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/eslibre.html</id>
        <category term="personal"/>
        <category term="eventos"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-tip&quot; title=&quot;Tip&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&quot;esLibre es un encuentro de personas interesadas en las tecnologías libres, enfocado a compartir conocimiento y experiencia alrededor de las mismas (&lt;a href=&quot;https://eslib.re/)&quot;&quot; class=&quot;bare&quot;&gt;https://eslib.re/)&quot;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por segundo año consecutivo he asistido al congreso esLibre y por segundo año he podido dar una charla.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El año pasado, en Granada, pude contar mis movidas con #OpenSource #OpenData (bots consumiendo datasets y cosas así) así como la de #DocAsCode, la cual he repetido (con variaciones) este año.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este año iba a ser en Madrid (bueno, en realidad Fuenlabrada que parece ser que es de la Comunidad de Madrid aunque yo me saqué el pasaporte por si acaso), pero por culpa del bichito la organización le dió un giro de 180º y se arriesgó a hacer un evento totalmente online y en mi opinión y a la vista del resultado fue un acierto completo.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;docascode&quot;&gt;DocAsCode&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque parezca egocéntrico voy a comentar primero sobre mi charla pero con la intención de quitarmelo cuanto antes y poder pasar al evento en sí
que lo considero más interesante.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando ví que se organizaba una nueva edición me lancé a enviar mi propuesta sobre #DocAsCode (bueno, miento, estuve dudando los primeros 5 minutos) pues la había estado dando vueltas y quería hacerla en plan &quot;teatrillo&quot;. Quería hacer una versión online donde crearía desde cero un
proyecto y publicaría la documentación en directo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Todo sonaba perfecto en mi cabeza hasta que comprobé al ser aceptada que el tiempo que se disponía era de 30 minutos. Y el pánico se apoderó de mí cuando dicho tiempo fue reducido a 20 tras organizar todas las propuestas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Acostumbrado a charlas de 40-45 minutos reducirla a la mitad se me hacía una locura. Iba a ser materialmente posible contar nada en 20 minutos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pues bien, tras darme cuenta que en realidad en una charla se trata de contar lo importante no de contar todo lo que quieres hasta aburrir me dispuse a hacer
recortes y optimizar mi teatro:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sabía por experiencia que publicar en Netlify es cuestión de segundos mientras
que en Gitlab puede llevar varios minutos así que investigué cómo enlazarlos y publicar automáticamente (algo más para la mochila y sobre
lo que escribiré en otro post)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Se me ocurrió que la mejor manera de crear el proyecto en Netlify era
inventarme unas slides que desplegar manualmente y evitar asi el proceso
&quot;administrativo&quot; de configurarlos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mientras Gitlab ejecutaba el pipeline yo podía estar creando otra rama
y trabajar sobre ella. Corría el peligro de hacerlo confunso así que
intenté repetir varias veces lo que se estaba viendo en pantalla, espero
haberlo conseguido.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tuve que eliminar, por ejemplo, la parte de código pero a cambio podía
detenerme unos segundos en explicar un poco mejor la idea en cada paso, o,
una vez más, es lo que espero haber conseguido.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente tuve que repetir muchas veces la charla pues no iba a tener
slides sobre las que apoyarme y el tiempo tan escaso obligaba a tenerlo todo encadenado.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al final creo que la charla salió bien e incluso hubo preguntas muy
interesantes después y me pasaron algún enlace relacionado para investigar.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;organización&quot;&gt;Organización&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quede claro que yo no he participado en la organización y sólo puedo
hablar de lo que he podido ver/oir.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No puedo imaginarme todo el esfuerzo
de coordinación, discusiones, trabajo y horas que la gente le tiene que dedicar
a un evento de este tipo y más en estas condiciones.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi opinión la organización ha sido im pe ca ble. Transmitiendo la
información de una forma clara y transparente en todo momento, con unos medios
que han funcionado de lujo &amp;#8230;&amp;#8203; y (casi)todo Software Libre.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De todos los aspectos de la organización yo me quedo con el proceso de
aceptar las charlas mediante un Merge Request (como el Pull Request de
Github pero en Gitlab que es Open Source, como debe ser):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cualquiera podía clonar el repo, crear una propuesta y solicitar un MR
para su aprobación. Así mismo los comentarios tanto con propuestas de mejoras, o solicitando aclaraciones, o las aprobaciones al MR se realizan de forma
transparente usando Gitlab, de tal forma que todo el mundo puede
ver el proceso de selección de charlas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para mí esto es un hecho diferenciador de muchas otras conferencias de renombre (y que no por ello dejo de admirar como GreachConf, CommitConf
o Codemotion). Es cierto que en algunas de estas se solicita la &quot;ayuda&quot; de ciertas
comunidades para votar pero el concepto de transparencia del esLibre lo
peta.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otra parte, cuando salga el video os animo a que veais el mensaje final
de Germán, @germaaan_, donde (machaconamente) remarca que es un evento de &quot;la gente para la gente&quot;. Pero es que es así. No hay marcas comerciales, no
hay empresas, no hay captación de CVs, sino gente queriendo compartir
libremente cosas con la gente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y esto me lleva a otro tema que creo que este evento puede ofrecer:
NO pretende restringuirse en el área del Software Libre típico sino que
se busca abarcar otras áreas de la cultura donde compartir de forma abierta (música, literatura, cualquier expresión de arte&amp;#8230;&amp;#8203;) o eso es lo
que yo he entendido, claro.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;medios_y_dinámica&quot;&gt;Medios y dinámica&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ni idea de los medios que han hecho posible el evento. Sé que los van a
publicar pero yo sólo me he quedado con que han puesto medios para asegurar
que no hubiera problemas. Al ser el primer evento de este tipo supongo que
se ha tirado la casa por la ventana pero en la clausura se habían sacado
conclusiones y estimaciones para eventos futuros.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La dinámica ha sido mucho más fluida de lo que yo creía en un primer momento. Se habilitaron una serie de tracks, cada uno con una sala en
BigBlueButton y con un canal de RocketChat asociado para poder hacer preguntas. Porqué ? pues su razones tendrían. Yo creo que por ejemplo
el sistema de preguntas de BBB es un poco peor y además al ir encadenadas
las charlas se permitía comenzar una nueva mientras todavía se estaba
chateando en RocketChat.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El formato de las charlas a 20 minutos, como ya he comentado, me ha parecido
un acierto. Al ponente le parece un stress de primeras pero una vez que centras tu charla creo que es mucho mejor. Simplificas el mensaje, vas a
lo importante, al no ver las caras del público te obligas a seguir tu
hilo, &amp;#8230;&amp;#8203; me ha gustado en resumen.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como oyente se favorece el que puedas estar
entrando y saliendo de las charlas sin molestar.
Tengo que admitir que en más de una charla estuve compaginandola con otra.
Obvio no atiendes a las 2 de forma completa pero en conjunto sí pude aprender
lecciones de ellas a pesar de simultanearlas.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;charlas&quot;&gt;Charlas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como no soy muy bueno haciendo resúmenes tendremos que esperar a que @luiyo haga el suyo de las charlas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Yo por mi parte flipé con el proyecto SoftwareHeritage (y que hayan
hecho una copia de TelotraigodemiPueblo, mi TFC),
trasteé con GodotEngine
y ya le estoy dando vueltas a ver qué juego simple puedo hacer,
o mis primeros pasitos con Raku (mucho más interesante que Python)
entre otras muchas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mención especial claro está a la de @MiguelRuGa sobre Groovy, que ha sido
su primera charla y casi tras ponerle una pistola en la cabeza&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;futura_edición&quot;&gt;Futura Edición&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ni idea de si tal como está el patio habrá próxima edición, ni donde
estaremos, obviamente, pero el mensaje que yo saqué es que hay muchas ganas
de repetir y si puede ser con más temas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si sirve de algo mi modesta opinión, yo creo que es un evento que no me perdería
y si te interesa el tema de &quot;Libre&quot; en cualquiera de sus vertientes
estoy seguro que van a encontrar una forma de que puedas compartir
lo que quieras contar.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Impresiones tras asistir al esLibre 2020</summary>
    </entry>
    <entry>
        <title>Pruebas de selección</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/pruebas-seleccion.html"/>
        <updated>2020-09-18T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/pruebas-seleccion.html</id>
        <category term="personal"/>
        <category term="rrhh"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-tip&quot; title=&quot;Tip&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Este post surge a raiz de artículo escrito por @Ari_Reinventada en Medium
(&lt;a href=&quot;https://medium.com/@AriJDB/procesos-de-selecci%C3%B3n-agobio-estr%C3%A9s-y-aprendizaje-2c761bf36207&quot; class=&quot;bare&quot;&gt;https://medium.com/@AriJDB/procesos-de-selecci%C3%B3n-agobio-estr%C3%A9s-y-aprendizaje-2c761bf36207&lt;/a&gt;)
donde comenta su paso por diferentes procesos de selección. Como &quot;contrapartida&quot; en este post voy a hablar
de mi experiencia estando al &quot;otro lado&quot;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;aquellos_maravillosos_años&quot;&gt;Aquellos maravillosos años&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Durante todos estos años de aplastar teclas he tenido que pasar por muchas
y variadas formas de entrevistas para optar a algún puesto de trabajo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde aquellas pruebas psicotécnicas de los 90 donde tenías que completar
secuencias de fichas de domino (me apuesto el cuello que todavía se hacen en algún sitio) hasta la de tener que instalar
un software que medía tus
tecleos, si consultabas páginas de Internet y cosas así para resolver un
problema algorítmico en 2 horas (que obviamente no pasé)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De muchas de ellas, tras el rechazo a mi candidatura, pude ver en qué había fallado (aunque eso no implique que
aprendiera de ello y lo corrigiera) pero una en especial sí me llegó al alma.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;mala_experiencia&quot;&gt;Mala experiencia&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras ponerse en contacto conmigo alguien de RRHH de una empresa tecnológica y pasar los preliminares me enviaron la
dichosa prueba de código. No era complicada, efectivamente, algo así como un servicio REST
al que se le actualizaban precios por un lado y los mostraba por otro, y me puse manos a la obra.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Hay mucha &quot;literatura&quot; al respecto de hacer pruebas de código. Gente que no quiere dedicar su tiempo libre
en ella, o que piden que se le remunere, etc. Para mí son posturas totalmente válidas pero en mi caso no me importa
pues las veo como retos a resolver,
si bien es verdad que nunca me han planteado una prueba que parezca que es para resolverles un marrón.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Creé el servicio (en Gradle, Groovy y Micronaut por supuesto) lo más sencillo posible y para completarlo le añadí lo
que me pareció lo mínimo que se debía crear para un proyecto:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;unos test mínimos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;docker-compose para levantar la base de datos y el servicio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;repositorio en Gitlab&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pipeline en Gitlab para construir el proyecto&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;documentación (obvio) con Asciidoctor incluida en el build del proyecto&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;README explicando cómo construir el proyecto y donde encontrar la documentación&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos, una oportunidad donde poder demostrar que tocas varios palos y poder discutir sobre ello.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A las 2 horas de haberles enviado la url al repo recibí la negativa (&quot;muchas gracias pero no has usado el patrón
sampitopato&quot;) y realmente estuve cagándome en sus muelas durante unos cuantos días.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Me parecía increíble que alguien les enviara una propuesta tan completa, como a mi me parecía, al día siguiente
de proponerla y la despacharan con ese argumento. Pero lo más alucinante es que &lt;strong&gt;ni tan siquiera ofrecieran la oportunidad de discutir mi propuesta&lt;/strong&gt;. Según me había comentado la persona que se puso en contacto
conmigo tenían la intención de contratar a &lt;strong&gt;200 técnicos en un año!!!&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo repito: no es que me sintiera ofendido porque no aceptaran mi propuesta. Es que pienso en el coste en el que estaban incurriendo para captar talento y lo desperdiciaban simplemente porque no usabas un patrón !!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
La parte buena de esta experiencia fue que me piqué con el patrón sanpitopato y a partir de ahí me líe a hacer
una serie de bots para Telegram &amp;#8230;&amp;#8203; donde seguí sin aplicar el patrón sanpitopato.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;tymit&quot;&gt;Tymit&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tiempo después, y gracias a contactos que te avisan de oportunidades que surgen, me &quot;enfrenté&quot; a la de Tymit la cual me
gustó bastante y sobre la que, una vez que formé parte del equipo, hemos ido trabajando para mejorarla, al menos en
nuestro entender.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente el proceso completo, que a priori puede parecer largo, se divide en una serie de entrevistas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;una charla corta con RRHH para conocerte y explicarte la vacante y el proceso&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una charla corta, 1/2 hora aprox, con dos miembros del equipo con los que no necesariamente vas a trabajar codo con codo.
Sirve principalmente para resolver dudas de cómo es la compañía, explicarte el producto y forma de trabajar por encima,
y que te pueda servir para ver si estarías cómodo trabajando en la empresa (y para qué mentir, sirve para ver si encajarías
en la misma)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una charla técnica de 1h aprox con otros dos técnicos diferentes (si se puede). Se trata de que el candidato conozca
tanta gente como sea posible del equipo así como a la inversa.
Es en esta charla donde me voy a centrar pues obviamente es la principal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una charla &quot;corta&quot; para conocer al CTO. Como buen CTO si le das pie puede estar horas hablando contigo.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;prueba_técnica_backend&quot;&gt;Prueba técnica (backend)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi opinión, en Tymit no vas a encontrar a grandes gurús tecnológicos capaces de hacer el algoritmo cuántico del momento,
(obviamente si así fuera yo no habría pasado la prueba) por lo que la prueba no consiste en resolver ningún algoritmo,
aplicar un patrón, y por supuesto nada de trabajar sobre una hoja en blanco (Me da el flato de la risa de pensar en
la pobre @Ari_Reinventada haciendo HTML en una hoja).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La prueba la hemos dividido en una serie de preguntas &quot;incrementales&quot;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;elegir entre dos trozos de código Java (no más de 10 líneas cada uno) y comentarnos qué hace, que problemas le ves, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dado un problema mundano con unas especificaciones supersimples, cómo plantearías los tests. Ni spock, ni junit, ni
cucumber, &amp;#8230;&amp;#8203; lo que tú harías, si bajas a pseudocódigo bien, si no tendrás que explicarte de viva voz.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un día a día con Git&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;algo de docker super fácil&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;elegir una pregunta entre dos sobre un API Rest y charlamos sobre ello.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;idem de microservicios&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una pregunta bastante rebuscada sobre arquitectura.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y sí, solamente tenemos este documento para cualquier nivel al que se opte por dos razones:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;no necesitas saber de todo, si de algo no sabes no se busca que te lo inventes sino que lo digas sin tapujos. Lo bueno
es que muchas veces la gente te sorprende y alguien que opta a un nivel medio se ha peleado con algún tema más raro y
así tiene la oportunidad de hablar de ello.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;somos tan vagos que para qué vamos a hacer dos versiones.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo que se busca en este prueba no es tanto que demuestres tus conocimientos resolviendo el problema del siglo sino que
discutamos y puedas expresar tus puntos de vista, forma de hacer las cosas, en definitva &lt;strong&gt;conocernos&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Se procura en cada apartado no sólo escuchar al candidato sino poder aportarle algo. Por ejemplo puede que no hayas
tenido que tocar nada de Docker y tengas interés en saber qué se buscaba en esa pregunta.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al final de la entrevista (que alguna vez se ha alargado mucho más de la hora) se procura reservar unos minutos para
resolver las dudas que se puedan tener principalmente sobre la forma de trabajar, organizarnos, etc e inmediatamente le
trasladamos nuestras impresiones a RRHH para que se pongan en contacto con la persona que opta cuanto antes y le
traslade si continúa o no.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;em&gt;Hemos evaluado en este punto si deberíamos enviarle nuestro feedback pero al menos a día
de hoy creemos que la entrevista es tan abierta y se comentan todos los puntos que creemos que es suficiente&lt;/em&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusiones&quot;&gt;Conclusiones&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El principal problema que le veo a esta &quot;metodología&quot; es que no &quot;escala&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quiero decir con ello que es un proceso que requiere su tiempo así como esfuerzo por parte del equipo
técnico que tiene que aparcar sus tareas durante un buen rato (lo bueno es que la hemos hecho ya algunas veces,
rotandonos entre nosotros, y ya tenemos claro lo que buscamos y cómo plantearla)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por parte de la persona que opta también es un esfuerzo obviamente pero a cambio obtiene el conocer, de forma gradual,
a mucha parte del equipo con el que vas a trabajar. Creo que en estos tiempos de teletrabajo es una buena manera porque
ya no se puede hacer una reunión física con el equipo (reunión que por otra parte no servía de mucho porque todo el
mundo intenta ser políticamente correcto y poco más)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por el contrario la principal ventaja es que se han propiciado al menos 2-3 entrevistas para conocernos y una de ellas con
temas tan diversos que se ha favorecido tanto el poder detectar si la persona se adaptaría al equipo como que la persona
piense si podrá trabajar con el equipo. O esa es la idea&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>(Mi) experiencia en la selección de personal técnico</summary>
    </entry>
    <entry>
        <title>Static Site Estaciones de Servicio</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/estaciones-servicio-antora.html"/>
        <updated>2020-08-22T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/estaciones-servicio-antora.html</id>
        <category term="netlify"/>
        <category term="antora"/>
        <category term="asciidoctor"/>
        <category term="gitlab"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El Ministerio de Industria y Consumo publica de forma diaria un dataset con los precios de todas las gasolineras
de España. En este post voy a explicar cómo, de forma autónoma, se ejecuta un script que lo descarga y crea un site
para poder navegar entre las más de 14.000 estaciones y consultar los precios del día.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El resultado final lo puedes ver aquí: &lt;a href=&quot;https://estaciones-de-servicio.netlify.app/main/final/index.html&quot; class=&quot;bare&quot;&gt;https://estaciones-de-servicio.netlify.app/main/final/index.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Tal vez te pueda interesar el post &lt;a href=&quot;estaciones-servicio-bot.html&quot; class=&quot;bare&quot;&gt;estaciones-servicio-bot.html&lt;/a&gt; donde detallo cómo hacer un bot de Telegram
para consumir dichos datos.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;open_data&quot;&gt;Open Data&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El site se basa en un conjunto de datos abiertos del Ministerio de Industria y Consumo donde se muestran los precios
de las estaciones de servicio de España: &lt;a href=&quot;https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/&quot; class=&quot;bare&quot;&gt;https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este xml (o json) se listan todas las estaciones con su nombre, dirección, marca, geoposición así como los
diferentes precios para gasolina 95, 98, los diferentes tipos de diesel, bios, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estos precios se actualizan al menos una vez al día (realmente no recuerdo donde leí cuando se realiza, ni la periodicidad)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;lt;PreciosEESSTerrestres xmlns=&quot;http://schemas.datacontract.org/2004/07/ServiciosCarburantes&quot; xmlns:i=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&amp;gt;
&amp;lt;Fecha&amp;gt;06/08/2020 21:31:14&amp;lt;/Fecha&amp;gt;
&amp;lt;ListaEESSPrecio&amp;gt;
    &amp;lt;EESSPrecio&amp;gt;
        &amp;lt;C.P.&amp;gt;02250&amp;lt;/C.P.&amp;gt;
        &amp;lt;Dirección&amp;gt;AVENIDA CASTILLA LA MANCHA, 26&amp;lt;/Dirección&amp;gt;
        &amp;lt;Horario&amp;gt;L-D: 07:00-22:00&amp;lt;/Horario&amp;gt;
        &amp;lt;Latitud&amp;gt;39,211417&amp;lt;/Latitud&amp;gt;
        &amp;lt;Localidad&amp;gt;ABENGIBRE&amp;lt;/Localidad&amp;gt;
        &amp;lt;Longitud_x0020__x0028_WGS84_x0029_&amp;gt;-1,539167&amp;lt;/Longitud_x0020__x0028_WGS84_x0029_&amp;gt;
        &amp;lt;Margen&amp;gt;D&amp;lt;/Margen&amp;gt;
        &amp;lt;Municipio&amp;gt;Abengibre&amp;lt;/Municipio&amp;gt;
        &amp;lt;Precio_x0020_Biodiesel/&amp;gt;
        &amp;lt;Precio_x0020_Bioetanol/&amp;gt;
        &amp;lt;Precio_x0020_Gas_x0020_Natural_x0020_Comprimido/&amp;gt;
        &amp;lt;Precio_x0020_Gas_x0020_Natural_x0020_Licuado/&amp;gt;
        &amp;lt;Precio_x0020_Gases_x0020_licuados_x0020_del_x0020_petróleo/&amp;gt;
        &amp;lt;Precio_x0020_Gasoleo_x0020_A&amp;gt;1,039&amp;lt;/Precio_x0020_Gasoleo_x0020_A&amp;gt;
        &amp;lt;Precio_x0020_Gasoleo_x0020_B&amp;gt;0,569&amp;lt;/Precio_x0020_Gasoleo_x0020_B&amp;gt;
        &amp;lt;Precio_x0020_Gasoleo_x0020_Premium/&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_95_x0020_E10/&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_95_x0020_E5&amp;gt;1,149&amp;lt;/Precio_x0020_Gasolina_x0020_95_x0020_E5&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_95_x0020_E5_x0020_Premium i:nil=&quot;true&quot;/&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_98_x0020_E10/&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_98_x0020_E5/&amp;gt;
        &amp;lt;Precio_x0020_Hidrogeno/&amp;gt;
        &amp;lt;Provincia&amp;gt;ALBACETE&amp;lt;/Provincia&amp;gt;
        &amp;lt;Remisión&amp;gt;dm&amp;lt;/Remisión&amp;gt;
        &amp;lt;Rótulo&amp;gt;Nº 10.935&amp;lt;/Rótulo&amp;gt;
        &amp;lt;Tipo_x0020_Venta&amp;gt;P&amp;lt;/Tipo_x0020_Venta&amp;gt;
        &amp;lt;_x0025__x0020_BioEtanol&amp;gt;0,0&amp;lt;/_x0025__x0020_BioEtanol&amp;gt;
        &amp;lt;_x0025__x0020_Éster_x0020_metílico&amp;gt;0,0&amp;lt;/_x0025__x0020_Éster_x0020_metílico&amp;gt;
        &amp;lt;IDEESS&amp;gt;4375&amp;lt;/IDEESS&amp;gt;
        &amp;lt;IDMunicipio&amp;gt;52&amp;lt;/IDMunicipio&amp;gt;
        &amp;lt;IDProvincia&amp;gt;02&amp;lt;/IDProvincia&amp;gt;
        &amp;lt;IDCCAA&amp;gt;07&amp;lt;/IDCCAA&amp;gt;
    &amp;lt;/EESSPrecio&amp;gt;

    &amp;lt;!-- más estaciones--&amp;gt;
&amp;lt;/ListaEESSPrecio&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A diferencia del bot en este caso lo que nos interesa es organizar todas las estaciones por provincias y municipios
y para cada una de ellas mostrar la dirección, un enlace a su ubicación y los diferentes precios que tiene (no todas
tienen todos los tipos de carburante).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El objetivo va a ser generar un site de &quot;puro HTML&quot; de tal forma que pueda ser publicado en alguno de los numerosos
servicios que ofrecen de forma gratuita su publicación, en nuestro caso Gitlab/Netlify (Github es otra opción pero no
lo uso)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;tools&quot;&gt;Tools&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para tener listo el site vamos a utilizar las siguientes herramientas. De todas ellas la que puede varíar en tu caso
es la que te ayude a generar los ficheros partiendo de los datos, en mi caso un script de groovy.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;groovy_script&quot;&gt;Groovy Script&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gracias a que con Groovy es muy fácil descargar e interpretar un XML (o un JSON) así como trabajar con ficheros de texto,
lo usaremos para particionar y generar los ficheros&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;antoraasciidoctor&quot;&gt;Antora/Asciidoctor&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El script va a generar ficheros &lt;code&gt;adoc&lt;/code&gt; (asciidoctor) que no es más que ficheros planos con un lenguaje de marcado que
será interpretado por Antora para generar la versión HTML correspondiente&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Baśicamente serán ficheros muy sencillos con un título para el nombre de la provincia, subtitulo para los municipios,
apartados para las gasolineras, enlaces, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta es la pieza fundamental para que funcione el sistema y la que nos va a marcar qué estructura de ficheros tenemos
que generar así como una serie de configuración&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;gitlab&quot;&gt;Gitlab&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proyecto va a estar alojado en mi cuenta gratuita de Gitlab como repositorio git. Una de las características de Gitlab
es que te permite ejecutar código en sus sistemas incluso de una forma recurrente mediante jobs programados lo cual nos
va a servir para tener actualizado el site todos los días.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;netlify&quot;&gt;Netlify&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gitlab nos permite publicar el contenido estático generado en el paso anterior pero para este post vamos a integrar
el repositorio con otra herramienta con capa gratuita que permite de igual forma publicar contenido estático más alguna
otra funcionalidad, que es Netlify (este paso te lo puedes saltar y utilizar Gitlab. La única diferencia es que Netlify
a mi parecer tiene mejor velocidad de descarga)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación&quot;&gt;Preparación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar es importante entender la estructura de directorios que requiere Antora así como alguno de los ficheros
necesarios.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Antora espera una estructura similar a la que se muestra en la imagen:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-b8bfe90db36d2cdd516cd6d9f75da107.png&quot; alt=&quot;Diagram&quot; width=&quot;157&quot; height=&quot;328&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar crearemos esta estructura de directorios y los ficheros excepto los que van en el directorio &lt;code&gt;pages&lt;/code&gt;
que serán creados por el script. Por ello este directorio se encuentra excluido en el .gitignore pues no hace falta
versionarlos al cambiar cada vez que ejecutemos el script). Por la misma razón &lt;code&gt;nav.adoc&lt;/code&gt; tampoco se versiona pues el
script se encargará de crear la navegación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock yml&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;name: main
title: Estaciones de Servicio
version: final
nav:
  - modules/ROOT/nav.adoc&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este fichero le indica a Antora qué modulos queremos cargar, en nuestro caso únicamente el ROOT&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock yml&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;site:
  title: Estaciones de Servicio
  url: https://estaciones-de-servicio.netlify.com
  start_page: main::index.adoc
  keys:
    google_analytics: &apos;UA-XXXXX-XX&apos;

content:
  sources:
    - url: ./
      branches: HEAD
      start_path: docs

ui:
  bundle:
    url: ui/ui.zip
  supplemental_files: antora-lunr/supplemental_ui

output:
  clean: true
  dir: ./build
  destinations:
    - provider: archive

asciidoc:
  extensions:
    - ./lib/tabs-block/extension.js&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este fichero es lo que en Antora se conoce como un playbook y sirve para indicarle de donde obtener los repositorios
(Antora puede unir múltiples repositorios para generar un único site), la presentación a usar (ui.zip), extensiones, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En nuestro caso le estamos indicando que entre otras características utilice el directorio &lt;code&gt;docs&lt;/code&gt; como origen para la
documentación junto con una serie de extras que nos van a ayudar a que nuestro site sea más funcional como una
extensión para mostrar multiples tabs, un buscador incluido (lunr)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para generar las páginas que va a usar Antora como fuente, el script simplemente va a :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;descargarse el XML y parsearlo. Con groovy esto se puede hacer con una simple linea&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;new XmlParser().parseText(new InputStreamReader(url.toURL().openStream(), &apos;UTF-8&apos;).text).ListaEESSPrecio&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;recorrer cada estación, extraer la información de interés de cada una e ir añadiendolas a un &quot;mapa de mapas&quot; por
provincia, municipios y localidades de tal forma que al terminar tenemos en memoria todas las estaciones agrupadas&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;[ Albacete : [Abengibre: [ [ Estacion1:[ precio95:1.23, precio98:1.33] ] ] ] ]&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;utilizando este mapa, el script genera el fichero de navegación:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;nav = new File(&apos;docs/modules/ROOT/nav.adoc&apos;)
nav.text = &quot;&quot;
map.each{ kvp -&amp;gt;
    provincia = kvp.value
    nav &amp;lt;&amp;lt; &quot;* xref:${kvp.key}.adoc[$provincia.name]\n&quot;
    provincia.municipios.each{ kvm -&amp;gt;
        municipio = kvm.value
        nav &amp;lt;&amp;lt; &quot;** xref:${kvp.key}.adoc#${municipio.name.toLowerCase().replaceAll(&apos; &apos;,&apos;_&apos;)}[$municipio.name]\n&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Añadir líneas a un fichero de texto en groovy es tan fácil como &lt;code&gt;file &amp;lt;&amp;lt; &quot;una cadena&quot;&lt;/code&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente indicamos que tendremos un fichero por cada provincia (01.adoc, 02.adoc, &amp;#8230;&amp;#8203;) y que dentro de él tendremos
secciones con el nombre de cada municipio. Para facilitar la navegación usaremos el nombre del municipio en minúsculas
y cambiaremos los espacios en blanco por guiones bajos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo que nos genera un fichero parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;nav.adoc&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;* xref:02.adoc[ALBACETE]
** xref:02.adoc#abengibre[Abengibre]
** xref:02.adoc#albacete[Albacete]
 ...
** xref:50.adoc#villarroya_de_la_sierra[Villarroya de la Sierra]
** xref:50.adoc#zaragoza[Zaragoza]
** xref:50.adoc#zuera[Zuera]&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;después el script vuelve a recorrer el mapa para ir generando los ficheros de cada provincia:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;map.each{ kvp -&amp;gt;
    provincia = kvp.value
    file = new File(&quot;docs/modules/ROOT/pages/${kvp.key}.adoc&quot;)  &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
    file.parentFile.mkdirs()
    file.text =&quot;= $provincia.name\n:tabs:\n\n&quot;  &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Creamos un fichero por provincia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Lo inicializamos con una cabecera asciidoctor con su titulo y el atributo :tabs:&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para cada provincia recorremos sus municipios y localidades&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;map.each{ kvp -&amp;gt;
    ...
    provincia.municipios.each{ kvm -&amp;gt;
        municipio = kvm.value
        file &amp;lt;&amp;lt; &quot;[#${municipio.name.toLowerCase().replaceAll(&apos; &apos;,&apos;_&apos;)}]\n&quot;
        file &amp;lt;&amp;lt; &quot;== $municipio.name \n\n&quot;
        municipio.localidades.each{ kvl -&amp;gt;
            kvl.value.sort{it.direccion}.each{ estacion -&amp;gt;
                file &amp;lt;&amp;lt; &quot;=== $estacion.direccion \n\n&quot;
                file &amp;lt;&amp;lt; &quot;*$estacion.rotulo* $estacion.horario \n\n&quot;
                file &amp;lt;&amp;lt; &quot;https://www.openstreetmap.org/?mlat=$estacion.latitud&amp;amp;mlon=$estacion.longitud#map=17/$estacion.latitud/$estacion.longitud[Ver en mapa,window=_blank]\n\n&quot;
                file &amp;lt;&amp;lt; &quot;[TIP]\n====\n&quot;
                estacion.precios.findAll{ it.value }.each{
                    file &amp;lt;&amp;lt; &quot;* _${it.key}_ a *${it.value}* €\n&quot;
                }
                file &amp;lt;&amp;lt; &quot;====\n\n&quot;
            }
        }
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se puede adivinar, simplemente vamos concatenando al fichero de la provincia texto en formato asciidoc hasta
que llegamos a una estación donde volcamos sus datos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para cada provincia se genera un fichero similar a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;= ARABA/ÁLAVA
:tabs:

[#alegría-dulantzi]
== Alegría-Dulantzi

=== CALLE GASTEIZBIDEA  59

*ES DULANTZI REPSOL* L-D: 07:00-22:00

https://www.openstreetmap.org/?mlat=42.842917&amp;amp;mlon=-2.519194#map=17/42.842917/-2.519194[Ver en mapa,window=_blank]

[TIP]
====
* _95 E5_ a *1.239* €
* _Gasoleo A_ a *1.139* €
====

(más estaciones)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El script actual genera algo más de código por cada provincia como una serie de tabs con las estaciones
más baratas al inicio del fichero. En este post no voy a explicarlo pero si te interesa es muy fácil de seguir.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;por último el script copia un fichero &lt;code&gt;index.adoc&lt;/code&gt; que se encuentra en el raiz del proyecto al raiz del pages del
módulo. De esta forma puedo cambiar la página principal sin liar más el script.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para generar las páginas simplemente ejecuto:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;groovy dump.groovy&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Este script será ejecutado por el pipeline de Gitlab por lo que al final no necesitarías ni tener Groovy instalado
en tu local, aunque sería bueno tenerlo para probarlo primero antes de pasar a publicarlo.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;generar_el_site&quot;&gt;Generar el site&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez ejecutado el script y las páginas generadas podemos proceder a ejecutar Antora para que nos genere el site.
Puedes instalarlo y ejecutarlo desde una consola o utilizar un docker-compose como el siguiente para evitarlo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;docker-compose.yml&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;version: &quot;2.1&quot;
services:
  antora:
    image: &quot;antora/antora&quot;
    volumes:
      - .:/antora&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y ejecutarlo mediante&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;docker-compose run antora estaciones.yml&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este docker usaría la imagen oficial de antora, básica pero suficiente para generar un site en el directorio &lt;code&gt;build&lt;/code&gt;
que podrías revisar con un navegador.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo esta imagen no incluye ciertas extensiones que me interesan como el buscador javascript por lo que
habría que instalarlo y configurarlo, así que yo me he creado mi propia imagen donde ya se encuentra instalado y este
es el docker-compose que utilizo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;version: &quot;2.1&quot;
services:
  antora:
    image: &quot;jagedn/antora-with-extensions&quot;
    environment:
      - DOCSEARCH_ENABLED=true
      - DOCSEARCH_ENGINE=lunr
      - NODE_PATH=&quot;$$(yarn global dir)/node_modules&quot;
    volumes:
      - .:/antora&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejecución_diaria&quot;&gt;Ejecución diaria&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como he comentado, una de las características de Gitlab es que puedes programar la ejecución de pipelines, no sólo
cuando realizas cambios en el código y los subes al repositorio sino de forma programada.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nuestro pipeline consistirá en 3 pasos consecutivos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;gitlab-ci.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;stages:
  - build
  - staging
  - deploy

groovy:
  stage: build
  image:
    name: groovy:2.5.9
  script:
    - groovy dump.groovy
  artifacts:
    paths:
      - docs

antora:
  stage: staging
  image:
    name: jagedn/antora-with-extensions
    entrypoint: [/bin/sh, -c]
  variables:
    ASCIIDOC_COPY_TO_CLIPBOARD: &quot;true&quot;
    DOCSEARCH_ENABLED: &quot;true&quot;
    DOCSEARCH_ENGINE: &quot;lunr&quot;
    NODE_PATH: &quot;$$(yarn global dir)/node_modules&quot;
  dependencies:
    - groovy
  script:
    - antora --generator antora-site-generator-lunr  estaciones.yml
  artifacts:
    paths:
      - build

pages:
  stage: deploy
  dependencies:
    - antora
  script:
    - mkdir -p public
    - cp -R build/* public/
  artifacts:
    paths:
      - public&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo va bien, Gitlab nos ofrecerá una url donde publicará el site generado por antora. En este caso en
&lt;a href=&quot;https://jorge-aguilera.gitlab.io/estaciones-de-servicio&quot; class=&quot;bare&quot;&gt;https://jorge-aguilera.gitlab.io/estaciones-de-servicio&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;netlify_2&quot;&gt;Netlify&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Netlify es otro servicio de hosting con características muy interesantes que integra fácilmente con Gitlab (y otros)
de tal forma que podemos delegar en este segundo la ejecución de nuestro pipeline mientras que utilizamos a Netlify
como plataforma donde desplegarlo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ello simplemente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;crearemos un proyecto en Netlify&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;obtendremos su NETLIFY_SITE_ID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;desde la página del perfil crearemos un NETLIFY_AUTH_TOKEN&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crearemos en Gitlab dos variables de entorno nuevas en la seccion CD/CI con estos valores&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sustituiremos el último paso del fichero .gitlab-ci.yml por este otro (o si quieres publicar en los dos sitios lo
puedes mantener, simplemente estarás publicando en los dos URLs a la vez)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;netlify:
  stage: deploy
  image: node:10.15.3
  script:
    - npm i -g netlify-cli
    - netlify deploy --site $NETLIFY_SITE_ID --auth $NETLIFY_AUTH_TOKEN --prod
  dependencies:
    - antora
  only:
    - master&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Partiendo de un XML, cómo crear un static site con los precios de las gasolineras en Antora</summary>
    </entry>
    <entry>
        <title>Enviar alertas de Kibana a Telegram</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/kibana-telegram.html"/>
        <updated>2020-08-15T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/kibana-telegram.html</id>
        <category term="groovy"/>
        <category term="grails"/>
        <category term="kibana"/>
        <category term="logs"/>
        <category term="telegram"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
Lo que no se define no se puede medir. Lo que no se mide, no se puede mejorar. Lo que no se mejora, se degrada siempre
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; William Thomson Kelvin
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Este post no es continuación de &lt;a href=&quot;logs-metricas.html&quot; class=&quot;bare&quot;&gt;logs-metricas.html&lt;/a&gt; pero están muy relacionados. De hecho sin el primero
no se me habría ocurrido este otro, sin embargo como ya he dicho son independientes.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;kibana_elk&quot;&gt;Kibana (ELK)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;em&gt;Kibana es una interfaz de usuario gratuita y abierta que te permite visualizar los datos de Elasticsearch y navegar
en el Elastic Stack. Realiza lo que desees, desde rastrear la carga de búsqueda hasta comprender
la forma en que las solicitudes fluyen por tus apps&lt;/em&gt; (&lt;a href=&quot;https://www.elastic.co/es/kibana&quot; class=&quot;bare&quot;&gt;https://www.elastic.co/es/kibana&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Kibana es el interface que usamos para visualizar los datos (como por ejemplo logs) que Elasticsearch ingesta de
diferentes fuentes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un uso típico puede ser el ver de forma centralizada todos los logs que va generando un stack de servicios de tal
forma que podamos dar un contexto único a los mismos, realizar búsquedas complejas e incluso diseñar diagramas
explotando dichos datos. Así en el post &lt;a href=&quot;logs-metricas.html&quot; class=&quot;bare&quot;&gt;logs-metricas.html&lt;/a&gt; contaba cómo hacer una
anotación para servicios Grails tal que añadieran información en el contexto del log sobre la duración del método
junto con los parámetros recibidos en el mismo. Esta meta-información puede llegar al Kibana de tal forma que no sólo
puedas realizar búsquedas en el texto sino en esta meta-información.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Visualmente, esto sería un ejemplo de una línea de log en la consola web de Kibana junto con su meta-información:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;t @containerId	rm26xpdn3qyx4i0ugphtplq8o/43b08f1b4da4
t @id	35625187335574879357932181753924573842110613149578166277
t @log_group	docker-logs
t @log_stream	service_1.2.rm26xpdn3qyx4i0ugphtplq8o/43b08f1b4da4
t @message	{&quot;timestamp&quot;:&quot;2020-08-15T10:50:49.748+0000&quot;,&quot;level&quot;:&quot;INFO&quot;,&quot;thread&quot;:&quot;http-apr-8080-exec-4&quot;,&quot;logger&quot;:&quot;{...}&quot;}
t @owner	602122916959
t @payload.className	com.puravida.service.HelloController
# @payload.duration	1,680
t @payload.methodName	hello
t @replica	2
t @service	service_1
 @timestamp	Aug 15, 2020 @ 10:50:49.748
t _id	35625187335574879357932181753924573842110613149578166277
t _index	cwl-2020.08.15
# _score	 -
t _type	docker-logs-production
t context	default
t correlationId	8f9b465a-d08c-4d95-b65b-82398d3dc127
t level	INFO
t logger	com.puravida.service.HelloController
t message	com.puravida.service.HelloController.hello: duration=1680 ms;
t thread	http-apr-8080-exec-4
 timestamp	Aug 15, 2020 @ 10:50:49.748
t userId	 - not logged -&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver en este registro de Elasticsearch no sólo tenemos el &lt;code&gt;message&lt;/code&gt; generado sino una serie de meta-campos
extras como el &lt;em&gt;@payload&lt;/em&gt; generado por nuestra anotación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gracias al motor de búsqueda de Elasticsearch desde Kibana puedes filtrar logs que contengan un valor de interés en
estos metacampos, por ejemplo puedes filtrar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@payload.className:HelloController&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y obtener todos los logs generados por este controller y extraer de ellos el campo &lt;code&gt;@payload.duration&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Realizar gráficas que usen este campo junto, en un intervalo de tiempo definido en &lt;em&gt;@timestamp&lt;/em&gt; es cuestión de minutos.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;telegram&quot;&gt;Telegram&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A día de hoy asumo que prácticamente todo el mundo conoce Telegram (si estás leyendo este post probablemente es porque
incluso eres usuario de este sistema de mensajería).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una de las características de Telegram sobre otros sistemas es la posibilidad de crear canales y bots de forma simple
(y gratuita) incluso privados.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
En &lt;a href=&quot;https://core.telegram.org/bots#6-botfather&quot; class=&quot;bare&quot;&gt;https://core.telegram.org/bots#6-botfather&lt;/a&gt; tienes la documentación oficial sobre cómo crear bots
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar (y siendo usuarios de esta plataforma) crearemos un &lt;em&gt;bot&lt;/em&gt; mediante el &lt;em&gt;BotFather&lt;/em&gt; (el bot de Telegram
que nos sirve para crear y gestionar nuestros bots):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;buscar desde la aplicación &quot;botfather&quot; y comenzar un diálogo con él&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crear un bot siguiendo los pasos (dar un nombre y un id terminado en &quot;bot&quot; básicamente)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;obtener el &lt;code&gt;token&lt;/code&gt;. No hace falta guardarlo en lugar seguro pues podremos consultarlo en cualquier momento, pero no
lo compartas más que con gente de confianza y ojo &lt;strong&gt;no guardarlo en un repositorio git público&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En segundo lugar crearemos un canal privado, por ejemplo &quot;Puravida Alerts&quot; y añadir al bot como administrador del canal
. Puedes restringir sus permisos para que solamente pueda publicar mensajes. Así mismo hay que añadir a otros usuarios
que puedan estar interesados en las alarmas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como el canal será privado (a no ser que quieras que cualquiera pueda suscribirse y ver las alertas) necesitaremos
conocer su ID, que a diferencia del público donde es el @nombre_del_canal. Para ello tendremos que usar el interface
web de Telegram (desconozco si se puede con el móvil, yo no lo he conseguido). Simplemente navegaremos hasta el canal
creado con el navegador y nos fijaremos en la url que tiene asignada el canal:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;&lt;a href=&quot;https://web.telegram.org/#/im?p=cXXXXXXX_yyyyyyyyy&quot; class=&quot;bare&quot;&gt;https://web.telegram.org/#/im?p=cXXXXXXX_yyyyyyyyy&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El id del canal será &quot;XXXXXXX&quot;, es decir el número comprendido entre &lt;code&gt;c&lt;/code&gt; y &lt;code&gt;_&lt;/code&gt; en la URL&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;alertas&quot;&gt;Alertas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El stack ELK (Kibana+Elasticsearch) cuenta con un ecosistema de plugins bastante extenso del cual vamos a usar para
las alarmas el de &lt;strong&gt;opendistro&lt;/strong&gt; (&lt;a href=&quot;https://opendistro.github.io/for-elasticsearch/&quot; class=&quot;bare&quot;&gt;https://opendistro.github.io/for-elasticsearch/&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si quieres probarlo primero antes de instalarlo en tu stack, puedes usar este docker-compose como punto de partida:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
version: &apos;3&apos;
services:
  odfe-node1:
    image: amazon/opendistro-for-elasticsearch:1.9.0
    container_name: odfe-node1
    environment:
      - cluster.name=odfe-cluster
      - node.name=odfe-node1
      - discovery.seed_hosts=odfe-node1,odfe-node2
      - cluster.initial_master_nodes=odfe-node1,odfe-node2
      - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
      - &quot;ES_JAVA_OPTS=-Xms512m -Xmx512m&quot; # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536 # maximum number of open files for the Elasticsearch user, set to at least 65536 on modern systems
        hard: 65536
    volumes:
      - odfe-data1:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
      - 9600:9600 # required for Performance Analyzer
    networks:
      - odfe-net
  odfe-node2:
    image: amazon/opendistro-for-elasticsearch:1.9.0
    container_name: odfe-node2
    environment:
      - cluster.name=odfe-cluster
      - node.name=odfe-node2
      - discovery.seed_hosts=odfe-node1,odfe-node2
      - cluster.initial_master_nodes=odfe-node1,odfe-node2
      - bootstrap.memory_lock=true
      - &quot;ES_JAVA_OPTS=-Xms512m -Xmx512m&quot;
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - odfe-data2:/usr/share/elasticsearch/data
    networks:
      - odfe-net
  kibana:
    image: amazon/opendistro-for-elasticsearch-kibana:1.9.0
    container_name: odfe-kibana
    ports:
      - 5601:5601
    expose:
      - &quot;5601&quot;
    environment:
      ELASTICSEARCH_URL: https://odfe-node1:9200
      ELASTICSEARCH_HOSTS: https://odfe-node1:9200
    networks:
      - odfe-net

volumes:
  odfe-data1:
  odfe-data2:

networks:
  odfe-net:&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Sí, es el mismo que viene en la web de opendistro pero así hago este post más extenso&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente levanta dos nodos de Elasticsearch y uno de Kibana con los plugins configurados. Una vez levantado puedes
acceder a &lt;code&gt;localhost:9200&lt;/code&gt; y usar &lt;code&gt;admin:admin&lt;/code&gt; para logearte.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea principal va a consistir en crear:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;un &lt;em&gt;monitor&lt;/em&gt; que inspeccione los registros cada cierto tiempo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un &lt;em&gt;trigger&lt;/em&gt; que se ejecute dentro de este &lt;em&gt;monitor&lt;/em&gt; cuando los registros cumplan una condicion&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un &lt;em&gt;destination&lt;/em&gt; a donde enviar la alarma&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;destination&quot;&gt;Destination&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar crearemos un &lt;em&gt;destination&lt;/em&gt; (objetivo principal de este post) &lt;code&gt;Puravida Telegram&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;name: Puravida Telegram&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;type: Custom&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;custom attributes:&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;host: api.telegram.org&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;path: botELTOKENDELBOT-INCLUIDO-LOS-DOS-PUNTOS/sendMessage&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;header information:&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Content-Type: application/json&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es importante escribir bien la url:
- texto fijo &quot;bot&quot;
- el token obtenido con BotFather incluidos los dos puntos, tal que XXXXXXX:YYYYYYYYYY
- terminar en &lt;em&gt;/sendMessage&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Así mismo es necesario añadir un header con el Content-Type como application/json
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;monitor&quot;&gt;Monitor&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación crearemos un &lt;em&gt;Monitor&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;name: Slow HelloWorld&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;schedule: every 10 mnts&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y usando el interface visual definiremos el &lt;em&gt;monitor&lt;/em&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;index: cwl-* (o el que hayas definido)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;time field: @timestamp&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;crearemos una expresión para indicar el filtro a aplicar sobre los documentos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;/images/2020/elk/kibana-monitor.png&quot; alt=&quot;kibana monitor&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo le estamos indicando que seleccione la duración máxima del &lt;em&gt;@payload.duration&lt;/em&gt; en los registros
de la última hora.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;trigger&quot;&gt;Trigger&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podemos definir tantos triggers como queramos con diferente severidad y a diferentes &lt;em&gt;destinations&lt;/em&gt;. En nuestro caso
vamos a crear un trigger de severidad 1 al &lt;code&gt;PuraVida Telegram&lt;/code&gt; &lt;em&gt;destination&lt;/em&gt; creado anteriormente&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo único que tenemos que hacer es darle un nombre, establecer una severidad e indicar la condición de disparo,
por ejemplo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;IS ABOVE 500&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;lo cual enviará la alarma cuando la ejecución de &lt;code&gt;HelloController&lt;/code&gt; sea superior a 500 mills&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Añadiremos un &lt;code&gt;action&lt;/code&gt; indicando un &lt;em&gt;name&lt;/em&gt; a usar y seleccionando el &lt;em&gt;destination&lt;/em&gt; &lt;code&gt;PuraVida Telegram&lt;/code&gt;.
Así mismo nos interesará personalizar el mensaje a enviar incluyendo por ejemplo la duración que ha hecho disparar
la alarma por lo que cambiaremos el texto propuesto por defecto:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Message&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;{
&quot;chat_id&quot;:&quot;-100EL_ID_DEL_CHAT&quot;,
&quot;text&quot;: &quot;{{ctx.monitor.name}} alert\n Some HelloWorld takes {{#ctx.results}}{{#aggregations}}{{when.value}}{{/aggregations}}{{/ctx.results}} millisecs to be completed&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Fíjate que el canal se compone de &lt;code&gt;-100&lt;/code&gt; y el id extraído de la URL del mismo
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mediante el uso de &lt;code&gt;{{`y `}}&lt;/code&gt; (handlebars) podremos incluir información relativa al evento. En este caso vamos a
enviar el valor del &lt;code&gt;aggregation: when&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para otro tipo de alarmas tienes que buscar qué información puedes extraer del contexto, básicamente inspeccionando la
propiedad &lt;code&gt;hits&lt;/code&gt; del mismo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde esta consola puedes realizar una prueba de envío usando el enlace que aparece debajo &lt;code&gt;Send test message&lt;/code&gt;.
Si todo está bien configurado recibirás una notificación en el canal indicado.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo lo anterior ha ido correctamente a partir de ahora tendrás un job que se ejecutará según le hayas programado
y si se cumple alguna de las condiciones impuestas recibirás un mensaje en el canal.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente el &lt;em&gt;destination&lt;/em&gt; puede ser un canal de #Slack por ejemplo, pero con los cientos de canales que tengo abiertos
ahí al final no les presto atención a ninguno.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;todo&quot;&gt;TODO&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con el API de Telegram es muy fácil enviar stickers así que me anoto como tarea pendiente en lugar de enviar un
mensaje de texto, definir varios niveles de criticidad y enviar un sticker diferente para cada nivel&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Cómo enviar alertas de Kibana a un canal de Telegram</summary>
    </entry>
    <entry>
        <title>Bot Estaciones de Servicio</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/estaciones-servicio-bot.html"/>
        <updated>2020-08-06T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/estaciones-servicio-bot.html</id>
        <category term="micronaut"/>
        <category term="telegram"/>
        <category term="bot"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El bot &quot;EstacionesServico&quot; es un bot de Telegram que te permite saber el precio de la gasolinera más cercana, así
como ubicarla en un mapa además de recibir una notificación si cambia el precio de la gasolinera que marques como
favorita.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post voy a destripar (a grandes rasgos) cómo funciona el bot por si te da alguna idea de cómo hacer el tuyo
o algún otro caso de uso de los datos.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;open_data&quot;&gt;Open Data&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El bot se basa en un conjunto de datos abiertos del Ministerio de Industria y Consumo donde se muestran los precios
de las estaciones de servicio de España: &lt;a href=&quot;https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/&quot; class=&quot;bare&quot;&gt;https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este xml (o json) se listan todas las estaciones con su nombre, dirección, marca, geoposición así como los
diferentes precios para gasolina 95, 98, los diferentes tipos de diesel, bios, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estos precios se actualizan al menos una vez al día (realmente no recuerdo donde leí cúando se realiza ni la periodicidad)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;lt;PreciosEESSTerrestres xmlns=&quot;http://schemas.datacontract.org/2004/07/ServiciosCarburantes&quot; xmlns:i=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&amp;gt;
&amp;lt;Fecha&amp;gt;06/08/2020 21:31:14&amp;lt;/Fecha&amp;gt;
&amp;lt;ListaEESSPrecio&amp;gt;
    &amp;lt;EESSPrecio&amp;gt;
        &amp;lt;C.P.&amp;gt;02250&amp;lt;/C.P.&amp;gt;
        &amp;lt;Dirección&amp;gt;AVENIDA CASTILLA LA MANCHA, 26&amp;lt;/Dirección&amp;gt;
        &amp;lt;Horario&amp;gt;L-D: 07:00-22:00&amp;lt;/Horario&amp;gt;
        &amp;lt;Latitud&amp;gt;39,211417&amp;lt;/Latitud&amp;gt;
        &amp;lt;Localidad&amp;gt;ABENGIBRE&amp;lt;/Localidad&amp;gt;
        &amp;lt;Longitud_x0020__x0028_WGS84_x0029_&amp;gt;-1,539167&amp;lt;/Longitud_x0020__x0028_WGS84_x0029_&amp;gt;
        &amp;lt;Margen&amp;gt;D&amp;lt;/Margen&amp;gt;
        &amp;lt;Municipio&amp;gt;Abengibre&amp;lt;/Municipio&amp;gt;
        &amp;lt;Precio_x0020_Biodiesel/&amp;gt;
        &amp;lt;Precio_x0020_Bioetanol/&amp;gt;
        &amp;lt;Precio_x0020_Gas_x0020_Natural_x0020_Comprimido/&amp;gt;
        &amp;lt;Precio_x0020_Gas_x0020_Natural_x0020_Licuado/&amp;gt;
        &amp;lt;Precio_x0020_Gases_x0020_licuados_x0020_del_x0020_petróleo/&amp;gt;
        &amp;lt;Precio_x0020_Gasoleo_x0020_A&amp;gt;1,039&amp;lt;/Precio_x0020_Gasoleo_x0020_A&amp;gt;
        &amp;lt;Precio_x0020_Gasoleo_x0020_B&amp;gt;0,569&amp;lt;/Precio_x0020_Gasoleo_x0020_B&amp;gt;
        &amp;lt;Precio_x0020_Gasoleo_x0020_Premium/&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_95_x0020_E10/&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_95_x0020_E5&amp;gt;1,149&amp;lt;/Precio_x0020_Gasolina_x0020_95_x0020_E5&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_95_x0020_E5_x0020_Premium i:nil=&quot;true&quot;/&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_98_x0020_E10/&amp;gt;
        &amp;lt;Precio_x0020_Gasolina_x0020_98_x0020_E5/&amp;gt;
        &amp;lt;Precio_x0020_Hidrogeno/&amp;gt;
        &amp;lt;Provincia&amp;gt;ALBACETE&amp;lt;/Provincia&amp;gt;
        &amp;lt;Remisión&amp;gt;dm&amp;lt;/Remisión&amp;gt;
        &amp;lt;Rótulo&amp;gt;Nº 10.935&amp;lt;/Rótulo&amp;gt;
        &amp;lt;Tipo_x0020_Venta&amp;gt;P&amp;lt;/Tipo_x0020_Venta&amp;gt;
        &amp;lt;_x0025__x0020_BioEtanol&amp;gt;0,0&amp;lt;/_x0025__x0020_BioEtanol&amp;gt;
        &amp;lt;_x0025__x0020_Éster_x0020_metílico&amp;gt;0,0&amp;lt;/_x0025__x0020_Éster_x0020_metílico&amp;gt;
        &amp;lt;IDEESS&amp;gt;4375&amp;lt;/IDEESS&amp;gt;
        &amp;lt;IDMunicipio&amp;gt;52&amp;lt;/IDMunicipio&amp;gt;
        &amp;lt;IDProvincia&amp;gt;02&amp;lt;/IDProvincia&amp;gt;
        &amp;lt;IDCCAA&amp;gt;07&amp;lt;/IDCCAA&amp;gt;
    &amp;lt;/EESSPrecio&amp;gt;

    &amp;lt;!-- unas 10500 estaciones--&amp;gt;
&amp;lt;/ListaEESSPrecio&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver hay bastante información pero la que le interesa al bot son básicamente los diferentes precios,
el nombre y la latitud+longitud&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;parseo&quot;&gt;Parseo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El bot al arrancar se descarga el xml y lo parsea. Como utilizo Groovy, el
parseo de un XML o un JSON es inmediato. Además cuenta con clases que permiten realizar el parseo de ficheros tan
grandes sin consumir muchos recursos. Así mismo, cada hora, realiza una actualización del fichero volviendoselo a bajar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para poder disponer de los precios de forma inmediata se mantiene una lista en memoria con los precios de cada estación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Esta parte es fundamental según diseñes tu bot. Si diseñas un bot stateless deberás contar con algún proceso que
realize este parseo y te deje preparados los datos de alguna forma más óptima. Por ejemplo una versión de este bot
la hice en Google Cloud Run y aquí el tiempo de ejecución del bot es importante pues no puedes exceder de un tiempo
determinado (y además si tu bot tiene muchas visitas podrias sobrepasar la capa generosa grautita de Google)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En mi caso, como el bot va a estar desplegado en un servicio que me deja correr la aplicación 24h puedo permitirme el
mantenerla en memoria.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;sesión&quot;&gt;Sesión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Otra de las características de este bot es que mantiene una sesión por cada usuario con el que dialoga para poder
saber qué tipo de carburante usa así como la estación favorita y el precio último para poder avisarle de cambios
en el mismo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La persistencia de las sesiones en este caso es una simple carpeta de un volumen persistente. La versión Google
Cloud Run usaba por ejemplo un Datastore de Google, pero también se podría usar otras soluciones de base de datos
como un mysql u otros servicios con capa gratuita como FaunaDB.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo una vez más el servicio donde se encuentra desplegado me permite crear volúmenes persistentes de tal forma
que los datos no se pierden aunque destruya el contenedor (para desplegar nuevas versiones por ejemplo). Así que
en este caso la persistencia es un fichero por cada chat con el que el bot dialoga&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Aunque Telegram ofrece en el api conocer algunos datos del usuario en este caso prefiero NO guardar ningún
dato que no sea imprescindible y que mantenga el ananimato del usuario. Así simplemente guardo el id del chat
(que no del usuario) así como el id de la gasolinera, el tipo y el precio del carburante elegido.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;diálogo&quot;&gt;Diálogo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada vez que el usuario interacciona con el bot a través de Telegram, recibimos un json mediante un POST al controller
que hayamos configurado.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este json viene o bien el texto que ha introducido el usuario o bien datos asociados a cada botón que hayamos
mostrado o bien un mensaje con la localización del usuario (cuando este la envía usando el clip del móvil)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente los comandos que acepta el bot son:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;/start, inicio del diálogo. Preparamos un fichero &lt;code&gt;chat_id&lt;/code&gt; para mantener la sesion&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/carburante , enviamos un teclado con los diferentes tipos de carburante admitidos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/favorita, muestra el precio actual de la estación guardadas en la sesion&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo mediante los datos asociados a los botones de teclado podemos recibir del usuario:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;info_estacion xxxxx, cuando el usuario ha seleccionado la estación XXXX la buscamos y mostramos su precio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;send_localizacion xxxxx, cuando el usuario selecciona &quot;ver en mapa&quot; la estaacion XXXXX buscamos sus coordenadas
y le indicamos al movil que abra un mapa con las mismas&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;save_estacaion xxxx, actualizamos la sesion del usuario con el id de la estacion&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;algunas otras como &lt;code&gt;back&lt;/code&gt; y &lt;code&gt;cancel&lt;/code&gt; para gestionar los menús emergentes&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por su parte cuando el usuario nos envia su localización, lo recibimos en una estructura del mensaje y procedemos
a actualizar la sesion con estas coordenadas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así cuando el bot recibe un mensaje y dispone de una estación o localizacion así como un carburante de interés
para el usuario puede realizar la búsqueda y filtrar aquellas estaciones de interés&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;búsqueda_y_filtrado&quot;&gt;Búsqueda y filtrado&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La búsqueda de estaciones de interes es realmente fácil si sabemos cómo calcular la distancia esntre dos puntos
geográficos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;static float metersTo(float lat1, float lng1, float lat2, float lng2) {
    double radioTierra = 6371;
    double dLat = Math.toRadians(lat2 - lat1);
    double dLng = Math.toRadians(lng2 - lng1);
    double sindLat = Math.sin(dLat / 2);
    double sindLng = Math.sin(dLng / 2);
    double va1 = Math.pow(sindLat, 2) + Math.pow(sindLng, 2) * Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2));
    double va2 = 2 * Math.atan2(Math.sqrt(va1), Math.sqrt(1 - va1));
    double meters = radioTierra * va2;
    meters as float;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente el servicio de búsqueda recibe unas coordenadas de donde se encuentra el usuario así que ordena la
base de datos (una lista en memoria) en función de la distancia a las mismas de cada gasolinera y se queda con las
n primeras a las que ordena por el precio de interés más barato&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;chequeo_y_notificación&quot;&gt;Chequeo y notificación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para realizar un chequeo diario se podía haber creado un Job en la propia aplicación que se ejecutara una vez al día.
Sin embargo me pareció más interesante tener un &lt;code&gt;endpoint&lt;/code&gt; al que invocar para que realizar la comprobación. Este
endpoint puede recibir un id de sessión y realizar el chequeo sólo para esta (útil para el modo debug o si hubiera
un plan premium por ejemplo) o si no recibe sesion realiza el chequeo para todas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El chequeo es simplemente para cada estación comprobar el último precio guardado en cada sesioón con el precio
de la base de datos y enviar un mensaje al chat del usuario&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;tecnología&quot;&gt;Tecnología&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para este bot he usado&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;groovy, como lenguaje de programación. Soy un fanático de este lenguaje y la facilidad de parsear xml es un plus.
La performance y el compilado estático que tienen generan una aplicación bastante ligera y funcional.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Micronaut, como framework de desarrollo. La facilidad para crear controllers, services, etc es increíble. El mismo
bot lo he desplegado en Heroku, Google AppEngine (Flex), Kubernetes o Google Cloud Run (con ligeras adaptaciones)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kubernetes. Las primeras versiones eran un simple Docker corriendo en Heroku pero tras descubrir Okteto y su SAAS
para desplegar pequeños proyectos la adaptación a un kubernetes sencillo fue muy fácil y así de paso aprendo algo
de esta tecnología&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente, estas son las herramientas que yo he elegido por mis motivos, pero no son las únicas.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Destripando el bot de estaciones de servicio</summary>
    </entry>
    <entry>
        <title>PlantUML y C4</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/plantuml-c4.html"/>
        <updated>2020-07-18T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/plantuml-c4.html</id>
        <category term="asciidoctor"/>
        <category term="plantuml"/>
        <category term="model"/>
        <category term="c4"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde hace muchos años, el mundo del desarrollo del software cuenta con lenguajes de modelado de sistemas,
tipo UML, ArchiMate o SysML, usados para definir la arquitectura mediante diagramas que ayuden a la comprensión del
mismo. Sin embargo en la práctica (al menos la que yo he vivido) poca gente conoce y mucha menos usa algunos de estos
lenguajes. Probablemente son tan completos que al final resultan demasiado complejos y con demasiados detalles
por lo que se termina optando por diagramas con solo cajas y líneas sin mucho sentido e incongruentes entre sí.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;c4&quot;&gt;C4&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El modelo C4 pretende simplificar la forma de explicar la arquitectura software mediante una &quot;aproximación&quot; al detalle
dividida en 4 pasos, o zooms:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Contexto&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Contenedores (nada que ver con Docker)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Componentes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Código&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podemos ver el primer nivel, Contexto, como el diagrama que representa al más alto nivel la solución que tratamos de
explicar. A su vez, el siguiente nivel de Contenedores (repetimos, nada que ver con contenedores Docker) muestra bloques
software de alto nivel mientras que el de Componentes explica contenedor a contenedor qué partes componen cada uno.
Por último quien busca el detalle más exhaustivo acude al diagrama de Código donde podemos representar las clases,
interfaces, etc y sus relaciones internas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://c4model.com/img/c4-overview.png&quot; alt=&quot;c4 overview&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;C4 busca la simplicidad así que trabaja simplemente con:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Person, el típico actor, role, persona, etc en definitiva un humano usando el software&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Contenedor, entendido como una aplicación o una base de datos. Algo que tiene que se tiene que ejecutar, por
ejemplo: un war corriendo en un Tomcat o un Node, una aplicación que se ejecuta en el desktop del cliente, una
base de datos o un simple script.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Componente, en este contexto se entiende como un grupo de funcionalidades relacionadas con un interface común.
Lo normal es que un conjunto de componentes que comparten un contenedor se ejecutan en el mismo espacio de trabajo&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post vamos a ver cómo podemos aplicar este modelo en nuestra documentación usando PlantUML (y/o Asciidoctor).&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;plantuml&quot;&gt;PlantUML&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;PlantUML es una herramienta para generar diagramas de software (y otros) partiendo de texto. La idea es muy potente
porque te permite tener tus diagramas versionados como si fueran parte del código pudiendo versionarlos en un
repositorio, fomentar la revisión, etc. Unido a herramientas como Asciidoctor, donde tu documentación sigue el mismo
principio de ser texto y que la herramienta genere el resultado visual, disponemos de una forma cómoda y potente
de tener nuestra documentación al día y visualmente atractiva.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;PlantUML (&lt;a href=&quot;https://www.plantuml.com&quot; class=&quot;bare&quot;&gt;https://www.plantuml.com&lt;/a&gt;) te permite generar diagramas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;de clases&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;de actividad&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;secuencia&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;componentes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;etc&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo cuenta con un sistema para poder extender sus capacidades pudiendo incluir iconos, otros tipos de diagramas,
etc que es lo que vamos a usar en este post para demostrar cómo documentar una arquitectura software con C4.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente la idea es que puedas adjuntar junto a tus explicaciones en Asciidoctor, bloques de texto como los que se muestran a
continuación, de tal forma que no necesites herramientas externas ni incluir imágenes que no puedas volver a editar
cuando quieras añadir o quitar información.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;contexto&quot;&gt;Contexto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El primer diagrama que generaremos es un diagrama de Contexto donde mostraremos la foto general sin mucho detalle.
Mostraremos las Persons principales así como sistemas externos. En este diagrama no buscamos mostrar tecnología, sino
relaciones entre las personas y los sistemas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;context.adoc&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[plantuml]
----
!include &amp;lt;c4/C4_Context.puml&amp;gt;
!include &amp;lt;office/Users/user.puml&amp;gt;
!include &amp;lt;office/Users/mobile_user.puml&amp;gt;

title Te lo traigo de mi Pueblo

LAYOUT_TOP_DOWN
LAYOUT_WITH_LEGEND()

Person(customer, &quot;&amp;lt;$user&amp;gt;\nCliente&quot;, &quot;Un cliente de TelotraigodemiPueblo.&quot;)

Enterprise_Boundary(c0, &quot;Te lo traigo de mi Pueblo&quot;) {
    Person(csa, &quot;&amp;lt;$mobile_user&amp;gt;\nCustomer Service Agent&quot;, &quot;Help Desk.&quot;)

    System(ecommerce, &quot;E-commerce System&quot;, &quot;Registro, planificacion y suscripcion.&quot;)
}

System_Ext(banco, &quot;PasarelaPago&quot;, &quot;Pagos de pedidos.&quot;)

Rel_D(customer, csa, &quot;Soporte&quot;, &quot;Telefono&quot;)

Rel_D(customer, ecommerce, &quot;Crea viajes y realiza pedidos&quot;)

Rel_D(ecommerce, banco, &quot;Procesa pago&quot;)
----&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/context.png&quot; alt=&quot;Diagram&quot; width=&quot;490&quot; height=&quot;736&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. Context&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el diagrama de contexto simplemente reflejamos Personas (Cliente y Help Desk) e indicamos qué partes
pertenecen al dominio a representar y cuales son externas ( &lt;code&gt;System&lt;/code&gt; vs &lt;code&gt;System_Ext&lt;/code&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La extensión nos permite de forma muy simple indicar cómo queremos ubicar los elementos entre sí:
&lt;code&gt;Rel(a,b)&lt;/code&gt; una relación entre a y b normal, &lt;code&gt;Rel_D(a,b)&lt;/code&gt; una relación top-down mientras que &lt;code&gt;Rel_U(a,b)&lt;/code&gt; hace que
la relación sea down-top. Podemos usar &lt;code&gt;Rel_L&lt;/code&gt; y &lt;code&gt;Rel_R&lt;/code&gt; para izquierda y derecha&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;container&quot;&gt;Container&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como hemos dicho, un container es un backend, una página web, una aplicación mobile, en resumen una unidad &quot;ejecutable&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En nuestro ejemplo vamos a disponer de 3 containers: un Single Page Application como front que a través de llamadas
http comunicará con otro container Backend, el cual usa una base de datos para guardar la información. (
En nuestro ejemplo por simplicidad, el backend
se comunicará con la pasarela de pago directamente aunque podríamos crear otros containers especializados)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Container.adoc&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[plantuml]
----
!include &amp;lt;c4/C4_Container.puml&amp;gt;
!include &amp;lt;office/Users/user.puml&amp;gt;
!include &amp;lt;office/Users/mobile_user.puml&amp;gt;
!include https://raw.githubusercontent.com/jagedn/mn-plantuml-sprites/master/sprites/grails.puml

title Te lo traigo de mi Pueblo

LAYOUT_TOP_DOWN
LAYOUT_WITH_LEGEND()

Person_Ext(anonymous_user, &quot;Anonymous User&quot;)
Person(aggregated_user, &quot;Aggregated User&quot;)
Person(administration_user, &quot;Administration User&quot;)

System_Boundary(c1, &quot;telotraigodemipueblo&quot;){

    Container(web_app, &quot;Web Application&quot;, &quot;Vue.js&quot;, &quot;Permite ver amigos, viajes, realizar pedidos, etc&quot;)

    Container(api, &quot;Api&quot;, &quot;&amp;lt;$grails&amp;gt;&quot;, &quot;Permite ver amigos, viajes, realizar pedidos, etc&quot;)

    ContainerDb(rel_db, &quot;Relational Database&quot;, &quot;MySQL 5.5.x&quot;, &quot;Guarda viajes, pedidos, pagos, etc.&quot;)

    Container(filesystem, &quot;File System&quot;, &quot;FAT32&quot;, &quot;Imágenes de productos organizadas por carpetas&quot;)
}

System_Ext(pasarela, &quot;Pasarela Pagos&quot;)

Rel(anonymous_user, web_app, &quot;Uses&quot;, &quot;HTTPS&quot;)
Rel(aggregated_user, web_app, &quot;Uses&quot;, &quot;HTTPS&quot;)
Rel(administration_user, web_app, &quot;Uses&quot;, &quot;HTTPS&quot;)

Rel_D(web_app, api, &quot;Uses&quot;, &quot;HTTPS&quot;)

Rel(api, rel_db, &quot;Reads from and writes to&quot;, &quot;SQL/JDBC, post 3306&quot;)
Rel(web_app, filesystem, &quot;Reads from&quot;)
Rel_D(api, pasarela, &quot;JSON/Https&quot;, &quot;SSL point to point&quot;)
Lay_R(rel_db, filesystem)
----&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/container.png&quot; alt=&quot;Diagram&quot; width=&quot;731&quot; height=&quot;1053&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. Container&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como podemos ver en este nivel especificamos tecnología (MySQL, Vue, Grails), así como tratamos por igual
diferentes tipos de Containers, tanto aplicaciones como base de datos o ficheros. Mediante &lt;code&gt;ContainerDb&lt;/code&gt; la
librería nos permite personalizar mejor el container para especificar su naturaleza. Así mismo podemos ver
que PlantUML nos permite añadir elementos visuales que mejoren la comprensión del sistema como por ejemplo
el icono de &lt;code&gt;Grails&lt;/code&gt; (usando una librería propia publicada en Github con iconos para Grails, Micronaut y Groovy)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;component&quot;&gt;Component&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El tercer nivel de zoom dentro del modelo C4 detalla cada Componente o varios en el mismo diagrama si así se desea,
plasmando las diferentes partes de aquel.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo en nuestro caso vamos a representar el Container &lt;code&gt;Api&lt;/code&gt; mostrando sus diferentes partes y cómo
se relacionan entre sí.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;single_page.adoc&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[plantuml]
----
!include &amp;lt;c4/C4_Component.puml&amp;gt;

title Api Backend de TelotraigodemiPueblo

Container(spa, &quot;Single Page Application&quot;, &quot;Vue.js&quot;, &quot;Realiza peticiones en nombre del usuario.&quot;)

Container_Boundary(api, &quot;API Application&quot;) {
    Component(sign, &quot;Sign In Controller&quot;, &quot;MVC Rest Controlle&quot;, &quot;Allows users to sign in to the internet banking system&quot;)
    Component(accounts, &quot;Accounts Summary Controller&quot;, &quot;MVC Rest Controlle&quot;, &quot;Provides customers with a summory of their bank accounts&quot;)
    Component(orders, &quot;Orders Controller&quot;, &quot;MVC Rest Controlle&quot;, &quot;Provides customers with a summory of their orders&quot;)

    Component(security, &quot;Security Component&quot;, &quot;Spring Bean&quot;, &quot;Provides functionality related to singing in, changing passwords, etc.&quot;)
    Component(accounts_srv, &quot;Accounts Service&quot;, &quot;Spring Bean&quot;, &quot;Provides functionality related to accounts.&quot;)

    Component(gwfacade, &quot;Banking System Facade&quot;, &quot;Spring Bean&quot;, &quot;A facade onto the mainframe banking system.&quot;)
}
ContainerDb(rel_db, &quot;Relational Database&quot;, &quot;MySQL 5.5.x&quot;, &quot;Guarda viajes, pedidos, pagos, etc.&quot;)
System_Ext(pasarela, &quot;Pasarela Pagos&quot;, &quot;Realiza el cobro de pedidos.&quot;)

Rel(spa, sign, &quot;Uses&quot;, &quot;JSON/HTTPS&quot;)
Rel(spa, accounts, &quot;Uses&quot;, &quot;JSON/HTTPS&quot;)
Rel(spa, orders, &quot;Uses&quot;, &quot;JSON/HTTPS&quot;)

Rel(sign, security, &quot;Uses&quot;)
Rel(security, rel_db, &quot;Read &amp;amp; write to&quot;, &quot;JDBC&quot;)

Rel(accounts, accounts_srv, &quot;Uses&quot;)
Rel(accounts_srv, rel_db, &quot;Read &amp;amp; write to&quot;, &quot;JDBC&quot;)

Rel(orders, gwfacade, &quot;Uses&quot;)
Rel(gwfacade, pasarela, &quot;Uses&quot;, &quot;XML/HTTPS&quot;)
----&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/single_page.png&quot; alt=&quot;Diagram&quot; width=&quot;789&quot; height=&quot;975&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;code&quot;&gt;Code&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El último nivel de detalle propuesto por C4 correspondería al de código en el cual el detalle baja hasta especificar
las clases, interfaces, relaciones entre ellos etc, en un típico diagrama UML&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;security_service.adoc&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[plantuml]
----
title Login Service

package &quot;com.puravida.telotraigo.security&quot; #DDDDDD {

  class SpringUserService

  class UserDetails

  class UserNotFoundException

  SpringUserService -- UserDetails : create

  SpringUserService - UserNotFoundException : throws

}
UserService ()-- SpringUserService
----&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/security_service.png&quot; alt=&quot;Diagram&quot; width=&quot;456&quot; height=&quot;329&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea fundamental que subyace sobre todo esto es por un lado el demostrar mediante un ejemplo sencillo las posibilidades de
realizar diagramas de arquitectura mediante texto versionable y por otra acercarnos a un modelo simple pero potente
como es el modelo C4 donde prima la sencillez y una aproximación top-bottom&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Una guía básica sobre modelado de componentes software usando C4</summary>
    </entry>
    <entry>
        <title>Abuelo cebolleta</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/abuelo-cebolleta.html"/>
        <updated>2020-06-27T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/abuelo-cebolleta.html</id>
        <category term="pensamientos"/>
        <category term="introspección"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/abuelo_cebolleta.jpg&quot; alt=&quot;abuelo cebolleta&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hace unas semanas contactaron conmigo por si quería colaborar para un artículo en Xataka dando mi opinión como
programador viejuno sobre como ha cambiado en todos estos años la industria del software y dando algún consejo a alguien
que abandonara hace 20 años la programación y quisiera volver.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como el artículo incluía a otros programadores recojo aquí alguna de mis respuestas, añadiendo algunos comentarios que
por no venir a cuento no incluí en su momento.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El artículo en cuestión lo puedes leer en
&lt;a href=&quot;https://www.xataka.com/otros/20-anos-despues-quiero-volver-a-programar-que-grandes-cambios-ha-habido-donde-empezar&quot; class=&quot;bare&quot;&gt;https://www.xataka.com/otros/20-anos-despues-quiero-volver-a-programar-que-grandes-cambios-ha-habido-donde-empezar&lt;/a&gt; junto
con las opiniones de otros 3 programadores&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;presentación&quot;&gt;Presentación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Soy Jorge Aguilera, un Senior Software Engineer, actualmente trabajando en Tymit, con unos
25 años trabajando en el sector del desarrollo de software.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(&lt;em&gt;Repasando luego mi vida laboral descubrí que el mes que viene hare 28 años&lt;/em&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como muchos de mi generación mi adolescencia se vio marcada por la aparición de aquellas
consolas que te dejaban hacer alguna cosa más que las máquinas de marcianos y siempre sentí
atracción por ellas. Pero cuando decidí dar el salto fue
cuando empezamos a usar Multiplan (una hoja de cálculo) en la carrera de Económicas, y ví
que me saltaba las clases de todas las asignaturas por estar diseñando funciones estadísticas en el aula de informática.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;inicios&quot;&gt;Inicios&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gracias a un amigo ( _ Merchán, @jmerespi, amigo desde el colegio y al que le debo un huevo de cosas_)
me apunté a un programa de becas de aquella época: 3 meses a media jornada y hasta me pagaban!!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(&lt;em&gt;La beca fue de
3 meses durante el verano y la compaginé con un trabajo de vigilante los fines de semana, así que esos meses gané una
pasta gansa, unas 40.000 pesetas al mes. Recuerdo que eramos unos
20 y de muy diversos perfiles y allí conocí a quien sería mi mentor durante muchos años&lt;/em&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por aquellos tiempos, y creo que todavía de alguna manera en estos, la evolución normal era que
estuvieras unos años de junior, otros de senior, analista, jefe de proyecto &amp;#8230;&amp;#8203; y debo admitir
que en aquellos días lo veía como la evolución normal. De hecho llegué a montar una empresa donde
ejercí como director técnico.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(&lt;em&gt;Releo esto y veo que no venía muy a cuento pero tenía que decirlo&lt;/em&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por suerte y gracias a buenos consejeros opté hace muchos años por la vía &quot;arriesgada&quot; y decidí dedicarme al desarrollo
como tal y desde entonces no me arrepiento ni me veo cambiando el rumbo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(&lt;em&gt;Ahora veo que esto era el meollo de la cuestión:
cómo por buenos consejos, suerte e introspección conseguí escapar de la corriente del momento y dedicarme
únicamente a programar que era lo que me gustaba&lt;/em&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;trayectoria&quot;&gt;Trayectoria&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Echando la vista atrás veo que mientras antes podías abarcar prácticamente todo el espectro de un
desarrollo hoy en día eso es imposible. Salvo honrosas excepciones nadie puede abarcar todos los
conocimientos, herramientas y técnicas para desarrollar un producto. El tan manido término &quot;fullstack&quot;
es una quimera. Podrás conocer muchas herramientas de todas las capas pero no podrás dedicarte a todas
ellas pues las soluciones de hoy en día son muchísimo más complejas y requieren de mucho esfuerzo
y dedicación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A lo largo de todos estos años uno piensa que ha usado muchos lenguajes y herramientas asociadas, y probablemente
es verdad, pero creo que no es tanto la cantidad de lenguajes que hayas usado sino cómo te hayas sentido
identificado con ellos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Yo, por ejemplo, en un momento dado usé Rexx en un proyecto pero a parte de la satisfacción de que sirviera para lo
que se necesitaba no me siento identificado como programador Rexx (y no porque sea malo).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ello, para mí, hay 3 o 4 lenguajes que me marcaron en cada época:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;mis comienzos con C y el momento en que fuí capaz de explicar los punteros (&lt;em&gt;llegue a dar unos cuantos cursos de C&lt;/em&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C++ como paso a eso que se empezaba a conocer como OOP (&lt;em&gt;aún recuerdo explicando que un objeto es como una librería
y una estructura de datos, todo junto&lt;/em&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java y su posibilidad de emplearlo tanto en el cliente como en el servidor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Groovy, el cual descubrí casi por casualidad y con el que mi productividad se multiplicó por 1k y por ello
lo uso a diario&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya he comentado una etapa (de bastantes años) de mi vida profesional fue como director de una empresa
de servicios que monté junto con un socio. Probablemente lo peor de mi carrera haya venido de cuando una
de tantas crisis me hizo ver que o estaba en una cosa o en otra, pero no en las dos. Así que, tras casi quedar
en quiebra, opté por el rumbo de técnico y a partir de ahí todo ha sido más fácil.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algunas veces pienso que &lt;strong&gt;lo que más echo de menos de aquella época era la emoción de estar haciendo algo
por donde &quot;nadie había pasado&quot;&lt;/strong&gt;. Sin Internet, casi todo te parecía que lo tenías que hacer o inventar tú
(luego descubres que alguien lo había hecho y mejor). Hoy es a la inversa: parece como que todo está inventando
, pero por suerte siempre descubres algo nuevo que te vuelve a traer esa emoción.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;mayor_avance_de_la_industria&quot;&gt;Mayor avance de la industria&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Después de más una década como autodidacta decidí sacarme la ingeniería y creo que es donde tomé conciencia de lo
realmente difícil que es hoy en día programar. Tal vez la industria está madurando y especializándose pero también
se está diversificando de una manera increíble y alguien que termina la carrera no es realmente consciente de
la cantidad de herramientas, técnicas y procedimientos que tiene que emplear en el día a día.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ya no es sólo saber compilar un programa y copiarlo en un sitio. Hay que diseñar y programar test que aseguren
la calidad, hay que trabajar con un equipo usando herramientas colaborativas como git, hacer code reviews,
despliegues continuos, y un sinfín más de pasos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero casi todo esto es producto de la madurez de la profesión y en mi opinión Git es un claro exponente de esto.
De todas las herramientas o lenguajes con los que haya podido trabajar en todo este tiempo, Git ha sido el
mayor cambio que he sufrido.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podías venir de herramientas como CSV o SVN pero las posibilidades que ofrece Git para que un equipo de trabajo
cree software son inmensas.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;volver_a_programar&quot;&gt;Volver a programar&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A pesar de toda esta ingente cantidad de herramientas y lenguajes que parece que hay que emplear para todo,
lo que creo es que se siguen necesitando muchas manos (y alguna cabeza). Cualquiera que se haya dedicado a
esta profesión puede actualizarse sin problema si acude con una mentalidad abierta. Tiene que entender
que las cosas han evolucionado, que no todo es como se hacía antes, probablemente por esa especialización,
pero que en el fondo el asunto sigue siendo el mismo: resolver una necesidad de la forma más adecuada en el momento
. Lo que sí le digo es que tiene que estar dispuesto a aceptar el cambio constante que le caracteriza a esta
profesión&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si hay algo que no debería desanimarte a la hora de optar por ser programador, es la edad. Creo que a diferencia
de otras profesiones, en esta, la gente no siente tanto recelo a personas mayores, pero eso sí tienes que estar
dispuesto a convivir con gente de la que puede que no entiendas sus aficiones y a la que no puedes ver como tus
hijos.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;por_donde_empezar&quot;&gt;Por donde empezar&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estoy prácticamente seguro que lo primero que piensa una persona que hace 20 años programara y quisiera retomarlo
sería algo parecido a &quot;esto ha crecido demasiado y ahora hay muchísimas cosas y herramientas que antes no había,
no voy a llegar a entenderlas nunca&quot;. Siendo esto cierto, lo que tenemos que ver, es que ya no tenemos que estar
en todas las partes como pasaba antes. Ahora hay más especialización, incluso los mal llamados full-stack no llegan
a cubrir todas las áreas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No sólo es cuestión de backend vs frontent.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Existen los QA que también tienen que programar test, los Business Analytics que tienen que programar cargas de datos y consultas, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sí creo que hay ciertas herramientas o lenguajes que debes aprender cuanto antes aunque sea a un nivel básico:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Git&lt;/strong&gt;, es imprescindible. Hace 20 años con suerte el control de versiones era una carpeta compartida.
Hoy en día Git y todo el ecosistema que se ha generado alrededor lo ha cambiado todo.
Es la manera de trabajar con tus compañeros y de no pisarles el trabajo,
así que los conceptos básicos y cierta soltura es imprescindible.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt; (que conste que yo no sé Python) creo que es un lenguaje que no envejece y se está reinventando muy bien.
A día de hoy se está aplicando en muchas áreas diferentes y podrás encontrar una que te guste.
Administración de sistemas, despliegues en la nube, análisis matemático, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Si te gusta más la parte visual y crees HTML y Javascript es sencillo, prepárate porque la explosión que ha
tenido de frameworks, herramientas y áreas es enorme. Te abrumará la cantidad de cosas que hay que aprender
así que lo mejor será acotarlas. Busca un framework pequeño y centrate en él. Hay suficiente demanda como para que
encuentres tu hueco. Yo por ejemplo &lt;strong&gt;no te recomendaría que empezaras con Angular&lt;/strong&gt; pues es un monstruo y te puede
sobrepasar. &lt;strong&gt;Comienza con VueJS&lt;/strong&gt; por ejemplo, mucho más asequible y con mucho tirón.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En resumen, en mi opinión hay mucha demanda en todas las áreas y no por haber estado 20 años fuera no vas a aportar nada.
Sí creo que necesitarás una formación mínima en un área determinada probablemente de unos cuantos meses (tal vez 6),
es decir, olvidate el &quot;yo lo aprendo por mi cuenta&quot; porque no va a funcionar.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En verdad es un tema sobre el que llevo los últimos años hablando con algunos managers.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Se centran en los juniors y no saben ver el potencial que aporta alguien que pasa los 40 sin conocimientos de programación.
Madurez, responsabilidad, fidelidad, ganas de mejorar, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probablemente alguien de 45 años y que se está reciclando no vaya a captar todos los detalles (tampoco un Junior lo hace
, lo único que este pueda tener más reciente los estudios o últimas tendencias del mercado), pero probablemente será
alguien que pueda aportar cuidado en el detalle y algo de cabeza crítica.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Apuntes tomados para la entrevista en Xataka "20 años después quiero volver a programar: qué grandes cambios ha habido y por dónde empezar"</summary>
    </entry>
    <entry>
        <title>Semestre 1/2020</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/uoc/s1-2020.html"/>
        <updated>2020-06-27T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/uoc/s1-2020.html</id>
        <category term="data science"/>
        <category term="uoc"/>
        <category term="learning"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este semestre ha sido el segundo haciendo el &quot;Grado de Ciencia de Datos Aplicada&quot; por la UOC (&lt;a href=&quot;https://estudios.uoc.edu/es/grados/data-science/presentacion&quot; class=&quot;bare&quot;&gt;https://estudios.uoc.edu/es/grados/data-science/presentacion&lt;/a&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;No he escrito ningún post sobre el primer semestre que cursé el año pasado así que hago un resumen rápido sobre este:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Fundamentos de programación, básicamente iniciación a Python&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Introducción ciencia de datos, ubicar la figura del data science, sus roles, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Métodos numéricos, matématicas aplicadas con Python&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De las 3 asignaturas no pude completar Métodos numéricos. Si bien la primera PEC fue asequible, rápidamente la segunda
crece en complejidad y necesitas de más dominio de Python, así que la tercera no supe ni por donde abordarla. Las otras
dos son realmente sencillas, aunque Introducción a la ciencia de datos es bastante aburrida.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este semestre me he enfrentado a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Programación en scripting, bash, awk, curl, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tipología y Fuentes de datos, semántica de datos, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bases de datos analíticas, diseño de bases de datos con herramientas Micro$oft&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;programación_en_scripting_notable&quot;&gt;Programación en scripting (Notable)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si bien no tiene mucha dificultad y los materiales están bastante completos, hay que dedicarle su rato para resolver
los ejercicios de una forma decente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Entre otras te enfrentas a ejercicios para usar curl, leer json o xml, crear algunos bash, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Después de muchísimos años sin haber tocado AWK me ha sido muy placentero volver a usarlo y tener que esforzarme en
hacer alguna cosa más allá del buscar y reemplazar algún carácter, usando &lt;code&gt;BEGIN&lt;/code&gt;, &lt;code&gt;END&lt;/code&gt;, funciones internas, arrays
, etc. (aquí mi &lt;a href=&quot;s1-2020/AguileraGonzalezJorge_pec3.pdf&quot;&gt;PEC3&lt;/a&gt; que ya te digo que no fue de las más brillantes)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La última práctica es un ejercicio libre donde elegimos un dataset cualquiera en Internet y debemos tratarle con
herramientas bash para generar un report HTML incluyendo algunos plots sobre los datos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Yo he analizado los accidentes en la M30 durante los años 2016 al 2019
(&lt;a href=&quot;https://datos.madrid.es/sites/v/index.jsp?vgnextoid=6e1ce0f3e8e22610VgnVCM1000001d4a900aRCRD&amp;amp;vgnextchannel=374512b9ace9f310VgnVCM100000171f5a0aRCRD&quot; class=&quot;bare&quot;&gt;https://datos.madrid.es/sites/v/index.jsp?vgnextoid=6e1ce0f3e8e22610VgnVCM1000001d4a900aRCRD&amp;amp;vgnextchannel=374512b9ace9f310VgnVCM100000171f5a0aRCRD&lt;/a&gt;)
generando al final un report de resumen semanal buscando qué día y hora son los peores, utilizando únicamente bash,
awk y gnuplot&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;/images/uoc/s1-2020/5.png&quot; alt=&quot;5&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;em&gt;En resumen una asignatura muy interesante y divertida. Si bien en el día a día seguramente uses herramientas más
complejas, tener un dominio básico de estas otras te podrá ahorrar muchas horas&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;tipología_y_fuentes_de_datos_aprobado&quot;&gt;Tipología y Fuentes de datos (Aprobado)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta asignatura tengo sentimientos encontrados. Como con muchas asignaturas, ves que tienen mucho que contar pero
la mayoría de ello no consigue despertar mi interés.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La teoría de cómo usar los metadatos, la nomenclatura Dublin Core (que por cierto no viene de la capital de Irlanda,
sino de una ciudad de EEUU), etc no me llega a despertar mucho interés.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo los últimos ejercicios sobre cómo atacar Wikidata mediante SPARQL me han dejado tocado. Tras la curva
inicial para entender cómo se realizan las sentencias SQL (la cual no he terminado de superar y terminé en un estado
de prueba-error) me quedé impresionado con las posibilidades que ofrece.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Poder consultar infinidad de datos, no sólo los típicos sobre películas y actores, sino política, geografía, espacio
y un largo etc desde la línea de consola y obtener unos datos estructurados para su tratamiento me parece bestial.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Soccer players, who are born in a country with more than 10 million inhabitants, who played as goalkeeper for a club
that has a stadium with more than 30.000 seats and the club country is different from the birth country&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;PREFIX dbo: &amp;lt;http://dbpedia.org/ontology/&amp;gt;

SELECT distinct ?soccerplayer ?countryOfBirth ?team ?countryOfTeam ?stadiumcapacity
{
?soccerplayer a dbo:SoccerPlayer ;
   dbo:position|dbp:position &amp;lt;http://dbpedia.org/resource/Goalkeeper_(association_football)&amp;gt; ;
   dbo:birthPlace/dbo:country* ?countryOfBirth ;
   #dbo:number 13 ;
   dbo:team ?team .
   ?team dbo:capacity ?stadiumcapacity ; dbo:ground ?countryOfTeam .
   ?countryOfBirth a dbo:Country ; dbo:populationTotal ?population .
   ?countryOfTeam a dbo:Country .
FILTER (?countryOfTeam != ?countryOfBirth)
FILTER (?stadiumcapacity &amp;gt; 30000)
FILTER (?population &amp;gt; 10000000)
} order by ?soccerplayer&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otra parte, esta asignatura cuenta con muchos ejercicios a realizar en Python, usando la plataforma de Google
&lt;a href=&quot;https://colab.research.google.com/&quot; class=&quot;bare&quot;&gt;https://colab.research.google.com/&lt;/a&gt; donde puedes crear y ejecutar programas en Python complejos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Durante toda mi carrera profesional me he tenido que aprender muchos lenguajes y Python no es de los más complejos
en absoluto, pero tal vez porque ya no tengo los mismos reflejos y dedicación de antes no termina de enamorarme.
Le encuentro un lenguaje &lt;em&gt;guarro&lt;/em&gt; , pero es una opinión personal sin ningún fundamento&lt;/strong&gt; De todas formas es un lenguaje
básico en este Grado así que tocará poco a poco ir usándolo cada vez más.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;bases_de_datos_analíticas_no_presentado&quot;&gt;Bases de datos analíticas (No presentado)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sobre esta asignatura poco que decir pues a mitad de curso sucedieron dos cosas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;cambio de trabajo, con toda la presión que conlleva ponerte a un nivel aceptable donde puedas aportar&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;covid19 y todo lo que supuso. Probablemente para algunos esta situación les ofreciera oportunidad para poder
dedicarse a estudiar pero en mi caso fue a la inversa.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A todo ello se une que la asignatura me pareció de lo más aburrida y no por tener que usar Microsoft SQLServer en
un escritorio remoto, sino porque el tipo de ejercicios eran de lo más insulsos y la teoría tampoco acompañaba a hacerlo
más interesante.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así que juntando todas las circunstancias decidí sacrificar esta asignatura pues era la que menos me aportaba, y
la dejaremos para más adelante cuando las circunstancias y ganas cambien.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hasta donde realicé de la asignatura, la teoría era de lo más simple sin representar ninguna complicación. Sin embargo
el tener que usar un escritorio remoto representó un montón de problemas de cabeza pues no estaba muy fino ni explicado.
Usar herramientas como Pentaho y guardar tus ficheros y que se perdieran tras cerrar sesión nos traía de cabeza,
atinar a configurar las conexiones con SQLServer, &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;próximo_semestre&quot;&gt;Próximo semestre&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para el próximo semestre he optado por retomar Métodos numéricos unicamente. No hay prisa y de esta forma espero poder
dedicarme en exclusiva a ella para enterarme bien y prestar más atención al Python necesario&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Resumen del semestre 1/2020 de la carrera Data Science en la UOC</summary>
    </entry>
    <entry>
        <title>Logs y métricas</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/logs-metricas.html"/>
        <updated>2020-06-16T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/logs-metricas.html</id>
        <category term="groovy"/>
        <category term="grails"/>
        <category term="kibana"/>
        <category term="logs"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
Lo que no se define no se puede medir. Lo que no se mide, no se puede mejorar. Lo que no se mejora, se degrada siempre
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; William Thomson Kelvin
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post voy a hablar sobre un caso real que hace poco hemos tenido en &lt;a href=&quot;https://tymit.io&quot;&gt;Tymit&lt;/a&gt; relativo a la
importancia de poder medir la ejecución de tu software.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin entrar en muchos detalles, porque seguramente muchos os sentiréis identificados, en Tymit tenemos un proceso
de negocio crítico donde intervienen varios componentes (llámalos microservicios, aplicaciones, librerías) y que de
vez en cuando tardaba algo más de lo esperado generando los problemas típicos (timeouts, reintentos, consumo de recursos,
etc).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por supuesto contamos con toda la infraestructura necesaria para no perder ni una sola línea de logs, capacidad para
asumir los reintentos y demás mecanismos para mantener el nivel de servicio &amp;#8230;&amp;#8203; actual. Pero ¿qué pasaría ante un
incremento de la demanda de dicha funcionalidad? Obviamente era un flujo de trabajo que había que revisar y mejorar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya he comentado, todo el flujo se encuentra debidamente traceado y todos los logs son capturados y enviados a
un ELK (ElasticSearch y Kibana) para su supervisión de tal forma que es relativamente fácil saber
cuando comienza un flujo y los pasos principales por donde va pasando.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo el problema principal es que esta línea de seguimiento de logs suele estar pensaba para que el desarrollador pueda
identificar los pasos por los que ha transcurrido una transacción (casi siempre comparándolo con el código fuente e
identificando las decisiones tomadas en función de la traza que veamos). Obviamente cuanto mayor información se disponga
en el log más fácil será identificar las decisiones tomadas por el código&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;WARNING&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;No, tracear un método con &quot;entra aquí&quot; o &quot;paso 1&quot;, no te va a sacar del apuro&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como es obvio, en esta situación un desarrollador puede ir realizando un seguimiento de cómo se comporta su código en
producción pero el sistema sigue adoleciendo de algunas deficiencias:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Negocio y QA, en el mejor de los casos tienen una curva de aprendizaje elevada en el aprendizaje de lo que están viendo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Se asume que como cada línea de log incluye su marca de tiempo seremos capaces de extraer información sobre el tiempo
empleado en resolver cada paso, lo cual puede ser cierto en el mejor de los casos, pero la realidad es que o se dispone
de un sistema robusto o el análisis se hace con un coste de análisis elevado&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;traceo_básico&quot;&gt;Traceo básico&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una primera aproximación puede ser la de tracear la entrada a cada método de interés así como todos los posibles retornos
que se puedan producir dentro del método.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;int calculoCostoso( int param1, String param2){
    log.info &quot;calculoCostoso:enter &quot;+new Date().time
    if( !param2 ){
        log.info &quot;calculoCostoso:exit &quot;+new Date().time
        return -1
    }
    int ret = param1*10
    log.info &quot;calculoCostoso:exit &quot;+new Date().time
    return ret
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente esta es una solución rápida pero costosa y propensa a error. Así mismo delegamos en el sistema de explotación
de los logs el cálculo de cuánto ha tardado la función en ejecutarse.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una posible mejora podría ser tomar tiempos de entrada y de salida y tracear la diferencia:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;int calculoCostoso( int param1, String param2){
    Date start = new Date()
    if( !param2 ){
        log.info &quot;calculoCostoso:duration &quot;+ (new Date().time-start.time)
        return -1
    }
    int ret = param1*10
    log.info &quot;calculoCostoso:duration &quot;+ (new Date().time-start.time)
    return ret
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A parte de lo tedioso y propenso a error del sistema, existen otras situaciones que no estaríamos contemplando como
por ejemplo si se producen excepciones dentro del método, por lo que podríamos reescribir cada método de interés
con algo parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;int calculoCostoso( int param1, String param2){
    Date start = new Date()
    try{
        if( !param2 ){
            return -1
        }
        int ret = param1*10
        return ret
    }finally{
        log.info &quot;calculoCostoso:duration &quot;+ (new Date().time-start.time)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;slf4j_y_mdc&quot;&gt;Slf4j y MDC&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Log4j y Logback ofrecen, además del traceo básico, la posibilidad de utilizar MDC (Mapped Diagnostic Context)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con MDC puedes &quot;enriquecer&quot; el sistema de traceo con otros tipos de datos además de la línea a tracear. Un caso típico
puede ser por ejemplo añadir un id de contexto al inicio del flujo de tal forma que todos los logs que se generen
en ese thread compartirán este &lt;code&gt;correlation Id&lt;/code&gt; facilitando su agrupación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estos campos pueden ser utilizados en sistemas tipo ELK (ElasticSearch-Logstack-Kibana) para mejorar los sistemas
de búsqueda y filtrado así como generar gráficas en tiempo real, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nosotros podemos aprovechar esta funcionalidad para añadir una pequeña carga de información en el log. Así por ejemplo
podríamos mejorar nuestro método:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;int calculoCostoso( int param1, String param2){
    Date start = new Date()
    log.info &quot;calculoCostoso:enter&quot;
    try{
        if( !param2 ){
            return -1
        }
        int ret = param1*10
        return ret
    }finally{
        MDC.put(&quot;payload&quot;, JsonOutput.toJson([className: &apos;BussinesService&apos;,
                                            methodName: &apos;calculoCostoso&apos;,
                                            duration: new Date().time-start.time]))
        log.info &quot;calculoCostoso:exit&quot;
        MDC.remove(&quot;payload&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;metermethod&quot;&gt;MeterMethod&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;MeterMethod es una clase muy simple destinada a medir el tiempo desde su creación hasta que se invoca el método &lt;code&gt;toString&lt;/code&gt;
. Además permite incluir en la cadena a generar una lista de key-value (&lt;code&gt;tags&lt;/code&gt;) así como el incluir/remover en el MDC
un &lt;code&gt;payload&lt;/code&gt; con la información de interés&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;payload puede llamarse como quieras, es un campo que se añade en el contexto de la traza y simplemente debes
buscar un nombre que no &quot;pise&quot; a alguno ya existente. La idea es que estos campos del MDC lleguen al ELK y puedan ser
indexados para su búsqueda y explotación&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;class MeterMethod {

    long start
    Map&amp;lt;String, String&amp;gt; tags = [:]
    String methodName
    String className

    MeterMethod(String className, String methodName) {
        this.className = className
        this.methodName = methodName
        start = Calendar.instance.timeInMillis
    }

    long getDuration() {
        Calendar.instance.timeInMillis - start
    }

    void addTag(String key, Object value) {
        tags[key] = &apos;&apos; + value
    }

    @Override
    String toString() {
        String args = tags.size() ? tags.collect { &quot;$it.key=$it.value&quot; }.join(&apos;;&apos;) : &apos;&apos;
        &quot;$className.$methodName: duration=$duration ms;$args&quot;
    }

    void addPayload() {
        MDC.put(&quot;payload&quot;, JsonOutput.toJson([className: className, methodName: methodName, duration: duration]))
    }

    void removePayload() {
        MDC.remove(&quot;payload&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Un posible ejemplo de su uso sería:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;int calculoCostoso( int param1, String param2){
    def meterMethod = new MeterMethdo(this.class.Name, &apos;calculoCostoso&apos;)
    meterMethod.addTag(&apos;param1&apos;, param1)

    try{
        if( !param2 ){
            return -1
        }
        int ret = param1*10
        return ret
    }finally{
        meterMethod.addPayload()
        log.info meterMethod.toString()
        meterMethod.removePayload()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;anotación_tymittrace&quot;&gt;Anotación TymitTrace&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;El stack tecnológico que utilizamos en Tymit es Groovy&amp;amp;Grails pero no debería ser difícil adaptar esta anotación
a Java.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con todas estas ideas y teniendo en cuenta que ya disponemos de toda una infraestructura capaz de realizar la ingesta
(me gusta esta palabra) de los logs y su visualización en Kibana, nos planteamos usar esta estrategia para poder disponer
de unas métricas básicas pero evitando el tener que tocar en la medida de lo posible un código que estaba funcionando,
por lo que hemos creado una anotación a nivel de método que aplique la técnica descrita anteriormente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;tymittrace&quot;&gt;TymitTrace&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;@Local(
        value = TymitTraceTransformation,
        applyTo = Local.TO.METHOD)
@interface TymitTrace {
    /**
     * A list of variables to trace, i.e &apos;a,b,c&apos;
     * @return
     */
    String value()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para usar dicha anotación, simplemente anotaremos los métodos de interés:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;@TymitTrace
int calculoCostoso( int param1, String param2){
    if( !param2 ){
        return -1
    }
    int ret = param1*10
    return ret
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;dando_vida_a_la_anotación&quot;&gt;Dando vida a la anotación&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una anotación en sí no es nada más que una declaración de buenas intenciones entre el que la usa y el que la implementa.
Es un contrato, de ahí que sea una interface, que hay que implementar. Dicha implementación en el mundo de las
anotaciones corresponderá a un código que inyecta código en otro código (qué vértigo!!)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando el compilador encuentre una anotación, bien sea de clase, de propiedad o de método buscará la clase que se encuentra
indicada en dicha anotación y &lt;em&gt;le cederá el turno&lt;/em&gt; en el proceso de generar código.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues deberemos crear la clase &lt;em&gt;TymitTraceTransformation&lt;/em&gt; que implementará dicha anotación tal como indicamos en
la anotación de la anotación (doble vértigo!!)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Gracias a la librería de @marioggar (grooviter/asteroid) implementar una anotación para Groovy es bastante fácil.&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Gracias a @ilopmar por publicar un Gist (&lt;a href=&quot;https://gist.github.com/ilopmar/6037796&quot; class=&quot;bare&quot;&gt;https://gist.github.com/ilopmar/6037796&lt;/a&gt;) que me sirvió para inspirarme
en esta anotación&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;@CompileStatic
@Phase(CompilePhase.SEMANTIC_ANALYSIS)
class TymitTraceTransformation extends AbstractLocalTransformation&amp;lt;TymitTrace, MethodNode&amp;gt; {
    static private List pool = [&apos;a&apos;..&apos;z&apos;, &apos;A&apos;..&apos;Z&apos;, &apos;0&apos;..&apos;9&apos;, &apos;_&apos;].flatten()

    @Override
    void doVisit(final AnnotationNode annotation, final MethodNode methodNode) {
        def oldCode = methodNode.code
        def className = methodNode.declaringClass.name
        def methodName = methodNode.name
        def parameters = Utils.NODE.get(annotation, &apos;value&apos;, String) ?: &apos;&apos;
        def rand = new Random(System.currentTimeMillis())
        def randomVariableName = &apos;_&apos; + (0..10).collect { pool[rand.nextInt(pool.size())] }.join(&apos;&apos;)
        methodNode.code = blockS(
            declareMeterMethod(randomVariableName, className, methodName),
            declareClosure(randomVariableName, parameters.split(&apos;,&apos;), meter),
            tryCatchSBuilder()
                    .tryStmt(
                            oldCode
                    )
                    .finallyStmt(
                            callFinish(randomVariableName)
                    )
                    .build()
        )
    }

    // generamos código similar a
    // def _a12BadF123 = new MeterMethod(&apos;BusinessService&apos;, &apos;calculoCostoso&apos;)
    Statement declareMeterMethod(String randomVariableName, String className, String methodName) {
        stmt(varDeclarationX(randomVariableName, MeterMethod,
                newX(MeterMethod, constX(className), constX(methodName))
        ))
    }

    // generamos código similar a
    /**
      def _a12BadF123Closure = {
        _a12BadF123.addTag(&apos;param1&apos;, param1)
        _a12BadF123.addPayload()
        log.info _a12BadF123.toString()
        _a12BadF123.removePayload()
    }*/
    Statement declareClosure(String randomVariableName, String[] parameters, Boolean meter) {
        String closureStr = parameters.findAll { it }.collect { String p -&amp;gt;
            &quot;${randomVariableName}.addTag(&apos;$p&apos;,$p)&quot;
        }.join(&apos;\n&apos;)

        closureStr += &quot;&quot;&quot;
                ${randomVariableName}.addPayload()
                log.info ${randomVariableName}.toString()
                ${randomVariableName}.removePayload()
        &quot;&quot;&quot;

        Statement closure = blockSFromString(closureStr)
        stmt(varDeclarationX(randomVariableName + &apos;Closure&apos;, Closure, closureX(closure)))
    }

    // generamos código similar a :
    // _a12BadF123Closure.call()
    Statement callFinish(String randomVariableName) {
        stmt(callX(varX(&quot;${randomVariableName}Closure&quot;), &apos;call&apos;))
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente lo que hacemos es EN TIEMPO DE COMPILACIÓN recubrir nuestro método con un try-finally:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cambiamos el código del nodo por uno nuevo:&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;declaramos una variable &lt;code&gt;randomVariableName&lt;/code&gt; del tipo MeterMethod&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;creamos una Closure para ser llamada al final&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;recubrimos el método original con un try-finally donde en el try llamaremos al método original y en el finally
invocaremos nuestra closure. Al ser un try-finally el retorno de la función seguirá siendo el que realice el método
original&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La Closure que ejecutamos en el finally es la encargada de recabar, una vez ejecutado el método original, la información
necesaria para el traceo (&lt;code&gt;duration&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt; y preparar el MDC). Como puede verse la hemos implementado con un &quot;simple&quot;
String de tal forma que es más fácil de entender.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;resultado_final&quot;&gt;Resultado final&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con todo ello podremos proceder a anotar nuestros métodos con &lt;em&gt;TymitTrace&lt;/em&gt; . Si lo hacemos en el ejemplo inicial
de esta manera:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;@TymitTrace(&apos;param1,param2&apos;) &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
int calculoCostoso( int param1, String param2){
    if( !param2 ){
        return -1
    }
    int ret = param1*10
    return ret
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Simplemente añadimos esta línea sin tocar el código original&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A grandes rasgos el código final que se genera EN TIEMPO DE COMPILACIÓN podría ser parecido a:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;int calculoCostoso( int param1, String param2){
    MeterMethod _123aAdfkl123lf = new MeterMethod( &apos;BusinessService&apos;, &apos;calculoCostoso&apos;)
    try{
        if( !param2 ){
            return -1
        }
        int ret = param1*10
        return ret
    }finally{
        _123aAdfkl123lf.addTag(&apos;param1&apos;, param1)
        _123aAdfkl123lf.addTag(&apos;param2&apos;, param2)
        _123aAdfkl123lf.addPayload()
        log.info _123aAdfkl123lf.toString()
        _123aAdfkl123lf.removePayload()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;log&quot;&gt;Log&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si simplemente usas la consola (o un volcado a fichero de la misma) esta anotación te proporciona información &quot;en bruto&quot;
valiosa pues tendrás trazas como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;2020-06-18 12:00:00 INFO:BusinessService BusinessService.calculoCostoso: duration=123 ms;1,hola&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero si conectas el sistema de logs con un ELK podrás acceder a los campos del payload y obtener la información de
forma más cómoda y práctica, pudiendo realizar por ejemplo gráficas en tiempo real.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En nuestro caso, por ejemplo, queríamos poder medir las mejoras que íbamos realizando al proceso pesado y que no quedara
en un &lt;code&gt;parece que ahora bien&lt;/code&gt; así que una vez implementando este sistema de traceo-métrica pudimos ir abordando las
mejoras al proceso y obtener este gráfico donde se veía el antes y el después del despliegue de una de las versiones&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/elk/15-06-2020.png&quot; alt=&quot;15 06 2020&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Cómo usar los logs para obtener métricas en los métodos</summary>
    </entry>
    <entry>
        <title>Charla OpenData en HatThieves</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/opendata-hatthieves.html"/>
        <updated>2020-05-17T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/opendata-hatthieves.html</id>
        <category term="personal"/>
        <category term="charlas"/>
        <category term="opendata"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;hatthieves&quot;&gt;HatThieves&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;HatThieves, &quot;el placer de hacer&quot;, es un grupo de personas que he descubierto hace relativamente poco
gracias a la red social Mastodon y que se están dedicando a organizar eventos todos los domingos por la tarde
(#domingosNegros)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta red soy realmente muy novato, pues apenas llevo unos meses usándola pero es cierto que desde el principio
adapté el plugin que uso para publicar en canales de Telegram y en la cuenta de Twitter @madrid_data para poder
publicar también en Mastodon (@OpenDataMadrid.mastodon.madrid) de forma diaria eventos de la ciudad.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El caso es que también he ido publicando algunos de los gifs animados que crean los bots de las cámaras de tráfico,
etc y de alguna manera tuve que captar la atención de este grupo que al final me propusieron dar una charla sobre el tema
de OpenData. Como a mí sólo me tienes que tocar las palmas pues acepté en seguida y más teniéndola ya preparada del
año pasado en el Codemotion de Madrid.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras una pequeña revisión para reestructurarla tras lo aprendido de la última charla así como quitar y poner algún
nuevo desarrollo la charla estaba lista y ahora sólo me tocaba enfrentarme a una charla &quot;sin público&quot;, pues la idea
era retransmitirla en directo por streaming y no en un chat tipo Zoom, Jitsi, etc en el que podría verle la cara
al menos a alguno de los asistentes y poder captar así el nivel de aburrimiento que estaba generando.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, independientemente del nivel de interés que despertara, me sentí bastante cómodo. Ayudó mucho que la
gente de HatTieves unos días antes estuviera al tanto para que hicieramos prueba de sonido hasta conseguir eliminar
ese ruido del que se quejaban en mi curro, así como practicar un poco antes de la charla. Así mismo el haber pactado
que no había tiempo límite, sólo la recomendación de no sobrepasar la hora, hizo que pudiera explicar tranquilamente
cada punto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero lo mejor de la experiencia sin duda fue el debate que se organizó después donde la gente se podía conectar a una
dirección para hablar (o escribir si eras capaz de encontrar la caja de texto) y hacer preguntas. Para mí fue
una experiencia fantástica. Sin ser un orador habitual he podido dar algunas charlas (siempre técnicas) y en todas
la gente ha participado en mayor o menor medida pero en esta la predisposición a debatir fue abrumadora y surgieron
muchas ideas o temas que ni me había planteado.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;charla&quot;&gt;Charla&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La charla fue &quot;OpenData: datos por el pueblo y para el pueblo&quot; (sí, un poco clickbait) y la puedes ver en
&lt;a href=&quot;https://www.hatthieves.es/2020/05/12/domingos-negros-0x04-opendata-data-por-el-pueblo-para-el-pueblo/&quot; class=&quot;bare&quot;&gt;https://www.hatthieves.es/2020/05/12/domingos-negros-0x04-opendata-data-por-el-pueblo-para-el-pueblo/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente es una charla &quot;demo&quot; donde no enseño nada de teoría (básicamente porque no soy quién para ello) sino
que muestro algunos de los pet-projects que he estado haciendo jugando con catálogos de datos abiertos tanto
de Madrid como del Gobierno de España (por no aburrir no enseñé todos, sino los que creo más relevantes) y que
me gusta ver como &quot;inspiradores&quot; para gente inquietudes y que no sepan por donde empezar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Está dividida en 3 apartados:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;publicar en canales o redes sociales&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ejemplos de bots&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;little data&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada uno de ellos con ejemplos y explicaciones de donde ejecutarlos de forma económica&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;medios&quot;&gt;Medios&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para la captura y retransmisión, sin poder entrar en detalles porque los desconozco, HatThieves han montado un
servidor con un software llamado BigBlueButton el cual permite realizar conferencias (parece ser que de hasta
cientos de personas simultáneas) y/o clases virtuales. Básicamente tras darme acceso yo use la funcionalidad de
compartir una ventana y el audio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;INFO: Solo funcionó bien con Firefox&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Detrás de ello, la gente de HatThieves montaron la historia para capturarlo y transmitirlo con un delay de un par de
segundos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez terminada la charla procedimos a parar la emisión y pasarnos a usar una instancia de Mumble para poder hacer
el debate. Mumble es una aplicación muy ligera orientada al audio donde puedes tener diferentes salas que te permite
hablar mientras haces otras tareas (jugar, programar, ver películas, &amp;#8230;&amp;#8203;). Se han currado un sistema de turnos de tal
forma que la peña va pidiendo la vez y el sistema va asignando quien le toca hablar&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusiones&quot;&gt;Conclusiones&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La primera, como ya he comentado, fue el debate. Por un lado los asistentes tenían un dominio e inquietudes del tema
muy fuerte para un simple programador for-fun como yo. Ideas interesantes que surgieron, que recuerde, podrían ser:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;qué nivel de datos no quieren publicar los organismos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;qué podemos hacer y cómo para controlarlo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;existirá algún control de calidad interno/externo, tipo auditoria, que puntúe los datasets&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dónde está el límite a la legalidad de publicar datos que debían ser públicos pero que se ocultan&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;conseguirá el Covid19 hacernos ver que realmente NO contamos con una madurez suficiente del dato&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mumble, me ha creado la necesidad de que quiero instalarlo para la empresa. Ahora mismo estamos todos en remoto y
hay gente inglesa, italiana y españoles. Creo que además de ayudar a la productividad sin tener que tirar tanto de
Slack me ayudaría a practicar el inglés&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En resumen, ha sido una experiencia genial y muy oportuna para estos tiempos tan raros que nos toca vivir&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Impresiones tras la charla de OpenSource OpenData con la gente de HatThieves</summary>
    </entry>
    <entry>
        <title>El blog de Dani</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/dani-blog.html"/>
        <updated>2020-04-30T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/dani-blog.html</id>
        <category term="documentation"/>
        <category term="write"/>
        <category term="blog"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dani, para los que no lo saben, es mi chaval y tiene 9 años (y medio).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es un chaval, como todos, inquieto, divertido, alegre y, como todos, pasa de lo que le diga su padre.
Para nada le interesa la programación y la tecnología que no sea jugar con la tablet y hablar por videoconferencia
con sus amigos de cromos, aunque las pocas veces que le he dicho de sentarse conmigo a trastear con Scratch o AppInventor
lo ha hecho. Incluso se inventó un juego de cromos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quiero decir con esto, que creo que es un chico normal. No tiene inquietudes tecnológicas más allá de la
diversión inmediata que le pueda reportar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por eso me sorprendió que, tras varios meses sin comentarle que podía tener un blog donde contar sus cosas, esta
vez me dijera que sí con buen ánimo. Probablemente sea cosa del confinamiento pero como no hay que dejar pasar las
oportunidades me dispuse a crearle la infraestructura mínima y guiarle en el proceso de publicar su blog.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
A día de hoy ha publicado 3 post y aunque no me hago ilusiones, espero que sean los primeros de muchos más.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues he pensado que tal vez, si andas queriendo tener un blog simple y totalmente gratis, esta guía te sirva.
&lt;em&gt;Piensa que si un niño de 9 años es capaz de hacerlo no veo porqué tú no&lt;/em&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;objetivo&quot;&gt;Objetivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Publicar un blog de forma sencilla y cómoda, con un diseño agradable y personalizable pero simple. Que se pueda
escribir texto (obvio), imágenes y/o vídeos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gastándose lo mínimo, rayando el cero.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin necesidad de conocimientos informáticos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;NO se pretende tener un carrito de la compra, ni miles de widgets de integración con nada.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta fase no se permitirá que los lectores dejen comentarios o likes, pero sí querremos saber el impacto de las
publicaciones&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tener una cuenta de correo electrónico&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crearse una cuenta en Gitlab. El usuario con el que crees la cuenta será parte de la url de tu blog, así por ejemplo
para Dani creamos la cuenta de Gitalb &lt;code&gt;danidorami&lt;/code&gt; y su blog se encuentra en &lt;code&gt;&lt;a href=&quot;http://danidorami.gitlab.io/&quot; class=&quot;bare&quot;&gt;http://danidorami.gitlab.io/&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Existen 1000 formas diferentes de conseguir tener un blog con 1000 herramientas diferentes. En este post
voy a contar las que he usado y comentaré brevemente porqué pero no quiere decir que sea la mejor ni la única.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Prefiero Gitlab sobre otras herramientas como el famoso Github principalmente porque Gitlab es open source (a la mayoría
eso os dará igual) y porque desde antes que Github o similares ya ofrecía desde hacer muchos años
una cuenta gratuita con funcionalidades
como ejecutar pipelines y publicar páginas estáticas (justo las dos funcionalidades que vamos a usar en este caso)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;pasos&quot;&gt;Pasos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero es crear un proyecto desde la consola web de Gitlab.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proyecto deberá llamarse &lt;code&gt;TUSUARIO.gitlab.io&lt;/code&gt;  , en el caso de Dani &lt;code&gt;danidorami.gitlab.io&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
el proyecto debe incluir &lt;code&gt;gitlab.io&lt;/code&gt;. La primera vez que lo intenté creí que esa parte no había que ponerla,
sólo el nombre del usuario y no funcionó
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Seleccionar &lt;strong&gt;Crear desde plantilla&lt;/strong&gt; (Create from template) y elegir de la lista &lt;strong&gt;Pages/Hugo&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/pages_hugo1.png&quot; alt=&quot;pages hugo1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gitlab te creará un proyecto completo usando &lt;em&gt;Hugo&lt;/em&gt; un generador de blog estático, lo cual no quiere decir que tu
blog no pueda modificarse, sino que generará páginas HTML puras sin necesidad de un motor que las genere, base de datos
etc por lo que Gitlab podrá servirlas sin problema ni requisitos extras como sucede por ejemplo con WordPress&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que se haya generado el proyecto (unos segundos), tu consola web será algo parecido a&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/pages_hugo2.png&quot; alt=&quot;pages hugo2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y desde ahí seleccionaremos &lt;code&gt;Web Ide&lt;/code&gt; para acceder al interface de Gitlab que te permite añadir y editar ficheros
de una forma cómoda (y de un sólo commit, lo cual veremos más adelante que significa)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/pages_hugo3.png&quot; alt=&quot;pages hugo3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;configuración&quot;&gt;Configuración&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar vamos a configurar algunas cosas como el título. Seleccionamos el fichero &lt;code&gt;config.toml&lt;/code&gt; y cambiamos
algunos valores (ajusta a los tuyos):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;baseurl= &quot;https://danidorami.gitlab.io&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;title= &quot;Blog de Dani&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;subtitle= &quot;DaniDorami&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y eliminamos algunas secciones de &lt;a id=&quot;menu.main&quot;&gt;&lt;/a&gt; con &lt;code&gt;url&lt;/code&gt; igual a &lt;code&gt;page/xxxxx&lt;/code&gt; dejando unicamente la principal,
about y tags&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la sección Author he quitado un montón de RRSS porque no las vamos a usar por ahora, pero tú puedes configurar
las tuyas a tu gusto. Las que no tengas puedes eliminar la clave-valor&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;estructura_y_formato&quot;&gt;Estructura y formato&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;content es el directorio de contenidos, donde crearemos los post&amp;#8201;&amp;#8212;&amp;#8201;content/post será donde crearemos nuestros artículos&amp;#8201;&amp;#8212;&amp;#8201;content/_index.md es el fichero destinado para la página principal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;static es el directorio donde pondremos elementos como imágenes&amp;#8201;&amp;#8212;&amp;#8201;static/post será el directorio donde crearemos directorios para las imágenes&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A grandes rasgos trabajaremos de forma cotidiana en los subdirectorios &lt;code&gt;content/post&lt;/code&gt; y &lt;code&gt;static/post&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De &lt;code&gt;content/post&lt;/code&gt; borraremos todos los ficheros que vienen de ejemplo (si quieres puedes estudiarlo primero y verás
que son muy sencillos de seguir)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El formato principal a usar es Markdown. Este formato es texto plano con marcadores para que el interprete sepa qué
queremos expresar. Así de esta forma, en las primeras líneas del fichero podemos añadir, por ejemplo, una sección de atributos
entre dos líneas con &lt;code&gt;+&lt;/code&gt; donde pondremos el título, fecha y el estado del post (preview o publicado) o bien
mediante una sintáxis determinada indicar que queremos que incluya una imagen, cree una lista de elementos, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;primer_post&quot;&gt;Primer post&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En &lt;code&gt;content/post&lt;/code&gt; crearemos un fichero &lt;code&gt;primerpost.md&lt;/code&gt; (mediante un desplegable que aparece al lado del directorio
&lt;code&gt;post&lt;/code&gt;) y escribiremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;+++
title=&quot;Mi primer post&quot;
date=&quot;2020-04-25&quot;
+++

Hola, esta frase es la que se verá en la lista de post y sirve para indicar de qué vas a hablar

&amp;lt;!--more--&amp;gt;

El texto anterior, sirve para que el interprete sepa donde cortar la introducción.

A partir de aqui puedes escribir todo lo que quieras. Mira un tutorial de Markdown para aprender su sintaxis

E intentaremos poner una imagen con esta sintaxis

{{&amp;lt;figure src=&quot;primerpost.jpg&quot;&amp;gt;}}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Nosotros nos hemos puesto esta chuleta frente al ordenador para recordar lo básico:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/pages_hugo5.jpeg&quot; alt=&quot;pages hugo5&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En &lt;code&gt;static/post&lt;/code&gt; subiremos un jpg &lt;code&gt;primerpost.jpg&lt;/code&gt; usando el menu desplegable que aparece al lado del nombre del
directorio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez tengamos nuestro post a nuestro gusto, &quot;subiremos&quot; nuestros cambios haciendo &quot;commit&quot; (botón azul, abajo a la
izquierda). Hasta que se coja soltura con los conceptos de git realizaremos un commit a la &lt;code&gt;master&lt;/code&gt; en lugar de crear
una rama nueva&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/pages_hugo4.png&quot; alt=&quot;pages hugo4&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;en cuanto hagamos el commit, veremos un mensaje en la barra inferior donde aparecerá un reloj en azul indicándonos
que Gitlab ha comenzado a crear nuestro site con los últimos cambios.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien dispondremos de nuestro blog en la url que se comentaba al principio (&lt;a href=&quot;https://usuario.gitlab.io&quot; class=&quot;bare&quot;&gt;https://usuario.gitlab.io&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
La primera vez tu web no estará disponible hasta pasados unos 10 minutos, mientras Gitlab aprovisiona el
nombre, etc, pero las siguientes veces el cambio será casi inmediato (entre unos pocos segundos a menos de un
minuto según la capacidad de los servidores de Gitlab en ese momento. Recuerda que estás usando una cuenta gratuita)
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;día_a_día&quot;&gt;Día a día&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente los primeros post he ayudado a Dani a encontrar dónde crear los ficheros, subir la imágen, nombre del fichero
, etc. pero una vez que teníamos preparado el fichero le he dejado escribir a su manera (me sigo preguntando si
debería revisarle todas las faltas de ortografía. Por ahora sólo intervengo si me pregunta o si la falta es demasiado
escandalosa)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el tercer post ya recuerda cómo hacer commit y esperar a que el pipeline indique que ha terminado para ir a su
post y leerlo.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;pendientes&quot;&gt;Pendientes&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A todos nos gusta escribir para que nos lean (creo) y una de las primeras cosas que se echan en falta es un sistema
donde los usuarios puedan reaccionar a nuestros artículos. Existen muchas herramientas pero por ahora no las voy a
incluir para intentar que escriba por el placer de escribir y que la familia, amigos y allegados puedan leerle, pero
si te interesa saber el impacto de tus artículos una forma fácil de saberlo es habilitando en el fichero &lt;code&gt;config.toml&lt;/code&gt;
el id de una cuenta de Google Analytics que crees y así tener reports de visitas, países, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque el editor web de Gitlab es muy sencillo pero potente me gustaría que comenzara a usar un editor local para
tener mayor control de cuando y qué publicar, poder revisar en local antes, etc&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si realmente quieres tener un blog sencillo pero potente existen muchas formas de tenerlo sin tener que ceder el
control a terceros como Medium, o usando formatos propietarios como WordPress. Siguiendo estos pasos todo tu blog
te pertenece en todo momento y podrás ir migrandolo a otros sistemas más potentes de una forma (más o menos) sencilla&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por la parte de Dani no sé cuanto podré hacer que le dure las ganas de contaros sus movidas pero con un poco de
interés no veo porqué no vas a poder crear algo parecido. Y SI TU CHAVAL/CHAVALA se anima a tener el suyo que no dude
en preguntarle a Dani para que le asesore ;)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Dani (9) ha comenzado su blog, o primeros pasos para montar el tuyo</summary>
    </entry>
    <entry>
        <title>Introducción a Antora</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/intro-antora.html"/>
        <updated>2020-04-11T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/intro-antora.html</id>
        <category term="asciidoc"/>
        <category term="documentation"/>
        <category term="write"/>
        <category term="antora"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Creo que para aprovechar al máximo esta entrada, puede resultar de interés la serie de post sobre asciidoc
(&lt;a href=&quot;intro-asciidoctor-1.html&quot; class=&quot;bare&quot;&gt;intro-asciidoctor-1.html&lt;/a&gt;])
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Este post es una breve introducción a Antora y no sirve como guía completa. Simplemente intenta dar una
visión general de qué es, para qué sirve y las líneas generales de cómo usarlo. Para un mayor detalle recomiendo
la documentación del proyecto, generada con el propio Antora (&lt;a href=&quot;https://docs.antora.org/&quot; class=&quot;bare&quot;&gt;https://docs.antora.org/&lt;/a&gt;)
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;antora&quot;&gt;Antora&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Antora es un generador de sites estáticos (static-site) destinado, principalmente, a la documentación de proyectos
con múltiples repositorios y/o versiones de desarrollo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A diferencia, o mejor dicho, como añadido, a usar simplemente Asciidoctor en un proyecto es que mientras que este
último toma los fuentes de un proyecto en un momento dado, Antora da un paso más y permite centralizar la documentación
de múltiples repositorios así como utilizar diferentes ramas de cada uno de ellos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para entenderlo mejor veamos un caso de uso bastante común.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que tenemos un producto muy molón donde participan tres equipos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;servicios, mantienen una aplicación REST que ofrece endpoints al core del negocio&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;admin, usando los endpoints y otras fuentes de datos mantienen un dashboard de administración&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;front, equipo que desarrolla el aplicativo que los clientes consumen en su día a día&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cada equipo trabaja en un repositorio diferente y tiene a su vez diferentes ciclos de vida (con todas sus dependencias
y problemas que esto pueda llevar) y cada uno tiene, dentro de su repositorio, su apartado de documentación. Como
son gente que se preocupa por ella usan #doc-as-code como filosofía y tanto la documentación como el código residen
en el mismo repo, se revisa que cada pull request lleve su documentación actualizada, etc. e incluso despliegan a un
servidor cada proyecto despues de cada despliegue.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero el problema es que cada site es independiente del otro siendo la única forma de tenerlos enlazados el alojarlos
en el mismo servidor pero en diferentes directorios. Así mismo tienen un infierno para mantener la documentación
de las diferentes versiones del producto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es en estas situaciones donde Antora puede ayudarnos a centralizar en un único sitio la documentación de todos ellos
como un único site.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/multi-branch.svg&quot; alt=&quot;Multibranch&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. &lt;a href=&quot;https://antora.org/&quot; class=&quot;bare&quot;&gt;https://antora.org/&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;repo_central&quot;&gt;Repo central&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vamos a crear un proyecto que servirá como aglutinador del resto. Por simplicidad en este repo vamos a poner
la documentación común a todos los demás, como puede ser documentación corporativo, un glosario de términos de negocio,
etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crearemos la siguiente estructura de directorios y ficheros:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-00797abd87ecb9a2b8902248106df8c5.png&quot; alt=&quot;Diagram&quot; width=&quot;176&quot; height=&quot;251&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En &lt;code&gt;pages&lt;/code&gt; va a residir nuestra documentación. Para este ejemplo va a consistir en simples páginas sin reutilizar partes,
ni imágenes, etc y son documentos asciidoctor típicos, donde puedes incluir tus secciones, diagramas, etc. Así mismo
puedes enlazar una página con otra pero en lugar de usar la macro &lt;code&gt;link&lt;/code&gt; usaremos una nueva macro de Antora, &lt;code&gt;xref&lt;/code&gt;
la cual podemos ver como una extensión a aquella y que nos servirá más adelante para enlazar con otros módulos&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;nav.adoc&lt;/code&gt; es un fichero asciidoctor destinado a customizar el menú de navegación de este módulo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;* xref:index.adoc[Home]
* xref:glossary.adoc[Glosario]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;antora.yml&lt;/code&gt; es el fichero que configura el módulo en cuestión. Cuando &quot;apuntamos&quot; al generador sobre un proyecto,
este buscara de forma recursiva ficheros &lt;code&gt;antora.yml&lt;/code&gt; y donde lo encuentre asumirá que se trata de un módulo de antora.
De esta forma podemos tener múltiples módulos en un repo y en el directorio que queramos sin estar estado a uno
determinado por Antora. El contenido de este fichero es simple:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;name: main
title: Welcome to XXXX
version: final
nav:
  - modules/ROOT/nav.adoc&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;donde lo más destacado es el atributo &lt;code&gt;name&lt;/code&gt; que da un nombre al módulo para ser encontrado cuando usemos &lt;code&gt;xref&lt;/code&gt;.
Versión es un identificador de texto y sirve para cualificar aún más cuando usemos &lt;code&gt;xref&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;public.yml&lt;/code&gt; es el fichero de configuración principal y en donde decimos qué repositorios, ramas, diseño, etc
vamos a usar para generar un site. Puede tener el nombre que quieras y puedes tener incluso varios cada uno de ellos
destinado a generar un site diferente (por ejemplo &lt;code&gt;public.yml&lt;/code&gt; para unir un moódulo de marketing junto con un
modulo de tu API, y otro &lt;code&gt;private.yml&lt;/code&gt; donde documentas la arquitectura interna, junto con código, etc)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;site:
  title: My Site
  url: https://blog.jagedn.dev
  start_page: main::index.adoc
content:
  sources:
    - url: ./
      start_path: docs
      branches: HEAD

    - url: https://gitlab.org/jorge-aguilera/product-api.git
      start_path: src/docs
      branches: [develop, 1.0]

ui:
  bundle:
    url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/master/raw/build/ui-bundle.zip?job=bundle-stable
    snapshot: true

asciidoc:
  extensions:
    - asciidoctor-plantuml
  attributes:
    plantuml-server-url: &apos;http://www.plantuml.com/plantuml&apos;
    plantuml-fetch-diagram: true

output:
  clean: true
  dir: ./build/public
  destinations:
    - provider: archive&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente le indicamos a antora qué repositorios queremos utilizar (en este ejemplo el propio repositorio más
otro que se aloja en gitlab.org llamado product-api dentro del grupo jorge-aguilera). Además le indicamos de cada
repositorio qué branch queremos usar. Puedes indicar varios branch separandolos por coma.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;También le indicamos qué diseño usar (ui.zip) el cual tiene que ser una url a un fichero zip (local
o una URL de donde obtenerlo). Por ahora vamos a usar un bundle público por defecto de Antora alojado en gitlab.com&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mediante &lt;code&gt;output&lt;/code&gt; configuraremos el directorio de salida (recuerda que al poder tener varios ficheros yml puedes
hacer que cada uno de ellos genere en un directorio diferente)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;servicios&quot;&gt;Servicios&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otra parte el equipo de Servicios ha creado una estructura similar en un directorio de su proyecto (por ejemplo
&lt;code&gt;src/docs&lt;/code&gt; tal como indicamos en &lt;code&gt;public.yml&lt;/code&gt; en el apartado anterior)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-854b30c22709ee167251cda2d50d7e86.png&quot; alt=&quot;Diagram&quot; width=&quot;226&quot; height=&quot;385&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Recuerda que el directorio puede estar donde te interese en este proyecto y lo único que tenemos que especificarlo
en el &lt;code&gt;yaml&lt;/code&gt; que une los repositorios.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este ejemplo hemos creado dos módulos: ROOT e internal pero sólo vamos a incluir el primero como parte de la
documentación pública mientras que el otro lo podríamos usar en uno para cosas internas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo este proyecto tiene al menos 2 branches: &lt;code&gt;develop&lt;/code&gt; y &lt;code&gt;1.0&lt;/code&gt;. Para cada una de ellas Antora generará la
documentación que contengan de forma separada &lt;strong&gt;permitiendo al lector cambiar entre ambas versiones&lt;/strong&gt; en el mismo site&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;build&quot;&gt;Build&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para construir el site podemos instalar Antora y ejecutarla en local o bien si dispones de docker ejecutar el siguiente
comando:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;ANTORA_IMAGE=jagedn/antora-plantuml
docker run -u $UID --privileged -v $(pwd):/antora --rm -it $ANTORA_IMAGE --cache-dir=./.cache/antora public.yml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ANTORA_IMAGE es una imagen que he generado usando el Dockerfile oficial pero añadiendo la extensión de diagramas (no
incluida por defecto). Si quieres usar la imagen oficial simplemente cambialo por &lt;code&gt;antora/antora&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como puedes ver el comando monta el directorio actual como un directorio &lt;code&gt;/antora&lt;/code&gt; y le indica el yaml que queremos
usar. Tras ejecutarse el propio docker borra el contenedor con --rm.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo como usamos repositorios externos podemos bajarlos una sola vez y mantenerlos en una cache o quitar esta
opción si queremos que cada vez que ejecutemos el build se vuelva a bajar (recomendado al principio cuando estás
haciendo muchos cambios en ambos proyectos)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien únicamente resta publicar el contenido generado mediante algún sistema tipo Apache, Nginx, Github
Pages, etc copiando el directorio &lt;code&gt;build/public&lt;/code&gt; generado por el comando.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;referencias&quot;&gt;Referencias&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algunos sites generados con Antora:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;El propio Antora (&lt;a href=&quot;https://docs.antora.org&quot; class=&quot;bare&quot;&gt;https://docs.antora.org&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fedora (&lt;a href=&quot;https://docs.fedoraproject.org/en-US/&quot; class=&quot;bare&quot;&gt;https://docs.fedoraproject.org/en-US/&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Couchbase (&lt;a href=&quot;https://docs.couchbase.com/home/contribute/index.html&quot; class=&quot;bare&quot;&gt;https://docs.couchbase.com/home/contribute/index.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Puravida Software (&lt;a href=&quot;https://puravida-software.gitlab.io/main/index.html&quot; class=&quot;bare&quot;&gt;https://puravida-software.gitlab.io/main/index.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Breve introducción a Antora, un static site generator para proyectos multi-repositorio</summary>
    </entry>
    <entry>
        <title>Groogle Raffle</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/mn-raffle-idea.html"/>
        <updated>2020-03-22T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/mn-raffle-idea.html</id>
        <category term="groovy"/>
        <category term="micronaut"/>
        <category term="google"/>
        <content type="html">
            &lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;mn-raffle es (otra) implementación de un sistema para sortear premios entre una serie de participantes,
como por ejemplo los asistentes a una charla (presencial o virtual) usando GoogleSheet como repositorio para guardar
los datos de los participantes y del sorteo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A diferencia de la implementación &lt;a href=&quot;google-raffle.html&quot; class=&quot;bare&quot;&gt;google-raffle.html&lt;/a&gt;, puramente en javascript y embebida en una hoja GoogleSheet,
en esta ocasión vamos a usar una aplicación web, desarrollada en Micronaut para el backend y Vue para el frontend&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
El código del proyecto se encuentra alojado en &lt;a href=&quot;https://gitlab.com/groogle/mn-raffle&quot; class=&quot;bare&quot;&gt;https://gitlab.com/groogle/mn-raffle&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
La aplicación desplegada se encuentra en &lt;a href=&quot;https://mn-raffle-pvidasoftware.cloud.okteto.net/&quot; class=&quot;bare&quot;&gt;https://mn-raffle-pvidasoftware.cloud.okteto.net/&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
En este post NO voy a describir exhaustivamente paso a paso cómo se ha desarollado la aplicación, sino
las partes que considero más interesantes
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente, un usuario organizador de un sorteo accederá a la aplicación vía web la cual, a
través de llamadas REST solicitará datos de participantes, premios, etc contra el backend.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El backend a su vez no tendrá nada de persistencia sino que delegará en Google Sheet la misma.
El usuario deberá haberse identificado ante el sistema usando así mismo el mecanismo de
autentificación de Google.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;arquitectura&quot;&gt;Arquitectura&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-67519beae77171541f4ee0c6d2ca22d3.png&quot; alt=&quot;Diagram&quot; width=&quot;228&quot; height=&quot;612&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se puede ver en el diagrama, la aplicación se divide en los siguientes componentes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Una aplicación Vue (Javascript y HTML) que correrá en el navegador del usuario&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Un backend REST en Micronaut que devuelve la lista de participantes, premios y al que se
le indica quién ha ganado qué.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Google: GoogleSheet como persistencia y GoogleAuth como autentificación de usuarios (
para futuras funcionalidades que lo requieran)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La aplicación constará de 2 módulos (client y server) que podrán ser desarrollados de forma
independiente pero que se empaquetarán como un artefacto único para ser desplegado.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;proyecto&quot;&gt;Proyecto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya se ha mencionado, el proyecto va a ser un multi-module de Gradle, client y backend.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-ad7f45157c449483bc979bea9c6444d1.png&quot; alt=&quot;Diagram&quot; width=&quot;102&quot; height=&quot;175&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;client&quot;&gt;Client&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para la parte cliente (Vue) lo normal es usar gulp, o herramientas similares para construirlo, sin embargo gracias a
los plugins &lt;code&gt;npm&lt;/code&gt; que tenemos en gradle, vamos a usarlo tanto para construir la parte cliente como la parte server y
de esta forma tener una única tarea capaz de construir ambos y juntarlos en un único artefacto a desplegar.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por lo demás el proyecto semilla lo crearemos mediante &lt;code&gt;vue-cli&lt;/code&gt; siguiendo los tutoriales disponibles en la página
oficial de Vue siendo relevante que usaremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;bootstrap-vue para la parte visual&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;route para enrutar las diferentes partes de la aplicación&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vuex para gestionar el estado de la misma&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo crearemos un interface &lt;code&gt;service&lt;/code&gt; que sirva para dialogar con el backend con una implementación &lt;code&gt;fake&lt;/code&gt; a usar en el
desarrollo del cliente y evitar la necesidad de tener el backend levantado&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;backend&quot;&gt;Backend&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El server será una aplicación micronaut típica con los siguientes componentes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;micronaut security, en concreto usaremos Google para la autentificación&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;groogle-sheet, un DSL que nos permite acceder a una hoja Google de forma fácil&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Debido a que el backend accederá a servicios remotos en Google,
deberemos de crear un proyecto en Google Cloud Platform para obtener unas credenciales de servicio.
Descargaremos el fichero JSON que las contiene pero nos aseguraremos que &lt;strong&gt;NO se versiona junto al código&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para ejecutar el server y poder acceder a la hoja de cálculo que nos indique el usuario deberemos tener una variable
de entorno GOOGLE_APPLICATION_CREDENTIALS apuntando a la ruta completa del fichero JSON anterior.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;google&quot;&gt;Google&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La persistencia se va a realizar utilizando GoogleSheet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El organizador del sorteo podrá crear tantos documentos y hojas como desee, correspondiendo
cada una de ellas a un sorteo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En la hoja el organizador dispondrá de los nombres de los participantes en una columna,
los premios a sortear en otra junto con la cantidad de cada uno de ellos y el sistema anotará a qué participante
le ha tocado qué premio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si se desea se puede proporcionar un email por cada participante para notificarle vía correo
que ha sido agraciado con un premio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La imagen siguiente es un ejemplo con las filas y columnas a utilizar en cada hoja:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/mn-raffle.png&quot; alt=&quot;mn raffle&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Para que el backend pueda acceder al documento este deberá ser compartido con la cuenta de servicio que se
creó en Google Cloud Console.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;backend_2&quot;&gt;Backend&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se ha comentado el backend será una aplicación Micronaut ofreciendo un API:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-43163a1c8a858e3fb35210a7a9243b74.png&quot; alt=&quot;Diagram&quot; width=&quot;909&quot; height=&quot;125&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mediante &lt;code&gt;UserController&lt;/code&gt; el front podrá obtener detalles del organizador del sorteo (previa autentificación del mismo
usando Google como proveedor de autentificación) como el nombre y el email. Sólo se usa para para fines estadísticos
de uso.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RaffleController&lt;/code&gt; es el encargado de ofrecer la lista de participantes así como de precios disponibles en cada momento.
Requiere para ello que se le indique el &lt;code&gt;id&lt;/code&gt; de la hoja de Google así como el nombre del tab que se quiere usar para
ese sorteo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo acepta que tras un sorteo se le indique quién ha salido ganador y aceptado el premio o bien si no está
presente para no volver a ofrecerlo en la lista de participantes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para que el backend pueda acceder a la hoja indicada el propietario de esta deberá haberla compartido con una
cuenta de servicio creada para ello a través de las opciones de compartir disponible en la propia hoja de GoogleSheet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Raffle usa el DSL &apos;Groogle&apos; para leer y escribir en la hoja. Por ejemplo, para leer la lista de participantes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;List&amp;lt;Participant&amp;gt; loadParticipants(String sheetId, String tabId){
    List people = []

    sheetService.withSpreadSheet sheetId, {
        withSheet tabId, {
            writeRange&quot;A3&quot;, &quot;C99&quot;, {
                get().eachWithIndex{ def entry, int i -&amp;gt;    &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
                    if( entry[0] &amp;amp;&amp;amp; !entry[1])
                        people.add new Participant(name:entry[0], email: entry[2])
                }
            }
        }
    }
    people.sort { Math.random() }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Leemos un rango de filas-columnas y rellenamos un array con aquellas que tienen datos&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para escribir en la hoja, por ejemplo si un participante no ha acudido y no volver a ofrecerlo, el código sería:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt; Boolean notPressent(String sheetId, String tabId, String name){
    sheetService.withSpreadSheet sheetId, {
        withSheet tabId, {
            List&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; range = writeRange(&quot;A3&quot;, &quot;C99&quot;).get() &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
            List&amp;lt;String&amp;gt; rwinner = range.find{ it[0] &amp;amp;&amp;amp; it[0] == name}
            rwinner[1] = &quot;NOT PRESSENT&quot;
            writeRange(&quot;A3&quot;,&quot;C99&quot;).set(range)   &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
        }
    }
    true

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Leemos un rango determinado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;escribimos un array en la hoja&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se ha mencionado, el backend contendrá a su vez el build del client como recursos incluidos en el propio jar.
De esta forma evitamos requerir otro componente para servir la parte html+javascript&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;client_2&quot;&gt;Client&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El client va a consistir en una aplicación Vue (con route+state y bootstrap-vue para la parte visual)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al ser un submodulo del proyecto Gradle, podemos usar el mismo IDE a la vez que mantenemos alineados los dos proyectos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo único que neceitamos para ello es establecer &quot;un puente&quot; entre Gradle y npm para lo que usaremos el plugin
&lt;code&gt;org.ysb33r.nodejs.npm&lt;/code&gt; de Gradle y crearemos unas task especificas para construir y ejecutar la parte cliente&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;plugins {
    id &apos;org.ysb33r.nodejs.base&apos;  version &apos;0.6.2&apos;
    id &apos;org.ysb33r.nodejs.npm&apos;   version &apos;0.6.2&apos;
}

task npmInstall( type : org.ysb33r.gradle.nodejs.tasks.NpmTask ) {
    group &apos;build&apos;
    description = &apos;Install dependencies&apos;
    command &apos;install&apos;
}

task start( type : org.ysb33r.gradle.nodejs.tasks.NpmTask ) {
    group &apos;build&apos;
    description = &apos;Run the client app&apos;
    command &apos;run&apos;
    cmdArgs &apos;serve&apos;
}

//others task&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;state&quot;&gt;State&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La aplicación va a consistir en unos estados muy simples&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/store/index.ts&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;class State {

    busy = false;

    user = {
        name:&apos;&apos;,
        email:&apos;&apos;
    };

    googleForm = {
        sheetId:&apos;&apos;,
        tabId:&apos;&apos;
    };

    participants = [];
    prizes =  [];

    winner =  &apos;&apos;;
    prize =  &apos;&apos;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;con unas actions también simples:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/store/index.ts&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;    actions: {
    //....
        fetchParticipants( context ){
            return api
                .loadParticipants(context.state.googleForm)
                .then((participants: any) =&amp;gt; context.commit(&apos;participants&apos;, participants))
        },
    //....
        raffle( context, prize ){
            const arr = context.state.participants
            const winner = arr[Math.floor(Math.random() * arr.length)]
            context.commit(&apos;prize&apos;, prize)
            context.commit(&apos;winner&apos;, winner[&apos;name&apos;])
        },
        acceptWinner( context){
            return api
                .winner( context.state.googleForm, context.state.prize, context.state.winner)
                .then( () =&amp;gt; context.dispatch(&apos;fetchPrizes&apos;) )
                .then( () =&amp;gt; context.dispatch(&apos;fetchParticipants&apos;) )
        },
        notPressent(context){
            return api
                .notPressent( context.state.googleForm, context.state.winner)
                .then( () =&amp;gt; context.dispatch(&apos;fetchParticipants&apos;) )
        }
    },&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usando la configuración por entorno de Vue podremos indicar al store qué implementación de API usar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;env.development&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;VUE_APP_API_CLIENT = &apos;mock&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;env.production&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;VUE_APP_API_CLIENT = &apos;server&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;service&quot;&gt;Service&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para poder desarrollar el front sin depender de tener corriendo el backend, vamos a usar un &lt;code&gt;mock&lt;/code&gt; que devolverá
datos de pruebas contenidos en un JSON con un cierto retraso, simulando que se está realizando una petición http:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;src/services/mock/index.ts&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;const user = mock.user
const prizes = mock.prizes
const vparticipants = mock.participants

const mockFetchData = (mockData: any, time = 0) =&amp;gt; {
    return new Promise((resolve) =&amp;gt; {
        setTimeout(() =&amp;gt; {
            resolve(mockData)
        }, time)
    })
}

export default new class {
    loadParticipants(groogleData: any){
        return mockFetchData(vparticipants, 1000) // wait 1s before returning posts
    }
    // others methods
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;componentes&quot;&gt;Componentes&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último tendremos una serie de componentes aislados entre sí y desacoplados del api mediante el &lt;code&gt;store&lt;/code&gt; anterior&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así un componente como el de capturar la hoja y tab a usar, &lt;code&gt;SheetInput&lt;/code&gt;, simplemente se adjunta al &lt;code&gt;state&lt;/code&gt; y cuando el
usuario rellena el formulario este se actualiza automaticamente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;    &amp;lt;b-input
            id=&quot;inline-form-input-tab&quot;
            v-model=&quot;$store.state.googleForm.tabId&quot;
            class=&quot;mb-2 mr-sm-2 mb-sm-0&quot;
            required
            placeholder=&quot;Sheet 1&quot;&amp;gt;
    &amp;lt;/b-input&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cuando el usuario pulsa el botón de cargar, el componente ejecutará un &lt;code&gt;action&lt;/code&gt; del &lt;code&gt;store&lt;/code&gt; que cargue los participantes
y premios:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;SheetInput.vue&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;    ...
    &amp;lt;b-form @submit=&quot;load&quot; v-if=&quot;$store.state.user.name&quot; inline&amp;gt;
    ...

    @Component
    export default class SheetInput extends Vue {

        private busy = false

        load() {
            this.$store.dispatch(&apos;fetchPrizes&apos;)
            this.$store.dispatch(&apos;fetchParticipants&apos;)
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo &lt;code&gt;WinnerModal&lt;/code&gt; es un componente que mostrará un diálogo modal cuando el sistema eliga un ganador de un premio
para que el organizador pueda indicar si lo quiere o no o incluso si no está presente. Para ello se subscribe como
un listener del &lt;code&gt;$store&lt;/code&gt; esperando a que se produzca una mutación del &lt;code&gt;winner&lt;/code&gt; momento en el cual simplemente
mostrará el diálogo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;created(){
    this.$store.subscribe( (mutation, state) =&amp;gt;{
        if( mutation.type === &apos;winner&apos;){
            this.lastWinner = state.winner
            this.$bvModal.show(&apos;modal-winner&apos;)
        }
    })
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;build_and_deploy_okteto&quot;&gt;Build and deploy (Okteto)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que queramos desplegar una nueva versión simplemente ejecutaremos &lt;code&gt;./gradlew assembleServerAndClient&lt;/code&gt; la cual
preparará un jar con los dos componentes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo mediante el plugin &lt;code&gt;jib&lt;/code&gt; de Gradle podremos generar y subir una imagen Docker con la aplicación a DockerHub
(o a tu repositorio).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como plataforma donde desplegar la aplicación he elegido Okteto (&lt;a href=&quot;https://okteto.com&quot; class=&quot;bare&quot;&gt;https://okteto.com&lt;/a&gt;) el cual cuenta con un plan gratuito
muy generoso donde ejecutar este tipo de aplicaciones usando Kubernetes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para kubernetizar la aplicación simplemente usamos el fichero que nos creó micronaut (con algunos ajustes)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;k8s.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: &quot;mn-raffle&quot;
spec:
  selector:
    matchLabels:
      app: &quot;mn-raffle&quot;
  template:
    metadata:
      labels:
        app: &quot;mn-raffle&quot;
    spec:
      volumes:
      - name: google-cloud-key
        secret:
          secretName: mn-raffle-key
      containers:
        - name: &quot;mn-raffle&quot;
          image: &quot;jagedn/mn-raffle&quot;
          imagePullPolicy: &quot;Always&quot;
          volumeMounts:
            - name: google-cloud-key
              mountPath: /var/secrets/google
          env:
            - name: GOOGLE_APPLICATION_CREDENTIALS
              value: /var/secrets/google/client_secret.json
            - name: MICRONAUT_SECURITY_OAUTH2_CLIENTS_GOOGLE_CLIENT_SECRET
              valueFrom:
                configMapKeyRef:
                  name: mn-raffle
                  key: google_client_secret
            - name: MICRONAUT_SECURITY_OAUTH2_CLIENTS_GOOGLE_CLIENT_ID
              valueFrom:
                configMapKeyRef:
                  name: mn-raffle
                  key: google_client_id
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
            failureThreshold: 10
---
apiVersion: v1
kind: Service
metadata:
  name: &quot;mn-raffle&quot;
  annotations:
    dev.okteto.com/auto-ingress: &quot;true&quot;
spec:
  selector:
    app: &quot;mn-raffle&quot;
  type: ClusterIP
  ports:
    - port: 8080
      protocol: TCP
      targetPort: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;mediante el comando `kubectl apply -f k8s.yml&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(como se puede observar las credenciales se encuentran guardadas en un configMap dentro del kubernetes)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejemplo&quot;&gt;Ejemplo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación se muestran algunas pantallas de cómo sería un sorteo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/mn-raffle-1.png&quot; alt=&quot;mn raffle 1&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. FormEntry&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/mn-raffle-2.png&quot; alt=&quot;mn raffle 2&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. PariticpantsAndPrizes&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/mn-raffle-3.png&quot; alt=&quot;mn raffle 3&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 3. Winner&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/mn-raffle-4.png&quot; alt=&quot;mn raffle 4&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 4. DiscountPrizeAfterWinner&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Groogle Raffle, una aplicación para sorteo de premios entre participantes basada en Google Sheet</summary>
    </entry>
    <entry>
        <title>Introducción a Asciidoc y Asciidoctor (5): diagramas</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/intro-asciidoctor-5.html"/>
        <updated>2020-02-25T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/intro-asciidoctor-5.html</id>
        <category term="asciidoc"/>
        <category term="documentation"/>
        <category term="write"/>
        <category term="diagram"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Este post es la continuación a &lt;a href=&quot;intro-asciidoctor-4.html&quot; class=&quot;bare&quot;&gt;intro-asciidoctor-4.html&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Yo utilizo sistemas Linux por lo que los comando que indico para instalar software estan orientados a este.
Si tienes interés en saber cómo hacerlo en Windows, escribeme y lo investigaré
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;objetivo&quot;&gt;Objetivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post veremos cómo crear diferentes tipos de diagramas usando sólo un editor de texto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los tipos de diagramas son variados y de diferentes ámbitos como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Diagramas de clases, relaciones, actividades, etc tipicos de UML&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Diagramas de arquitectura software&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Diagramas Gant para organización de proyectos, tareas, equipos, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Representación de componentes UI (textbox, listbox, buttons, checks, etc)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gramatica, para representar partes de una sintaxis por ejemplo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Otros&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requisitos&quot;&gt;Requisitos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El primer requisito que necesitamos es tener instalado  asciidoctor, tal como se ha explicado
en los post anteriores de esta serie.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Para este post vamos a usar asciidoctorj
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$sdk man install asciidoctorj&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La instalacion de asciidoctorj incluye por defecto el segundo requisito que necesitaremos para dotar a nuestros documentos
de diagramas, &lt;code&gt;asciidoctor-diagram&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En caso de que estés usando la versión ruby deberás instalar la gema correspondiente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ gem install asciidoctor-diagram&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En realidad &lt;em&gt;asciidoctor_diagram&lt;/em&gt; por si mismo NO genera ningun diagrama, sino que podriamos decir que es el &quot;pegamento&quot;
entre &lt;em&gt;asciidoctor&lt;/em&gt; y los diferentes generadores de diagramas. En la página oficial podemos ver los tipos soportados:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;The extensions supports the AsciiToSVG, BlockDiag (BlockDiag, SeqDiag, ActDiag, NwDiag),
Ditaa, Erd, GraphViz, Mermaid, Msc, PlantUML, Shaape, SvgBob, Syntrax, UMLet, Vega,
Vega-Lite and WaveDrom syntax.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Esta lista no es exclusiva, pudiendo incorporar nuevos interpretes gracias al modelo de extensiones
de Asciidoctor. Yo mismo he hecho alguna extension en Java y es realmente fácil
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si utilizamos el método indicado anteriormente para la instalación de Asciidoctor (AsciidoctorJ) tendremos incluidos
por defecto los diagramas Ditaa y PlantUML (tal vez algun otro) asi que si vamos a usar algun otro tipo tendremos que
tener instalado el ejecutable correspondiente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De forma generica, y como consejo, yo siempre instalo Graphviz puesto que muchos de estos generadores lo usan&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$sudo apt-get install graphviz&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;mermaid&quot;&gt;Mermaid&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si por ejemplo queremos generar diagramas usando Mermaid tendremos que ejecutar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ npm install -g mermaid.cli&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;o si usamos yarn&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ yarn global add mermaid.cli&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;syntrax&quot;&gt;Syntrax&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por su parte, syntrax esta desarrollado en python por lo que tendremos que tener instalado este software y ejecutar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$pip install --upgrade syntrax&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Efectivamente, como has podido adivinar te toca conocer la sintaxis de cada generador que quieras utilizar.
Asciidoctor-diagram solo sirve como puente entre tu documento y el generador
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;sintaxis&quot;&gt;Sintaxis&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La potencia de poder adjuntar a nuestra documentacion diagramas definidos en formato texto es inmensa, pudiendo aprovechar
todas las caracteristicas propias de la filosofía &quot;doc-as-code&quot;, es decir, crear y manejar nuestra documentación igual
que lo hacemos con el código: editores de texto, versionable, mergear ramas, aprovaciones, despliegues continuos, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;include_implícito&quot;&gt;Include implícito&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Asi pues la sintaxis para definir un diagrama a lo largo de nuestra documentación consiste simplemente en crear un
bloque donde se especifique el tipo de diagrama que vamos a usar e incluir el cuerpo del diagrama dentro del mismo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por ejemplo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;asciidoctor&quot;&gt;Este parrafo es parte de nuestra documentacion, donde queremos poner a continuacion un diagrama de clases

plantuml::fichero_mi_diagrama.txt[]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;donde &lt;em&gt;fichero_mi_diagrama.txt&lt;/em&gt; puede ser un fichero de texto como el siguiente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;class Car

Driver - Car : drives &amp;gt;
Car *- Wheel : have 4 &amp;gt;
Car -- Person : &amp;lt; owns&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De tal forma que nuestro documento se veria como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Este parrafo es parte de nuestra documentación, donde queremos poner a continuación un diagrama de clases&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-ce10eaf325fe034a3c9fe082d7ae5d2e.png&quot; alt=&quot;Diagram&quot; width=&quot;376&quot; height=&quot;193&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;bloques&quot;&gt;Bloques&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si no quieres crear más ficheros también puedes usar bloques para incluir tus diagramas junto con la documentacion:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Este parrafo es parte de nuestra documentación, donde queremos poner a continuación un diagrama de clases

[plantuml]
++++
class Car

Driver - Car : drives &amp;gt;
Car *- Wheel : have 4 &amp;gt;
Car -- Person : &amp;lt; owns
++++&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y el resultado sería el mismo.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejemplos&quot;&gt;Ejemplos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuacion algunos diagramas de ejemplos generados con Ditaa, PlantUML, Mermaid y Syntrax&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;diseño_software&quot;&gt;Diseño Software&lt;/h3&gt;

&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;procesos&quot;&gt;Procesos&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[ditaa]
....
                   +-------------+
                   | Asciidoctor |-------+
                   |   diagram   |       |
                   +-------------+       | PNG out
                       ^                 |
                       | ditaa in        |
                       |                 v
 +--------+   +--------+----+    /---------------\
 |        | --+ Asciidoctor +--&amp;gt; |               |
 |  Text  |   +-------------+    |   Beautiful   |
 |Document|   |   !magic!   |    |    Output     |
 |     {d}|   |             |    |               |
 +---+----+   +-------------+    \---------------/
     :                                   ^
     |          Lots of work             |
     +-----------------------------------+
....&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/ditaa.png&quot; alt=&quot;Ditaa box&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;uml&quot;&gt;UML&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;(incluir complex.txt)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/complex.png&quot; alt=&quot;Clases&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;(incluir parallel.txt)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/parallel.png&quot; alt=&quot;Activiy Parallel&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;(incluir conditional.txt)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/conditional.png&quot; alt=&quot;Activiy Conditional&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[mermaid]
++++
classDiagram
	Animal &amp;lt;|-- Duck
	Animal &amp;lt;|-- Fish
	Animal &amp;lt;|-- Zebra
	Animal : +int age
	Animal : +String gender
	Animal: +isMammal()
	Animal: +mate()
	class Duck{
		+String beakColor
		+swim()
		+quack()
	}
	class Fish{
		-int sizeInFeet
		-canEat()
	}
	class Zebra{
		+bool is_wild
		+run()
	}
++++&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/mermaid.png&quot; alt=&quot;Clases con Mermaid&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;arquitectura&quot;&gt;Arquitectura&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[plantuml]
++++

!define SPRITESURL https://raw.githubusercontent.com/rabelenda/cicon-plantuml-sprites/v1.0/sprites
!includeurl SPRITESURL/tomcat.puml
!includeurl SPRITESURL/kafka.puml
!includeurl SPRITESURL/java.puml
!includeurl SPRITESURL/cassandra.puml
!includeurl SPRITESURL/python.puml
!includeurl SPRITESURL/redis.puml


title Cloudinsight sprites example

skinparam monochrome true

rectangle &quot;&amp;lt;$tomcat&amp;gt;\nwebapp&quot; as webapp
queue &quot;&amp;lt;$kafka&amp;gt;&quot; as kafka
rectangle &quot;&amp;lt;$java&amp;gt;\ndaemon&quot; as daemon
rectangle &quot;&amp;lt;$python&amp;gt;\ndaemon2&quot; as daemon2
database &quot;&amp;lt;$cassandra&amp;gt;&quot; as cassandra
database &quot;&amp;lt;$redis&amp;gt;&quot; as redis

webapp -&amp;gt; kafka
kafka -&amp;gt; daemon
kafka -&amp;gt; daemon2
daemon --&amp;gt; cassandra
daemon2 --&amp;gt; redis
++++&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/arquitectura.png&quot; alt=&quot;Arquitectura software&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;gestion_proyectos&quot;&gt;Gestion proyectos&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[mermaid]
++++
gantt
	title A Gantt Diagram
	dateFormat  YYYY-MM-DD
	section Section
	A task           :a1, 2014-01-01, 30d
	Another task     :after a1  , 20d
	section Another
	Task in sec      :2014-01-12  , 12d
	another task      : 24d
++++&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/grant.png&quot; alt=&quot;Grant&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;uiux&quot;&gt;UI/UX&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;(incluir salt.txt)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/salt.png&quot; alt=&quot;UI&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/salt2.png&quot; alt=&quot;UI&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/salt3.png&quot; alt=&quot;UX&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;sintaxis_2&quot;&gt;Sintaxis&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;(incluir syntrax1.txt)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/syntrax1.png&quot; alt=&quot;Railroad&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;otros&quot;&gt;Otros&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/snippets/1779830/raw&quot;&gt;Source Tabla periódica&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/blog/2020/diagram/tablaperiodica.png&quot; alt=&quot;Tabla periodica&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Breve introducción al lenguaje de marcado asciidoc y su ecosistema, parte 5</summary>
    </entry>
    <entry>
        <title>Build a Telegram Bot using Netlify</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/telegram-bot-netlify.html"/>
        <updated>2020-02-01T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/telegram-bot-netlify.html</id>
        <category term="telegram"/>
        <category term="bot"/>
        <category term="netlify"/>
        <category term="javascript"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post I&amp;#8217;ll show you how to build a (simple) bot for Telegram using Netlify lambda functions for execution.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;tocamos_a_bot_divide_an_expense&quot;&gt;&quot;Tocamos a&amp;#8230;&amp;#8203;&quot; bot (divide an expense)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;@tocamosbot is an &lt;code&gt;inline&lt;/code&gt; Bot who accepts a number (a bill for example), ask for how many participants are in
the group and returns the division or how much every participant needs to pay (a simple division)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/tocamos1.png&quot; alt=&quot;tocamos1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;NOTE&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Yes, you can do the same with a calculator but whit @tocamosbot the result is posted in the Chat ;)&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll need:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a Telegram account in your mobile phone&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a Netlify account (free tier) where host our bot&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a Github/Gitlab/Bitbucket repository where upload our code. Netlify will read from there&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;first_steps&quot;&gt;First steps&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Firstly we&amp;#8217;ll use the @BotFather bot (from Telegram) to create our bot, so find this bot and start a &lt;em&gt;conversation&lt;/em&gt;
with it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/tocamos2.png&quot; alt=&quot;tocamos2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And after you can customize your bot:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/tocamos3.png&quot; alt=&quot;tocamos3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The most &lt;strong&gt;IMPORTANT&lt;/strong&gt; thing in this conversation is &lt;strong&gt;NOT TO SHARE THE API TOKEN&lt;/strong&gt;. Be sure you don&amp;#8217;t store it into
your code, use Environment variables to access it.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;coding&quot;&gt;Coding&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In an empty directory create following files:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;package.json&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &quot;name&quot;: &quot;tocamosbot&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;&quot;,
  &quot;main&quot;: &quot;functions/tocamos.js&quot;,
  &quot;dependencies&quot;: {
    &quot;netlify-lambda&quot;: &quot;^2.0.1&quot;,
    &quot;telegraf&quot;: &quot;^3.36.0&quot;
  },
  &quot;scripts&quot;: {
    &quot;postinstall&quot;: &quot;netlify-lambda install&quot;,
    &quot;buildNetlify&quot;: &quot;netlify-lambda build functions&quot;,
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;,
    &quot;devNetlify&quot;: &quot;netlify-lambda serve functions&quot;
  },
  &quot;keywords&quot;: [],
  &quot;author&quot;: &quot;&quot;,
  &quot;license&quot;: &quot;ISC&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;netlify.toml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;[build]
  publish = &quot;build&quot;
  command = &quot;find . -name \*.mjs -type f -delete; npm run buildNetlify&quot;
  functions = &quot;build/functions&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create a &lt;code&gt;functions&lt;/code&gt; subdirectory and create following file into it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;functions/tocamosa.js&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;const Telegraf = require(&apos;telegraf&apos;);
const startAction = require(&apos;./tocamosa/start&apos;)
const inlineAction = require(&apos;./tocamosa/inline&apos;)
const bot = new Telegraf(process.env.TOCAMOSA_BOT_TOKEN);

bot.start(ctx =&amp;gt; {
return startAction(ctx)
})

bot.on(&apos;inline_query&apos;, (ctx) =&amp;gt; {
return inlineAction(ctx)
})

exports.handler = async event =&amp;gt; {
await bot.handleUpdate(JSON.parse(event.body));
return { statusCode: 200, body: &apos;&apos; };
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Create another subdirectory &lt;code&gt;functions/tocamosa&lt;/code&gt; and put these files:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;functions/tocamosa/start.js&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;module.exports = async (ctx, porciento) =&amp;gt; {
	return ctx.reply(`Hi`) // better explain what the bot does
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;functions/tocamos/inline.js&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;module.exports = async (ctx) =&amp;gt; {
    const search = (ctx.inlineQuery.query || &quot;&quot;)
    if ( search===&quot;&quot; || isNaN(search)) {
        return
    } else {
        const answer = []
        const tocamos = [2,3,4,5,6,7,8,9,10]
        tocamos.forEach(function(tocamos) {
            answer.push({
                id: tocamos,
                title: tocamos+&quot; (&quot;+search+&quot; entre &quot;+tocamos+&quot;)&quot;,
                type: &apos;article&apos;,
                input_message_content: {
                    message_text: &quot;Tocais cada uno a &quot; + (Math.round(search/tocamos)*100)/100+&quot; (&quot;+search+&quot; entre &quot;+tocamos+&quot;)&quot;,
                    parse_mode: &apos;HTML&apos;
                }
            })
        })
        return ctx.answerInlineQuery(answer)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The code is very simple:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;tocamosa.js&lt;/code&gt; is the entry point to our bot and we prepare all available commands to redirect to the right function.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;start.js&lt;/code&gt; is a simple response when an user start a conversation with the bot&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;inline.js&lt;/code&gt; has the main logic. It&amp;#8217;s called every time the user use the bot in an inline way and Telegram calls
the bot providing an argument with the text written by the user in &lt;code&gt;ctx.inlineQuery.query&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After check if the argument is a number we prepare a menu with 10 entries (more than 10 is a wedding and the fathers pay)
putting in an array 10 elements.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Every element has an &lt;code&gt;index&lt;/code&gt; (required but you can use whatever you want as Id), a &lt;code&gt;title&lt;/code&gt; to show and a
message_text to show in case the element is selected.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;commit_and_push&quot;&gt;Commit and push&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You need to commit and push all files into your git repository. For example, after created the repo into your Git
provider:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;git init
git repository add [url-to-your-repository]
git commit -a -m &quot;First commit, as usual&quot;
git push&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Check against your Git provider that all the files are uploaded and they follow the right structure&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;netlify&quot;&gt;Netlify&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once you have a Netlify account you need to create a new &lt;code&gt;site&lt;/code&gt; from your Git repository using &lt;code&gt;New site from Git&lt;/code&gt;
and linked it with your repository (you&amp;#8217;ll need to authorize the access)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;From now, every push you do in your repository will fire a &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;deploy&lt;/code&gt; process into Netlify.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the last steps (or once deploy into the Build section) you can provide the API TOKEN of your bot using the
Environment section.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As we are using in our code &lt;code&gt;const bot = new Telegraf(process.env.TOCAMOSA_BOT_TOKEN);&lt;/code&gt; we need to set a new
environment &lt;code&gt;TOCAMOSA_BOT_TOKEN&lt;/code&gt; with the token obtained from the BotFather&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If all was deployed ok you&amp;#8217;ll find in the &lt;code&gt;Functions&lt;/code&gt; section the url of your bot. Something similar to
&lt;code&gt;&lt;a href=&quot;https://a-random-name-choosen-by-netlify.netlify.com/.netlify/functions/tocamosa&quot; class=&quot;bare&quot;&gt;https://a-random-name-choosen-by-netlify.netlify.com/.netlify/functions/tocamosa&lt;/a&gt;&lt;/code&gt; , grab this URL and go to this
URL:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://api.telegram.org/botYOURBOT:API_TOKEN_HERE/setWebhook?url=YOUR_NETLIFY_URL&quot; class=&quot;bare&quot;&gt;https://api.telegram.org/botYOURBOT:API_TOKEN_HERE/setWebhook?url=YOUR_NETLIFY_URL&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(for example &lt;a href=&quot;https://api.telegram.org/bot123123:AAG12bdbsdfdsHXqBOBPACmXKnz6mBLHmGY/setWebhook?url=https://a-random-name-choosen-by-netlify.netlify.com/.netlify/functions/tocamosa&quot; class=&quot;bare&quot;&gt;https://api.telegram.org/bot123123:AAG12bdbsdfdsHXqBOBPACmXKnz6mBLHmGY/setWebhook?url=https://a-random-name-choosen-by-netlify.netlify.com/.netlify/functions/tocamosa&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;With this URL what we&amp;#8217;re doing is to notify to Telegram where can locate our bot&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now you can test your bot in a chat and if all it&amp;#8217;s ok the bot will answer with the menu.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
With the free tier of Netlify we have 125K request per month
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post we are see many things:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;How to create, develop and configure a simple bot&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;How to answer with a simple message and how to build an inline menu&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;How to deploy automatically in Netlify a Git repository&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Keep secret our TOKEN using environment variables&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Learnt how to build a simple Telegram bot and deploy it using Netlify lambda functions</summary>
    </entry>
    <entry>
        <title>Build a Raffle with Google Sheet</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/google-raffle.html"/>
        <updated>2020-02-01T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/google-raffle.html</id>
        <category term="google"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
This post was published in Dev.to (&lt;a href=&quot;https://dev.to/jagedn/build-a-raffle-with-google-sheet-part-2-1n1a&quot; class=&quot;bare&quot;&gt;https://dev.to/jagedn/build-a-raffle-with-google-sheet-part-2-1n1a&lt;/a&gt;)
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this post we&amp;#8217;ll see how to build a simple application using Google Sheet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The application consists in a raffle from a list of assistant to an event from
we have the names and surnames in a sheet:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/google-raffle1.png&quot; alt=&quot;google raffle1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;raffle_the_idea&quot;&gt;Raffle, the idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After installing our raffle function, a new option&amp;#8217;ll appear in the main menu of the sheet called Raffle with a sub-item Raffle.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When the user select this item a sidebar&amp;#8217;ll appear with a (customizable) interface with a button to start a raffle:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/google-raffle2.png&quot; alt=&quot;google raffle2&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;At this moment the application will choose a random element from the sheet and show it.
If the participant is present (and want the prize) the user will click at yepes or nopes it the user reject the prize:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2020/google-raffle3.png&quot; alt=&quot;google raffle3&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In both case the application will mark the participant as &quot;used&quot; to avoid pick him again.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The admin can repeat the raffle as many times he want meanwhile remain participants.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;coding_the_ui&quot;&gt;Coding the UI&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Select Tools / Script commands from main menu and create a new file called &lt;code&gt;Client.html&lt;/code&gt; and another file called
&lt;code&gt;Dialog.html&lt;/code&gt; plus the default &lt;code&gt;Code.gs&lt;/code&gt; file.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Client.html&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;base target=&quot;_top&quot;&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://ssl.gstatic.com/docs/script/css/add-ons1.css&quot;&amp;gt;
    &amp;lt;script src=&quot;//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;&amp;lt;?!= LanguageApp.translate(&apos;Bienvenido&apos;,&apos;&apos;,Session.getActiveUserLocale()) ?&amp;gt;&amp;lt;/h1&amp;gt;

    &amp;lt;div&amp;gt;
    &amp;lt;p&amp;gt;
    &amp;lt;?!= LanguageApp.translate(&apos;Total Participantes activos&apos;,&apos;&apos;,Session.getActiveUserLocale()) ?&amp;gt;
    &amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;
    &amp;lt;?!= totalRemains ?&amp;gt;
    &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;? for(var i in prizes){ ?&amp;gt;
    &amp;lt;div&amp;gt;

    &amp;lt;? for(var c=0; c &amp;lt; prizes[i][1]; c++){ ?&amp;gt;
    &amp;lt;p&amp;gt;
    &amp;lt;input type=&quot;button&quot; value=&quot;&amp;lt;?!=prizes[i][0]?&amp;gt;&quot; onclick=&quot;google.script.run.raffle(&apos;&amp;lt;?!=prizes[i][0]?&amp;gt;&apos;)&quot;/&amp;gt;
    &amp;lt;/p&amp;gt;
    &amp;lt;? } ?&amp;gt;

    &amp;lt;/div&amp;gt;
    &amp;lt;? } ?&amp;gt;

  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This will render the sidebar once the user select the Raffle option in the menu.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll show how many participants remains to participate and we&amp;#8217;ll build a list of buttons, once per prize.
In this way the admin can choose what prize to raffle in every moment&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see, when the admin click a prize button we&amp;#8217;ll call a remote function sending the prize selected.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Dialog.html&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&amp;lt;script&amp;gt;
var suertudoIdx = &amp;lt;?=suertudo[0]?&amp;gt;;
var prize = &apos;&amp;lt;?=prize?&amp;gt;&apos;;

function yepes(){
   google.script.run.withSuccessHandler(google.script.host.close).yepes(suertudoIdx,prize)
}

function nopes(){
  google.script.run.withSuccessHandler(google.script.host.close).nopes(suertudoIdx,prize)
}

function notPresent(){
  google.script.run.withSuccessHandler(google.script.host.close).notPresent(suertudoIdx,prize)
}
&amp;lt;/script&amp;gt;

&amp;lt;h1&amp;gt;
   Congratulations &amp;lt;?=suertudo[1]+&quot; &quot;+suertudo[2]?&amp;gt;
&amp;lt;/h1&amp;gt;

&amp;lt;p&amp;gt;
  &amp;lt;image src=&quot;https://media.giphy.com/media/11sBLVxNs7v6WA/giphy.gif&quot;/&amp;gt;
&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;
   &amp;lt;div&amp;gt;
     &amp;lt;input type=&quot;button&quot; value=&quot;Yepes&quot; onclick=&quot;yepes()&quot; /&amp;gt;
     &amp;amp;nbsp;
     &amp;lt;input type=&quot;button&quot; value=&quot;Nopes&quot; onclick=&quot;nopes()&quot; /&amp;gt;
     &amp;amp;nbsp;
     &amp;lt;input type=&quot;button&quot; value=&quot;No Present&quot; onclick=&quot;notPresent()&quot; /&amp;gt;
   &amp;lt;/div&amp;gt;
&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This file is the template to render the winner of a prize and let to choose an action (accept, denied, and not present)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once the admin click one of buttons following actions happen:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;the dialog call a remote function to notify the action selected&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;when the remote function is executed the dialog is closed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For example if the winner accept the prize the dialog will execute this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;google.script.run.withSuccessHandler(google.script.host.close).yepes(suertudoIdx,prize)&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;where yepes is a remote function&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;coding_the_business&quot;&gt;Coding the Business&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Paste this code into the &lt;code&gt;Code.gs&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;//Called by Google Sheet
function onOpen(e) {
  var ui = SpreadsheetApp.getUi();
  ui.createAddonMenu().addItem(&apos;Raffle&apos;, &apos;raffleUI&apos;).addToUi();
}

// Read G3:H999 searching prizzes (title and quantity)
function getPrizzes(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var ret = [];

  for(var i=3; i&amp;lt;ss.getLastRow()+1; i++){
    var row = ss.getRange(&quot;G&quot;+i+&quot;:H&quot;+i).getValues()[0];
    if( !row[0] ){
      break
    }
    // extract the title and the quantity
    ret.push( [row[0],row[1]] )
  }
  return ret;
}

// Write the G3:H999 range with new prizzes status
function updatePrizzes(prizzes){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  ss.getRange(&quot;G3:H&quot;+(2+prizzes.length)).setValues(prizzes)
}

// Search particpants without prizzes in range A3:D999
function getRemains(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var ret = [];

  for(var i=3; i&amp;lt;ss.getLastRow()+1; i++){
    var row = ss.getRange(&quot;A&quot;+i+&quot;:D&quot;+i).getValues()[0];
    if( !row[0] ){
      break
    }
    if( !row[3] ){
      // rowIndex, name and surname
      ret.push( [i,row[0],row[1]] )
    }
  }
  return ret;
}

// The winner accept the prize:
// update their cell and decrement the prize
function yepes(idx, prize){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var cell = sheet.getRange(&quot;D&quot;+idx);
  cell.setValue(&quot;winner of &quot;+prize);
  var prizzes = getPrizzes();
  for(var i in prizzes){
    if( prizzes[i][0] === prize ){
      prizzes[i][1]--;
      updatePrizzes(prizzes)
      break;
    }
  }

  raffleUI();
}

// the winner decline the prize: do nothing
function nopes(idx, prize){
  raffleUI();
}

// the winner is not pressent: bad guy
function notPresent(idx){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var cell = sheet.getRange(&quot;D&quot;+idx);
  cell.setValue(&quot;no present&quot;);
  raffleUI();
}

// Show the winner and ask if they want the prize
function raffle(prize){
  var remains = getRemains();
  var suertudoIndex = Math.floor(Math.random()*remains.length);
  var suertudo = remains[suertudoIndex];

  var template = HtmlService.createTemplateFromFile(&apos;Dialog&apos;);
      template.suertudo = suertudo;
      template.prize = prize;
  var html = template.evaluate();

  var htmlOutput = HtmlService
    .createHtmlOutput(html)
    .setWidth(640)
    .setHeight(480);

  SpreadsheetApp.getUi().showModalDialog(htmlOutput, &apos;Wooaaaaa&apos;);
}

// main: prepare a sidebar with prizzes
function raffleUI(){
  var remains = getRemains();
  var template = HtmlService.createTemplateFromFile(&apos;Client&apos;);
  template.totalRemains = remains.length;
  template.prizzes = getPrizzes();
  var html = template.evaluate();
      html.setTitle(&quot;Raffle&quot;)
      .setSandboxMode(HtmlService.SandboxMode.IFRAME)
      .setWidth(300);
  SpreadsheetApp.getUi().showSidebar(html);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;prepare_your_raffle&quot;&gt;Prepare your Raffle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In a clean tab write the participants and the prizes following this screen:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pay attention to use the same rows and columns or if you want to use different ranges remember
to adjust them into the &lt;code&gt;Code.gs&lt;/code&gt; file&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;see_in_action&quot;&gt;See in action&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this video you can see the raffle in action&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;videoblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;iframe src=&quot;https://www.youtube.com/embed/F8isRNpUJoU?rel=0&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Learnt how to build a simple Raffle using Google Sheet</summary>
    </entry>
    <entry>
        <title>Introducción a Asciidoc y Asciidoctor (4): crear una presentacion</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/intro-asciidoctor-4.html"/>
        <updated>2020-01-25T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/intro-asciidoctor-4.html</id>
        <category term="asciidoc"/>
        <category term="documentation"/>
        <category term="write"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Este post es la continuación a &lt;a href=&quot;intro-asciidoctor-3.html&quot; class=&quot;bare&quot;&gt;intro-asciidoctor-3.html&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;objetivo&quot;&gt;Objetivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post veremos cómo crear una presentación usando sólo un editor de texto (y asciidoctor
por supuesto).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
El objetivo es poder exponer de forma rápida y sencilla cualquier idea que tengas
y quieras transmitir, tanto a un público &quot;cercano&quot;, como compañeros, tu jefe, tu profesor, etc
o publicarlo en Internet bien como un documento HTML o incluso como un pdf en Slides o similar.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;a href=&quot;revealjs/charla.html&quot;&gt;Aquí&lt;/a&gt; para ver la versión HTML y aquí
puedes ver &lt;a href=&quot;revealjs/charla.adoc&quot;&gt;aquí&lt;/a&gt; el fichero para construirlo (texto plano)
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;instalación_sólo_1_vez&quot;&gt;Instalación (sólo 1 vez)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el primer post indicamos algunas de las formas que puedes usar para ejecutar asciidoctor,
bien sea usando la versión &lt;code&gt;ruby&lt;/code&gt; (la más popular), bien usando la versión &lt;code&gt;java&lt;/code&gt; (que puedes integrar
en tu aplicación como librería), etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para este ejemplo vamos a usar la versión &lt;code&gt;ruby&lt;/code&gt; por lo que tendrás que tener instalada esta versión.
Por ejemplo si usas una distribución Linux con apt puedes ejecutar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$ sudo apt-get install asciidoctor&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo siguiente, en el directorio que reservemos para nuestras presentaciones crearemos un fichero &lt;code&gt;Gemfile&lt;/code&gt;
para descargar las dependencias necesarias para poder crear presentaciones:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Gemfile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;ruby&quot;&gt;source &apos;https://rubygems.org&apos;

gem &apos;asciidoctor-revealjs&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y preparemos nuestro directorio&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ bundle config --local github.https true
$ bundle --path=.bundle/gems --binstubs=.bundle/.bin&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Si por ejemplo optas por usar AsciidoctorJ (Java), existe un plugin para Maven y otro para Gradle
que hace que todo esto sea innecesario y mucho más sencillo.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Aún más sencillo. Aunque no tengas conocimientos de programación, si tienes java instalado en tu máquina,
puedes descargarte esta plantilla con todo lo necesario para empezar a crear tus presentaciones:
&lt;a href=&quot;https://gitlab.com/jorge-aguilera/presentation-template&quot; class=&quot;bare&quot;&gt;https://gitlab.com/jorge-aguilera/presentation-template&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;reveal_js&quot;&gt;Reveal JS&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El backend de presentaciones revealjs, como su nombre indica, lo que va a usar es este famoso framework Javascript para
, partiendo de un documento .adoc, generar un .html con el javascript necesario incluido en él, pero sin necesidad
de tener ningún conocimiento de maquetación, html, css o javascript. Simplemente escribiendo en un fichero de texto
y usando los atributos asciidoctor del documento, podremos configurar el resultado, etc&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;estructura&quot;&gt;Estructura&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea básica del backend revealjs es que cada sección del documento corresponderá a una slide y el contenido de
cada sección será renderizado en esta, siendo la primera sección reservada a la slide de presentación&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Recuerda que una sección se crea comenzando una línea con &apos;=&apos; tantas veces repetidos como nivel de profundidad
queramos
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente en una slide insertaremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Párrafos de textos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Listas ordenadas o desordenadas de items&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;image, video&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tabla con datos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una imagen de fondo&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo querremos que nuestra presentación sea un poco amena y que las transiciones entre slides tengan algún efecto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación vamos a ir viendo cada uno de estos elementos paso a paso.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Según vayas practicando a crear presentaciones irás descubriendo que cada vez pierdes menos tiempo en &quot;cómo queda&quot; y
más en estructurar y mejorar el contenido.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;primera_iteración_la_portada&quot;&gt;Primera iteración, la portada&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero que vamos a hacer es crear la primera slide de presentación. Nuestra charla va a ser sobre XXXXXX por lo
que crearemos al inicio del documento la primera sección:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;asciidoctor&quot;&gt;= XXXXX : YYYYY
jorge.aguilera@puravida-software.com
#elije uno de: beige, black, league, night, serif, simple, sky, solarized, white
:revealjs_theme: league&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y generamos nuestra primera iteración:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;bundle exec asciidoctor-revealjs -a revealjsdir=https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.7.0 charla.adoc&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(donde charla.adoc es el nombre del fichero que estés creando para tu charla)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien tendrás un fichero HTML que puedes abrir con el navegador, y ver algo así
&lt;a href=&quot;revealjs/uno.html&quot; class=&quot;bare&quot;&gt;revealjs/uno.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Prueba a cambiar el atributo &lt;code&gt;revealjs_theme&lt;/code&gt; del documento con alguno de los valores indicados y vuelve a generar
la presentación. Verás que el tema se aplica y siendo la misma presentación el diseño cambia&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;slides_y_subslides&quot;&gt;Slides y &quot;subslides&quot;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como hemos mencionado antes, cada slide se define por una sección con &apos;==&apos;. Además RevealJS nos permite crear
&quot;subslides&quot;, o slides verticales, usando subsecciones con &apos;===&apos;. De esta forma podemos organizar los conceptos
a exponer de forma anidada, navegando entre los conceptos principales con las flechas derecha-izquierda y
los subconceptos con las flechas arriba-abajo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;= Titulo : subtitulo
:stem:

== Introducción

aqui la introducción

== Concepto 1

Concepto1 lo podemos explicar en subslides

=== Primera parte

Primera parte de concepto 1

- planificar
- definir
- revisar
- morir de inanicción

=== Segunda parte

Segunda parte de concepto 1

stem:[x^2=y^2]

== Concepto 2

.Una tabla
|===
|Column heading 1 |Column heading 2

| Un texto
a|latexmath:[a^2+b^2=c^2].

| Otro texto
a|latexmath:[k_{n+1} = n^2 + k_n^2 - k_{n-1}].
|===&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aquí como se vería esta presentación &lt;a href=&quot;revealjs/dos.html&quot; class=&quot;bare&quot;&gt;revealjs/dos.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
prueba a pulsar &lt;em&gt;Esc&lt;/em&gt; en la presentación y verás lo que pretendo transmitirte
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El contenido de las slides puede ser muy variado. Desde párrafos de textos, listas,
tablas, imágenes, e incluso fórmulas matemáticas, como puedes ver en el ejemplo anterior&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;funcionalidades&quot;&gt;Funcionalidades&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez vista la idea principal de cómo generar slides con asciidoctor + revealjs vamos a enumerar
alguna de las funcionalidades que se pueden hacer&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;notas_para_el_orador&quot;&gt;Notas para el orador&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podemos añadir notas visibles en una ventana aparte mediante un bloque &lt;code&gt;.notes&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;== Concepto
Este concepto es muy denso para explicarlo

[.notes]
--
Lo que va entre estos guiones solo lo ve el orador en una venta aparte
--&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;slides_sin_título&quot;&gt;Slides sin título&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algunas veces queremos que la slide no contenga título, tal vez porque queremos poner una foto
a pantalla completa.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Simplemente etiquetamos el título con &lt;code&gt;%notitle&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;[%notitle]
== Esta slide va sin titulo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;listas_animadas&quot;&gt;Listas animadas&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Muchas veces queremos mostrar una lista de pasos a ejecutar, ideas, etc pero queremos ir explicando
una a una para que el público nos preste atención y no se pongan a leer todas de una vez. Para ello
usaremos &lt;code&gt;%step&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;== Mis ideas para el 2020
[%step]
- Aparece la primera y no se ven las siguientes
- Al pulsar una tecla aparezco con una animación
- Si sigues pulsando teclas aparezco yo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;más_funcionalidades&quot;&gt;Más funcionalidades&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta página encontrarás todas las funcionalidades posibles con este backend:
&lt;a href=&quot;https://asciidoctor.org/docs/asciidoctor-revealjs/#syntax-examples&quot; class=&quot;bare&quot;&gt;https://asciidoctor.org/docs/asciidoctor-revealjs/#syntax-examples&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejemplos_completos&quot;&gt;Ejemplos completos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En esta presentación puedes encontrar (casi) todos los elementos y efectos que puedes hacer con
una presentación Asciidoctor (lo cual no quiero decir que haya que hacerlo. Ya sabes, a veces menos, es más):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://bentolor.github.io/java9to13/#/&quot; class=&quot;bare&quot;&gt;https://bentolor.github.io/java9to13/#/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;O esta otra más minimalista:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://puravida-software.gitlab.io/slides/doc-as-code.html#/&quot; class=&quot;bare&quot;&gt;https://puravida-software.gitlab.io/slides/doc-as-code.html#/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Breve introducción al lenguaje de marcado asciidoc y su ecosistema, parte 4</summary>
    </entry>
    <entry>
        <title>Despliega tu aplicación Grails/Spring en un cluster Kubernetes, made easy</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2019/okteto-grails.html"/>
        <updated>2020-01-18T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2019/okteto-grails.html</id>
        <category term="groovy"/>
        <category term="grails"/>
        <category term="kubernetes"/>
        <category term="okteto"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hace poco escríbi un post técnico donde contaba cómo puedes desarrollar, depurar y terminar desplegando una aplicación
Grails, SpringBot, o cualquier otro tipo de framework similar en un cluster Kubernetes, con la particularidad
de que en lugar de usar el típico entorno en local con &lt;code&gt;minikube&lt;/code&gt; , usaba un servicio cloud de &lt;a href=&quot;https://okteto.com&quot;&gt;Okteto&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con el servicio que ofrece Okteto no necesitaremos tener instalado en nuestra máquina prácticamente nada salvo un
pequeño programa (okteto cli) que será el que nos conecte con nuestro namespace remoto.
Además de &lt;strong&gt;ofrecer un entorno de desarrollo&lt;/strong&gt; puramente kubernetes, este servicio &lt;strong&gt;nos permitirá desplegar la aplicación&lt;/strong&gt;
en un cluster real del que no necesitaremos saber grandes detalles, haciéndolo ideal para dar los primeros pasos de iniciación a Kubernetes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El post completo, con todos los pasos y explicaciones lo puedes encontrar en
&lt;a href=&quot;https://okteto.com/blog/develop-and-deploy-a-grails-application-in-okteto-cloud/&quot; class=&quot;bare&quot;&gt;https://okteto.com/blog/develop-and-deploy-a-grails-application-in-okteto-cloud/&lt;/a&gt; así que en este post simplemente
escribiré algunas indicaciones y mis impresiones después de unos meses &quot;jugando&quot; con él.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Sí, como has adivinado, el post que les envíe fue revisado y corregido por la gente de Okteto hasta darle una
calidad excepcional.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;okteto&quot;&gt;Okteto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La primera vez que oí de Okteto fue hace unos meses, en una charla en el Codemotion de Madrid, donde
&lt;a href=&quot;https://twitter.com/micael_gallego&quot;&gt;Micael Gallego&lt;/a&gt; y &lt;a href=&quot;https://twitter.com/pchico83&quot;&gt;Pablo Chico&lt;/a&gt;, y me pareció una
herramienta muy buena para empezar a dar esos primeros pasos en Kubernetes que había intentando dar con Google Cloud
Heroku, o Digital Ocean, con la ventaja de que Okteto me ofrecía un cluster de prestaciones &quot;majas&quot; de forma gratuíta&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes verla completa en:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;videoblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;iframe src=&quot;https://www.youtube.com/embed/Q2-QjQ0KIW0?rel=0&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Supongamos que tienes una idea para una aplicación semi-sencilla y que tu entorno de programación es Grails o
SpringBot si no llegas a ser lo suficientemente bueno para usar Grails ;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sabes cómo desarrollarla en local, tienes tu PostgreSQL corriendo en tu máquina e incluso has dockerizado la aplicación
pero sabes que subirla a producción es otra cosa. Dónde desplegar, cómo depurar, etc se vuelve arduo y a veces
hay que poner una cantidad de dinero (pequeña pero hay que dar la tarjeta de crédito)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya he dicho antes, con este servicio lo que vas a poder tener es un cluster en la nube manejado por Okteto
donde poder desarrollar e incluso depurar tu aplicación y liberar versiones en producción&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;pasos&quot;&gt;Pasos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Los pasos a seguir para crear un entorno desarrollo serían, de forma resumida:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Tener una cuenta en Github&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Crear una cuenta en Okteto (usa Github como autentificador, por eso la cuenta anterior)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Instalar en local el cliente &lt;code&gt;okteto cli&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;hacer login con el comando &lt;code&gt;okteto login&lt;/code&gt; el cual baja las credenciales para poder conectarse a nuestra cuenta&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crear un directorio vacío, copiar el fichero &lt;code&gt;okteto.ini&lt;/code&gt; y ejecutar &lt;code&gt;okteto up&lt;/code&gt; para crear un entorno de
desarrollo en el cluster&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crear nuestra aplicación desde la shell que se crea. El servicio de Okteto lo que irá haciendo es sincronizar
todos los ficheros remotos con nuestro local, de tal forma que cualquier cambio en un entorno se refleje en el otro&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;desarrollar nuestra aplicación con el añadido de que podemos acceder a ella vía &lt;code&gt;localhost&lt;/code&gt; o vía Intenet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;una vez tenemos una versión para desplegar podemos dockerizarla directamente con &lt;code&gt;okteto build&lt;/code&gt; y aplicarla
al cluster con &lt;code&gt;kubectl apply&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es decir: crear un entorno de desarrollo en el cluster, desarrollar y depurar en él nuestra aplicación como siempre y
desplegarla (en Okteto o en otro proveedor de kubernetes)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;problemas_a_k_a_retos&quot;&gt;Problemas a.k.a Retos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probablemente tu aplicación va a requerir cierta infraestructura como base de datos, un sistema de ficheros
donde leer o escribir, etc y lamentablemente el post publicado en el blog de Okteto,
al ser una guía de primeros pasos, NO cubre nada de esto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por suerte desplegar una base de datos PostgreSQL en Okteto es sencillo y se puede hacer vía web con un par de
clicks o bien vía comando &lt;code&gt;kubectl&lt;/code&gt; y con ayuda de Internet para ver ejemplos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Actualmente NO se pueden crear volumenes persistentes vía web, aunque parece que está muy próxima una
actualización que lo permitirá, así que la reserva de volúmenes hay que hacerlo via &lt;code&gt;kubectl&lt;/code&gt; por lo que te tocará
aprender cómo definir uno. Por ejemplo, aplicando este fichero tendrías un volumen &lt;code&gt;my-volume&lt;/code&gt; que puedes
adjuntar a un pod:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;storage.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-volume
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 30Gi&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y aplicarlo con `kubectl apply -f storage.yml&apos;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo para no tener claves, passwords o api keys metidas en el código, necesitarás crear &lt;code&gt;ConfigMap&lt;/code&gt; y
&lt;code&gt;SecretMap&lt;/code&gt; , como por ejemplo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;secret.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Secret
metadata:
  name: my-secret
type: Opaque
stringData:
  TELEGRAM_TOKEN: &quot;783725094:AAAAAAAAAAAAAAAAAAAAAAAAAA&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y aplicarlo con `kubectl apply -f secret.yml&apos;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El desarrollar directamente en un cluster te ofrece la oportunidad de ir avanzando la aplicación
a la vez que vas definiendo la arquitectura (k8s) de la misma. Además el poder depurarla en este
te permite afinar esas situaciones complicadas de definir mediante tests (lo cual no quiere decir
que no tengas que seguir desarrollando test)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así mismo, aunque no lo he podido probar, puede ser una buena forma de trabajar en un equipo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último creo que es un entorno que te permite practicar de forma directa muchos conceptos
de kubernetes para ir adquiriendo experiencia. Yo mismo, por ejemplo, tras este par de meses usándolo
he comenzado un curso online de Kubernetes y prácticamente la mitad de los conceptos que se explican
me he tenido que pelear con ellos.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>primeros pasos para despegar una aplicación Grails o Springbot en un cluster kubernetes</summary>
    </entry>
    <entry>
        <title>Introducción a Asciidoc y Asciidoctor (3): artículo académico</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/intro-asciidoctor-3.html"/>
        <updated>2020-01-15T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/intro-asciidoctor-3.html</id>
        <category term="asciidoc"/>
        <category term="documentation"/>
        <category term="write"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Este post es la continuación a &lt;a href=&quot;intro-asciidoctor-2.html&quot; class=&quot;bare&quot;&gt;intro-asciidoctor-2.html&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;objetivo&quot;&gt;Objetivo&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post veremos cómo crear un artículo &quot;académico&quot; en Pdf.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Por académico quiero decir que desarrollaremos un texto que contendrá diferente contenido
como gráficos (inventados), formulas matemáticas (sin ningún sentido) etc, que den una idea de lo
que sería un caso real.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;a href=&quot;articulo.html&quot;&gt;Aquí&lt;/a&gt; para ver la versión HTML, o &lt;a href=&quot;articulo.pdf&quot;&gt;aquí&lt;/a&gt; para ver la
versión PDF del documento final. Ambas versiones parten del mismo documento adoc que analizaremos a continuación y que
puedes ver &lt;a href=&quot;articulo.adoc&quot;&gt;aquí&lt;/a&gt; (texto plano)
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cabecera&quot;&gt;Cabecera&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En un documento .adoc las primeras líneas hasta encontrar una en blanco,
contienen numerosa información respecto del mismo. Así, por ejemplo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;La primera línea comienza con un &lt;code&gt;=&lt;/code&gt; y es donde indicamos el título del documento, y si en esa misma línea incluimos
&lt;code&gt;: otro texto&lt;/code&gt;, entonces &lt;code&gt;otro texto&lt;/code&gt; es el subtítulo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;La siguiente línea está reservada para indicar el autor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;En la tercera indicamos la fecha&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estas tres líneas son opcionales, aunque aconsejo por lo menos indicar el título.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Antes de llegar a la primera línea en blanco que indicaría fín de cabecera, podemos especificar numerosos atributos
del documento, como por ejemplo si queremos que tenga una tabla de contenidos, la posición de esta, qué tipo de
documento es, variables globales, si queremos iconos, y un largo etcetera&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así pues nuestro artículo tendrá las primeras líneas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;asciidoc&quot;&gt;= Título del artículo
aqui.tu@email.com
v0.0.0.0.1, 2020-01-18
:toc:
:icons: font
:stem: latexmath&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;secciones&quot;&gt;Secciones&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A continuación comenzaría nuestro artículo, el cual iremos organizando en secciones, párrafos, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una sección se indica mediante el signo `=&apos; concatenado tantas veces como nivel de sección queramos crear. Por ejemplo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;asciidoc&quot;&gt;== Seccion 1
=== SubSeccion 1.1
==== Subseccion 1.1.1
== Seccion 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como el título es el nivel 1 es el que lleva un único &lt;code&gt;=&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Las secciones son importantes para organizar el texto y son marcadores para la generación de la tabla de contenido (TOC)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;texto_y_elementos&quot;&gt;Texto y elementos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post no voy a describir todos los elementos que puedes usar en el texto. Te remito a la página
principal donde están explicados completamente y con ejemplos: &lt;a href=&quot;https://asciidoctor-docs.netlify.com/asciidoc/1.5/&quot; class=&quot;bare&quot;&gt;https://asciidoctor-docs.netlify.com/asciidoc/1.5/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A modo de resumen indicar que:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;podemos remarcar texto en negrita, subrayado, itálica, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;embeber imágenes, vídeos, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crear enlaces a documentos tanto internos como externos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;crear tablas con una libertad absoluta sobre tamaños, formatos y contenido&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;stem&quot;&gt;Stem&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como nuestro artículo va a contener fórmulas es necesario activarlo en la cabecera del documento mediante el
atributo &lt;code&gt;:stem:&lt;/code&gt; . Asciidoctor tiene un par de procesadores para fórmulas: LaTex y AsciiMath y en este caso vamos
a usar por defecto LaTex indicandolo en el atributo &lt;code&gt;:stem: latexmath&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para incluir una formula en nuestro documento podremos hacerlo mediante un bloque &lt;code&gt;inline&lt;/code&gt; (en el mismo párrafo)
o con un bloque individual si queremos indicar varias líneas de fórmulas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;  La solución a stem:[a*x^2 + b*x +c = 0] se obtiene ... (bloque inline)

  [stem]
  ----
  k_{n+1} = n^2 + k_n^2 - k_{n-1}

  x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
  ----&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;generación&quot;&gt;Generación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez que tenemos nuestro documento preparado sólo tenemos que generar el documento final
(en realidad este proceso lo repetirás muchísimas veces hasta tener soltura)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En una consola donde esté el documento ejecutaremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;asciidoctor articulo.adoc&lt;/code&gt; y obtendremos en ese mismo directorio un fichero &lt;em&gt;articulo.html&lt;/em&gt; completo, tal que
puedes abrirlo con el navegador sin necesitar ningún otro archivo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para generar la versión pdf ejecutaremos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;asciidoctor -b pdf articulo.adoc&lt;/code&gt; y obtendremos un fichero &lt;em&gt;articulo.pdf&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
El backend de pdf necesita tener instaladas una serie de dependencias si vas a usar fórmulas como en este
ejemplo. Si estás interesado no dudes en decirmelo y lo explico en más detalle.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusión&quot;&gt;Conclusión&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Generar un artículo de opinión, redacción o una revisión de un producto, por poner unos ejemplos, de calidad
tanto HTML como Pdf es realmente fácil con Asciidoctor y cuentas con todas las ventajas que ya hemos visto
(texto plano, versionable, revisable, trabajo en grupo, etc)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Breve introducción al lenguaje de marcado asciidoc y su ecosistema, parte 3</summary>
    </entry>
    <entry>
        <title>Introducción a Asciidoc y Asciidoctor (2)</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/intro-asciidoctor-2.html"/>
        <updated>2020-01-08T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/intro-asciidoctor-2.html</id>
        <category term="asciidoc"/>
        <category term="documentation"/>
        <category term="write"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Este post es la continuación a &lt;a href=&quot;intro-asciidoctor-1.html&quot; class=&quot;bare&quot;&gt;intro-asciidoctor-1.html&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probablemente, tras leer el post anterior, te sigas preguntando por qué deberías usar asciidoc y
su ecosistema como herramienta para tu documentación.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De forma resumida destacaría los siguientes puntos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;formato abierto&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;texto plano. Te centras en escribir y te olvidas de si las páginas encajan, si las imágenes
de descolocan los parráfos, etc&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;versionable, revisable, mergeable por cualquier sistema de control de versiones&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Todos ellos compartidos con Mardkown y otros. Pero además con asciidoc tienes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;una &lt;strong&gt;única sintaxis&lt;/strong&gt;, a diferencia de los miles de sabores (incompatibles?) que tiene Markdown&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un mismo texto puede generar diferentes outputs, como el típico caso de tener un .adoc y convertirlo
a html y pdf&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;es semántico, está orientado a la documentación no a la presentación. Cuando lees asciidoc, lees el sentido
que se le está dando a un parrafo (esto es importante, eso es un párrafo, esto es un ejemplo, etc)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;un ecosistema supercompleto donde se han integrado multitud de herramientas: escribir fórmulas matemáticas
en LaTex o AsciiMath, diagramas de secuencia, clase, actividades, grant,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;al ser abierto existen muchos &quot;backends&quot;: html, pdf, manpages, revealjs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;si eres programador y usas Maven o Gradle, puedes integrarlo en tu proyecto fácilmente&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;casos_de_uso&quot;&gt;Casos de uso&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estos son algunos casos de uso donde yo lo he usado (no es por autobombo, es que así me ahorro tener que citar autores):&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Artículo&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Presentar una redacción o un artículo corto para la universidad.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://uoc.gitlab.io/2016/AdminRedesSO/AGUILERA_GONZALEZ_JORGE-ARSO/Modulo_6/ArticuloFontsecaBreach.pdf&quot; class=&quot;bare&quot;&gt;http://uoc.gitlab.io/2016/AdminRedesSO/AGUILERA_GONZALEZ_JORGE-ARSO/Modulo_6/ArticuloFontsecaBreach.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Trabajo Fin de Grado&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Presentar algo más complejo que una redacción, con capítulos, revisión de historial, diagramas, fórmulas, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://telotraigodemipueblo.gitlab.io/tfg/pdf/memoria.pdf&quot; class=&quot;bare&quot;&gt;https://telotraigodemipueblo.gitlab.io/tfg/pdf/memoria.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Documentar un proyecto&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Documentar un proyecto, explicando funcionalidades, ejemplos, etc,&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://puravida-asciidoctor.gitlab.io/asciidoctor-extensions/&quot; class=&quot;bare&quot;&gt;https://puravida-asciidoctor.gitlab.io/asciidoctor-extensions/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Página comercial de una empresa&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crear la landing page de una empresa/producto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://puravida-software.gitlab.io/main/index.html&quot; class=&quot;bare&quot;&gt;https://puravida-software.gitlab.io/main/index.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Un blog&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este mismo, por ejemplo, sin necesidad de bases de datos, editores complejos, etc y alojado sin coste en cualquiera
de los numerosos sistemas que ofrecen páginas estáticas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Blog multiformato&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este blog publica en html, pdf y epub el mismo contenido&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://groovy-lang.gitlab.io/101-scripts/&quot; class=&quot;bare&quot;&gt;https://groovy-lang.gitlab.io/101-scripts/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://groovy-lang.gitlab.io/101-scripts/101-groovy-scripts-pdf.pdf&quot; class=&quot;bare&quot;&gt;https://groovy-lang.gitlab.io/101-scripts/101-groovy-scripts-pdf.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://groovy-lang.gitlab.io/101-scripts/101-groovy-scripts-epub.epub&quot; class=&quot;bare&quot;&gt;https://groovy-lang.gitlab.io/101-scripts/101-groovy-scripts-epub.epub&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Presentación de una charla&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Uso de RevealJs para presentar la charla y después se exporta a SlideShare para compartirlas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;videoblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;iframe src=&quot;https://www.youtube.com/embed/WpHt5ku9xAs?rel=0&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Slides:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.slideshare.net/JorgeAguilera12/write-gradle-plugins&quot; class=&quot;bare&quot;&gt;https://www.slideshare.net/JorgeAguilera12/write-gradle-plugins&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Libro digital en Amazon&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Puedes usar el backend de ePub para generar un libro compatible con la plataforma de publicación de Amazon&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.amazon.es/Tutorial-Asciidoctor-b%C3%A1sica-Jorge-Aguilera-ebook/dp/B07518QBR4&quot; class=&quot;bare&quot;&gt;https://www.amazon.es/Tutorial-Asciidoctor-b%C3%A1sica-Jorge-Aguilera-ebook/dp/B07518QBR4&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(tengo pendiente hacer una revisión y publicarlo actualizado)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ventajas&quot;&gt;Ventajas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al ser sólo texto puedes editarlo desde muchos dispositivos y situaciones. Por ejemplo he revisado y corregido
numerosos errores desde el metro con el móvil e incluso al tener el proyecto integrado con un proceso de despliegue
contínuo, llegar a publicar las correcciones directamente.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como ya he comentado anteriormente, al centrarte en lo que quieres decir y no en cómo va a quedar te vuelves más productivo.
No te engaño, en tu primer intento vas a querer cambiar la presentación del documento y te vas a desesperar porque no
es fácil.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al poder integrarlo en la construcción de un producto (no hace falta que sea software) favoreces el trabajo en equipo,
revisión, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hablé sobre todo esto en un par de artículos en el blog de Panel Sistemas:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.panel.es/blog/aproximacion-documentacion-continua-parte-i/&quot; class=&quot;bare&quot;&gt;https://www.panel.es/blog/aproximacion-documentacion-continua-parte-i/&lt;/a&gt;
&lt;a href=&quot;https://www.panel.es/blog/aproximacion-documentacion-continua-parte-ii/&quot; class=&quot;bare&quot;&gt;https://www.panel.es/blog/aproximacion-documentacion-continua-parte-ii/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;siguiente&quot;&gt;Siguiente&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En el próximo post, una vez que tenemos instalado asciidoctor y hemos visto casos de uso, intentaré explicar paso a
paso un ejemplo sencillo pero completo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Breve introducción al lenguaje de marcado asciidoc y su ecosistema, parte 2</summary>
    </entry>
    <entry>
        <title>Introducción a Asciidoc y Asciidoctor (1)</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2020/intro-asciidoctor-1.html"/>
        <updated>2020-01-08T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2020/intro-asciidoctor-1.html</id>
        <category term="asciidoc"/>
        <category term="documentation"/>
        <category term="write"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Aunque todo el ecosistema de Asciidoctor funciona también en Windows yo no lo uso, así que en esta entrada todo lo
que explique sobre instalación, ejemplos y demás será bajo Linux
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
He añadido un pequeño comentario sobre cómo usarlo en Windows 10.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ecosistema&quot;&gt;Ecosistema&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Asciidoc&lt;/strong&gt; es un formato para documentos de texto usado para escribir notas, articulos, libros,
páginas web, manpages, blogs, presentaciones orientado a su transformación en html, pdf, epub, etc
( &lt;a href=&quot;http://asciidoc.org&quot; class=&quot;bare&quot;&gt;http://asciidoc.org&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Con esta definición lo primero que pensarás es que es otro &lt;strong&gt;Markdown&lt;/strong&gt; y que para eso
ya te quedas con este que es muy usado y lo soporta Github. Sin embargo, aunque ambos, como algún otro,
están orientados a documentar en texto plano las &lt;strong&gt;diferencias son enormes&lt;/strong&gt;.&lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_1&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_1&quot; title=&quot;View footnote.&quot;&gt;1&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Se podría decir que &lt;strong&gt;Asciidoc&lt;/strong&gt; define las reglas sintácticas sobre los elementos del documento pues especifica dónde
va el título, el autor, los párrafos, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Si te suena o has usado &lt;strong&gt;reStructuredText&lt;/strong&gt; entonces puedes ver a &lt;strong&gt;asciidoc&lt;/strong&gt; como otra alternativa a este.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Asciidoctor&lt;/strong&gt; (&lt;a href=&quot;https://asciidoctor.org&quot; class=&quot;bare&quot;&gt;https://asciidoctor.org&lt;/a&gt;) es una &lt;strong&gt;implementación&lt;/strong&gt; , un programa, un ejecutable, que es capaz de leer
documentos &lt;em&gt;asciidoc&lt;/em&gt; y conventirlos a html5, docbook, pdf, etc. Como ves son parecidos, e incluso confusos, pero son
dos cosas diferentes: uno podríamos verlo como las especificaciones y al otro como un traductor&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Para complicar más la cosa, existe una implementación llamada &lt;strong&gt;asciidoc&lt;/strong&gt; , igual que las especificaciones,
pero que su última actualización es del 2013. Así, cuando en este documento hable de &lt;strong&gt;asciidoc&lt;/strong&gt; estaré hablando de
las especificaciones y no de esta implementación.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La implementación &lt;strong&gt;asciidoctor&lt;/strong&gt; está desarrollada en Ruby pero existen otras implementaciones como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;asciidoctorJ, un jar de java que puedes ejecutar desde la línea de comando o integrarlo en tu aplicación&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;asciidoctor-js, un paquete Node en javascript al estilo del de Java&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estas 3 implementaciones (y a lo mejor alguna más que no conozca) son fáciles de instalar y están destinadas a
ser ejecutadas por línea de comando o integradas en un proceso de construcción de software.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;instalación&quot;&gt;Instalación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si usas Ruby, Asciidoctor se puede instalar como una gema:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;gem install asciidoctor&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;O bien puedes instalarlo como paquete del sistema operativo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sudo apt-get install asciidoctor&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si no quieres instalar dependencias de Ruby y prefieres utilizar la implementación en Java puedes descargar el &lt;em&gt;jar&lt;/em&gt;
de su web o si tienes SdkMan como gestor de aplicaciones, puedes instalarlo fácilmente:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sdk install asciidoctorj&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;windows_10&quot;&gt;Windows 10&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si utilizas Windows 10 existe una forma de instalar &lt;code&gt;asciidoctorj&lt;/code&gt; mediante Chocolatey (&lt;a href=&quot;https://chocolatey.org/&quot; class=&quot;bare&quot;&gt;https://chocolatey.org/&lt;/a&gt;),
un gestor de paquetes para este sistema operativo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En primer lugar deberás instalar Chocolatey mediante una consola PowerShell como administrador y ejecutar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;`Set-ExecutionPolicy Bypass -Scope Process -Force; `
iex ((New-Object System.Net.WebClient).DownloadString(&apos;https://chocolatey.org/install.ps1&apos;))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Después de cerrar y volver a abrir la consola PowerShell (como administrador) podrás instalar asciidoctorj
ejecutando&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;choco install asciidoctorj&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
NO uses Notepad de Windows para editar los ficheros .adoc (y mucho menos Word) pues añaden caracteres de
control al inicio del fichero incompatibles con asciidoctor. Usa Notepad++ , VSCode, Atom, o similar
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ejemplo_básico&quot;&gt;Ejemplo básico&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Abre un editor de texto plano (recuerda ni Word ni LibreOffice son editores de texto plano) como notepad, gedit, vi o nano
y escribe:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ejemplo.adoc&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;= El titulo : el subtitulo
jorge.aguilera@puravida-software.com

(debes dejar un espacio en blanco entre el email y esta linea)

== Una sección

*Todo* lo que ven tus ojos algún día _sera_ tuyo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Guarda el documento como &lt;code&gt;ejemplo.adoc&lt;/code&gt; y desde una línea de comando nos situamos donde esté el documento y ejecutamos:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;asciidoctor ejemplo.adoc&lt;/code&gt; ( o &lt;code&gt;asciidoctorj ejemplo.doc&lt;/code&gt; si usamos la versión java)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien debería haberse generado un documento &lt;code&gt;ejemplo.html&lt;/code&gt; que puedes abrir con un navegador y comprobar
que es un documento html5 completo y que se ha generado sin necesidad de tener conocimientos de maquetar html&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;editores&quot;&gt;Editores&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La sintáxis de asciidoc es muy completa y extensa pero a la vez muy sencilla por lo que una vez practicado con ella
serás capaz de memorizar gran parte de ella así que no deberías necesitar más que un editor plano.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo hay editores y/o plugins para diferentes IDEs que nos pueden ayudar a escribir en asciidoc.
A modo de ejemplo dispones:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AsciidocFX (requiere Java)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ (requiere instalar un plugin)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VSCode (requiere instalar un plugin)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De forma general, y aunque estos editores tengan la opción de generar el html (o el pdf, etc) es importante entender
que estos editores trabajan sobre el &lt;code&gt;adoc&lt;/code&gt; y que nosotros podremos ejecutar la generación del documento utilizando
todas las opciones que ofrecen.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;footnotes&quot;&gt;
&lt;hr&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_1&quot;&gt;
&lt;a href=&quot;#_footnoteref_1&quot;&gt;1&lt;/a&gt;. &lt;a href=&quot;https://asciidoctor.org/docs/asciidoc-vs-markdown/&quot; class=&quot;bare&quot;&gt;https://asciidoctor.org/docs/asciidoc-vs-markdown/&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Breve introducción al lenguaje de marcado asciidoc y su ecosistema</summary>
    </entry>
    <entry>
        <title>Resumen 2019</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2019/resumen.html"/>
        <updated>2019-12-15T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2019/resumen.html</id>
        <category term="pensamientos"/>
        <category term="introspección"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Termina el 2019 y es tiempo de echar un ojo a lo que me ha pasado durante este año.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;puravida_software&quot;&gt;PuraVida Software&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una de las cosas más difíciles de explicar a aquellos que no me conocen (y a muchos que sí) es mi perfil profesional
, sobre todo por la existencia de PuraVida Software.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;PuraVida Software es una empresa como tal, con su CIF, razón social, etc que creé hace ya unos 10 años y con la que
ofrecía mis servicios como desarrollador. Sin embargo, durante este tiempo han surgido proyectos interesantes donde
,por las razones que fuera, la empresa prefería tener a alguien en nómina por lo que digamos de alguna forma PuraVida
Software pasaba a estar &quot;congelada&quot;. Sé que esto a veces genera cierta desconfianza porque da un aire de temporalidad
pero si lo piensas bien, en nuestro sector lo único que hace que algo sea duradero es que el proyecto sea interesante
y apoyes a la gente que participa en él, no la figura jurídica que se adopte.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por otra parte, independientemente de la situación fiscal de PuraVida Software, yo he seguido usando
su paragüas para seguir haciendo aquellas cosas personales que me apetecen, como:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;publicar proyectos OpenSource&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;patrocinios de comunidades como &lt;a href=&quot;https://www.madridgug.com/&quot;&gt;MadridGUG&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sponsors de eventos como &lt;a href=&quot;https://greachconf.com&quot;&gt;Greachconf&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;charlas en diferentes eventos&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;etc&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;la_q_en_qa&quot;&gt;La Q en QA&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Este año he tenido la suerte (?) de poder trabajar en un equipo multidisciplinar como integrante del QA (quality assurance)
y sufrir en mis propias carnes la poca valoración que se ha tenido (he tenido) a esta parte del desarrollo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El proyecto era para una mega-empresa en un mega-proyecto con decenas de equipos que tras dejar atrás el monolito ya
se habían adentrado en el agile y con sus más y sus menos iba dando sus frutos. Tras probar diferentes organizaciones
ahora tenían grupos (pods que dirían los spotiferos, creo) con front, back, qa, su product owner, etc&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mi participación en este proyecto tenía una fecha fin muy concreta, de 3 meses, y como objetivo el intentar ver en qué
forma podía ayudar a los QA a mejorar como reutilizar scripts, mejorar el uso de SoapUI, documentación, y cosas así
a la vez que hacía labores propias del QA.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De este período creo que debería acuñar el término &quot;Síndrome del último mono&quot; o del QA que asume que su papel es probar
las mierdas de los otros. Un QA debe hacer ver al resto del equipo que TODO debe pasar por ellos: desde que se está
haciendo la planificación, mientras se está desarrollando las funcionalidades, hasta que se hace la demo al product
owner. Y esto lo tiene que hacer ver desde el minuto cero.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Básicamente en todos los pods he visto que sólo se cuenta con los QAs para las pruebas finales, con escaso tiempo antes
de la entrega y que estos lo asumían como algo normal.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;La Q de QA significa Calidad&lt;/strong&gt;. Si quisieramos que sólo se dedicaran a hacer test los llamaríamos TA.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A título personal lo que sí les diría a los QA es que dejaran a un lado el maldito SoapUI que es el chocolate del loro.
Mediante un interface gráfico (bastante feo) y a base de cajitas te hacen creer que estás automatizando tareas de test
y lo que estás es cavando en un agujero donde no existe la reutilización ni el compartir. Refuerza a tus QA en
programación (sobre todo en Groovy que es perfecto para este entorno), obligales a usar Git y que sean parte de los
code review y que automatizen todo. Si además consigues que dejen de lado Confluence como sistema para documentar
sabrás que estás en el camino correcto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Si estás pensando que la solución es NO tener el equipo QA y delegar en el equipo, no te preocupes también tengo
algunos cachitos de historia que contar delante de unas cervezas.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;la_k_de_kubernetes&quot;&gt;La K de Kubernetes&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Después de estar jugando con la idea de Kubernetes, y una vez que Docker para lo que yo lo necesito está
en mi día a día, este año he decidido dar unos primeros pasos con él.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Muy temerariamente estuve &quot;asesorando&quot; a una empresa amiga que se estaban moviendo hacia k8s, siempre conscientes de
que eran nuestros primeros pasos. Por suerte no sólo contaban con mis nulos conocimientos sino que los estaban
haciendo de la mano de una empresa de servicios con experiencia en el tema.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por mi parte estuve probando a desplegar alguna de mis ideas en diferentes plataformas: Digital Ocean, AWS Fargate
y Google Cloud. Me puse un presupuesto limitado de unos 10-15 euros al mes que pocas veces superé (creo que rondaron
los 10€) durante los 3 ó 4 meses. Con diferencia, para lo poco que sabía me quedaría con Google Cloud&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, tras una charla de &lt;a href=&quot;https://twitter.com/pchico83&quot;&gt;@pchico83&lt;/a&gt; y &lt;a href=&quot;https://twitter.com/micael_gallego&quot;&gt;@micael_gallego&lt;/a&gt;
en el &lt;a href=&quot;https://twitter.com/CodemoMadrid&quot;&gt;@CodemoMadrid&lt;/a&gt; sobre &lt;a href=&quot;https://twitter.com/oktetohq&quot;&gt;@oktetohq&lt;/a&gt; he descubierto una
plataforma perfecta para el aprendizaje de Kubernetes. Con sólo una cuenta en Github para identificarte dispones
de un cluster con 5 namespaces, 8GB y mogollón de disco en cada namespace para desplegar tus servicios.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Tanto como con Docker (y tantas herramientas) antes de usar un entorno gráfico utiliza la línea de consola.
Kubernetes tiene &lt;code&gt;kubectl&lt;/code&gt; y te permite administrar todos tus cluster desde la línea de comando. De esta forma
aprenderás los conceptos y sufrirás todas las dificultades existentes para saber valorar luego qué aportan toda
la pléyade de herramientas que hay alrededor
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;la_b_de_bots&quot;&gt;La B de Bots&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algunos piensan que este año ha sido mi año de los Bots (de Telegram). Razón no les falta. Lo que empezó como una
pataleta por una prueba de código a una oferta de trabajo fallida me llevó a publicar unos 5 bots,
un par de canales de Telegram y un plugin para Gradle &lt;code&gt;Social Network&lt;/code&gt; (el cual había creado a finales del 2018
pero ha sido este donde lo he usado de forma intensiva)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El bot de las cámaras de tráfico de Madrid, el de Barcelona o el de Granada, el localizador de fuentes públicas,
en este verano tan caluroso, para Madrid, Barcelona, Granada y Cáceres, o el de los precios de las estaciones de
servicio (gaolineras) con un sistema de diálogo un poco más elaborado han aportado, sin ser ninguno nada del otro mundo,
muchas cosas para la mochila (material para charlas, conocer gente, descubrir nuevos canales de negocio, &amp;#8230;&amp;#8203;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tal vez de las cosas más interesantes que ha surgido de este tema, ha sido ver cómo alguien de Twitter,
&lt;a href=&quot;https://twitter.com/Garciolo&quot;&gt;@Garciolo&lt;/a&gt;, le gustó la idea de las fuentes y empezamos a chatear para incluir Granada
en el bot. Como no lo encontrábamos para Granada &lt;strong&gt;se movilizó e intentó que el ayuntamiento le proporcionara los datos&lt;/strong&gt;.
Tras varios intentos y en vista de que no iba a ser posible, se hizo un repositorio Github y &lt;strong&gt;geolocalizó
un buen número de ellas &quot;a mano&quot; para que las incluyera en el bot&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;la_c_de_charlas&quot;&gt;La C de Charlas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Echando la vista atrás he visto con sorpresa que este año no he presentado ninguna charla a &lt;a href=&quot;https://www.madridgug.com/&quot;&gt;MadridGUG&lt;/a&gt;.
Si bien soy consciente de que tras las vacaciones de verano me dije de apoyar a gente que querían dar alguna en lugar de
presentarlas yo, creía que al principio de año habría dado alguna.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tengo varias ideas que me rondan:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cómo hacer una app micronaut-desktop y controlarla vía web, al estilo de Torrent&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Revisar la prueba conceptual que hice de Micronaut y Ethereum y mostrar cómo unir Java con blockchain&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Primeros pasos de Kubernetes en Okteto&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y alguna otra que todavía no tienen forma&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A cambio de no proponer nada en &lt;a href=&quot;https://www.madridgug.com/&quot;&gt;MadridGUG&lt;/a&gt; he tenido otras experiencias bastante interesantes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En Mayo me despierto con un mensaje de Picolini, &lt;a href=&quot;https://twitter.com/francjp&quot;&gt;@francjp&lt;/a&gt;, proponiendome dar una 1/2 charla para un Meetup &quot;la
semana que viene sobre el tema que tú quieras&quot;. En estos temas si me tocan las palmas pues yo bailo así que le
propuse &quot;DocAsCode, una aproximación a la documentación contínua&quot; algo sobre lo que luego hablaré.
En su inconsciencia Fran pretendía llevar a dos ponentes para dos charlas
cortas (de ahí lo de 1/2 charla) y obviamente al final tuvo dos charlas megalargas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El tema de DocAsCode me tiene muy enganchado desde que descubrí &lt;a href=&quot;https://twitter.com/asciidoctor&quot;&gt;@asciidoctor&lt;/a&gt; y personalmente me sentí muy cómodo
con la charla pues además da muchas posibilidades de meterse con Word y Confluence. Señal de que no fue mal es que
un par de asistentes, al terminar, me comentaran que les había parecido muy buena por hablar de un tema que nadie
habla.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tras ver un tweet de &lt;a href=&quot;https://twitter.com/jjmerelo&quot;&gt;@jjmerelo&lt;/a&gt; anunciando que el C4P para
&lt;a href=&quot;https://twitter.com/esLibre_&quot;&gt;@esLibre_&lt;/a&gt; (conferencias sobre Software Libre en Granada)
estaba abierto creo que dude 1 minuto en enviar la propuesta de &quot;OpenSource para un mundo OpenData&quot; y un poco
después la de &quot;DocAsCode&quot;. La verdad es que me apetecía salir de Madrid (visitar las provincias) y era una excusa
perfecta. El proceso abierto, usando Pull Request de Github, tanto de enviar como de aceptar las charlas me encantó
y creo que otras comunidades deberían plantearselo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ahora me doy cuenta que tenía que haber hecho un resumen en aquel momento más detallado de las charlas que asistí pero
recuerdo a dos chavales de no más de 25 años hablando sobre Inteligencia Artificial y Programación Cuántica que me
hicieron sentir muy viejo a la vez que ilusionado. También recuerdo el debate sobre el estado del OpenSource en
España de miembros muy activos de este movimiento (confieso que a muchos de ellos los tuve que buscar después, pero
eso es porque no soy de quedarme con los nombres)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De mis dos charlas la verdad es que no salí todo lo satisfecho que me hubiera gustado, probablemente por culpa del
troll que llevo dentro:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;En la de OpenData me sentí como que estaba fardando de lo que había hecho con los datos
abiertos del Ayuntamiento de Madrid, cuando nada más lejos de la realidad. Mi intención era &quot;si un programador
normal como yo puede hacerlo, cualquiera puede. Sólo hay que echarle tiempo y ganas&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;La de DocAsCode creo que podría haberla hecho más amena o tal vez es que la comparo demasiado con la que había dado
en el Meetup. En cualquier caso sí dió para discutir sobre ello con un par de interesados así que me quedo conque
el tema tiene mucha chicha que contar&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último @CodemoMadrid me confirmó que la charla de &quot;OpenSource para un mundo OpenData&quot; había sido elegida y
tras la experiencia de haberla podido dar en @esLibre_ pude replantearla y personalmente salir muy satisfecho.
Después de la charla pude hablar con un par de personas sobre ella y creo que algo de inquietud en el tema sí caló.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;otra_k_de_kanban&quot;&gt;Otra K de Kanban&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Llevo unos pocos meses en un equipo de trabajo donde se usa Kanban y la verdad que no está mal. Focalizar el esfuerzo
en tirar de los tickets en lugar de empujarlos, sin sprints sino con entregas cuasi-contínuas y cosas así hace que
si las cosas van medio bien uno se sienta productivo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo, por mucho que quiera, Kanban no se escapa
a la constante universal de que &lt;strong&gt;negocio te impondrá siempre lo urgente por encima de lo importante&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;resumen&quot;&gt;Resumen&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque no lo creas, este post había tenido una versión previa muchísimo más negativa y pesimista. Este año, como
tantos otros, ha tenido sus &quot;palos&quot;, sus cosas malas, decepcionantes y desagradables y me encontré escribiendo sobre ellas
más que sobre lo que el año me había aportado.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin saber nada de medicina sospecho que debemos segregar algún tipo de compuesto químico que nos produce cierto placer
, un revolcarnos en el barro, un cavar en un hoyo, que nos impide levantarnos y seguir adelante. Por suerte
@montalegos estuvo al tanto y lo he reescrito. Con ello no quiero tapar las cosas &quot;malas&quot; y sólo mostrar las buenas
sino que, partiendo que principalmente escribo para mí, opto por estas antes que por aquellas&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A diferencia de otros referentes que tengo, yo no hago planes para el año que viene, lo más a 3 meses vista por lo que
por ahora sólo tengo como tema a hacer el Workshop sobre Bot que voy a dar en el @Greachconf 2020. &lt;strong&gt;Son 3 horas y en
inglés, por lo que las risas y momentos difíciles están asegurados&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>null</summary>
    </entry>
    <entry>
        <title>Kubernetes, Jobs (3)</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-3.html"/>
        <updated>2019-11-30T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-3.html</id>
        <category term="micronaut"/>
        <category term="kubernetes"/>
        <category term="k8s"/>
        <category term="groovy"/>
        <category term="okteto"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is the third part of a series of posts about how I&amp;#8217;ll develop an application in Kubernetes (k8s)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;first post: Idea (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-1.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-1.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;second post: Infraestructure (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-2.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-2.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;third post: Job (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-3.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-3.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The main ot these posts is to document the process of deploying a solution in k8s at the same
time I&amp;#8217;m writting the application so probably all posts will have a lot of errors and mistakes that I need to
correct in the next post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Be aware that I&amp;#8217;m a very nobel with Kubernetes and these are my first steps with it.
I hope to catch up the attention of people with more knowledge than me and maybe they can review these posts and suggest
to us some improvements.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-tip&quot; title=&quot;Tip&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
I&amp;#8217;ve created a git repository at &lt;a href=&quot;https://gitlab.com/puravida-software/k8s-bibliomadrid&quot; class=&quot;bare&quot;&gt;https://gitlab.com/puravida-software/k8s-bibliomadrid&lt;/a&gt;
with the code of the application
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gradle_multiproject&quot;&gt;Gradle multiproject&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ve splitted the application into a multimodule Gradle projects with:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;com-puravida-biblios-model as a Micronaut jar library with the model and the repository.
Also this library will have the liquidbase files to update the database schema&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;com-puravida-biblios-etl as a Micronaut cli application who takes a year and a month as
arguments to download the csv file and import it into the database.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;These are typical project created with the cli as:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;mn create-app --lang groovy --profile cli com.puravida.biblios.etl&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As at some point I&amp;#8217;ll need a database (PostgreSQL) I use Okteto&amp;#8217;s hability to develop an
application into a kubernetes cluster with a PostgreSQL deployed (see step2) in a similar
way as if I use a local instance of PostgreSQL or TestContainer,etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-tip&quot; title=&quot;Tip&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
For this use case, Okteto doesn&amp;#8217;t add a lot of value because I have only a database
dependency that I can solve with local solutions. If the application requires more artifacts
as specific services, databases or tools that are difficult to install and maintain in
every developer desktop, Okteto can be a good solution to develop directly in a kubernetes
cluster.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So basically I&amp;#8217;ve created an okteto.ini file in the root of the gradle proyect&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;okteto.ini&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;name: gradle
image: gradle:latest
command:
- bash
volumes:
  - /home/gradle/.gradle
forward:
  - 8080:8080
  - 8088:8088
environment:
      - POSTGRES_PASSWORD=okteto
      - POSTGRES_USER=okteto
      - POSTGRES_DB=okteto
      - POSTGRESQL_SERVICE_HOST=10.0.7.172&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And when I want to develop directly in the cluster I only need to execute:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ okteto up
groovy:groovy$ ./gradlew build&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;With &lt;code&gt;okteto up&lt;/code&gt; I created a new Pod called &lt;code&gt;gradle&lt;/code&gt; with the gradle docker image
where I can run commands as &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also I can edit files in my local disk and okteto will synchronize them with the
remote pod. In the same way I can run the application in the pod
and connect to it with my IntelliJ in order to debug it as if it was running in my
laptop&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see I&amp;#8217;ve injected some environment values related to my Postgre database
so the application can works against it (insert records at develop time, etc)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;model&quot;&gt;Model&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By the moment the model is very simple with only two Domain Object and two repositories:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-b400011773409541dc44cf1c3de0b70d.png&quot; alt=&quot;Diagram&quot; width=&quot;342&quot; height=&quot;398&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;job&quot;&gt;Job&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once I had the &lt;code&gt;etl&lt;/code&gt; ready, able to download a file and parse and insert into the database
I wanted to have a kubernetes way to run it using differents years and months.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;One posibility is to have a pod and via command line or with a web endpoint invoque the import
process but in this case I&amp;#8217;ve used a Job.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Kubernetes has the possibility to run a container via a Job in a lot of different scenaries
(one-shot, with a chron, rety if fails, etc). For my purpose I want to run a single Job,
and launch it mannualy (once I verified a new file is ready in the portal of OpenData).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This job needs to connect to the database so I&amp;#8217;ll need to use the ConfigMap where I saved
the connection details (host, user, password and database). Also I need to indicate the
year and month to process (via arguments command line)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After some reseachs (trial and error) I learnt some lessons:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;When you run a Job (with &lt;code&gt;kubectl apply -f job-file.yaml&lt;/code&gt; for example),
kubernetes creates a new pod and meanwhile you don&amp;#8217;t remove it with
&lt;code&gt;kubectl delete name-of-the-job-id&lt;/code&gt; it remains into your cluster (as finished). This can be
usefull to inspect the logs for example&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You can run a Job multiple times and kubernetes will create new pods every time but you need
to inspect what&amp;#8217;s the last executed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once executed a Job you &lt;strong&gt;CAN&amp;#8217;T&lt;/strong&gt; modify the spec and try again to run it.
You&amp;#8217;ll have a &lt;strong&gt;field inmmutable&lt;/strong&gt; error. To solve it simple delete the old job and retry&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As I want to run a job using different arguments (year and month) I can&amp;#8217;t use a single yaml file
without deleting jobs previously executed, so I ended with a template, and using &lt;code&gt;sed&lt;/code&gt; command
I replace some variables to produce final yaml files:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;apiVersion: batch/v1
kind: Job
metadata:
  name: com-puravida-biblios-etl-$YEAR-$MONTH
  labels:
    jobgroup: etl
spec:
  backoffLimit: 0
  template:
    spec:
      containers:
        - name: com-puravida-biblios-etl
          image: jagedn/k8s-bibliomadrid-etl:$VERSION
          args: [ &quot;-y&quot;, &quot;$YEAR&quot;, &quot;-m&quot;, &quot;$MONTH&quot;]
          envFrom:
            - configMapRef:
                name: postgres-config
      restartPolicy: Never
      terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;as you can see we have 3 variables (YEAR, MONTH and VERSION) to replace&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;etl-tpl.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ sed  -e &apos;s/$YEAR/2019/g&apos; -e &apos;s/$MONTH/04/g&apos; -e &apos;s/$VERSION/0.1/g&apos; k8s/etl-tpl.yaml &amp;gt; k8s/jobs/etl-2019-04.yaml
$ kubectl apply -f k8s/jobs/etl-2019-04.yaml
$ kubectl logs -f com-puravida-biblios-etl-2019-06-fdx6f&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this way not only I&amp;#8217;ll have one file per year-month that I can run in every namespace I had (dev and prod)
but also I&amp;#8217;ll have in the repo all files versioned&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;next_step&quot;&gt;Next step&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once we have some datas into our database we can develop a new service able to serve them via REST (or maybe GrapQL?)
to Internet&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;acknowledgment&quot;&gt;Acknowledgment&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Thanks to Pablo Chico de Guzman, @pchico83, to confirm a Job is a good alternative to do a ETL&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Thanks to JJ Merelo, @jjmerelo, for suggesting me to use Celery (&lt;a href=&quot;https://twitter.com/jjmerelo/status/1199935927931592705&quot; class=&quot;bare&quot;&gt;https://twitter.com/jjmerelo/status/1199935927931592705&lt;/a&gt;)
Another tool to learn!!!&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Thanks to Fran, @franco87ES, for confirm I can use ConfigMap in a Job (&lt;a href=&quot;https://twitter.com/franco87ES/status/1200059717965504513&quot; class=&quot;bare&quot;&gt;https://twitter.com/franco87ES/status/1200059717965504513&lt;/a&gt;)
I was lost because after modified a yaml I wanted to re-apply it, without removed the old job and Kubernetes
was rejecting the action with a &apos;field inmutable&apos; error and I thought was due to the ConfigMap section. Thanks to Fran&amp;#8217;s
advice I digged more into the problem and at the end I realized my error.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>A series about how I'm developping an application with Kubernetes</summary>
    </entry>
    <entry>
        <title>Prices scrapping</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2019/scrapping-prices.html"/>
        <updated>2019-11-24T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2019/scrapping-prices.html</id>
        <category term="groovy"/>
        <category term="geb"/>
        <category term="google"/>
        <category term="groogle"/>
        <category term="telegram"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is a simple &apos;prices scrapping&apos; application to check a list of products
and notify via Telegram if some of them are under or bellow a price&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This post is an example to show how we can mix different technologies without spend
money to have a daily alert if some prices are changed. Be aware this is not a &quot;proffesional&quot;
way to do it and we can consider it as a &quot;toy&quot; or as a &quot;lab&quot; to learn&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In order to have a fully functional example you&amp;#8217;ll need:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a bot Telegram&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a Google Sheet plus&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;credentials from Google Project&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a Gitlab account (you can use Github or similar but this example use Gitlab pipeline)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;telegram&quot;&gt;Telegram&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll use Telegram as the channel to notify us about the changes of prices. You&amp;#8217;ll need to have
two things:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Telegram installed into our mobile phone (also you can access via web browser)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a Telegram bot&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;First step it&amp;#8217;s easy and similar as install other messanger applications.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To create our bot we&amp;#8217;ll use the Telegram application to talk with the &apos;BotFather&apos;, a bot from Telegram
able to create bots (&lt;a href=&quot;https://core.telegram.org/bots&quot; class=&quot;bare&quot;&gt;https://core.telegram.org/bots&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2019/scrapping/botfather.png&quot; alt=&quot;botfather&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically we&amp;#8217;ll order it to create a new boot writting &quot;/newbot&quot; and following his instructions
(a name, a description and so on) to obtain a &lt;strong&gt;token&lt;/strong&gt; similar as 12312312:AAAAAAAAAAAAAAAAAAAaaY
. &lt;strong&gt;DON&amp;#8217;T SHARE THIS TOKEN AND DON&amp;#8217;T STORE IT IN YOUR REPO&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To allow our bot to talk to you, you need to start the conversation, so search your bot with the
Telegram&amp;#8217;s search button and send it a hello with the &apos;/start&apos; command&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also we&amp;#8217;ll need to know your telegram client id.
You can use the existing bot &apos;@userinfobot&apos; who reply every message with info about your account
to obtain your client id plus other information.
&lt;strong&gt;YOU CAN SHARE THIS ID, IT&amp;#8217;S NOT SO IMPORTANT, BUT AS WITH THE TOKEN WE&amp;#8217;LL KEEP IT SECRET&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;google_service&quot;&gt;Google Service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Probably this is the part most obscure of the process. If you have a Google account,
you can create projects and
deploy Google AppEngine, Kubernetes, and a lot of Google services.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So open &lt;a href=&quot;https://console.cloud.google.com/&quot; class=&quot;bare&quot;&gt;https://console.cloud.google.com/&lt;/a&gt; and follow instructions to create your first
project (but for this tutorial you don&amp;#8217;t need deploy anything, only create the project)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once the project is created we&amp;#8217;ll need to enable the Google Sheet API:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;go &lt;a href=&quot;https://console.cloud.google.com/apis/library&quot; class=&quot;bare&quot;&gt;https://console.cloud.google.com/apis/library&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;search Sheet and enable it&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also we&amp;#8217;ll need create a service and generate a credentials file from it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://console.cloud.google.com/apis/credentials&quot; class=&quot;bare&quot;&gt;https://console.cloud.google.com/apis/credentials&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;create a &lt;strong&gt;SERVICE&lt;/strong&gt; credentials&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2019/scrapping/crear_credenciales.png&quot; alt=&quot;crear credenciales&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After the service is created, Google will download automatically a JSON file.
&lt;strong&gt;KEEP IT SECRET AND DON&amp;#8217;T STORE INTO YOUR REPO&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;ll need the email of the service account (something similar to &lt;a href=&quot;mailto:your-awesome-service@your-awesome-project.iam.gserviceaccount.com&quot;&gt;your-awesome-service@your-awesome-project.iam.gserviceaccount.com&lt;/a&gt;)
in the next step&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;google_sheet&quot;&gt;Google Sheet&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The application will read a Google Sheet with a simple structure as this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2019/scrapping/google_sheet.png&quot; alt=&quot;google sheet&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When you are editing the sheet you can find the ID of it in the URL:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You we&amp;#8217;ll need this ID and &lt;strong&gt;IT&amp;#8217;S BETTER NOT KEEP IT INTO YOUR REPO&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In order the application can read this sheet we need to share it with the service email created
previously so click on &apos;Share&apos; and add your service as collaborator (don&amp;#8217;t send the notification email because
nobody will be listening and you will receive a notification error email)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;application&quot;&gt;Application&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can download the application from &lt;a href=&quot;https://gitlab.com/jorge-aguilera/scrapping-prices&quot; class=&quot;bare&quot;&gt;https://gitlab.com/jorge-aguilera/scrapping-prices&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically is a &quot;one only class application&quot; who&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;reads a google sheet&amp;#8217;s range &quot;A1:E99&quot; (yes, this example
only works with a max of 99 articles),&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;opens a Geb Browser per row&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;use a custom css selector to find the price element.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If the value of the item is lower or upper than the associate rule it add the item
to a list.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At the end it sends an http POST to the channel with the summary&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gitlab&quot;&gt;Gitlab&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The repo is allocate at Gitlab and uses the pipeline capability of it to run every day the &lt;code&gt;run&lt;/code&gt; task&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically we need to configure some environment variables:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SHEET_ID (the id of the sheet)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TABS (&quot;Sheet 1&quot; or whatever you use)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TELEGRAM_TOKEN  (the token obtained via BotFather)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TELEGRAM_CHANNEL (the telegram userId)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GOOGLE_APPLICATION_CREDENTIALS (use as File instead of variable and paste the content of the credentials.json)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And set the schedule we want to use, for example:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/2019/scrapping/gitlab_pipeline.png&quot; alt=&quot;0 9 * * * (every day at 9:00)&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ll recapt:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;you have a bot telegram with a TOKEN&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;you have your telegram id and you&amp;#8217;ve started a conversation with your bot&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;you have a Google Sheet, you have the Sheet Id and you&amp;#8217;ve added the service as collaborator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;you have a credentials file in a safe place&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;you have a repo in Gitlab with several environments variables configured&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically we have someone (Gitlab Pipeline) running our application every day, reading a Google Sheet
(via a service account) and sending us a message (vía our bot telegram)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>A custom prices scrapping with telegram notification</summary>
    </entry>
    <entry>
        <title>Postureo</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2019/postureo.html"/>
        <updated>2019-11-20T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2019/postureo.html</id>
        <category term="pensamientos"/>
        <category term="introspección"/>
        <content type="html">
            &lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hace ya unas semanas se produjo una conmoción en mi TimeLine de Twitter a raiz de un tweet de alguien del sector
en el que exponía algo así como que no eres mejor profesional por dedicarte fuera del trabajo a seguir &quot;pegado&quot;
al ordenador y que es bueno hacer otras cosas diferentes, principalmente dedicar más tiempo a la familia.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En realidad algo &lt;strong&gt;tan obvio&lt;/strong&gt; no es lo que conmocionó mi TL sino los comentarios y ramificaciones que fuí viendo a raiz de él.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tengo perfectamente asumido que Twitter no es el mejor medio para explicar una opinión, incluso
muchas veces lo que expresamos no es realmente lo que querríamos decir en un entorno más &quot;reposado&quot;, pero la
palabra &lt;strong&gt;postureo&lt;/strong&gt; se iba repitiendo cada vez más, subiendo la apuesta hasta llegar a afirmar que si alguien dedica
su tiempo libre a ir a eventos, compartir código o similar claramente era por &lt;strong&gt;postureo&lt;/strong&gt; y que seguro que no eras ni un buen profesional
, y ahí ya sí me sentí especialmente dolido.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Soy un programador con muchos años en el sector, la mitad de mi edad ahora mismo,
y durante muchos de ellos fuí el típico &quot;infiltrado&quot; por no haber llegado a ella con los estudios oficiales,
hasta que me decidí a hacer el Grado de Informática (online). Mis motivos para ello escapan a este post pero lo que
no cambió el terminar los estudios fue las ganas de &quot;hacer&quot; que tenía desde el principio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde mis comienzos siempre he sentido esa inquietud de al menos intentar &quot;hacerlo&quot; sin importar si otros lo habían hecho
ya o si lo podían hacer mejor (prácticamente siempre ocurría). Quería hacerlo por mí y. porqué no, por el orgullo de poder
decir &quot;yo lo he hecho&quot;. Spoiler: ninguna de mis ideas revolucionarias han servido para nada&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Así fue como monté una empresa de servicios de software donde llegamos a ser hasta 10 personas trabajando en múltiples
proyectos a la vez (y que alguna de las crisis y mi mal saber hacer se llevaron por delante). Y &lt;strong&gt;es cierto&lt;/strong&gt;, en aquella época
intentaba transmitir mi pasión por el &quot;hacer&quot;, dedicando los tiempos que podía a investigar, descubrir problemas e imaginar
soluciones y aunque no juzgaba a la gente por hacer lo que quisiera en su tiempo libre &lt;strong&gt;sí tenía predilección&lt;/strong&gt; por los que
como yo &lt;em&gt;sacrificaban&lt;/em&gt; ese tiempo en aplicarlo en mejorar su día a día. Pero ¿es que acaso no tenemos todos
afinidad por quien comparte nuestras aficiones y puntos de vista?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Luego llegaron los meetups, los eventos, los proyectos OpenSource, los blogs, el (podría yo) dar una charla, el para la
próxima lo haré de esta otra forma. Con cada evento al que he asistido y cada charla que he perpetrado
no me han hecho sentirme superior a tí en el más mínimo ápice, de hecho prácticamente todo lo contrario,
en cada uno me ha hecho sentirme más ignorante.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Y efectivamente todo eso es consumiendo parte de mi tiempo libre que a veces me hacen
encerrarme en mi habitación y rechazar invitaciones de mis amigos e incluso familia.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero &amp;#8230;&amp;#8203; si es mi decisión, consciente y que me satisface ¿quién eres tú para juzgar cómo empleo mi tiempo libre?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si yo no cuestiono tu profesionalidad por dedicar sólo horas de trabajo a tu mejora, ¿porqué tienes que
asumir que mi deseo de compartir lo que voy aprendiendo es para tapar mi falta de profesionalidad o por aparentar?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;¿Acaso no puede existir en mí el deseo de enseñarte cosas que creo te pueden servir y que si no las quieres me dé igual?
Yo no te las impongo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al menos en todos esos comentarios hubo un comentario/reflexión que me ha hecho reflexionar:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;¿La gente que está en mil eventos, da charlas y escribe sobre mil asuntos transmite una imagen hacia personas
que acaban de aterrizar como de necesidad de hacer lo mismo si quieres prosperar en esta profesión?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La respuesta parece ser un rotundo sí. Personas que se inician ven toda esa actividad y exhibición de conocimientos,
que puede abrumarles y hacerles creer que eso es lo que hay que hacer.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;¿Son, al menos en parte, responsables de cómo afecta esta imagen a los que empiezan?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Difícil pregunta a la que no he encontrado una respuesta de blanco o negro.&lt;/p&gt;
&lt;/div&gt;
        </content><summary>null</summary>
    </entry>
    <entry>
        <title>Galería de fotos con Kubernetes</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2019/okteto-gallery.html"/>
        <updated>2019-11-19T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2019/okteto-gallery.html</id>
        <category term="photos"/>
        <category term="kubernetes"/>
        <category term="okteto"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este artículo vamos a ver cómo podemos publicar una galería de fotos usando herramientas de kubernetes.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Este caso de uso no deja de ser una práctica básica de conceptos kubernetes como crear volúmenes,
desplegar un servicio o ejecutar un job. Para ello podemos usar los servicios de cloud de cualquier provedor
como Google, AWS, DigitalOcean, etc. En concreto voy a usar el cloud de Okteto por lo que si quieres
seguir este ejemplo deberás haber creado una cuenta en él aunque en principio todo lo que voy a utilizar es
kubernetes puro por lo que debería dar igual el proveedor.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Estoy dando mis primeros pasos en Kubernetes, aprendiendo sobre lo que leo y practico, así que
puede ser (seguro) que alguna o todas las cosas que aquí cuente no tengan por ser la mejor solución.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Existen muchas plataformas para publicar fotos, tipo Instagram, Google Photos, etc pero en mi opinión todas
tienen la &quot;pega&quot; de que pierdes la oportunidad de aprender así como que una vez subidas a estas plataformas
pierdes el control de lo que subes. Como alternativa tenemos herramientas tipo Piwigo que es un gestor
completo orientado a la fotografía (una especie de &quot;el WordPress de la fotografía ) pero requiere una base
de datos MySQL, tener PHP instalado &amp;#8230;&amp;#8203; y sufrir los ataques típicos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo también existen multitud de programas que dado un directorio/subdirectorio con fotos nos generan
un site estático (no necesita base de datos, ni lenguajes, puritito html y javascript para ejecutar en el
browser del cliente). He probado varios pero para este ejercicio voy a usar Thumbshup
&lt;a href=&quot;https://thumbsup.github.io/&quot; class=&quot;bare&quot;&gt;https://thumbsup.github.io/&lt;/a&gt; básicamente porque me ha parecido de los más rápidos y con suficientes opciones
para poder tunear el resultado a tu gusto.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;La idea final es tener desplegado un servicio Nginx que sirva un directorio generado previamente.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El siguiente diagrama resume la arquitectura que vamos a usar para desplegar nuestra static-gallery&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-b137384d70f1e3bedf1a9f859e441d04.png&quot; alt=&quot;Diagram&quot; width=&quot;331&quot; height=&quot;645&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se puede apreciar, básicamente tendremos dos &quot;discos&quot;,
uno donde copiaremos nuestras fotos y otro que contendrá el site ya convertido,
un servidor Nginx que ofrecerá vía http el site convertido y un Job que ejecutará la generación del contenido
cuando tengamos nuevas fotos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El administrador (nosotros) copiaremos las fotos en el volumen photos usando el container &lt;code&gt;nginx&lt;/code&gt; mientras
que el visitante accederá a &lt;code&gt;nginx&lt;/code&gt; vía http para verlas ya convertidas en el volumen generated&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;preparación&quot;&gt;Preparación&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como se ha comentado lo primero que deberás tener es una cuenta en algún proveedor de Kubernetes (podrías
usar minikube en local pero la idea es llegar a publicar el album en Internet). En este caso voy a usar
mi cuenta en Okteto&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En segundo lugar vamos a necesitar la herramienta de consola &lt;code&gt;kubectl&lt;/code&gt;. Si prefieres usar herramientas
gráficas en lugar de la línea de consola &amp;#8230;&amp;#8203; este no es tu sitio.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En tercer lugar necesitaremos (obviamente) un directorio/subdirectorios con las fotos que queremos publicar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último necesitaremos bajarnos las credenciales de nuestro proveedor y configurar &lt;code&gt;kubectl&lt;/code&gt; para que las use
. Por ejemplo, en una consola lo primero que ejecutaré será:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ export KUBECONFIG=$(pwd)/okteto-kube.config &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;con $(pwd) ajusto a kubectl para que use una ruta absoluta a las credenciales&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;volumenes&quot;&gt;Volumenes&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero que vamos a preparar son 2 volúmenes, uno donde copiaremos nuestros fotos que tenemos en local y otro
donde volcaremos el static-site generado para que nginx lo lea&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;storage.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-source
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-gallery
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este fichero estamos especificando que queremos crear 2 volumenes (en realidad el volumen es uno y ya está creado en nuestra cuenta, lo que hacemos es
reservar &quot;trozos&quot; de nuestro volumen)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Usaremos &lt;code&gt;example-source&lt;/code&gt; como lugar donde copiar las fotos y &lt;code&gt;example-gallery&lt;/code&gt; como lugar donde ubicar el static&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ kubectl apply -f storage.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;copiar_fotos&quot;&gt;Copiar fotos&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En kubernetes necesitas &quot;montar&quot; un volumen a alguna instancia para poder copiar (o extraer) ficheros de el mismo.
Para este ejemplo vamos a usar el mismo container de &lt;code&gt;nginx&lt;/code&gt; (aunque podríamos usar un &lt;code&gt;busybox&lt;/code&gt; por ejemplo
que es una imagen mínima)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;nginx.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: ejemplo
spec:
  selector:
    matchLabels:
      app: ejemplo
  replicas: 1
  template:
    metadata:
      labels:
        app: ejemplo
    spec:
      containers:
      - name: ejemplo
        image: nginx:1.7.9
        ports:
        - containerPort: 80
        resources:
          limits:
            memory: &quot;1024Mi&quot;
          requests:
            memory: &quot;1024Mi&quot;
        volumeMounts:
        - mountPath: /usr/share/nginx/html
          name: gallery
        - mountPath: /photos
          name: photos
      volumes:
        - name: gallery
          persistentVolumeClaim:
            claimName: &apos;example-gallery&apos;
        - name: photos
          persistentVolumeClaim:
            claimName: &apos;example-source&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este punto el volumen de interés es el llamado &lt;code&gt;photos&lt;/code&gt; que se montará en la ruta &lt;code&gt;/photos&lt;/code&gt; del contenedor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Una vez desplegado podremos transferir nuestras fotos al volumen:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ kubectl apply -f nginx.yaml &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
$ kubectl  cp photos nginx:/input  &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
$ kubectl delete -f nginx.yaml  &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
deployment.apps &quot;ejemplo&quot; deleted&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Habrá que esperar unos segundos para que el pod se cree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Copiamos nuestras fotos en local, &lt;code&gt;photos&lt;/code&gt;, al volumen &lt;code&gt;input&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Una vez copiadas ya no necesitamos este container&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;generar_el_site&quot;&gt;Generar el site&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Para generar el site vamos a usar una instancia de Thumbshup y la vamos a ejecutar
como un job de una sóla ejecución&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;job.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;apiVersion: batch/v1
kind: Job
metadata:
  name: thumbsup
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: thumbsup
        image: thumbsupgallery/thumbsup
        command: [&quot;thumbsup&quot;,&quot;--input&quot;,&quot;/photos/photos&quot;,&quot;--output&quot;,&quot;/gallery&quot;]
        volumeMounts:
          - mountPath: /photos
            name: input
          - mountPath: /gallery
            name: output
      volumes:
        - name: input
          persistentVolumeClaim:
            claimName: &apos;example-source&apos;
        - name: output
          persistentVolumeClaim:
            claimName: &apos;example-gallery&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este job montamos los dos volumenes y le indicamos a thumbsup cúal es el de entrada
y cúal el de salida. Una vez que se ejecute el job tendremos en &apos;gallery&apos; el site generado&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ kubectl apply -f job.yaml
job.batch/thumbsup created

$ kubectl get jobs.batch
NAME       COMPLETIONS   DURATION   AGE
thumbsup   1/1           25s        46s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Podemos ver que para este ejemplo, con solo un par de fotos, thumbsup ha tardado unos segundos&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nginx&quot;&gt;Nginx&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por último simplemente nos queda crear un pod donde un Nginx pueda servir el site generado
previamente. Para ello vamos a volver a desplegar el mismo deployment de la parte de copy&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ kubectl apply -f nginx.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y por último desplegaremos un &lt;code&gt;service&lt;/code&gt; que conecte nuestro &lt;code&gt;nginx&lt;/code&gt; con el mundo exterior&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;service.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;apiVersion: v1
kind: Service
metadata:
  name: ejemplo
  annotations:
    dev.okteto.com/auto-ingress: &quot;true&quot;
spec:
  type: ClusterIP
  ports:
    - name: &quot;ejemplo&quot;
      port: 80
  selector:
    app: ejemplo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ kubectl apply -f service.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;resultado&quot;&gt;Resultado&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si todo ha ido bien tendremos un site estático como este&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://viajes-jagedn.cloud.okteto.net&quot; class=&quot;bare&quot;&gt;https://viajes-jagedn.cloud.okteto.net&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;custom_domain_y_okteto&quot;&gt;Custom domain y Okteto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Actualmente Okteto no ofrece la posibilidad de customizar la aplicación con tu
propio dominio pues es un producto orientado más al desarrollo pero quién sabe
en un futuro próximo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>En este artículo vamos a ver cómo podemos publicar una galería de fotos usando herramientas de kubernetes.</summary>
    </entry>
    <entry>
        <title>Grails and Sonarqube</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2019/grails-sonarqube.html"/>
        <updated>2019-11-18T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2019/grails-sonarqube.html</id>
        <category term="grails"/>
        <category term="sonarqube"/>
        <category term="coverage"/>
        <category term="codenarc"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Recently I&amp;#8217;ve had to add SonarQube to our Grails application to gain visibility in our metrics and though at the end the change was only a few of config lines
I think it might be interesting to document it in this post due the lack of updated documentation about it&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Grails is a groovy-lang framework where you can develop a CRUD web application in a few seconds with a great plugin ecosystem. It has close to 10 years and it has evolved from initial heavy versions to a more light framework. Currently I&amp;#8217;m working in a 3.x application althouht version 4.x is out a few months ago.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We&amp;#8217;re using some plugins as Codenarc and Coverage how generate reports about the quality of your code (rule violations, check style, coverture, and so on) and we have some asserts to abort the build if we&amp;#8217;re in a low coverture situtation for example&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SonarQube is an open-source platform developed by SonarSource for continuous inspection of code quality to perform automatic reviews with static analysis of code to detect bugs, code smells and security vulnerabilities on 20+ programming languages. SonarQube offers reports on duplicated code, coding standards, unit tests, code coverage, code complexity, comments, bugs and security vulnerabilities. ( &lt;a href=&quot;https://en.wikipedia.org/wiki/SonarQube&quot; class=&quot;bare&quot;&gt;https://en.wikipedia.org/wiki/SonarQube&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;With SonarQube you can have a good dashboard your QA team will appreciate, so we&amp;#8217;ll try to integrate our gradle build with it.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;docker_sonarqube&quot;&gt;Docker SonarQube&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Firstly you need to have a SonarQube instance running due the build needs to comunicate with it to send the information. The idea is to have an instance for multiple projects and in this way compare among them, align force, styles etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;For development I&amp;#8217;ll used a docker instance at port 9000:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;$ docker run -d --name sonarqube -p 9000:9000 sonarqube&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;grails&quot;&gt;Grails&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is part of our build.gradle configuration (nothing special to comment) where, in addition to the standard Grails configuration, we&amp;#8217;ve codenarc and clover installed&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;buildscript {
    repositories {
        mavenLocal()
        maven { url &quot;https://repo.grails.org/grails/core&quot; }
        maven { url &quot;https://plugins.gradle.org/m2/&quot; }
        jcenter()
    }
    dependencies {
        classpath &quot;org.grails:grails-gradle-plugin:$grailsVersion&quot;
        classpath &quot;org.grails.plugins:hibernate4:${gormVersion - &quot;.RELEASE&quot;}&quot;
        classpath &quot;com.bertramlabs.plugins:asset-pipeline-gradle:2.14.2&quot;
        classpath &apos;com.bmuschko:gradle-clover-plugin:2.2.3&apos;
        // others stuff
    }
}

// others plugins
apply plugin: &quot;codenarc&quot;
apply plugin: &apos;com.bmuschko.clover&apos;

dependencies {
    clover &apos;org.openclover:clover:4.4.1&apos;
}

codenarc {
    config = file(&apos;conf/codenarc-rulsets.groovy&apos;)
    //... some more configs
}

clover {
    excludes = [&apos;/test/**&apos;, ]

    testIncludes = [&apos;**/*Spec.groovy&apos;]

    compiler {

    }
    compiler {
        encoding = &apos;UTF-8&apos;

        // used to add debug information for Spring applications
        debug = true
        additionalArgs = &apos;-Dclover.pertest.coverage=off&apos;
        additionalGroovycOpts = [configscript: project.file(&apos;cloverXtraConfig.groovy&apos;).absolutePath]
    }

    report {
        html = true
        xml = true
        pdf = true
        columns{
            complexity format: &apos;raw&apos;
            coveredBranches format: &apos;%&apos;
            totalBranches format: &apos;raw&apos;
            coveredStatements format: &apos;%&apos;
            lineCount format: &apos;raw&apos;
        }
    }
}
cloverGenerateReport.doLast {
    run(new File(&quot;scripts/AbortIfNotCoverage.groovy&quot;))
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;grails_sonarqube&quot;&gt;Grails &amp;amp; SonarQube&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;First thing I did was a quick search with &lt;code&gt;grails sonarqube&lt;/code&gt;
terms and I realized more usefull post were a little out of date (2014-2015)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After reading some of these posts I was confused because in some of them I understood the required plugins need to be installed in local project downloading some jars but in others talking to download to a sonarqube directory.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So after some investigation the photo finish is:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;you need to install in your Grails (Gradle) project &lt;strong&gt;only&lt;/strong&gt; the sonarqube plugin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;you install the &lt;code&gt;groovy&lt;/code&gt; and &lt;code&gt;coverage&lt;/code&gt; plugins via sonarqube web application&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;at the build time the plugin downloads both of them (and others), perfom the analisys and send the result to the
backend&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To sumarize, we only need to include the &lt;code&gt;sonarqube&lt;/code&gt; plugin and set some properties:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;buildscript {
    ...
    dependencies {
        ...
        classpath &quot;org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8&quot;
    }
}

...
apply plugin: &quot;org.sonarqube&quot;

...
sonarqube {
    properties {
        property &apos;sonar.verbose&apos;, &apos;true&apos;
        property &quot;sonar.host.url&quot;, &quot;http://localhost:9000/&quot;  &lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
        property &quot;sonar.sourceEncoding&quot;,&quot;UTF-8&quot;
        property &quot;sonar.projectName&quot;, &quot;hello-grails&quot;
        property &quot;sonar.projectKey&quot;, &quot;hello-grails&quot;

        property &quot;sonar.dynamicAnalysis&quot;,&quot;reuseReports&quot;  &lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
        property &quot;sonar.clover.reportPath&quot;, file(&quot;build/reports/clover/clover.xml&quot;).absolutePath

        property &quot;sonar.projectBaseDir&quot;,&quot;&quot;
        property &quot;sonar.sources&quot;,&quot;grails-app,src/main/groovy&quot; &lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
        property &quot;sonar.exclusions&quot;,&quot;**/*.properties,**/*.js,**/*.html&quot;
        property &quot;sonar.test.exclusions&quot;,&quot;**/*.properties&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;1&quot;&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Only to test, needed to change to your QA instance. You can overwrite via -D or -P&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;2&quot;&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;We&amp;#8217;ll use the report generated by clover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class=&quot;conum&quot; data-value=&quot;3&quot;&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Sonar will scan all the grails applicaion plus src except js and html&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;sonarqube_dashboard&quot;&gt;Sonarqube dashboard&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;These are some images from official page to show you what kind of information you can obtain&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://docs.sonarqube.org/download/attachments/3670978/Dashboards-Global.png&quot; alt=&quot;Dashboards Global&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://docs.sonarqube.org/download/attachments/3670978/Dashboards-Project.png&quot; alt=&quot;Dashboards Project&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;more_information&quot;&gt;More information&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you want to read more about Sonarqube these links are helpfull&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sonarqube.org/sonarqube-7-9-lts/&quot; class=&quot;bare&quot;&gt;https://www.sonarqube.org/sonarqube-7-9-lts/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.baeldung.com/sonar-qube&quot; class=&quot;bare&quot;&gt;https://www.baeldung.com/sonar-qube&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>Recently I've had to add SonarQube to our Grails application to gain visibility in our metrics</summary>
    </entry>
    <entry>
        <title>Kubernetes, Infraestructure (2)</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-2.html"/>
        <updated>2019-11-03T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-2.html</id>
        <category term="micronaut"/>
        <category term="kubernetes"/>
        <category term="k8s"/>
        <category term="groovy"/>
        <category term="okteto"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is the second part of a series of posts about how I&amp;#8217;ll develop an application in Kubernetes (k8s)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;first post: Idea (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-1.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-1.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;second post: Infraestructure (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-2.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-2.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;third post: Job (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-3.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-3.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The main ot these posts is to document the process of deploying a solution in k8s at the same
time I&amp;#8217;m writting the application so probably all posts will have a lot of errors and mistakes that I need to
correct in the next post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Be aware that I&amp;#8217;m a very nobel with Kubernetes and these are my first steps with it.
I hope to catch up the attention of people with more knowledge than me and maybe they can review these posts and suggest
to us some improvements.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-tip&quot; title=&quot;Tip&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
I&amp;#8217;ve created a git repository at &lt;a href=&quot;https://gitlab.com/puravida-software/k8s-bibliomadrid&quot; class=&quot;bare&quot;&gt;https://gitlab.com/puravida-software/k8s-bibliomadrid&lt;/a&gt;
with the code of the application
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To develop this application I&amp;#8217;ll need some accounts in differents services plus some tools installed locally.
Also I use Linux. If you use Windows, probably you&amp;#8217;ll need extra tools but with Windows &amp;#8230;&amp;#8203; who knows ?&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;tools&quot;&gt;Tools&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ as IDE (Visual Code is another great option)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kubectl&lt;/code&gt; (command line tool to interact with your k8s cluster) Probably there are visual tools but at the end you&amp;#8217;ll be more productive from the command line&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;okteto cli&lt;/code&gt; (more info about it bellow)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;services_free_account&quot;&gt;Services (free account)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Docker Hub (&lt;a href=&quot;https://hub.docker.com/&quot; class=&quot;bare&quot;&gt;https://hub.docker.com/&lt;/a&gt;) a public repository where you can upload your Docker images (the free tier also
provide 1 private repository but I&amp;#8217;ll work only with public repos). I&amp;#8217;ll investigate if I can use the Gitlab repository
due I&amp;#8217;m a big fan of Gitlab&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Okteto (&lt;a href=&quot;http://okteto.com/&quot; class=&quot;bare&quot;&gt;http://okteto.com/&lt;/a&gt;) as kubernetes provider due not only because they offer a generous free cluster to play
with k8s but as you can develop and test the code directly in it. Instead Okteto you can try to use &lt;code&gt;minikube&lt;/code&gt; as
local kubernetes provider and after test your application you can deploy it to Google Cloud, AWS, Digital Ocean, etc
(most of them with several months to try it)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Github. Worst thing (in my opinion) of Okteto is that you need to have a Github account to identify you in Okteto. I&amp;#8217;ll
use my Github account but in fact I&amp;#8217;ll not use it to publish my application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gitlab. I&amp;#8217;ll use my account in Gitlab to publish the code of the application. With Gitlab you can implement a
CD/CI (continuous deployment) to a k8s cluster. If you use Google Cloud as Kubernetes provider
it&amp;#8217;s very easy to deploy your application after every commit.
I was not able to implement it with Okteto and Gitlab so by the moment the deploy will be done manually or semi-automatic&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-tip&quot; title=&quot;Tip&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;title&quot;&gt;CD/CI with Okteto&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pablo Chico, @pchico83, gave me some guides to integrate a CD/CI with Okteto but by the moment I&amp;#8217;ll deploy manually.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Basically with the &lt;code&gt;okteto cli&lt;/code&gt; installed you can execute following commands:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;$&amp;gt; export OKTETO_TOKEN=YOUR_TOKEN&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;$&amp;gt; okteto create namespace $(CI_COMMIT_TAG)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;$&amp;gt; kubectl apply -f src/main/k8s/deployment.yaml&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and okteto will create a namespace with the tag into your cluster and configure kubectl to deploy your application.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;okteto&quot;&gt;Okteto&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once you have an account in Okteto (using your Github account as login provider) you have up to 3 namespaces.
By the moment I&amp;#8217;ll have &lt;strong&gt;dev&lt;/strong&gt; and &lt;strong&gt;prod&lt;/strong&gt; so I&amp;#8217;ll create a new namespace called &lt;strong&gt;dev&lt;/strong&gt; where I&amp;#8217;ll deploy and test the application&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
if you&amp;#8217;re thinking you need more than 3 namespaces you&amp;#8217;re lucky because it&amp;#8217;s seems this number will be increase soon!!!
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/okteto1.png&quot; alt=&quot;okteto1&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-tip&quot; title=&quot;Tip&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
main idea in k8s is to have all the details of your infraestructure in files so you can replicate the application
in every namespace with minimal changes. Typical files are YAML format and you &quot;need&quot; to version them in the same way you version your code
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
to work with &lt;code&gt;kubectl&lt;/code&gt; from command line, you&amp;#8217;ll need to download from Okteto the credentials file but &lt;strong&gt;DON&amp;#8217;T ADD IT TO YOUR REPO&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;database&quot;&gt;Database&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As we&amp;#8217;ll need a database to store the loans we can use the &lt;code&gt;deploy application&lt;/code&gt; feature from Okteto to deploy a Postgresql database in only a few seconds&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/oktetopostgre.png&quot; alt=&quot;oktetopostgre&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this screen you can set the user, password and database name. Remember your values because we&amp;#8217;ll store them in a secret vault and &quot;inject&quot; them
as environment variables into our container&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/okteto_infraestruture.png&quot; alt=&quot;okteto infraestruture&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock warning&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-warning&quot; title=&quot;Warning&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
As @michael_gallego and @pchico83 advised us, this is not the kubernetes way to implement a database. You can read more about it at:
&lt;a href=&quot;https://twitter.com/micael_gallego/status/1190691281036627970&quot; class=&quot;bare&quot;&gt;https://twitter.com/micael_gallego/status/1190691281036627970&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;project&quot;&gt;Project&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;ll use &lt;code&gt;Gradle&lt;/code&gt; as build tool (you can use &lt;code&gt;maven&lt;/code&gt; if you preffer) so I&amp;#8217;m thinking to have a multiproject repository similar to:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;k8s-bibliomadrid&amp;#8201;&amp;#8212;&amp;#8201;okteto&amp;#8201;&amp;#8212;&amp;#8201;k8s&amp;#8201;&amp;#8212;&amp;#8201;job&amp;#8201;&amp;#8212;&amp;#8201;service&amp;#8201;&amp;#8212;&amp;#8201;front&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In &lt;code&gt;okteto&lt;/code&gt; I&amp;#8217;ll store files related with it , for example the credentials files (remember not store them into your git repo)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In &lt;code&gt;k8s&lt;/code&gt; I&amp;#8217;ll store the yaml files to deploy artifacts as volumes, secrets, etc. By the moment not sure if I&amp;#8217;ll store also deployment files
for &lt;code&gt;service&lt;/code&gt; and &lt;code&gt;front&lt;/code&gt; or I&amp;#8217;ll use every specific folder (i.e. &lt;code&gt;service/k8s&lt;/code&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By the moment, once initialized the main project with &lt;code&gt;gradle init&lt;/code&gt; I&amp;#8217;ve created the &lt;code&gt;okteto&lt;/code&gt; directory and prepare a secret file per environment
with the Postgresql details (as this is a POC project I&amp;#8217;ll use plain text but you must use encrypt format for this)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/okteto_secrets.png&quot; alt=&quot;okteto secrets&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;dev/postgre-secrets.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight linenums&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  labels:
    app: postgres
data:
  POSTGRES_DB: okteto
  POSTGRES_USER: okteto
  POSTGRES_PASSWORD: okteto
  POSTGRESQL_SERVICE_HOST: postgresql-headless&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;and apply the &lt;code&gt;dev/postgre-secrets.yaml&lt;/code&gt; into they environment:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;console&quot;&gt;export KUBECONFIG=$(pwd)/okteto/okteto-kube.config
kubectl apply -n dev-prestamos-bibliotecas -f k8s/dev/postgre-secrets.yaml
kubectl get secrets -n dev-prestamos-bibliotecas&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When we&amp;#8217;ll create a container we&amp;#8217;ll inject these environment variables into it to avoid hard-code configuration.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;next_steps&quot;&gt;Next steps&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I think next step will be a more &quot;typical&quot; task as create an application able to read a CSV file and insert into
a database. I&amp;#8217;m thinking in a &quot;one-shot&quot; application (probably a micronaut cli application with micronaut-data)
so I&amp;#8217;ll need some way to:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;upload files (I want to control what file to process instead to delegate in the application) to a volume&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;connect the application with the database (I hope will not be very dificult)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;consume the application into the cluster as a Job (executed manually by the moment)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;so 50% typical development task vs 50% k8s task&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>A series about how I'm developping an application with Kubernetes</summary>
    </entry>
    <entry>
        <title>Kubernetes, Idea (1)</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-1.html"/>
        <updated>2019-11-01T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-1.html</id>
        <category term="micronaut"/>
        <category term="kubernetes"/>
        <category term="k8s"/>
        <category term="groovy"/>
        <category term="okteto"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is the first part of a series of posts about how I&amp;#8217;ll develop an application in Kubernetes (k8s)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;first post: Idea (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-1.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-1.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;second post: Infraestructure (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-2.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-2.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;third post: Job (&lt;a href=&quot;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-3.html&quot; class=&quot;bare&quot;&gt;https://blog.jagedn.dev/blog/prestamos-bibliotecas/k8s-3.html&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The main ot these posts is to document the process of deploying a solution in k8s at the same
time I&amp;#8217;m writting the application so probably all posts will have a lot of errors and mistakes that I need to
correct in the next post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
Be aware that I&amp;#8217;m a very nobel with Kubernetes and these are my first steps with it.
I hope to catch up the attention of people with more knowledge than me and maybe they can review these posts and suggest
to us some improvements.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-tip&quot; title=&quot;Tip&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
I&amp;#8217;ve created a git repository at &lt;a href=&quot;https://gitlab.com/puravida-software/k8s-bibliomadrid&quot; class=&quot;bare&quot;&gt;https://gitlab.com/puravida-software/k8s-bibliomadrid&lt;/a&gt;
with the code of the application
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;My idea is to develop a &quot;solution&quot; with some level of complexity (use a database, some services and a public front)
and build it step by step. So, a possible roadmap could be:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;define the architecture of the application&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;design the application, tools and basic infraestructure&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;import data into database using a service or script. It needs to be executed every month&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a REST service to expose dome domains&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a simple web frontend to consume the service&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The town hall of Madrid releases every month a huge file with the details of loans of public libraries to their users
from the previous month.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In this file you can find when a library loan a resource to a user (you can&amp;#8217;t know the user, only if it&amp;#8217;s adult,
child, staff and so on). The resource can be a book, a newspaper, a DVD, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Also, in the same portal, we can find another file with the catalog of all books in the libraries with the detail of it (author, num of copies,
where to find it, etc)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The idea will be to build an application to ask simple questions about what was the most popular book loaned last month
(or the date you want), compare libraries, etc&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;architecture&quot;&gt;Architecture&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A first approach to the architecture can be:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;/images/diag-7493c66b7d138afe72d6abdcf4d75cc5.png&quot; alt=&quot;Diagram&quot; width=&quot;460&quot; height=&quot;589&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As you can see, we&amp;#8217;ll have a k8s pod with a database (I&amp;#8217;m curious about Postgre so this time I&amp;#8217;ll try to use it), some
kind of script to import csv into it, maybe using a k8s filesystem (persistent volume claim or PVC) where the admin
upload the files, a service REST (pretty sure it&amp;#8217;ll be a micronaut service) talking against a webapp (I&amp;#8217;m thinking
in a simple Vue application)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;next_steps&quot;&gt;Next steps&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The next step will be to define the application and select the tools and infraestructure&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>A series about how I'm developping an application with Kubernetes</summary>
    </entry>
    <entry>
        <title>First post</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2019/first-post.html"/>
        <updated>2019-10-24T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2019/first-post.html</id>
        <category term="jbake"/>
        <category term="netlify"/>
        <category term="blog"/>
        <content type="html">
            &lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lo primero es lo primero, así que lo primero será presentarse:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Soy un programador que ya peina canas y que ha pasado por algunos lenguajes de programación como Natural, C, C++,
Java y en los últimos años Groovy.
Me gusta mucho (demasiado) saltar de idea en idea por lo que dificilmente me verás en un proyecto durante mucho tiempo seguido.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Digamos que en un tiempo pasado me llamaban &quot;el que abre caminos&quot; mientras que ahora ya sólo me llaman &quot;culo inquieto&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Desde hace ya muchos años todas las cosas que ideo o pruebo suelo publicarlas como código abierto en Gitlab y
alguna cosilla en Github, además de comentarlas en Twitter (como @jagedn). Por eso me &quot;sorprendió&quot; cuando un buen amigo me dijo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;tío no has pensado en hacerte en vez de tanto twitter un blog con la tecnología que vas descubriendo&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Como me conozco como si me hubiera parido mi respuesta instantánea fue:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que tú me preguntes algo asi, el que me conoce mejor que yo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El que me ha visto empalmarme con 101 groovy y luego hundirme en que lo quiero borrar&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;El que vio nacer el blog de pura vida que sólo duro 2 post&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Bueno lo dejo ya que me canso&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Es cierto, y reconocido abiertamente, que Twitter me tiene enganchado (no sólo por cosas tech)
y que lo uso para dar algunas pinceladas de mis movidas, básicamente por la comodidad de con una o dos frases, y
 alguna imágen, &quot;pescar&quot; a quien lo lea.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero por otro lado prácticamente todos mis proyectos frikadas incluyen su documentación online,
con su @asciidoctor y hasta alguno con su diagrama UML. También he estado escribiendo un blog sobre pequeños
scripts en groovy de las cosas más variadas (&quot;101-scripts en groovy&quot;) e incluso dado alguna charla intentando explicarlos.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;De ahí que el consejo de este amigo me dejara un poco descolocado.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero por suerte (o desgracia) es un perro de presa y continuó:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Vale mal formulada&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Pero es que macho con todo lo que sabes y descubres no solo de una tecnología&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Sino de todo general&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Quizá no has dado con la tecla para mantenerlo durante más tiempo&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Obviamente el peloteo nos gusta a todos pero ya tengo experiencia en no caer en él y quedarme con la &quot;chicha&quot;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;¿Tal vez no lo estoy haciendo bien? ¿debería dejar Twitter ? ¿Realmente tengo algo que contar?&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;así que mi contestación no pudo ser otra que:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algun dia lo lamentare ya lo se&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Algun dia le dire a Dani todo lo que no leen tus ojos lo escribi yo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Me he dado de alta en dev.to&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Que es mejor q medium&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pero todavia no he escrito nada&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;y aquí estoy, preguntándome si será buena idea o no abrir este nuevo melón y si realmente tendré algo que contar más que si a alguien le interesará&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;i class=&quot;fa icon-note&quot; title=&quot;Note&quot;&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hasta aquí fue el post original que escribí como primera entrada en Dev.to (&lt;a href=&quot;https://dev.to/jagedn/primer-post-ffb&quot; class=&quot;bare&quot;&gt;https://dev.to/jagedn/primer-post-ffb&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A pesar de ser una plataforma muy buena para publicar contenido con una cantidad enorme de lectores sigo perdiendo esa
independencia de hacer mi propio blog, aprender de las herramientas y &quot;fabricarme&quot; las mías, así que he creado
este blog que servirá como &quot;fuente de la verdad&quot; y del que Dev.to se nutrirá&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
        </content><summary>Lo primero es lo primero, así que lo primero será presentarse</summary>
    </entry>
    <entry>
        <title>JBake y Netlify</title>
        <author>
            <name>Jorge Aguilera</name>
        </author>
        <link href="https://blog.jagedn.dev/blog/2019/jbake-netlify.html"/>
        <updated>2019-10-24T00:00:00Z</updated>
        <id>https://blog.jagedn.dev/blog/2019/jbake-netlify.html</id>
        <category term="jbake"/>
        <category term="netlify"/>
        <category term="blog"/>
        <content type="html">
            &lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;En este post voy a explicar de forma resumida (porque aunque no lo creas está escrito completamente con el móvil) cómo y porqué he decidido usar JBake como generador del blog y Netlify como plataforma donde desplegarlo&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jbake&quot;&gt;JBake&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Al estilo de Hugo, Gastby, etc JBake es un generador de contenidos estático principalmente blogs. Pero lo que me atrajo de él es que está escrito en Java y hay un plugin de Gradle para gestionarlo, dos de las herramientas que uso a diario, así que no tengo que instalar nada nuevo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por si fuera poco NO necesito usar Markdown el cual en mi opinión es la mayor mierda que se ha inventado. En su lugar uso @asciidoctor del que tambien soy fanático&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Además al tener el build en Gradle puedo añadirle  extensiones (twittear al subir un post, añadir presentaciones, etc). Si quieres ver un ejemplo real de un blog en JBake echa un ojo a &lt;a href=&quot;https://groovy-lang.gitlab.io/101-scripts/&quot; class=&quot;bare&quot;&gt;https://groovy-lang.gitlab.io/101-scripts/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;netlify&quot;&gt;Netlify&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Realmente he descubierto este site deployment no hace mucho. La funcionalidad de desplegar un site estático la cubre perfectamente Gitlab pages por lo que no lo necesitaba&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sin embargo después de jugar un poco con él he ido cambiando de opinión. Super rápido, permite desplegar muchos sites de forma gratuita y ofrece unas funcionalidades que complementan a un site estático como son los formularios que te notifican automáticamente cuando un usuario lo ha rellenado y las functions&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Estas en especial me parecen que tienen mucho potencial. Son lamdas AWS que Netlify despliega por ti de una forma super cómoda&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Se me ocurren algunas cosas que se pueden hacer con ellas en un static site:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;notificar a un canal de telegram cuando un usuario accede a una página o rellena un formulario&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;leer y/o leer de una hoja Google Sheet los resultados de una encuesta&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;etc&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;template&quot;&gt;Template&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Si te interesa probarlo (todo o parte) he preparado un template con unas instrucciones que sirvan de guía en  &lt;a href=&quot;http://jbake-netlify.blog.jagedn.dev/&quot; class=&quot;bare&quot;&gt;http://jbake-netlify.blog.jagedn.dev/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;dev_to&quot;&gt;Dev.to&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aunque muchos y muy buenos blogeros usan Medium yo no he llegado ni a crearme una cuenta. No soy experto pero tiene algunos &quot;detalles&quot; que me harían pensar si usarlo&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Por el contrario sí he usado algo Dev.to y me ha parecido un altavoz bastante bueno, así que mi idea es escribir en este blog e integrarlo con esa plataforma usando
la funcionalidad de importar RSS.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
        </content><summary>En este post voy a explicar cómo y porqué he decidido usar JBake como generador del blog y Netlify como plataforma donde desplegarlo</summary>
    </entry>
    
</feed>