Quantcast
Channel: Adictos al trabajo
Viewing all 991 articles
Browse latest View live

KitchenCI – Prueba tu infraestructura

$
0
0

Índice de contenidos

1. Introducción

Estamos en una época en la que cada vez más vivimos el desarrollo y la creación de infraestructuras como código. Esto nos trae una serie de ventajas cómo:

  • Capacidad de replicar de forma fiable un entorno determinado.
  • Uso de herramientas de versionado para tener un control de la evolución del sistema en el tiempo.
  • Uso de las herramientas de integración continua para gestionar la evolución de la infraestructura de la misma forma que gestionamos la evolución del código.
  • Uso de prácticas del desarrollo del software para nuestra infraestructura, como el testing y uso de TDD.

En este tutorial se explica qué es Kitchen CI y cómo funciona su ciclo de vida.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.6
  • KitchenCI 1.20.0

3. KitchenCI

3.1. ¿Qué es?

KitchenCI es un framework que te permite realizar testing unitarios para gestionar todo el ciclo de vida de tu infraestructura: creación, aprovisionamiento y testing. Además esta creada de forma modular, permitiendo:

  • Probar tu infraestructura con distintas herramientas de virtualización (Vagrant, Docker).
  • Está preparada para distintos sistemas operativos (Ubuntu, Centos, Windows…)
  • Permite el uso de varios proveedores (Chef, Puppet, Ansible)
  • Permite el uso de varias herramientas para probar la infraestructura (InSpec, ServerSpec, RSpec, BATS)

3.2. Ciclo de vida

Entender el ciclo de vida de KitchenCI nos permitirá comprobar «Cómo prepara la cocina» para montar nuestra infraestructura y cómo ayuda el tener una herramienta que gestione cada una de las fases de la prueba de forma que te permita parar en cualquier parte del proceso y ver el estado de la máquina.

Suponiendo que estamos utilizando Docker como herramienta de virtualización y Ansible como herramienta de aprovisionamiento; cuando queremos probar que nuestra configuración de aprovisionamiento está correcta por ejemplo, en una máquina virtual seguimos los siguientes pasos:

  • Creamos el contenedor en el queremos probar nuestros playbooks.
  • Creamos la configuración de Ansible pertinente para instalar lo que deseemos.
  • Ejecutamos Ansible apuntando a nuestro contenedor.
  • Entramos en el contenedor y comprobamos que todo está a nuestro gusto.

KitchenCI usa un procedimiento parecido en el que gestiona y coordina gran parte de la configuración de estos pasos de forma automática.

Esta configuración se lee del fichero .kitchen.yml que contiene:

  • driver: la herramienta de virtualización que utilizaremos para nuestras pruebas.
  • transport: la comunicación entre el host y la máquina virtual (SSH/WinRM).
  • provisioner: herramienta de provisionamiento a utilizar, como Ansible, Puppet o Chef.
  • verifier: aquí ponemos el componente encargado de verificar que se ha provisionado correctamente el entorno, como InSpec.
  • platforms: listado de sistemas operativos donde realizamos las pruebas.
  • suites: batería de test que serán aplicados en estos entornos.

A partir de esta configuración KitchenCI creará un listado de contenedores a partir de la suite de test y las plataformas de ejecución que se podrán ver con el comando kitchen list Cada una de estas instancias se corresponde con una máquina virtual en caso de Vagrant o un contenedor en caso de Docker o LXD, con un sistema operativo y un test. De forma que si nuestra suite de pruebas se conforma por un solo test y tenemos dos plataformas tendríamos dos instancias de nuestro driver, cada una con una plataforma.

Después crearíamos nuestra configuración con nuestra herramienta de provisionamiento preferida, asegurándonos de que nuestro .kitchen.yml esté bien configurado.

Para crear el contenedor se utiliza el comando kitchen create.

Para aplicar estos cambios en nuestras instancias ejecutamos el comando kitchen converge.

Para comprobar de forma manual que todo funciona podemos entrar de forma manual a la máquina con el comando kitchen login.

Si queremos confirmar que se ha provisionado de forma correcta, tenemos que crear el test con la herramienta de prueba que hayamos elegido. Para ello ejecutamos el comando kitchen verify.

Vemos como es muy sencillo ir paso por paso por las fases de probar la infraestructura. Ahora bien, si queremos ejecutar el ciclo de vida completo, podemos utilizar el comando kitchen test, donde se añaden el paso kitchen destroy tanto al principio como al final de forma que crea las instancias de cero si ya están creadas y al final las destruye.

4. Conclusiones

KitchenCI mejora muchísimo la gestión de la infraestructura a la hora de crear y gestionar pruebas de la infraestructura. Además su diseño modular permite que se puedan añadir herramientas en cada uno de los ciclos de vida, dotándola de una gran capacidad de persistir a lo largo del tiempo debido a que no se casa con ninguna herramienta de provisionamiento.

En el siguiente tutorial comprobaremos cómo gestionar esta configuración en un ejemplo práctico.

5. Referencias


Álgebras y funciones: patrones en programación funcional

$
0
0

Índice de contenidos

1. Introducción

Como todos ya sabemos, la programación funcional establece un enfoque mediante el cual las computaciones se consideran como la evaluación de funciones matemáticas, tratando de evitar en todo momento los, tan perjudiciales, cambios de estado o los efectos de lado.

En ocasiones, lograr evitar este tipo de problemas puede llegar a resultar imposible si no contamos con un conjunto de herramientas que nos permitan lidiar con ello.

Es precisamente en el ámbito matemático, donde encontramos las herramientas más potentes de abstracción que nos permiten mantener la transparencia referencial y/o la mutación de datos en contextos complejos. Más concretamente en la teoría de categorías nos ofrece una serie de conceptos que nos permiten alcanzar las cotas de abstracción necesarias para dar solución a este tipo de problemas.

En este tutorial vamos a ir repasando algunos de los conceptos más importantes y su representación en una de las librerías de referencia para Scala: Cats.

La aproximación que vamos a realizar será plantear inicialmente plantearemos la definición matemática del concepto en si mismo y a continuación veremos las implementaciones que se han realizado para traspasar la idea al código.

2. Estructuras Algebraicas – Los básicos

2.1. Semigrupo

Sistema algebraico compuesto por (A,⊚), donde:

  • A es un conjunto no vacío.
  • es una operación que debe cumplir, junto con A, dos propiedades:
    • Operación interna.
    • Operación asociativa.

En notación matemática se expresaría como:

Semigrupo cumple:
    * ∀ x,y ∈ A  : x ⊚ y ∈ A
    * ∀ x,y,z ∈ A: x ⊚ ( y ⊚ z ) = ( x ⊚ y ) ⊚ z

Un ejemplo muy sencillo de Semigrupo podría ser la estructura algebraica formada por los números enteros con la suma (ℤ,+).

Este semigrupo cumple con las reglas requeridas:

  • ∀ x,y ∈ A : x ⊚ y ∈ A: Al sumar un número natural con otro obtendremos otro número natural.
  • ∀ x,y,z ∈ A: x ⊚ ( y ⊚ z ) = ( x ⊚ y ) ⊚ z: No importa el orden, puesto que siempre dará el mismo resultado.

La implementación que mantiene Cats sobre este concepto es la siguiente:

trait Semigroup[A] {
    def combine(x: A, y: A): A
}

A través de este trait se establece una mecánica para definir Semigrupos, sobre un tipo A y con una operación combine que parte de dos valores de A y devuelve otro valor de A.

A continuación se muestra el ejemplo del semigrupo (ℤ,+), en este caso con el conjunto de los enteros:

implicit val intAdditionSemigroup: Semigroup[Int] = new Semigroup[Int] {
    def combine(x: Int, y: Int): Int = x + y
}

2.2. Monoide

Los monoides son sistemas algebraicos compuestos por (A,⊚,e) que cumplen con la estructura de Semigrupo y añaden una nueva restricción: el elemento neutro.

En notación matemática:

* ∃!e ∈ , ∀ x ∈ A: x ⊚ e = e ⊚ x = x

Continuando con el ejemplo que planteábamos inicialmente, podemos decir que el sistema algebraico formado por los números enteros, con la operación suma y el elemento neutro 0, (ℤ,+,0), es un monoide.

A continuación se muestra la implementación establecida en Cats:

trait Monoid[A] extends Semigroup[A] {
    def empty: A
}

Un ejemplo de implementación de monoide:

implicit val intAdditionMonoid: Monoid[Int] = new Monoid[Int] {
    def combine(x: Int, y: Int): Int = x + y
    def empty: Int = 0
}

3. Estructuras Algebraicas – Categorías

3.1. Introducción

Una categoría es un sistema algebraico compuesto por objetos, morfismos, y una serie de propiedades que debe cumplir.

  • Objetos: El concepto matemático de objeto se puede definir como cualquier cosa que se puede definir formalmente y con la cual se puede realizar razonamientos deductivos y demostraciones matemáticas. Entre los objetos más comunes encontramos, por ejemplo, números, conjuntos o cuerpos geométricos.
  • Morfismos: Representados como flechas normalmente, para cada par de objetos X e Y, un morfismo (f) es una función de X a Y
  • Propiedades:
    • Composición de morfismos (∘): Dada una función f de X a Y y dada una función g de Y a Z, entonces g ∘ f sería el morfismo de X en a Z.
    • Propiedad asociativa en la composición: ( h ∘ g ) ∘ f = h ∘ ( g ∘ f )
    • Morfismo identidad: f ∘ f’ = f’ ∘ f = f

3.2. Funtor

Los funtores definen correspondencias entre categorías, es decir, para dos categorías A y B un funtor F asocia a cada objeto de la categoría A con un objeto de la categoría B.

* A:
        a -f-> b
    * B:
        Fa -Ff-> Fb

Cats establece la siguiente implementación para los funtores:

trait Functor[F[_]] {
    def map[A, B](fa: F[A])(f: A => B): F[B]
}

Y un ejemplo sería el funtor para Option:

implicit val functorForOption: Functor[Option] = new Functor[Option] {
    def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa match {
        case None => None
        case Some(a) => Some(f(a))
    }
}

El nivel de abstracción representado por F[_] establece que trabajamos a nivel de constructores de tipos, también se le puede llamar efecto o contexto de computación. En el ejemplo trabajamos con el contexto “Option” que nos abstrae de valores potencialmente ausentes, con lo que map solo aplicará sobre los valores de tipo Some. Otros ejemplos de efectos los tenemos en Try para abstraernos de posibles excepciones o Either, para modelar flujos alternativos, por citar algunos ejemplos.

De manera general podemos decir que los Funtores nos permiten trabajar con un único efecto.

3.3. Aplicativos

Los aplicativos son una estructura a mitad de camino entre los funtores y las mónadas. Estas estructuras nos permiten secuenciar computaciones independientes entre sí.

Los aplicativos, desde el punto de vista matemático, son funtores monoidales, esto es un funtor que trabaja entre dos categorías monoidales.

Una categoría monoidal o (categoría tensorial) es una categoría que cumple con:

  • Tiene un bifuntor: ⊗: C x C –> C
  • Un objeto identidad: I
  • Tres isomorfismos naturales que cumplen con:
    • Son asociativos
    • Objeto identidad por la derecha.
    • Objeto identidad por la izquierda.

La implementación que aporta Cats a este respecto:

trait Applicative[F[_]] extends Functor[F]{
    def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

    def pure[A](a: A): F[A]

    def map[A, B](fa: F[A])(f: A => B): F[B] = ap(pure(f))(fa)
}

Esta implementación, básicamente, amplia la definición de Funtor añadiendo dos operaciones: ap y pure.

Mediante la operación pure podemos introducir un valor en el contexto de computación que aporta F.

La operación ap nos permite aplicar una función que se encuentra en el contexto de computación sobre un valor que se encuentra en el mismo contexto de computación.

A continuación, se muestra un ejemplo de implementación de Aplicativo que trabaja en el contexto de valores ausentes (Option):

implicit def optionApplicative extends Applicative[Option] {
    
    def ap[A, B](ff: Option[A => B])(fa: Option[A]): Option[B] = for {
        f <- ff
        a  B): Option[B] = ap(pure(f))(fa)

}

3.4. Mónadas

Las mónadas son endofuntores (funtores que mapean una categoría sobre si misma) con dos tranformaciones naturales:

  • Dadas dos categorías C y D y Dos Functores F y G de C a D, para un mismo objeto a en la categoría C cada Functor mapeará el objeto a en dos objetos distintos en la categoría D: Fa y Ga
  • Al morfismo en D que transforma Fa en Ga se le denomina Transformación Natural (αa).

Una Monada se crea definiendo:

  • Un constructor de tipos M.
  • Una operación unaria return ==> Esta operación toma un valor a y crea un valor monádico M[a] mediante el constructor de tipos.
  • Una operación binaria bind ==> Esta operación toma como argumentos un valor monádico M[a] y una función a -> M[b] que transforme el valor.

La implementación aportada por Cats para las mónadas es la siguiente:

trait Monad[F[_]] with Applicative[F] {
     def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]     
 }

Se puede observar que la implementación de la Mónada extiende la implementación de Aplicativo añadiendo la operación flatMap, que nos permitirá secuenciar computaciones dependientes entre sí.

Continuando con los ejemplos de implementación para Option, a continuación se muestra la implementación de Monad:

implicit def optionMonad(implicit app: Applicative[Option]) = new Monad[Option] {

    override def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = app.map(fa)(f).flatten //bind

    override def pure[A](a: A): Option[A] = app.pure(a)

}

¿Por qué utilizar aplicativos en lugar de mónadas?

  1. En algunas ocasiones no se tiene la posibilidad de elegir entre un código aplicativo o monádico.
  2. Es buena práctica utilizar la abstracción menos potente que realice el trabajo.
  3. Evita establecer dependencias innecesarias entre computaciones

A continuación, se muestra un ejemplo a través del cual comparar las distintas aproximaciones:

case class Foo(s: Symbol, n: Int)

/*Código monádico:*/

val maybeFoo = for {
  s <- maybeComputeS(whatever)
  n <- maybeComputeN(whatever)
} yield Foo(s, n)

/*Código aplicativo:*/

 val maybeFoo = (maybeComputeS(whatever) |@| maybeComputeN(whatever))(Foo(_, _))

4. Conclusiones

A lo largo de este tutorial hemos visto algunas de las estructuras algebraicas más utilizadas en la programación funcional y una aproximación muy superficial a la teoría subyacente con la intención de dejar constancia de la relación entre la teoría y la práctica, en concreto, a través de la librería cats.

En futuros tutoriales veremos algunas aplicaciones prácticas de estas estructuras y de que manera nos pueden ayudar a la hora de alcanzar grados mayores de abstracción a la hora de abordar nuestros problemas desde la programación funcional.

5. Referencias

Proyecto Jersey + Spring Boot + MyBatis

$
0
0

0. Índice de contenidos.


1. Introducción.

El objetivo que perseguimos con este tutorial es integrar el uso de Jersey en una aplicación con Spring Boot. Para ello, la mejor manera de conocer una tecnología es construir un prototipo con ella, así que eso es justamente lo que vamos a hacer. Vamos a construir una aplicación sencilla que gestione “cursos” a través de un API REST, se trata de construir un CRUD sobre una simple tabla (Cursos), haciéndolo a través de servicios web REST. Aunque es bastante sencillo, el ejemplo resalta las anotaciones más comunes que se necesitarán para construir nuestra API REST.

Antes de continuar vamos a explicar brevemente qué es JAX-RS y Jersey:

Java API for RESTful Web Services (JAX-RS) es un modelo de programación que proporciona soporte en la creación de servicios web que cumplan los principios REST (Representational State Transfer). JAX-RS usa anotaciones, introducidas en Java SE 5, para simplificar el desarrollo y despliegue de los clientes y puntos finales de los servicios web.

El framework de Jersey no es más que la Implementación de referencia JAX-RS. Jersey proporciona su propia API que amplía el conjunto de herramientas JAX-RS con características y utilidades adicionales para simplificar aún más el servicio RESTful y el desarrollo del cliente. Jersey también expone numerosos SPI de extensión para que los desarrolladores puedan extender Jersey y que se adapte mejor a sus necesidades.


2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.3 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS High Sierra 10.13.3
  • Oracle Java: 1.8.0_171
  • Apache Maven: 3.5.4
  • Spring Boot: 2.0.3.RELEASE
  • Jersey: 2.26

3. Configuración.

Vamos a crear nuestro proyecto, se recomienda la configuración mediante el sistema de gestión de dependencias de maven y, para ello, vamos a hacer uso del wizard que nos proporciona el IDE, que se va a encargar de incluir las dependencias que seleccionemos en el proceso en nuestro pom.xml, establecemos los datos principales que necesita el wizard tal y como se puede ver en la siguiente imagen:


A continuación marcamos las dependencias para Jersey, JPA y MySQL y finalizamos el proceso de creación de proyecto.


Deberíamos tener nuestro proyecto configurado en nuestro IDE con al menos las siguientes dependencias en el pom.xml:

<dependencies>
    <!-- Spring -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jersey</artifactId>
    </dependency>

    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>

</dependencies>

Esta es la configuración mínima que necesitamos para empezar a trabajar con nuestra API REST utilizando Jersey, a continuación vamos a configurar el contenedor de Jersey, para ello creamos un paquete Java para nuestras clases de configuración (por ser un poco organizados con nuestro código) y posteriormente creamos una nueva clase de configuración que vamos a llamar ‘JerseyConfiguration’ dentro de él, con el siguiente código:

package com.autentia.training.coursemanagement.configurations;

import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.context.annotation.Configuration;

@Configuration 
@ApplicationPath("/rest")
public class JerseyConfiguration extends ResourceConfig {

    public JerseyConfiguration() {
        packages("com.autentia.training.coursemanagement.web.resources");

    }
}

El servlet Jersey se registra y asigna al path “/*” de forma predeterminada, en nuestro ejemplo hemos querido cambiar dicha asignación a “/rest”, para ello hemos agregado la anotación @ApplicationPath a nuestra clase de configuración. También hemos configurado nuestro contenedor de Jersey para registrar los recursos mediante la definición del paquete donde residirán los mismos, pero podríamos registrar los recursos individualmente como se puede ver a continuación:

package com.autentia.training.coursemanagement.configurations;

import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.context.annotation.Configuration;

import com.autentia.training.coursemanagement.web.resources.CourseResource;

@Configuration 
@ApplicationPath("/rest")
public class JerseyConfiguration extends ResourceConfig {

    public JerseyConfiguration() {
        register(CourseResource.class);

    }
}

4. Configuración de la base de datos.

Antes de comenzar con nuestro CRUD vamos a configurar una base de datos h2 (ya que estamos en un entorno de pruebas) donde podremos mantener nuestra tabla de cursos, lo primero de todo es configurar nuestro fichero pom.xml añadiendo las siguientes dependencias:

<dependencies>
    ...
                
    <!-- Runtime -->
    <dependency>
        <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
    </dependency>

    ...
</dependencies>

Modificamos nuestro fichero application.properties para añadir la siguiente configuración:

### Enable H2 Console Access
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

### Define H2 Datasource configurations
spring.datasource.platform=h2
spring.datasource.url=jdbc:h2:mem:app_db;DB_CLOSE_ON_EXIT=FALSE
#spring.datasource.url = jdbc:h2:file:~/h2/app_db;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver

Creamos un nuevo fichero que vamos a llamar ‘schema.sql’ en src/main/resources para que Spring Boot se encargue de crear y poblar nuestra tabla ‘Cursos’, para ello añadimos las siguientes sql:

DROP TABLE IF EXISTS COURSES;

CREATE TABLE COURSES (
  ID            BIGINT(11)      NOT NULL AUTO_INCREMENT,
  TITLE   		VARCHAR(250)  	NOT NULL,
  LEVEL			VARCHAR(80)  	NOT NULL,
  HOURS		   	INT(3)  		NULL,
  TEACHER		VARCHAR(250)  	NOT NULL,
  STATE		 	INT(1)  		NOT NULL,
  PRIMARY KEY (ID),
  UNIQUE KEY (TITLE)
);

INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('Aumenta el rendimiento de tus tests de integración con BBDD usando un pool de conexiones dbcp2 con BasicDataSource de Apache', 'Intermedio', 25, 'Alberto Barranco Ramó', 1);
INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('Aplicando TDD en concursos', 'Básico', 10, 'Alejandro Acebes Cabrera', 1);
INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('Ejecutando test de SoapUI Open Source en JUnit en un proyecto Maven', 'Básico', 20, 'Alberto Moratilla Ocaña', 1);
INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('Primeros pasos con Concordion', 'Intermedio', 15, 'Daniel Rodríguez Hernández', 0);
INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('TDD: Outside-In vs Inside-Out', 'Avanzado', 25, 'Jose Luis Rodríguez Villapecellín', 1);

5. Configuración de MyBatis.

Ahora vamos con la capa de persistencia, MyBatis en nuestro proyecto, en este apartado no me voy a explayar mucho ya que podéis encontrar más información en los tutoriales de Adictos, para ello deberíamos tener incluida la siguiente dependencia en el pom.xml:

<dependencies>
    ...

    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>

    ...
</dependencies>

Modificamos nuestro fichero application.properties para añadir la siguiente configuración:

mybatis.typeAliasesPackage=com.autentia.training.coursemanagement.model.entities

Por último añadimos nuestra clase de aplicación para Spring Boot (‘CourseManagementApplication’) para añadir la anotación de configuración ‘MapperScan’ para indicar el paquete donde iremos añadiendo nuestras clases ‘Mapper’ de MyBatis:

@SpringBootApplication
@MapperScan("com.autentia.training.coursemanagement.model.mappers")
public class CourseManagementApplication {

    public static void main(String[] args) {
        SpringApplication.run(CourseManagementApplication.class, args);
    }
}

6. Implementación de la capa de persistencia de nuestro CRUD.

Debido a que este punto no es el foco principal de este tutorial, esta parte voy a ir más rápido dejando los ejemplos de código sin comentar mucho para no hacer este tutorial muy extenso, en nuestra web de Adictos podéis encontrar tutoriales que os ayuden a entender esta parte mejor.

Entidad ‘Curso’:

package com.autentia.training.coursemanagement.model.entities;

import java.io.Serializable;

/**
* Entidad para la gestión de los cursos
* @author Autentia
* @version 1.0
*/
public class Course implements Serializable {
    package com.autentia.training.coursemanagement.model.entities;

    import java.io.Serializable;
    
    import org.apache.commons.lang3.builder.EqualsBuilder;
    import org.apache.commons.lang3.builder.HashCodeBuilder;
    import org.apache.commons.lang3.builder.ToStringBuilder;
    
    /**
     * Entidad para la gestión de los cursos
     * @author Autentia
     * @version 1.0
     */
    public class Course implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        /**
         * Identificador del curso
         */
        private Long id;
    
        /**
         * Titulo del curso
         */
        private String title;
    
        /**
         * Nivel del curso
         */
        private String level;
    
        /**
         * Número de horas del curso
         */
        private Integer numberOfHours;
    
        /**
         * Profesor del curso
         */
        private String teacher;
    
        /**
         * Estado del curso
         */
        private Boolean state;
    
        /**
         * Devuelve el identificador del curso
         * @return id
         */
        public Long getId() {
            return id;
        }
    
        /**
         * Establece el identificador del curso
         * @param id
         */
        public void setId(Long id) {
            this.id = id;
        }
    
        /**
         * Devuelve el título del curso
         * @return title
         */
        public String getTitle() {
            return title;
        }
    
        /**
         * Establece el título del curso
         * @param title
         */
        public void setTitle(String title) {
            this.title = title;
        }
    
        /**
         * Devuelve el nivel del curso
         * @return level
         */
        public String getLevel() {
            return level;
        }
    
        /**
         * Establece el nivel del curso
         * @param levelId
         */
        public void setLevel(String level) {
            this.level = level;
        }
    
        /**
         * Devuelve el número de horas del curso
         * @return numberOfHours
         */
        public Integer getNumberOfHours() {
            return numberOfHours;
        }
    
        /**
         * Establece el número de horas del curso
         * @param numberOfHours
         */
        public void setNumberOfHours(Integer numberOfHours) {
            this.numberOfHours = numberOfHours;
        }
    
        /**
         * Devuelve el profesor del curso
         * @return the teacher
         */
        public String getTeacher() {
            return teacher;
        }
    
        /**
         * Establece el profesor del curso
         * @param teacher
         */
        public void setTeacher(String teacher) {
            this.teacher = teacher;
        }
    
        /**
         * Devuelve el estado del curso
         * @return the state
         */
        public Boolean getState() {
            return state;
        }
    
        /**
         * Establece el estado del curso
         * @param state
         */
        public void setState(Boolean state) {
            this.state = state;
        }
    
        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            return HashCodeBuilder.reflectionHashCode(this);
        }
    
        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            return EqualsBuilder.reflectionEquals(this, obj);
        }
    
        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return ToStringBuilder.reflectionToString(this);
        }
    
    
    }
}

Mapper para nuestra entidad ‘Curso’:

package com.autentia.training.coursemanagement.model.mappers;

import java.util.List;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.autentia.training.coursemanagement.model.entities.Course;


/**
    * Mapper de MyBatis para la gestión de la entidad 'Course'
    * @author Autentia
    * @version 1.0
    */
@Mapper
public interface CourseMapper {

    @Insert("INSERT INTO COURSES(TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES(#{title}, #{level}, #{numberOfHours}, #{teacher}, #{state})")
    @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="ID")
    public int insert(Course course);

    @Update("UPDATE COURSES SET TITLE = #{title}, LEVEL = #{level}, HOURS = #{numberOfHours}, TEACHER = #{teacher}, STATE = #{state} WHERE ID=#{id}")
    public int update(Course course);

    @Delete("DELETE FROM COURSES WHERE ID=#{id}")
    public int deleteById(Long id);

    @Select("SELECT c.ID, c.TITLE, c.LEVEL, c.HOURS, c.TEACHER, c.STATE FROM COURSES c")
    @Results(value = {
                @Result(property = "id", column = "ID"),
                @Result(property = "title", column = "TITLE"),
                @Result(property = "level", column = "LEVEL"),
                @Result(property = "numberOfHours", column = "HOURS"),
                @Result(property = "teacher", column = "TEACHER"),
                @Result(property = "state", column = "STATE")
            })
    public List getAll();

    @Select("SELECT c.ID, c.TITLE, c.LEVEL, c.HOURS, c.TEACHER, c.STATE FROM COURSES c WHERE c.ID = #{id}")
    @Results(value = {
                @Result(property = "id", column = "ID"),
                @Result(property = "title", column = "TITLE"),
                @Result(property = "level", column = "LEVEL"),
                @Result(property = "numberOfHours", column = "HOURS"),
                @Result(property = "teacher", column = "TEACHER"),
                @Result(property = "state", column = "STATE")
            })
    public Course getById(@Param("id") Long id);

}

Interfaz de servicio para nuestra entidad ‘Curso’:

package com.autentia.training.coursemanagement.model.services;

import java.util.List;

import com.autentia.training.coursemanagement.model.entities.Course;
import com.autentia.training.coursemanagement.model.exceptions.EntityNotFoundException;

public interface CourseService {

    public Course create(Course course);
    public Course update(Long id, Course course) throws EntityNotFoundException;
    public void delete(Long id) throws EntityNotFoundException;
    public List findAll();
    public Course findOne(Long id) throws EntityNotFoundException;
}

Implementación MyBatis del servicio para nuestra entidad ‘Curso’:

package com.autentia.training.coursemanagement.model.services.mybatis;

import java.util.List;

import org.springframework.stereotype.Service;

import com.autentia.training.coursemanagement.model.entities.Course;
import com.autentia.training.coursemanagement.model.exceptions.EntityNotFoundException;
import com.autentia.training.coursemanagement.model.mappers.CourseMapper;
import com.autentia.training.coursemanagement.model.services.CourseService;

/**
    * Implementación de MyBatis del servicio para la gestión de la entidad 'Course'
    * @author Autentia
    * @version 1.0
    */
@Service
public class MyBatisCourseService implements CourseService {

    private CourseMapper courseMapper;

    public MyBatisCourseService(final CourseMapper courseMapper) {
        this.courseMapper = courseMapper;
    }

    @Override
    public Course create(Course course) {
        this.courseMapper.insert(course);
        return course;
    }

    @Override
    public Course update(Long id, Course course) throws EntityNotFoundException {
        Course courseBD = this.courseMapper.getById(id);
        if (courseBD == null) {
            throw new EntityNotFoundException("No existe el registro a modificar");
        }
        courseBD.setTitle(course.getTitle());
        courseBD.setLevel(course.getLevel());
        courseBD.setNumberOfHours(course.getNumberOfHours());
        courseBD.setTeacher(course.getTeacher());
        courseBD.setState(course.getState());

        this.courseMapper.update(courseBD);
        return courseBD;
    }

    @Override
    public void delete(Long id) throws EntityNotFoundException {
        if (this.courseMapper.getById(id) == null) {
            throw new EntityNotFoundException("No existe el registro a eliminar");
        }
        this.courseMapper.deleteById(id);
    }

    @Override
    public List findAll() {
        return this.courseMapper.getAll();
    }

    @Override
    public Course findOne(Long id) throws EntityNotFoundException {
        Course course = this.courseMapper.getById(id);
        if (course == null) {
            throw new EntityNotFoundException("No existe el registro en el sistema");
        }
        return course;
    }

}

7. Implementación del recurso REST de nuestro CRUD.

Hasta ahora todo lo que hemos hecho ha sido crear y configurar el proyecto, además de desarrollar nuestra capa de persistencia y negocio. Ahora vamos a comenzar con el objetivo de nuestro tutorial, vamos a crear nuestro servicio REST que, basándonos en el acrónimo CRUD, inserte un curso en nuestra tabla ‘COURSES’:

package com.autentia.training.coursemanagement.web.resources;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.autentia.training.coursemanagement.model.entities.Course;
import com.autentia.training.coursemanagement.model.exceptions.EntityNotFoundException;
import com.autentia.training.coursemanagement.model.services.CourseService;

/**
* Servicio REST para la gestión de la entidad 'Course'
* @author Autentia
* @version 1.0
*/
@Component
@Path("/course")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CourseResource {

    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    private CourseService courseService;
    
    public CourseResource(CourseService courseService) {
        this.courseService = courseService;
    }
    
    @POST
	public Response create(Course course) {
		if(course.getTitle() == null || course.getLevel() == null || course.getTeacher() == null || course.getState() == null) {
			return Response.status(500).entity("Falta por rellenar campos obligatorios").build();
		}
		return Response.ok().entity(courseService.create(course)).build();
    }
}

La anotación @Path identifica la plantilla de ruta de la URI a la que responde el recurso, esta anotación puede especificarse a nivel de clase o método de un recurso. El valor de la anotación @Path es una plantilla de ruta de la URI parcial en relación con la URI base del servidor en el cual se implementa el recurso, la raíz del contexto de la aplicación y el patrón de URL a la que responde el runtime de JAX-RS.

La anotación @POST indica que el método anotado responde a las peticiones HTTP POST del API REST, para usar en llamadas de tipo ‘CREATE’.

A continuación os muestro un ejemplo de invocación desde postman con la confirmación de la creación del recurso mediante nuestro API REST:

NOTA: Recordar que para evitar errores hay que añadir en la cabecera de la petición el ‘Content-Type’ y establecerlo a ‘application/json’


El siguiente paso lógico sería realizar un servicio que se encargue de la consulta de nuestro recurso, para ello añadimos en nuestro recurso los siguientes métodos:

...
public class CourseResource {
    ...
    @GET
    public Response getAll() {
        return Response.ok().entity(courseService.findAll()).build();
    }

    @GET
	@Path("/{id}")
	public Response get(@PathParam("id") Long id) {
		Course course = null;
		try {
			course = courseService.findOne(id);
		} catch (EntityNotFoundException e) {
			log.error(e.getMessage());
			return Response.status(404).build();
		}
		return Response.ok().entity(course).build();
    }
    ...
}

La anotación @GET indica que el método anotado responde a las peticiones HTTP GET del API REST, para usarse en llamadas del tipo ‘READ’, además podemos observar que en nuestro segundo método hemos añadido la anotación @Path de nuevo para indicar que queremos añadir a nuestra URI base una parte más, en este campos queremos indicar el identificador del recurso que será enviado como parámetro a nuestro método.

A continuación os muestro un ejemplo de invocación desde postman con el resultado de la consulta general mediante nuestro API REST:


Y aquí os muestro un ejemplo de invocación desde postman con el resultado de la consulta por identificador mediante nuestro API REST:


Continuamos con el siguiente paso, la modificación de nuestro recurso, para ello añadimos en nuestra clase el siguiente método:

...
public class CourseResource {
    ...
	@PUT
    @Path("/{id}")
    public Response update(@PathParam("id") Long id, Course course) {
    	Course courseUpdated = null;
    	try {
    		if(course.getTitle() == null || course.getLevel() == null || course.getTeacher() == null || course.getState() == null) {
    			return Response.status(500).entity("Falta rellenar campos obligatorios").build();
    		}
    		courseUpdated = courseService.update(id, course);
		} catch (EntityNotFoundException e) {
			log.error(e.getMessage());
			return Response.status(404).build();
		}
		return Response.ok().entity(courseUpdated).build();
    }
    ...
}

La anotación @PUT indica que el método anotado responde a las peticiones HTTP PUT del API REST, para usarse en llamadas del tipo ‘UPDATE’.

A continuación os muestro un ejemplo de invocación desde postman con la confirmación de la modificación del recurso mediante nuestro API REST:

NOTA: Recordar el ‘Content-Type’, se establece a ‘application/json’


Por último vamos a realizar la eliminación de nuestro recurso, para ello añadimos en nuestra clase el siguiente método:

...
public class CourseResource {
    ...
	@DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") Long id) {
    	try {
			courseService.delete(id);
		} catch (EntityNotFoundException e) {
			log.error(e.getMessage());
			return Response.status(404).build();
		}
        return Response.ok().entity("Curso eliminado con éxito!").build();
    }
    ...
}

La anotación @DELETE indica que el método anotado responde a las peticiones HTTP DELETE del API REST, para usarse en llamadas del tipo ‘DELETE’.

A continuación os muestro un ejemplo de invocación desde postman con la confirmación de la eliminación del recurso mediante nuestro API REST:


8. Referencias.


9. Conclusiones.

En este tutorial hemos visto cómo crear un sencillo CRUD mediante API REST de Jersey. Algunos se preguntarán porque usar JAX-RS con Jersey en lugar del estandar de Spring. La respuesta sería porque Jersey es la implementación del estándar de JEE, por ello estamos menos acoplados a Spring y nuestro código podría ejecutarse sin mayores problemas en cualquier servidor JEE.

Espero que os haya gustado y que os haya servido, con esa intención ha sido creado. 😉

Un saludo.

Javi

Test E2E en Angular con Cypress

$
0
0

Índice de contenidos

1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: LUbuntu 18.04
  • Visual Studio Code 1.24.0
  • @angular/cli 6
  • @angular 6

2. Introducción

Cada vez se le está dando mayor importancia a los tests E2E, tanto es así que en el mundo front la típica pirámide de testing se está invirtiendo.

Esto no debería ser así y ya hemos visto en otro tutorial cómo hacer test unitarios y de integración con Angular; pero bien es cierto que en el front lo que manda es la visualización y que funcionalmente la aplicación haga lo que tiene que hacer, sin importar tanto las “tripas”.

En Angular para hacer este tipo de tests sabemos que contamos con Protractor que a través del web driver de Selenium nos permite interactuar con el navegador. Esta herramienta es muy útil pero también presenta problemas de velocidad en el desarrollo y ejecución no tan estable como nos gustaría, lo que hace que a la larga se deje de utilizar.

Es aquí donde entra como un ciclón de aire fresco Cypress que nos ofrece un IDE open-source para lanzar y ver el resultado de las pruebas, basado en Electron, para lo que utiliza un navegador embebido real, en vez de hacer uso del modo “automated” que maneja Selenium. No es especifico de Angular sino que puedes probar cualquier cosa que se muestre en un navegador independientemente de con qué tecnología y calidad esté desarrollado.

Las principales ventajas que podemos destacar de Cypress frente a Protractor son:

  • El API no es que sea más sencillo pero si que parte siempre del objeto global cy que con el autocompletado hace que no tengamos que recordar todos los objetos que maneja Protractor como: browser, element, by, utils, …
  • Proporciona mayor estabilidad en la ejecución de los tests y facilidades para depurar los problemas al no tener que lidiar con Selenium.
  • Proporciona herramientas que nos facilitan el desarrollo de los tests, por ejemplo, el IDE tiene el “Selector Playground” que nos da la referencia del DOM que necesitamos para interactuar con cierto elemento de la página. No más “inspect” y navegar en el HTML.
  • Automáticamente graba la ejecución de los tests lo que hace que podamos utilizar estos vídeos en la reuniones de demo después de los Sprints para hacerlas más ágiles.
  • Permite crear comandos propios a modo de reutilización de funcionalidad como hacemos con el patrón Page Object cuando trabajamos con Protrator.
  • También puede realizar la comparación por screenshots al estilo de Jest.
  • Permite lanzar los tests en modo consola sin necesidad de tener un navegador instalado que facilitan los procesos de integración continua.
  • Cuando tenemos un test abierto en la herramienta cualquier cambio en el código del test provoca la ejecución automática en Cypress.

3. Vamos al lío

Para integrarlo con nuestros proyectos Angular, simplemente tenemos que ejecutar dentro de nuestro proyecto.

$> npm install --save-dev cypress

Este comando nos va a descargar el IDE de cypress y nos va a añadir la dependencia necesaria en nuestro proyecto.

Para comenzar a trabajar con él, simplemente tenemos que ejecutar:

$> npx cypress open

Este comando nos va a crear una carpeta llamada “cypress” en la raíz de nuestro proyecto, con 4 subcarpetas “fixtures”, “integration”, “plugins” y “support” y dentro de “integration” la carpeta “examples” con muchos ejemplos listos para ejecutar (pincha sobre ellos) y que nos ayudan a conocer la sintaxis.

Para empezar a desarrollar un test con Cypress tenemos que crear el fichero dentro del directorio “integrations” por ejemplo, en una carpeta “simples”, al que vamos a llamar simple.spec.js, es aquí donde encontramos las primeras diferencias con Protractor, ya que Cypress utiliza Mocha como lenguaje base y escribe los tests en fichero .js y no .ts (aunque esto se puede configurar).

El test más simple que podemos ejecutar es acceder a la URL del proyecto (que tiene que estar corriendo). Por lo tanto, escribimos:

/// 

it('should visit home', () => {
    cy.visit('http://localhost:4200');
});

Si ahora vamos a la herramienta (el refresco es automático) veremos en el árbol de tests que existe la carpeta simple y que podemos ejecutar el test “simple.spec.js”, al pinchar sobre él directamente se ejecuta mostrando la home de la aplicación (la aplicación tiene que estar levantada).

Si ahora pinchamos en el símbolo marcado en la imagen:

Vemos una utilidad llamada “Selector Playground” que nos ayuda a localizar cualquier elemento del DOM de forma automática y nos proporciona la sintaxis que tenemos que pegar en el test para interactuar con ese elemento.

De esta forma si te fijas en el tutorial antes mencionado de Protractor, el equivalente en Cypress para quedarnos con el primer elemento de la lista de usuarios sería:

/// 

it('nglabs List Users', () => {
    cy.visit('http://localhost:4200');
    cy.get('#user-0').contains('mojombo');
});

Por último, para poder ejecutar todos los tests sin necesidad de abrir el IDE, por ejemplo, en una fase de integración en un servidor de integración continua, podemos simplemente ejecutar:

$> npx cypress run

Este comando va a ejecutar los tests por consola y va a grabar un video por cada test ejecutado, que almacena en la carpeta vídeos que sobrescribe cada vez que ejecuta los tests.

Este sería un ejemplo de la salida, aunque se puede configurar para proporcionar otro tipo de reportes:

4. Conclusiones

Desde luego que por lo menos para el mundo de Angular mejora sustancialmente la experiencia de implementación de tests de aceptación y es seguro que lo vamos a incorporar a nuestro stack tecnológico en detrimento de Protractor.

Recordad que esta técnica y otras muchas más las encontraréis en la guía práctica de Angular y también ofrecemos cursos in-house y online.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.

Kubernetes en AWS con Kops

$
0
0

Índice de contenidos

1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: LUbuntu 18.04

2. Introducción

Nota importante: seguir los pasos de este tutorial supone costes en la cuenta de AWS.

En este tutorial vamos a ver los pasos necesarios para montar un clúster de Kubernetes en producción en el cloud de Amazon gracias a Kops (“Kubernetes Operations”).

Para ello vamos a seguir esta guía de Kumori Labs pero comentando los cambios que hay que hacer para adaptarla a las últimas versiones.

Para continuar con esta guía necesitamos como pre-requisitos:

  • Clientes de Vagrant, VirtualBox, Git y AWS cli instalados en nuestra máquina.
  • Una cuenta de administrador en AWS.
  • Un nombre de dominio real comprado en algún proveedor como el propio AWS, Google Domains, GoDaddy, etc…

3. Vamos al lío

Lo primero que vamos a hacer es crear una cuenta de usuario especifica para el manejo de Kubernetes con Kops.

Para ello haciendo uso del CLI de AWS creamos un grupo en AWS llamado “kops”.

$> aws iam create-group --group-name kops

Ahora establecemos una variable de entorno con todos los permisos que otorgamos a los miembros de este grupo.

export arns="
arn:aws:iam::aws:policy/AmazonEC2FullAccess
arn:aws:iam::aws:policy/AmazonRoute53FullAccess
arn:aws:iam::aws:policy/AmazonS3FullAccess
arn:aws:iam::aws:policy/IAMFullAccess
arn:aws:iam::aws:policy/AmazonVPCFullAccess"

Ejecutamos una sentencia donde recorremos cada uno de los anteriores permisos y se lo asignamos al grupo kops.

$> for arn in $arns; do aws iam attach-group-policy --policy-arn "$arn" --group-name kops; done

Creamos el usuario “kops”.

$> aws iam create-user --user-name kops

Ahora añadimos este usuario al grupo kops.

$> aws iam add-user-to-group --user-name kops --group-name kops
Y por último creamos el access key para el usuario “kops”.
$> aws iam create-access-key --user-name kops

Este comando nos va a devolver un JSON con los campos SecretAccessKey y AccessKeyID que vamos a almacenar en las siguientes variables de entorno para interactuar con AWS.

$> export AWS_ACCESS_KEY_ID="AWS Access Key ID"
$> export AWS_SECRET_ACCESS_KEY="AWS Secret Access Key"

También es necesario tener creadas las credenciales de SSH en nuestra máquina. Las cuales se almacenan en ~/.ssh/id_rsa.pub y ~/.ssh/id_rsa. Si trabajas habitualmente con Git casi seguro que ya las tienes creadas y si no es tan sencillo como ejecutar el siguiente comando dejando la ruta por defecto.

$> ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

Es el momento de clonar el siguiente proyecto.

$> git clone https://github.com/kumorilabs/getting-to-know-k8s.git
$> cd getting-to-know-k8s.git

Un paso importante antes de arrancar Vagrant es abrir el fichero /scripts/provision-vagrant.sh para verificar las versiones y su compatibilidad. A mi me ha ido bien con las siguientes versiones:

export KUBECTL_VERSION="1.11.1"
export KUBEFED_VERSION="1.11.1"
export KOPS_VERSION="1.10.0"
export HUGO_VERSION="0.47"
export TERRAFORM_VERSION="0.11.8"

En este punto ya estamos en disposición de arrancar el proyecto con Vagrant para que nos cree una instancia con todas las herramientas (awscli, kops, kubectl, …) instaladas y configuradas, así que ejecutamos:

$> vagrant up

Pasados unos minutos (tardará más porque se tendrá que descargar el box) ya podemos entrar en la máquina con el comando:

$> vagrant ssh

A partir de este punto todos los comandos los vamos a ejecutar desde la máquina de Vagrant que en el arranque ha cogido las credenciales del usuario “kops” y las claves de SSH.

El alcance de este tutorial es la creación del clúster de Kubernetes haciendo uso de Kops así que lo primero que vamos a hacer es preparar unas variables de entorno que nos ayudarán en el proceso.

Primero establecemos el valor para DOMAIN_NAME el cual tiene que ser el nombre de dominio o sudominio que tengamos comprado.

$> export DOMAIN_NAME="tunombredominio.org"

Después vamos a establecer un nombre de clúster que vamos a utilizar para identificarlo junto con el DOMAIN_NAME.

$> export CLUSTER_ALIAS="k8s"

Ahora componemos el nombre completo del clúster.

$> export CLUSTER_FULL_NAME="${CLUSTER_ALIAS}.${DOMAIN_NAME}"

Y establecemos la zona de disponibilidad donde queremos crear el clúster.

$> export CLUSTER_AWS_AZ="eu-west-1b"

Otro paso previo necesario para la creación del clúster es crear un bucket de S3 que Kops va a utilizar para almacenar los ficheros de configuración necesarios. Para crearlo ejecutamos:

$> aws s3api create-bucket --bucket ${CLUSTER_FULL_NAME}-state

Y establecemos la variable de entorno.

$> export KOPS_STATE_STORE="s3://${CLUSTER_FULL_NAME}-state"

Antes de crear el clúster vamos a verificar que nuestro nombre de dominio tiene disponible correctamente los servidores de DNS, ya que de lo contrario no podremos crear el clúster.

Para ello utilizamos la herramienta dig disponible en el box de Vagrant y ejecutamos:

$> dig NS tunombredominio.org

Hasta que este comando no responda con algo similar a esto, no podrás crear el clúster.

tunombredominio.org.		21600	IN	NS	ns-cloud-e1.googledomains.com.
tunombredominio.org.		21600	IN	NS	ns-cloud-e2.googledomains.com.
tunombredominio.org.		21600	IN	NS	ns-cloud-e3.googledomains.com.
tunombredominio.org.		21600	IN	NS	ns-cloud-e4.googledomains.com.

Si esto no es así tendrás que hacer las modificaciones necesarias en tu servidor de dominio y esperar a que las modificaciones de DNS se propaguen para volver a intentarlo.

En caso de que sea correcto, ya podemos crear el clúster con el siguiente comando:

$> kops create cluster \
    --name=${CLUSTER_FULL_NAME} \
    --zones=${CLUSTER_AWS_AZ} \
    --master-size="t2.medium" \
    --node-size="t2.medium" \
    --node-count="2" \
    --dns-zone=${DOMAIN_NAME} \
    --ssh-public-key="~/.ssh/id_rsa.pub" \
    --kubernetes-version="1.11.1"

Este comando no crea físicamente los elementos en AWS sino que nos permite un preview de lo que va a hacer. Para confirmar que queremos crear los elementos (y empezar con el coste) tenemos que ejecutar:

$> kops update cluster ${CLUSTER_FULL_NAME} --yes

Pasados unos minutos podremos ver en la web de AWS que se crean tres nuevas instancias, un master y dos nodos, con todos los grupos de permisos y el autoescalado configurado y que en la parte de Route 53 se han creado nuevas entradas de RecordSets dentro del Hosted Zone. Hasta que estas entradas no cambien la IP por defecto 203.0.113.123 por unas reales asociadas al nombre de dominio, el clúster no estará operativo.

Para comprobar que el clúster funciona, podemos ejecutar algún comando de Kubernetes como:

$> kubectl get nodes

Y ver que nos responde con el nodo master y los dos nodos asociados.

Otro paso que podemos hacer es instalar el dashboard ejecutando:

$> kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml

Por defecto la seguridad del dashboard está habilitada así que tenemos que ejecutar el siguiente comando para ver el username y la password:

$> kubectl config view --minify

Ahora accedemos al dashboard a través del navegador a la URL: https://api.k8s.tunombredominio.org/api/v1/namespaces/kube-system/services/kubernetes-dashboard:/proxy/

Ingresamos el username y la password y ya estamos dentro, pero nos damos cuenta de que pinchando en las opciones de menú vemos que por seguridad todas las operaciones están deshabilitadas, con un mensaje del tipo: deployments.apps is forbidden: User “system:serviceaccount:kube-system:kubernetes-dashboard” cannot list deployments.apps in the namespace “default”

Para habilitar los permisos tenemos que crear un fichero dashboard-admin.yaml con el siguiente contenido:

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: kubernetes-dashboard
  labels:
    k8s-app: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: kubernetes-dashboard
  namespace: kube-system

Y lo ejecutamos sobre el clúster con el comando:

$> kubectl create -f dashboard-admin.yaml

De esta forma ya podemos hacer las operaciones desde el dashboard aunque personalmente aconsejo realizarlas desde la línea de comandos.

Para eliminar el clúster, simplemente tenemos que ejecutar:

$> kops delete cluster ${CLUSTER_FULL_NAME} --yes

Y si no lo estamos usando para otra cosa, también podemos borrar el bucket asociado.

$> aws s3api delete-bucket --bucket ${CLUSTER_FULL_NAME}-state

4. Conclusiones

Kubernetes se está convirtiendo en el estándar de facto para orquestar el despliegue de aplicaciones basadas en contenedores; que mejor que tenerlo disponible en el cloud gracias a AWS y Kops.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.

Tello, el nuevo dron de Ryze Tech con HD

$
0
0

Índice de contenidos


1. Especificaciones

  • Peso (protectores de hélices incluidos): 87 gramos. (La nueva normativa permite la utilización de drones recreativos en zona urbana siempre que tengan un peso inferior a 250 gramos)
  • Cámara: 5 MP
  • Resolución: 720p a 30 fps
  • Formato vídeo: .mp4
  • Formato fotos: JPG
  • Procesador: Intel, Movidius Myriad 2
  • Autonomía batería: 13 min
  • Altura máxima vuelo: 10 m
  • Estabilización de imagen electrónica
  • Manejo fácil con la App de Tello
  • Funciones avanzadas: EZ Shots, fotos 360º, Bounce Mode, Circle, 8D flips
  • Barómetro para control de altura
  • Velocidad máxima: 28 km/h
  • Precio: 100€

2. Contenido del embalaje

  • Dron
  • Hélices y protectores de hélices
  • 2 hélices de repuesto
  • Guía de inicio rápido
  • Batería
  • Herramienta extracción de hélice
  • (*) No se incluye mando controlador ni cable micro USB para carga


     


    3. App de Tello

    Se puede descargar de App Store, Google Play o escaneando el código QR que aparece en la caja del dispositivo. La aplicación es compatible con iOS 9.0 (o posterior) y Android 4.4 (o posterior).

    4. Carga de batería

    La batería se carga con un cable micro USB. El tiempo de carga es de 1 hora y 30 minutos. Una luz parpadeando azul indica que está cargando y una luz fija azul indica que la carga ha finalizado. El dron se debe cargar apagado.

    5. Sistema de posicionamiento visual (para mantenerlo estable)

    Esta tecnología está basada en un sistema que utiliza datos de imagen y del módulo infrarrojo 3D situado en la parte inferior para ayudar a la aeronave a mantener su posición, permitiéndola volar en modo estacionario con gran precisión en interiores o en entornos en los que no se disponga de señal GPS. El sistema de detección 3D escanea constantemente en busca de obstáculos durante el vuelo, lo que le permite evitarlos en condiciones de baja iluminación.

    El sistema se activa automáticamente cuando se enciende el dron. Solamente será efectivo cuando se vuele a alturas entre 0.3 y 10m, aunque es más efectivo en alturas entre 0.3 y 6m. Si el dron vuela por encima de los 10m esta función se verá afectada.

    El dron cambia automáticamente al modo ATTI cuando el sistema de posicionamiento visual no esté disponible. En este modo el dron no puede mantener por sí mismo la posición.

    6. Modos de vuelo


    Throw & Go: permite lanzar el dron al aire y hacer que se recupere y flote. Para usar este modo, el dron debe estar en la palma de la mano. Las hélices empezarán a girar despacio y seguirán girando más rápido. Si pasan más de 5 segundos desde que empiezan a girar hasta que se lanza el dron, se pararán.

    Rebote: El dron vuela hacía arriba y hacia abajo desde cualquier superficie a una altura entre 0.5 y 1.2 m automáticamente como una pelota que rebota. Si el dron detecta un objeto por debajo (como por ejemplo la mano), aumentará la altura con respecto al objeto y seguirá en el modo.


     

    EZ Shots: Con esta opción Tello se aleja automáticamente para grabar unas vistas geniales y luego vuelve a su posición original.

    • 360º: grabar vídeo rotando 360º
    • Circle: grabar vídeo mientras vuela en círculo cerrando mirando hacia adentro.
    • Up & Away: graba vídeo moviéndose hacia adelante o hacia atrás.
    • 8D Flips: permite deslizar el dedo dentro de un recuadro en la pantalla para hacer que el dron voltee a esa dirección.

     

    7. Volar el dron

    Pulsar el botón de encendido una vez. Activar la conexión wifi en el móvil y conectarlo a la red Tello-XXXXX. Abrir la app de Tello. La conexión se ha establecido cuando el indicador de estado parpadea en amarillo y la vista de la cámara se muestra en vivo.

    8. Seguridad para principiantes

    El dron está diseñado con todas las facilidades para que una persona que nunca haya manejado uno no se preocupe al volarlo:

    • Aterrizaje y despegue automático: con un simple botón desde la app el dron aterriza y despega automáticamente, incluso cuando has perdido la conexión con él.
    • Protección de batería: en todo momento conocerás el nivel de batería del dron gracias a unos indicadores.
    • Modo Lento: por defecto está activado. El dron vuela más lento y los movimientos son más suaves. Resulta más fácil controlarlo pero puedes cambiar al modo rápido desde la rueda de ajustes.

    9. Programación

    La empresa Ryze Tech ha adecuado Tello para Scratch. Scratch es un sistema de programación visual que se basa en crear funciones para nuestros aparatos de una forma simple: arrastrando trozos de código de la forma más intuitiva posible.

    10. Conclusiones

    Lo más destacable de este dron es su pequeño tamaño que permite volarlo también a niños, además al tener la posibilidad de programar con Scratch cualquier niño o adulto que quiera iniciarse en la programación puede pasar un buen rato. Si eres un usuario más avanzado, puedes desarrollar aplicaciones de software para Tello usando Tello SDK. Cumple con la normativa de uso de drones en España y se puede usar en zona urbana debido a su poco peso. Se puede controlar fácilmente desde el móvil con la app y su precio es bastante asequible.


    Para mí lo que tiene más en contra es la autonomía tan corta de la batería. Es necesario adquirir baterías adicionales además de un hub para cargarlas, de manera que siempre tengas baterías de sobra cuando se te acaba una. La altura máxima de vuelo también me parece limitada al ser 10 m, pero por el precio del dron tampoco se puede tener todo.

    Lo que recomiendo adquirir adicionalmente:

    Funda para transporte

    Baterías adicionales

    Hub de carga

    Instalación de GitLab con HTTPS

    $
    0
    0

    Índice de contenidos

    1. Entorno

    Este tutorial está escrito usando el siguiente entorno:

    • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
    • Sistema Operativo: LUbuntu 18.04
    • GitLab CE 11.1.4

    2. Introducción

    Si hay una herramienta dentro del mundo DevOps que merece la pena conocer en profundidad sin duda alguna es GitLab. Incluso es su versión Community (gratuita para proyectos comerciales que tengan su propio servidor) cubre todos los aspectos del desarrollo desde la definición de historias en un Kanban (Scrum en la versión Enterprise) hasta su puesta en producción en Kubernetes, pasando por la definición de Pipelines como vimos en este tutorial

    Por esta razón cada vez es más común encontrarse con esta herramienta en los clientes, pero donde muchas veces solo la utilizan de repositorio de Git sin sacarle el máximo partido que tiene.

    En este tutorial vamos a ver cómo instalar la versión Community y tener acceso HTTPS de una forma muy sencilla y automática.

    Nota importante: para seguir este tutorial es imprescindible un nombre de dominio real asociado a una máquina que esté publica en Internet.

    3. Vamos al lío

    Partiendo de que tenemos acceso con los permisos necesarios a una máquina con Ubuntu Xenial (16.04) instalado, estos son los pasos que tenemos que seguir.

    Lo primero nos vamos a asegurar de tener todas las dependencias necesarias, para ello ejecutamos:

    $> sudo apt-get update
    $> sudo apt-get install ca-certificates curl openssh-server postfix gnupg apt-transport-https

    Descargamos e instalamos el GPG (GNU Privacy Guard) Key

    $> curl -L https://packages.gitlab.com/gitlab/gitlab-ce/gpgkey | sudo apt-key add -

    Refrescamos los paquetes

    $> sudo apt-get update

    Y ya estamos en disposición de instalar el paquete de GitLab:

    $> sudo apt-get install gitlab-ce

    La instalación cubre de forma automática todos los elementos necesarios para instalar y desplegar la aplicación; pero hay una serie de configuraciones que tenemos que realizar.

    Antes de nada tenemos que asegurarnos de que nuestra máquina tiene habilitados los puertos 80 y 443.

    Todos los cambios de configuración de GitLab se realizan en el fichero /etc/gitlab/gitlab.rb el cual podemos abrir para editar con el comando:

    $> sudo nano /etc/gitlab/gitlab.rb

    El primer cambio que tenemos que realizar en este fichero es la propiedad external_url que es la que va a utilizar para poder conectar con la herramienta a través del navegador. En este caso como queremos que sea a través de HTTPS en el dominio definido tenemos que poner:

    external_url 'https://tudominio.org'

    Ahora viene lo mejor, todos sabemos lo tedioso que es el tema de los certificados en el protocolo HTTPS, pues bien GitLab lo ha simplificado a modificar unas propiedades en este fichero.

    Habilitar que vamos a hacer uso de Let’s Encrypt, que para quien no lo conozca es un organismo certificador que ofrece certificados válidos gratuitos con el fin de facilitar la seguridad en las gestiones de Internet. Simplemente tenemos que buscar la siguiente propiedad y ponerla a true.

    letsencrypt['enable'] = true

    Es buena idea rellenar la propiedad contact_emails con los emails que Let’s Encrypt va a utilizar para comunicar eventos con el certificado.

    letsencrypt['contact_emails'] = ['tuusuario@tudominio.com']

    Para no tener problemas con la validación del dominio en Let’s Encrypt, el dominio tiene que ser real y válido, y hay que hacer las siguientes modificaciones en el fichero.

    nginx['redirect_http_to_https_port'] = 80
    nginx['redirect_http_to_https'] = true
    nginx['custom_nginx_config'] = "include /var/opt/gitlab/nginx/conf/custom/*.conf;"

    Hechas estas modificaciones, salvamos el fichero y ejecutamos el comando:

    $> sudo gitlab-ctl reconfigure

    Si nos fijamos en el log que saca por consola debemos ver la conexión con Let’s Encrypt para la descarga de los certificados que almacena en la ruta /etc/gitlab/ssl junto con el nombre de nuestro dominio.

    Dos son los errores que nos podemos encontrar en este punto. El primero relativo a la validación del dominio por parte de Let’s Encrypt, donde te tendrás que asegurar de que tu dominio es válido y público en Internet (cuidado con los tiempos de propagación de DNS) y el segundo relativo al timeout de nginx; donde para resolverlo simplemente tienes que volver a comentar la línea: nginx[‘custom_nginx_config’] = “include /var/opt/gitlab/nginx/conf/custom/*.conf;”

    Si todo es correcto ya tendremos nuestra instancia de GitLab a través de HTTPS. Una cosa que tienes que tener en cuenta es que los certificados de Let’s Encrypt solo durán 90 días, para resolver este problema de forma automática puedes crear el siguiente fichero:

    $> sudo nano /etc/cron.daily/gitlab-le

    Con el siguiente contenido:

    #!/bin/bash
    set -e
    /usr/bin/gitlab-ctl renew-le-certs > /dev/null

    Y darle los permisos de ejecución necesarios.

    $> sudo chmod +x /etc/cron.daily/gitlab-le

    Hecho esto y si todo es correcto ya podrás conectar con GitLab a través del dominio que le hayas dado vía HTTPS.

    Una vez dentro como administrador es buena idea deshabilitar el registro público. Esto se hace desde la sección “Admin Area” en la opciones de la izquierda “Settings” y en la sección “Sign-up settings” desmarcar la opción “Enable Sign-up”.

    4. Conclusiones

    Como ves GitLab no nos lo puede poner más fácil para aumentar la seguridad de los accesos a la herramienta a través de HTTPS.

    Cualquier duda o sugerencia en la zona de comentarios.

    Saludos.

    Crear una Red Neuronal con Spark MLlib

    $
    0
    0

    Índice de contenidos

    1. Introducción

    En este tutorial vamos a aprender a crear una red neuronal con Spark MLlib, la librería de Machine Learning de Spark, y la vamos a entrenar con datos de vendedores para que aprenda a clasificar aquellos que tienen un pico de ventas.

    Vamos a usar dos bases de datos, una de PostgreSQL, que almacena los datos históricos de los clientes y sus propiedades y otra de ElasticSearch, que almacena las ventas que hacen.

    Primero explicaremos cómo cargar los datos en spark y cómo procesarlos para poder pasárselos a nuestra red neuronal y luego veremos el funcionamiento interno de esta red neuronal y evaluaremos su eficacia.

    A pesar de que este tutorial está hecho íntegramente en Java, Spark también tiene APIs para Scala y Python.

    2. Entorno

    El tutorial está escrito usando el siguiente entorno:
    • Hardware: Portátil MacBook Pro 15′ (2 Ghz Intel Core i7, 8GB DDR3).
    • Sistema Operativo: Mac OS Sierra 10.12.6
    • Software:
      • Entorno de desarrollo: Eclipse Oxygen 4.7.3
      • Postman 6.1.3
      • DBeaver 5.1.1
      • Docker 18.03.1
      • Kitematic 0.17.2
      • PostgreSQL 9.5
      • elasticDump 3.3.18

    3. Configuración inicial

    Primero vamos a levantar un docker con PostgreSQL y ElasticSearch. Para ello, en la carpeta de nuestro proyecto creamos un docker-compose:

    docker-compose.yml
    version: '3'
    services:
    	ml-elasticsearch:
    	image: elasticsearch:5.0.0
    	ports:
    		- "9200:9200/tcp"
    		- "9300:9300/tcp"
    	container_name: ml-elasticsearch
    
    	ml-kibana:
    	image: kibana:5.0.0
    	depends_on:
    		- ml-elasticsearch
    	ports:
    		- "5601:5601/tcp"
    	container_name: ml-kibana
    
    	ml-postgres:
    	image: postgres:9.5.10
    	ports:
    		- "5432:5432/tcp"
    	environment:
    		POSTGRES_PASSWORD: example # Use postgres/example user/password credentials
    	container_name: ml-postgres

    Una vez creado abrimos una terminal en la carpeta del proyecto y ejecutamos

    docker-compose up

    Cuando se hayan cargado los contendores, hacemos Ctrl+C y cerramos la terminal.

    Vamos a Kitematic, y actualizamos la lista de contenedores desde view –> Refresh Container List, y los lanzamos.

    Si todo ha ido bien, veremos nuestros contenedores en verde:


    Vamos a cargar ahora la base de datos de PostgreSQL. Para ello nos descargamos las bases de datos, abrimos DBeaver y conectamos con la base de datos que acabamos de levantar. Cargamos la base de datos desde Tools –> Restore:


    Seleccionamos como backup file postgres/postgres.backup y le damos a continuar.

    Si nos sale el siguiente error:


    Lo corregimos dando click izquierdo en la conexión -> Properties -> Edit coneection –> Connection settings –> Local Client, y añadimos /Applications/Postgres.app/Contents/Versions/9.5/bin. La seleccionamos y guardamos.

    Vamos a cargar ahora la base de datos de ElasticSearch. Para ello descomprimimos el archivo elasticsearch/backup.zip, abrimos una terminal en la carpeta elasticsearch y escribimos:

    sh elasticdump_local_all_indices.sh backup http://localhost:9200

    Tardará unos 3-5 minutos en importar la base de datos entera.

    Podemos comprobar que la base de datos de ElasticSearch se ha cargado correctamente desde Postman.

    Importamos el archivo postman/machine learning tutorial.postman_collection.json y podemos lanzar las consultas status y Query todos, que nos dan las siguientes salidas:

    status
    {
    	"_shards": {
    		"total": 1060,
    		"successful": 530,
    		"failed": 0
    	},
    	"_all": {
    		...
    Query todos
    {
    	"took": 180,
    	"timed_out": false,
    	"_shards": {
    		"total": 530,
    		"successful": 530,
    		"failed": 0
    	},
    	"hits": {
    		"total": 462596,
    		"max_score": 1,
    		"hits": [
    			{
    				...

    Hay más consultas preparadas, para poder ejecutarlas es necesario lanzar previamente la consulta Campo idCliente como fielddata.

    No vamos a explicarlas en este tutorial, pero nos sirven para comprobar que los datos que cargaremos en Spark se han procesado correctamente.

    4. ¡A programar!

    El código de este tutorial se puede descargar desde github e importarlo directamente como un proyecto de maven en eclipse o cualquier otro IDE.

    En este tutorial explicaremos las partes más importantes del código.

    Antes de empezar es necesario añadir las dependencias necesarias a nuestro proyecto, que en nuestro caso son hadoop, las bases de datos y spark y su librería de machine learning.

    pom.xml
    <properties>
    	<maven.compiler.source>1.8</maven.compiler.source>
    	<maven.compiler.target>1.8</maven.compiler.target>
    	<encoding>UTF-8</encoding>
    
    	<hadoop.base>2.7.1</hadoop.base>
    	<spark.version>2.2.1</spark.version>
    	<scala.version>2.11</scala.version>
    	<scala.minversion>2.11.0</scala.minversion>
    </properties>
    
    <dependencies>
    	<!-- Spark -->
    	<dependency>
    		<groupId>org.apache.spark</groupId>
    		<artifactId>spark-core_{scala.version}</artifactId>
    		<version>${spark.version}</version>
    		<scope>compile</scope>
    	</dependency>
    	<dependency>
    		<groupId>org.apache.spark</groupId>
    		<artifactId>spark-sql_${scala.version}</artifactId>
    		<version>${spark.version}</version>
    		<scope>compile</scope>
    	</dependency>
    	<dependency>
    		<groupId>org.apache.spark</groupId>
    		<artifactId>spark-streaming_${scala.version}</artifactId>
    		<version>${spark.version}</version>
    		<scope>compile</scope>
    	</dependency>
    	<dependency>
    		<groupId>org.elasticsearch</groupId>
    		<artifactId>elasticsearch-spark-20_${scala.version}</artifactId>
    		<version>6.3.0</version>
    	</dependency>
    	<dependency>
    		<groupId>org.apache.spark</groupId>
    		<artifactId>spark-mllib_${scala.version}</artifactId>
    		<version>2.2.0</version>
    	</dependency>
    	
    
    
    	<!-- Hadoop -->
    	<dependency>
    		<groupId>org.apache.hadoop</groupId>
    		<artifactId>hadoop-common</artifactId>
    		<version>${hadoop.base}</version>
    	</dependency>
    	<dependency>
    		<groupId>org.apache.hadoop</groupId>
    		<artifactId>hadoop-hdfs</artifactId>
    		<version>${hadoop.base}</version>
    	</dependency>
    	<dependency>
    		<groupId>org.apache.hadoop</groupId>
    		<artifactId>hadoop-auth</artifactId>
    		<version>${hadoop.base}</version>
    	</dependency>
    	<dependency>
    		<groupId>org.apache.hadoop</groupId>
    		<artifactId>hadoop-client</artifactId>
    		<version>${hadoop.base}</version>
    	</dependency>
    	
    	
    	<!-- DDBB -->
    	<dependency>
    		<groupId>org.postgresql</groupId>
    		<artifactId>postgresql</artifactId>
    		<version>9.4-1201-jdbc41</version>
    	</dependency>
    	<dependency>
    		<groupId>org.elasticsearch</groupId>
    		<artifactId>elasticsearch</artifactId>
    		<version>1.0.1</version>
    	</dependency>
    </dependencies>

    4.1. Conectar con las bases de datos

    Para poder cargar datos desde postgres creamos la clase SparkPostgresConnection y definimos dos métodos, uno que devuelve la base de datos entera y otra que devuelve sólo el resultado de una query.

    SparkPostgresConnection.java
    public static Dataset<Row> getData(String ip, String 
    database, String user, String password, String table){
    	Map<String, String> options = new HashMap<String, String>();
    	String url = String.format("jdbc:postgresql://%s:5432/%s?user=%s&password=%s",ip,database,user,password);
    	options.put("url", url);
    	options.put("dbtable", table);
    	options.put("driver", "org.postgresql.Driver");
    
    	SparkSession spark = SparkSession.builder().appName("db-example").getOrCreate();
    	SQLContext sqlContext = new SQLContext(spark);
    
    	Dataset<Row> ds = sqlContext.read().format("org.apache.spark.sql.execution.datasources.jdbc.JdbcRelationProvider").options(options).load();
    	
    	return ds;
    }
    
    public static Dataset<Row> getDataFromQuery(Dataset<Row> ds, String tableName, String query){
    	SparkSession spark = SparkSession.builder().appName("db-example").getOrCreate();
    	ds.createOrReplaceTempView(tableName);
    
    	return  spark.sql(query);
    }

    Vemos que ambos devuelven Dataset<Row>, que es el tipo de datos con los que trabaja Spark. Podemos considerarlos como una tabla, en la que Spark se encarga de distribuir los datos y todas las optimizaciones, y lo hace transparente para nosotros.

    getData() conecta con la base de datos y descarga los datos de la table table.

    Primero configuramos las opciones de conexión, cargamos la sesión de Spark y el conexto sql.

    Una vez que tenemos el contexto sql podemos leer datos de la base de datos, especificando las opciones de conexión. En este caso cargamos todos los datos.

    getDataFromQuery() ejecuta una consulta SQL y devuelve los datos obtenidos.

    Aprovechamos que ya tenemos los datos descargados previamente para sólo especificar la tabla en la que se hace la consulta y la query.

    Para poder cargar datos desde ElasticSearch creamos la clase SparkElasticsearchConnection y definimos los mismos dos métodos de antes.

    SparkElasticsearchConnection.java
    public static Dataset<Row> getData(String ip, String port) {
    	SparkConf conf = new SparkConf().setAppName("db-example")
    		.set("es.nodes", ip)
    		.set("es.port", port)
    		.set("es.nodes.wan.only", "true")		//Esto es porque está en docker
    		.set("es.resource", "ventas-*");
    			
    	SparkSession spark = SparkSession.builder().config(conf).getOrCreate();
    
    	SQLContext sql = new SQLContext(spark);
    	
    	Dataset<Row> solds = JavaEsSparkSQL.esDF(sql);
    	
    	return solds;
    }
    
    public static Dataset<Row> getDataFromQuery(String ip, String port, String query) {
    	SparkConf conf = new SparkConf().setAppName("db-example")
    		.set("es.nodes", ip)
    		.set("es.port", port)
    		.set("es.nodes.wan.only", "true")		//Esto es porque está en docker
    		.set("es.resource", "ventas-*")
    		.set("es.query", query);
    	
    	SparkSession spark = SparkSession.builder().config(conf).getOrCreate();
    
    	SQLContext sql = new SQLContext(spark);
    		
    	return JavaEsSparkSQL.esDF(sql);
    }

    En getData() sólo es necesario especificar la ip y puerto de elastic.

    Especificamos la configuración set(“es.nodes.wan.only”, “true”) porque la base de datos está en docker o en la nube. Si tu caso te lo permite es mejor no especificarla porque afecta mucho al rendimiento.

    4.2. Cargar datos en Spark

    Primero cargamos el contexto de Spark con:

    SparkConf conf = new SparkConf().setAppName("db-example").setMaster("local[*]");
    JavaSparkContext sc = new JavaSparkContext(conf);

    Usamos las funciones que hemos creado para cargar los datos. Cargamos los datos de PostgreSQL con:

    Dataset<Row> historico = SparkPostgresConnection.getData("localhost", "postgres", "postgres", "example", "ventas.historico");
    Dataset<Row> configuracion = SparkPostgresConnection.getData("localhost", "postgres", "postgres", "example", "ventas.umbrales");

    Los datos de elastic los cargamos filtrando primero con una query que nos da los datos de los 90 días anteriores a la fecha especificada.

    Si queremos comprobar el resultado de la query a elastic directamente, podemos verla en postman en Query 90 días

    String date = "2018-06-12T08:50:01";
    
    String query="{" + 
    		"    \"range\" : {" + 
    		"        \"timestamp\" : {" +
    		"        	\"gte\":\"" + date+ "||-90d\"," +
    		"        	\"lt\":\"" + date + "\"" +
    		"        }" + 
    		"    }" + 
    		"  }";
    
    Dataset<Row> ventas = SparkElasticsearchConnection.getDataFromQuery("localhost","9200", query);

    4.3. Procesar los datos

    Ahora que tenemos los datos en Spark, vamos a procesarlos para tener en una sola tabla toda la información relevante de las ventas.

    De la base de datos de ElasticSearch, que es donde están las ventas, primero obtenemos las ventas que ha hecho cada cliente por hora:

    ventas = ventas.withColumn("timestamp_redondeado", col("timestamp").substr(0, 13));
    Dataset<Row> ventasAgrupadas = ventas.groupBy(col("detalles.idCliente").as("id_cliente"), col("detalles.pais"), col("timestamp_redondeado")).agg(sum("detalles.cantidad").as("cantidad"));
    ventasAgrupadas.cache().show(5);
    +----------+----+--------------------+------------------+
    |id_cliente|pais|timestamp_redondeado|          cantidad|
    +----------+----+--------------------+------------------+
    |   9999999|  SA|       2018-05-08 09|           35486.0|
    |   0095465|  DE|       2018-03-16 09|               0.0|
    |   9999999|  AU|       2018-03-16 23|29169.600067138672|
    |   1435133|  IN|       2018-04-08 08|           25710.0|
    |   1433009|  IN|       2018-05-31 09|            8280.0|
    +----------+----+--------------------+------------------+
    only showing top 5 rows

    Utilizamos una ventana de 24 horas para añadir a la tabla de ventas una nueva columna que recoge las ventas acumuladas por el cliente en el último día

    WindowSpec window24h = Window.partitionBy(col("id_cliente"), col("pais"))
    	.orderBy(col("timestamp_redondeado").cast("timestamp").cast("long"))
    	.rangeBetween(-60*60*24, 0);
    Dataset<Row> ventasAgrupadasConAnteriores = ventasAgrupadas.select(col("*"), sum("cantidad").over(window24h).alias("cantidad_ultimas_24h"));
    ventasAgrupadasConAnteriores.cache().show(5);
    +----------+----+--------------------+---------+--------------------+
    |id_cliente|pais|timestamp_redondeado| cantidad|cantidad_ultimas_24h|
    +----------+----+--------------------+---------+--------------------+
    |   1530517|  ID|       2018-03-21 05|2040000.0|           2040000.0|
    |   1530517|  ID|       2018-03-22 04|1.04974E7|           1.25374E7|
    |   1530517|  ID|       2018-03-29 06| 990000.0|            990000.0|
    |   1530517|  ID|       2018-06-08 04|1180000.0|           1180000.0|
    |   2830006|  PH|       2018-03-16 04|  26250.0|             26250.0|
    +----------+----+--------------------+---------+--------------------+
    only showing top 5 rows

    De las bases de datos SQL obtenemos del histórico lo que cada cliente suele vender por día y los umbrales en los que suele trabajar, tanto en porcentaje como en cantidad.

    Dataset<Row> ultimoMesDelHistorico = historico.groupBy(col("id_cliente"), col("pais")).agg(max("mes").as("mes"));
    Dataset<Row> cantidadHistorico = historico.select(col("id_cliente"),col("pais"),col("cantidad").divide(30).as("ventas_diarias"),col("mes")).join(ultimoMesDelHistorico, joinBy("id_cliente","pais","mes"));
    
    Dataset<Row> historicoYUmbrales = cantidadHistorico.join(configuracion,"pais")
    		.select(col("id_cliente"),col("pais"),col("ventas_diarias"),col("umbral_cantidad_pico"),col("umbral_cantidad_gran_pico"),col("umbral_porcentaje_pico"),col("umbral_porcentaje_gran_pico"));
    
    historicoYUmbrales.cache().show(5);
    +----------+----+--------------------+-------------------+--------------------+-------------------------+----------------------+---------------------------+
    |id_cliente|pais|timestamp_redondeado|     ventas_diarias|umbral_cantidad_pico|umbral_cantidad_gran_pico|umbral_porcentaje_pico|umbral_porcentaje_gran_pico|
    +----------+----+--------------------+-------------------+--------------------+-------------------------+----------------------+---------------------------+
    |   6832103|  LT|       2018-03-29 14|15025.7666666666667|            50000.00|                100000.00|                500.00|                    1000.00|
    |   6832103|  LT|       2018-03-22 11|15025.7666666666667|            50000.00|                100000.00|                500.00|                    1000.00|
    |   6832000|  LT|       2018-06-08 13|13021.5333333333333|            50000.00|                100000.00|                500.00|                    1000.00|
    |   6832000|  LT|       2018-06-08 12|13021.5333333333333|            50000.00|                100000.00|                500.00|                    1000.00|
    |   6832000|  LT|       2018-06-08 11|13021.5333333333333|            50000.00|                100000.00|                500.00|                    1000.00|
    +----------+----+--------------------+-------------------+--------------------+-------------------------+----------------------+---------------------------+
    only showing top 5 rows

    Vemos que para poder hacer join de las tablas hemos usado la función joinBy(), que es una función creada por nostros por comodidad. Esta función simplemente convierte una lista de String en una secuencia de scala:

    public static scala.collection.Seq<String> joinBy(String... strings){
    	List<String> stringList = new ArrayList<String>();
    	for(String s: strings) stringList.add(s);
    	return JavaConverters.asScalaIteratorConverter(stringList.iterator()).asScala().toSeq();
    }

    Combinamos ahora las dos tablas de datos y tendríamos ya todos los datos de entrada para la red neuronal

    Dataset<Row> ventasHistoricoYUmbrales = ventasAgrupadasConAnteriores.join(historicoYUmbrales, joinBy("id_cliente","pais"));

    Clasificaremos a un cliente como que tiene un pico de ventas si la diferencia entre sus ventas en las últimas 24 horas y lo que suele vender por día se sale de los umbrales que tiene especificados. Para clasificarlo como pico tiene que salirse de los umbrales tanto en cantidad como en porcentaje.

    Consideraremos que tiene un gran pico de ventas si esta diferencia se sale de los umbrales que hemos especificado para grandes picos.

    Para hacer estas clasificaciones creamos la función:

    private static Column calculateAlert(String threshold_amount, String threshold_percentage) {
    	return ((col(threshold_amount)).lt(col("cantidad_ultimas_24h").minus(col("ventas_diarias"))))
    			.and((col(threshold_percentage)).lt((col("cantidad_ultimas_24h").minus(col("ventas_diarias"))).divide(col("ventas_diarias")).multiply(100)));
    }

    Añadiendo las alarmas a nuestra tabla de datos ya tendríamos los datos preparados para que los procese la red neuronal.

    Dataset<Row> combinadas = ventasHistoricoYUmbrales.select(
    		col("cantidad_ultimas_24h"),col("ventas_diarias"),col("umbral_cantidad_pico"),col("umbral_cantidad_gran_pico"),col("umbral_porcentaje_pico"),col("umbral_porcentaje_gran_pico"),
    		calculateAlert("umbral_cantidad_pico", "umbral_porcentaje_pico").as("pico"),
    		calculateAlert("umbral_cantidad_gran_pico", "umbral_porcentaje_gran_pico").as("gran_pico"));
    
    Dataset<Row> datos = combinadas.select(col("*"), when(col("gran_pico").equalTo(true), "GRAN_PICO").when(col("pico").equalTo(true), "PICO").otherwise("NO_PICOS").as("picos"));
    datos.cache().show(5);
    +--------------------+-------------------+--------------------+-------------------------+----------------------+---------------------------+-----+---------+--------+
    |cantidad_ultimas_24h|     ventas_diarias|umbral_cantidad_pico|umbral_cantidad_gran_pico|umbral_porcentaje_pico|umbral_porcentaje_gran_pico| pico|gran_pico|   picos|
    +--------------------+-------------------+--------------------+-------------------------+----------------------+---------------------------+-----+---------+--------+
    |  116.13999938964844|15025.7666666666667|            50000.00|                100000.00|                500.00|                    1000.00|false|    false|NO_PICOS|
    |   96.58999633789062|15025.7666666666667|            50000.00|                100000.00|                500.00|                    1000.00|false|    false|NO_PICOS|
    |  22469.329894974828|13021.5333333333333|            50000.00|                100000.00|                500.00|                    1000.00|false|    false|NO_PICOS|
    |  22312.679891109467|13021.5333333333333|            50000.00|                100000.00|                500.00|                    1000.00|false|    false|NO_PICOS|
    |  16392.919895410538|13021.5333333333333|            50000.00|                100000.00|                500.00|                    1000.00|false|    false|NO_PICOS|
    +--------------------+-------------------+--------------------+-------------------------+----------------------+---------------------------+-----+---------+--------+
    only showing top 5 rows

    4.4. Crear la red neuronal

    Vamos a crear una clase llamada RedNeuronal que tendrá dos métodos públicos, uno para entrenarla, y otro para clasificar datos una vez finalizado el entrenamiento.

    Creamos dos constructores, uno que configura la red neuronal con los valores por defecto y otro que permite especificar estos valores.

    Al crear una red neuronal es necesario especificar las neuronas de entrada, que tienen que coicidir con el número de columnas de datos, y las neuronas de salida, que tienen que coincidir con el número de posibles calificaciones de los datos.

    public class RedNeuronal {
    	
    	private static final int DEFAULT_MAX_ITER = 300;
    	private static final int DEFAULT_BLOCK_SIZE = 128;
    	private static final long DEFAULT_SEED = System.currentTimeMillis();
    	private static final int DEFAULT_PORCENTAJE_TEST = 20;
    	private static final int[] DEFAULT_NEURONAS_INTERMEDIAS = new int[] {10,6,3};
    	
    	private int neuronasEntrada;
    	private int neuronasSalida;
    	private int[] neuronasIntermedias;
    	private int porcentajeTest;
    	private long seed;
    	private int blockSize;
    	private int maxIter;
    	
    	private int[] neuronas;
    	
    	private PipelineModel model;
    
    	public RedNeuronal(int neuronasEntrada, int neuronasSalida) {
    		this(neuronasEntrada, neuronasSalida, DEFAULT_NEURONAS_INTERMEDIAS, DEFAULT_PORCENTAJE_TEST, DEFAULT_SEED, DEFAULT_BLOCK_SIZE, DEFAULT_MAX_ITER);
    	}
    	
    	public RedNeuronal(int neuronasEntrada, int neuronasSalida, int[] neuronasIntermedias, int porcentajeTest, long seed, int blockSize, int maxIter) {
    		this.neuronasEntrada = neuronasEntrada;
    		this.neuronasSalida = neuronasSalida;
    		this.neuronasIntermedias = neuronasIntermedias;
    		this.porcentajeTest = porcentajeTest;
    		this.seed = seed;
    		this.blockSize = blockSize;
    		this.maxIter = maxIter;
    		
    		this.setNeuronas();
    	}
    	
    	private void setNeuronas() {
    		neuronas = new int[neuronasIntermedias.length+2];
    		
    		neuronas[0]=neuronasEntrada;
    		for (int i=0;i&lt;neuronasIntermedias.length;i++) {
    			neuronas[i+1]=neuronasIntermedias[i];
    		}
    		neuronas[neuronasIntermedias.length+1]=neuronasSalida;
    	}
    }

    Los parámetros que podemos pasar son:

    • neuronasIntermedias: Lista de capas ocultas de la red neuronal.
    • porcentajeTest: Porcentaje de datos que no se pasarán al entrenamiento de la red para luego poder comprobar su eficacia. Suele estar entre un 10% y un 40%.
    • seed: Semilla para las operaciones pseudoaleatorias. Con la que hay especificada debería valer.
    • blockSize: Tamaño de los bloques en que se dividen los datos. El tamaño recomenado es entre 10 y 1000.
    • maxIter: Número máximo de iteraciones para parar si el algoritmo no coverge.

    Creamos el método public void entrenar(Dataset<Row> datos, String columnaDeClasificacion, String… columnasDeDatos), que entrena la red con una tabla datos pasada.

    La red intentará encontrar una manera de deducir el valor de la columna de clasificación a partir de los datos en las columnas de datos.

    Primero dividimos la tabla de datos en datos de entrenamiento y de test.

    Dataset<Row>[] splits = datos.randomSplit(new double[]{(100-porcentajeTest)/100.0, porcentajeTest/100.0}, seed);
    Dataset<Row> train = splits[0];
    Dataset<Row> test = splits[1];

    El clasificador de la red neuronal necesita que los datos estén en una columna de vectores llamada “features” y que clasificación sea numérica y se llame “label”.

    Esto lo conseguimos creando una pipeline en la que aplicamos 4 transformaciones a la tabla de datos:

    Primero usamos VectorAssembler() para crear la columna features.

    Segundo usamos StringIndexer() para indexar la columna de clasificación en la columna label.

    Tercero definimos MultilayerPerceptronClassifier(), que es la red neuronal como tal.

    Por último desindexamos los datos clasificados por la red neuronal con IndexToString().

    VectorAssembler featureExtractor = new VectorAssembler()	//Pasamos todos los datos a procesar a la columna features
    	.setInputCols(columnasDeDatos)
    	.setOutputCol("features");
    
    StringIndexerModel labelIndexer = new StringIndexer()		//Indexamos las clasificaciones en la columna label
    	.setInputCol(columnaDeClasificacion)
    	.setOutputCol("label").fit(datos);
    
    MultilayerPerceptronClassifier trainer = new MultilayerPerceptronClassifier()	//Red neuronal. Intenta a partir de las features deducir el label
    	.setLayers(neuronas)
    	.setBlockSize(blockSize)
    	.setSeed(seed)
    	.setMaxIter(maxIter);
    
    IndexToString labelConverter = new IndexToString()		//Pasamos las predicciones a datos legibles
    	.setInputCol("prediction")
    	.setOutputCol("predictedLabel")
    	.setLabels(labelIndexer.labels());
    
    Pipeline pipeline = new Pipeline().setStages(new PipelineStage[] {featureExtractor, labelIndexer, trainer, labelConverter});

    Por último entrenamos la red neuronal y comprobamos su eficacia

    model = pipeline.fit(train);
    
    // Vemos la eficacia del modelo
    Dataset<Row> result = model.transform(test);
    Dataset<Row> predictionAndLabels = result.select("prediction", "label");
    MulticlassClassificationEvaluator evaluator = new MulticlassClassificationEvaluator()
    	.setMetricName("accuracy");
    
    System.out.println("\n\nPorcentaje de error del entrenamiento = " + new DecimalFormat("#.##").format((1.0 - evaluator.evaluate(predictionAndLabels))*100) + "%");
    Para acabar con la clase creamos el método que clasifica los datos una vez que ya se ha entrenado la red:
    public Dataset<Row> clasificar(Dataset<Row> datos) {
    	return model.transform(datos);
    }

    5. Probar y mejorar la red neuronal

    Dividimos los datos que hemos procesado en el apartado 4.3 en entrenamiento y test para poder probar la eficiacia de la red que acabamos de crear.

    int porcentajeTest=80;
    Dataset<Row>[] splits = datos.randomSplit(new double[]{(100-porcentajeTest)/100.0, porcentajeTest/100.0}, System.currentTimeMillis());
    Dataset<Row> train = splits[0];
    Dataset<Row> test = splits[1];

    Entrenamos la red neuronal con los datos que hemos procesado como entradas y con 3 posibles salidas: NO_PICOS, PICO o GRAN_PICO.

    Definiendo una sola capa intermedia con 5 neuronas, veamos la eficacia de clasificación de la red.

    String [] entradas = new String [] {"cantidad_ultimas_24h", "ventas_diarias", "umbral_cantidad_pico", "umbral_cantidad_gran_pico", "umbral_porcentaje_pico", "umbral_porcentaje_gran_pico"};
    RedNeuronal redNeuronal = new RedNeuronal(entradas.length, 3, new int[]{5});
    train.cache();
    redNeuronal.entrenar(train, "picos", entradas);
    
    
    //Clasificamos nuestros datos
    Dataset<Row> datosClasificados =  redNeuronal.clasificar(test);
    datosClasificados.cache();
    
    
    double picos = datosClasificados.filter(col("picos").equalTo("PICO")).count();
    double grandesPicos = datosClasificados.filter(col("picos").equalTo("GRAN_PICO")).count();
    double noPicos = datosClasificados.filter(col("picos").equalTo("NO_PICOS")).count();
    
    double grandesPicosBienPredecidos = datosClasificados.filter(col("predictedLabel").equalTo("GRAN_PICO")).filter(col("picos").equalTo("GRAN_PICO")).count();
    double picosBienPredecidos = datosClasificados.filter(col("predictedLabel").equalTo("PICO")).filter(col("picos").equalTo("PICO")).count();
    double noPicosBienPredecidos = datosClasificados.filter(col("predictedLabel").equalTo("NO_PICOS")).filter(col("picos").equalTo("NO_PICOS")).count();
    
    System.out.println(grandesPicosBienPredecidos/grandesPicos*100 + "% de grandes picos bien predecidos");
    System.out.println(picosBienPredecidos/picos*100 + "% de picos bien predecidos");
    System.out.println(noPicosBienPredecidos/noPicos*100  + "% de no picos bien predecidos\n");
    Porcentaje de error del entrenamiento = 3.31%
    
    16.584158415841586% de grandes picos bien predecidos
    0.0% de picos bien predecidos
    99.97145304025122% de no picos bien predecidos

    Vemos que los resultados dejan bastante que desear, pero se pueden mejorar bastante con una configuración diferente de las capas intermedias. Con sólo cambiar:

    RedNeuronal redNeuronal = new RedNeuronal(entradas.length, 3, new int[]{20});

    Vemos que los resultados mejoran bastante.

    Porcentaje de error del entrenamiento = 2.38%
    
    
    61.73469387755102% de grandes picos bien predecidos
    30.844155844155846% de picos bien predecidos
    99.54449695382338% de no picos bien predecidos

    Podemos mejorar estos resultados aún más, añadiendo un par de columnas de datos de entrada:

    Dataset<Row> combinadas = ventasHistoricoYUmbrales.select(
    		col("cantidad_ultimas_24h"),col("ventas_diarias"),col("umbral_cantidad_pico"),col("umbral_cantidad_gran_pico"),col("umbral_porcentaje_pico"),col("umbral_porcentaje_gran_pico"),
    		calculateAlert("umbral_cantidad_pico", "umbral_porcentaje_pico").as("pico"),
    		calculateAlert("umbral_cantidad_gran_pico", "umbral_porcentaje_gran_pico").as("gran_pico"),
    		(col("cantidad_ultimas_24h").minus(col("ventas_diarias"))).divide(col("ventas_diarias")).multiply(100).as("porcentaje"),
    		(col("cantidad_ultimas_24h").minus(col("ventas_diarias")).as("resta")));
    
    Dataset<Row> datos = combinadas.select(col("*"), when(col("gran_pico").equalTo(true), "GRAN_PICO").when(col("pico").equalTo(true), "PICO").otherwise("NO_PICOS").as("picos")).filter(col("porcentaje").isNotNull());
    
    String [] entradas = new String [] {"cantidad_ultimas_24h", "ventas_diarias", "umbral_cantidad_pico", "umbral_cantidad_gran_pico", "umbral_porcentaje_pico", "umbral_porcentaje_gran_pico", "resta", "porcentaje"};
    Vemos que llegamos a tasas de acierto cercanas al 100%
    Porcentaje de error del entrenamiento = 1.34%
    
    93.0990099009901% de grandes picos bien predecidos
    76.62145110410094% de picos bien predecidos
    99.10092807424594% de no picos bien predecidos

    6. Conclusiones

    Hemos visto lo relativamente rápido y sencillo que es crear con Spark una herramienta tan potente como es una red neuronal.

    En este tutorial hemos usado una red neuronal, pero Spark nos da la posibilidad de usar otro tipo de clasificadores como árboles de decisión, Naive Bayes, etc, y la base sería la misma que hemos seguido.

    7. Apéndice: árboles de decisión

    Podemos cambiar nuestro clasificador y en vez de tener una red neuronal, ponemos un árbol de decisión. Para ello nos basta con cambiar la línea:

    MultilayerPerceptronClassifier trainer = new MultilayerPerceptronClassifier()	//Red neuronal. Intenta a partir de las features deducir el label
    	.setLayers(neuronas)
    	.setBlockSize(blockSize)
    	.setSeed(seed)
    	.setMaxIter(maxIter);

    por:

    DecisionTreeClassifier trainer = new DecisionTreeClassifier()
    		.setMaxBins(512)
    		.setMaxDepth(30);

    Vemos la potencia de los árboles de decisión, ya que incluso sin añadir las columnas “resta” y “porcentaje”, tenemos tasas de aciertos mejores que con la red neuronal:

    Porcentaje de error del entrenamiento = 0.44%
    
    99.70945392237205% de aciertos
    98.60627177700349% de grandes picos bien predecidos
    89.0909090909091% de picos bien predecidos
    99.91483431402911% de no picos bien predecidos

    Backup y Restore en GitLab

    $
    0
    0

    Índice de contenidos

    1. Entorno

    Este tutorial está escrito usando el siguiente entorno:

    • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
    • Sistema Operativo: LUbuntu 18.04
    • GitLab 11.1.4

    2. Introducción

    No hay cosa más importante en una empresa que desarrolla software que mantener a salvo el código fuente que genera. Otros elementos de la integración continua como los pipelines, el resultado de Jenkins, las métricas de software se pueden recrear a través del código fuente pero si perdemos el código fuente perdemos un tiempo y dinero incalculable.

    En este tutorial vamos a ver cómo tenemos que configurar GitLab para poder hacer un backup periódico de nuestro código fuente, salvarlo en un lugar seguro y qué tenemos que hacer para restaurarlo.

    Toda la información oficial detallada la tenéis en la siguiente URL

    3. Vamos al lío

    Partimos de que ya tenemos una instancia en producción de GitLab.

    Primero vamos a hacer los cambios de configuración necesarios para hacer backup.

    Para ello editamos el fichero /etc/gitlab/gitlab.rb

    $> sudo nano /etc/gitlab/gitlab.rb

    Dentro de este fichero con CTRL + W buscamos la palabra Backup Settings que nos llevará directamente a la sección.

    En esta sección podemos ver dónde se almacenarán localmente los backups generados en la propiedad “backup_path”. Por defecto, en “/var/opt/gitlab/backups”

    En la propiedad “backup_keep_time” podemos definir en segundos el tiempo de vida de los ficheros de backup a fin de no llenar el disco de la máquina. Por defecto este valor se establece a 604800 segundos que son 7 días.

    Si queremos que el fichero de backup se almacene automáticamente en un bucket S3 de AWS solo tenemos que descomentar las líneas que se refieren a la propiedad “backup_upload_connection” y establecer el nombre del bucket en la propiedad “backup_upload_remote_directory”.

    Para que los cambios de configuración tengan efecto, tenemos que ejecutar:

    $> sudo gitlab-ctl reconfigure

    Hecho esto en cualquier momento que queramos generar un fichero de backup solo tendremos que ejecutar el siguiente comando:

    $> sudo gitlab-rake gitlab:backup:create

    Al ejecutar este comando se generará el archivo y tendremos que ver que también se almacena en el bucket de AWS previamente configurado. En el enlace a la documentación oficial tenéis otras estrategias de salvado.

    Nota: Por razones de seguridad los ficheros: /etc/gitlab/gitlab.rb y /etc/gitlab/gitlab-secrets.json no se incluyen dentro del fichero generado y tienen que ser salvados de forma independiente.

    Ahora si lo que queremos es que el backup se realice de forma automática todos los días a las 2 AM tenemos que crear un cron siguiendo estos pasos:

    $> sudo su -
    $> crontab -e

    Y añadimos la siguiente línea:

    0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1

    Una vez tenemos salvado el fichero con el backup, vamos a ver cómo podemos utilizarlo para restaurar nuestra instancia en caso de que sea necesario.

    Lo primero que necesitamos es que GitLab esté corriendo y al menos hayamos ejecutado una vez un reconfigure.

    $> sudo gitlab-ctl reconfigure

    Nota: En caso de no estar corriendo no se puede restaurar el backup, así que al menos necesitamos una instancia limpia con la misma versión de GitLab corriendo.

    El siguiente paso sería copiar el fichero de backup deseado en la ruta de la propiedad backup_path que por defecto es /var/opt/gitlab/backups/

    Ahora vamos a parar los servicios “unicorn” y “sidekiq” únicamente, el resto de servicios tienen que estar corriendo.

    $> sudo gitlab-ctl stop unicorn
    $> sudo gitlab-ctl stop sidekiq

    Podemos comprobar que solo estos servicios están parados ejecutando:

    $> sudo gitlab-ctl status

    Ahora ejecutamos el comando de resturación indicando el timestamp del fichero de backup.

    $> sudo gitlab-rake gitlab:backup:restore BACKUP=1534804928_2018_08_21_11.1.4_gitlab

    Hecho esto de forma satisfactoria es el momento de restaurar los ficheros /etc/gitlab/gitlab.rb y /etc/gitlab/gitlab-secrets.json y reiniciar.

    $> sudo gitlab-ctl restart

    Podemos comprobar que todos los procesos están correctamente con el comando:

    $> sudo gitlab-rake gitlab:check SANITIZE=true

    4. Conclusiones

    A nadie se le escapa la importancia de preservar el fruto de nuestro esfuerzo y GitLab nos lo pone realmente fácil para hacer y restaurar los backups.

    Cualquier duda o sugerencia en la zona de comentarios.

    Saludos.

    Kubernetes en local con microk8s

    $
    0
    0

    Índice de contenidos

    1. Entorno

    Este tutorial está escrito usando el siguiente entorno:

    • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
    • Sistema Operativo: LUbuntu 18.04
    • LXD 3.4
    • microk8s v1.11.2

    2. Introducción

    De todos ya es sabido que Kubernetes es ahora mismo el estándar de facto para el despliegue de aplicaciones, y aquí ya hemos hablado muchas veces de él y de las necesidades de máquina que hacen que la mejor solución sea llevarlo al cloud.

    Esto hace que sea muy costoso poder hacer pruebas en desarrollo de nuestros manifestos de Kubernetes y se hace prohibitivo para empresas que realmente por su negocio no necesitan un clúster de alta disponibilidad.

    Entonces la solución pasa por poder ejecutar Kubernetes de forma local y esto es lo que podemos hacer con microk8s, que a diferencia de Minikube no requiere de una máquina virtual sino que podemos instalarlo directamente en Ubuntu como un paquete de snap para tener Kubernetes corriendo en nuestra máquina en segundos y consumiendo muchos menos recursos que si levantamos un clúster como hicimos en este tutorial.

    3. Vamos al lío

    La instalación en Ubuntu es tan sencilla como ejecutar:

    $> sudo snap install microk8s --edge --classic

    ¡Y ya está! En segundos tendremos disponible el API de Kubernetes corriendo en la URL “http://localhost:8080”

    Podemos probar que responde a los comandos típicos de kubectl ejecutando:

    $> microk8s.kubectl get nodes

    Si nos queremos ahorrar el anteponer microk8s podemos crear un alias de kubectl con el siguiente comando:

    $> snap alias microk8s.kubectl kubectl

    Además podemos añadirle una serie de “addons” con el comando:

    $> microk8s.enable addon1 addon2

    Entre los addons disponibles destacamos:

    • dns: para desplegar kube dns, es requerido por otros addons así que siempre se aconseja habilitarlo.
    • dashboard: con este addon tenemos disponible el típico dashboard de Kubernetes y los gráficos con Grafana.
    • storage: para permitir la creación de volúmenes persistentes.
    • ingress: para poder hacer redirecciones y balanceos en local.
    • istio: para desplegar los servicios de Istio. Todo el manejo de los comandos de Istio se hace con microk8s.istioctl
    • registry: para habilitar un registro privado de Docker al que poder acceder desde localhost:32000 que se maneja con el comando microk8s.docker

    En caso de querer empezar de cero con la instancia de Kubernetes podemos resetearlo con el comando:

    $> microk8s.reset

    En caso de querer utilizar un docker privado no seguro, creado por ejemplo con una instancia de Nexus 3, podemos habilitar el acceso editando el fichero /var/snap/microk8s/current/args/docker-daemon.json con el siguiente contenido:

    {
      "insecure-registries": [
        "local.nexus.com:8083"
        "localhost:32000"
      ],
      "disable-legacy-registry": true
    }

    Y reiniciando el servicio con el comando:

    $> sudo systemctl restart snap.microk8s.daemon-docker.service

    En caso de querer hacer la instalación en un contenedor de LXD tenemos que crear un perfil especifico. Para ello vamos a crear un fichero llamado microk8s.profile con el siguiente contenido:

    name: microk8s
    config:
      boot.autostart: "true"
      linux.kernel_modules: ip_vs,ip_vs_rr,ip_vs_wrr,ip_vs_sh,nf_conntrack_ipv4,ip_tables,ip6_tables,netlink_diag,nf_nat,overlay
      raw.lxc: |
        lxc.apparmor.profile=unconfined
        lxc.mount.auto=proc:rw sys:rw
        lxc.cgroup.devices.allow=a
        lxc.cap.drop=
      security.nesting: "true"
      security.privileged: "true"
    description: ""
    devices:
      aadisable:
        path: /sys/module/nf_conntrack/parameters/hashsize
        source: /sys/module/nf_conntrack/parameters/hashsize
        type: disk
      aadisable1:
        path: /sys/module/apparmor/parameters/enabled
        source: /dev/null
        type: disk
      aadisable2:
        path: /dev/zfs
        source: /dev/zfs
        type: disk

    Ahora creamos el perfil “microk8s” como copia del perfil por defecto:

    $> lxc profile copy default microk8s

    Y le asignamos el contenido del fichero microk8s.profile con el siguiente comando:

    $> cat ./microk8s.profile | lxc profile edit microk8s

    Ahora solo tenemos que crear el contenedor aplicándole el perfil por defecto y el perfil “microk8s” de esta forma:

    $> lxc launch -p default -p microk8s ubuntu:18.04 microk8s

    Una vez creado el contenedor “microk8s” podemos acceder dentro de él con el comando:

    $> lxc exec microk8s -- bash

    Una vez dentro instalamos el paquete zfsutils con el comando:

    $> apt install zfsutils-linux

    Y ya podemos instalar el paquete de snap con el comando visto anteriormente.

    $> snap install microk8s --edge --classic

    Y ya todo lo visto anteriormente aplica exactamente igual que la instalación en local.

    4. Conclusiones

    Aunque solo sea para “jugar” con Kubernetes en local merece la pena instalar microk8s ya que se va a comportar igual que un clúster de Kubernetes “real”. Pero ahora imagina que lo instalas en un contenedor de LXD o en una instancia de AWS consumiendo muchos menos recursos podrías tener Kubernetes en producción, muy útil cuando tu negocio no requiere de alta disponibilidad y no tiene que manejar un gran número de contenedores.

    Cualquier duda o sugerencia en la zona de comentarios.

    Saludos.

    La calidad es una excusita inventada por los programadores

    $
    0
    0

    Bueno, a lo mejor el error está en las premisas de partida y confundimos las herramientas con los objetivos.

    Nos creemos que nuestro objetivo es hacer TDD, ATDD, BDD, DDD, … porque algún fulano lo ha dicho en un libro, pero eso son meras herramientas que en cada caso, y en cada equipo, habrá que ver si nos interesa usar o no.

    Nuestro verdadero objetivo es proporcionar el valor que quiere negocio, de forma rápida (o lo más rápida posible) y segura (libre de errores, porque sino tampoco sería rápida).


    Fijaos que en la introducción en ningún sitio hablo de “calidad”. De hecho por lo pronto yo dejaría de hablar de “calidad” y desterraría esa palabra de nuestro vocabulario de desarrolladores porque ¿qué es “calidad”?

    En sus dos primeras acepciones:

    1. Conjunto de propiedades inherentes a una cosa que permite caracterizarla y valorarla con respecto a las restantes de su especie: de buena calidad; de mala calidad; esta fruta es de una calidad excelente.
    2. Superioridad o excelencia de algo o de alguien.

    Es decir la “calidad” sirve para compararme con otros, y el código no queremos compararlo con otros códigos (a no ser que tengamos un problema de ego y queramos ir fardando por los bares).

    Yo empezaría a hablar de “mantenibilidad del código”, hacer “código mantenible”, “código que se puede mantener”. Pero nuevamente ¿qué es “mantener”?.

    Ponemos algunas de sus acepciones:

    1. Proporcionar el alimento o lo necesario para vivir.
    2. Hacer que una cosa continúe en determinado estado, situación o funcionamiento.
    3. Estar [una persona] durante un período de tiempo en determinada situación o realizando una acción o actividad.

    “Mantener” queda claramente ligado a tener algo vivo durante el mayor tiempo posible. Y esta sí es una característica necesaria en el código, puesto que si llega un momento en que nuestro código “muere” dejaremos de cumplir el objetivo de aportar valor al negocio. Con lo que será el momento perfecto para que nos despidan y le den el proyecto a otros.

    ¿Es necesario hacer TDD, ATDD, BDD, DDD, … para que nuestro código sea “mantenible”? Efectivamente y NO. O por lo menos no es necesario usar todas las herramientas a la vez, todas las veces, y hasta su última consecuencia.

    Todos esos acrónimos y palabros con los que se nos suele llenar la boca (ya estamos otra vez fardando en los bares) no son más que herramientas que no sirven para otra cosa que aportarnos “grados de confianza” sobre nuestro código (siempre grande @eferro).

    Y de nuevo tiramos de diccionario ¿qué es “confianza”?

    Su primera acepción nos viene al pelo:

    1. Esperanza firme que una persona tiene en que algo suceda, sea o funcione de una forma determinada, o en que otra persona actúe como ella desea.

    Es decir nosotros como desarrolladores con cada incremento que hacemos en el código tenemos la esperanza firme de que todo siga funcionando y no se rompa nada. Es decir perseguimos el objetivo de seguir aportando valor al negocio.

    De esta manera podríamos concluir que para aportar valor al negocio tendremos que usar sólo aquellas herramientas que en cada caso nos den la confianza suficiente para poder mantener vivo el código. Sin olvidar que el valor hay que aportarlo de forma rápida y segura.

    Así que si alguna herramienta, aunque nos dé mucha confianza y seguridad, rompe con el requisito de “rápida” puede que no sea la mejor opción para esa situación y que tengamos que buscar otras herramientas que nos permitan ser más “rápidos” aportándonos la misma confianza.

    Esto que estoy contando no es nada nuevo, por ejemplo llevamos años con la famosa “Pirámide de Tests”, donde los tests de Sistema o UI se colocan en la cima. Esto se debe a que, si bien son tests que nos aportan mucha confianza, son los más lentos de desarrollar y mantener (sí, los tests también tienen que ser “mantenibles”) y por lo tanto rompen el requisito de aportar valor de forma rápida, costándonos mucho dinerito tanto de forma directa, porque se tarda más en desarrollarlos, como de forma indirecta, porque al aportar valor al negocio de forma más lenta podemos estar perdiendo oportunidades de negocio.

    Cómo yo no soy nadie, os dejo una referencia al siempre querido por todos Martin Fowler donde ya hablaba de esto en el 2012 (https://martinfowler.com/bliki/TestPyramid.html).

    Conclusión

    La “calidad” es una excusita que nos hemos inventado los técnicos para golpear a negocio y sentir que somos importantes y que nuestro trabajo es vital, sin querer entender que el negocio es lo realmente importante y que deberíamos gastar nuestro tiempo en entenderlo para poder aportarle el mayor valor posible de la forma más rápida posible.

    Todo lo demás son simplemente problemas de autoestima ya que somos incapaces de aceptar que somos una mera herramienta para el negocio (si chiquitín, eres una llave inglesa) y que el día que encuentren una forma más barata o más rápida o más cómoda de hacer este trabajo, vamos todos a la calle.

    Espero haber removido algunas conciencias, porque sólo está en al mano de cada uno el no confundir las herramientas con los objetivos.

    Integrar Swift en proyectos antiguos y sobrevivir en el intento

    $
    0
    0

    Índice de contenidos

    1. Introducción
    2. Entorno
    3. ¿Deberíamos?
    4. Añadir soporte para Swift al proyecto
    5. Consejos, problemas, y cosas a tener en cuenta
    6. Conclusiones

    1. Introducción

    Swift es el lenguaje de programación de código abierto desarrollado por Apple. Ofrece otra posibilidad para desarrollar las aplicaciones de las plataformas de Apple frente a Objective-C. Aunque ambos lenguajes tienen sus ventajas y desventajas, cada vez son más los que apostamos por este último.

    No se pretende discutir en este tutorial sobre si es mejor o peor que Objective-C, al final dependerá de las características del proyecto y de las preferencias del equipo de desarrollo.

    Aunque lo ideal sería comenzar un proyecto desde cero en Swift, en el mundo de la consultoría lo normal es encontrarnos con Apps que ya llevan unos años a sus espaldas. Y quizás llegados a cierto punto, y sobre todo ahora que los cambios entre versiones de Swift no son tan abruptos como lo fueron las tres primeras versiones del lenguaje nos planteemos añadir Swift a nuestro proyecto para hacernos la vida más fácil.

    El objetivo de este tutorial no es tanto técnico como más bien contar nuestra experiencia al introducirlo en una aplicación y qué cosas hemos ido aprendiendo a base de palos por el camino y que no habría estado mal saber antes (y siempre hay margen para descubrir cosas nuevas).

    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro 15’ (2.5 GHz Intel Core i7, 16 GB 1600 MHz DDR3)
    • Sistema Operativo: Mac OS X Sierra 10.13.6
    • Xcode 9.4.1
    • iOS 11.2.6

    3. ¿Deberíamos?

    "Settings"

    Ante un proyecto de un tamaño considerable debemos plantearnos esta pregunta. En nuestro caso la respuesta fue: "¿Por qué no?", pero obviamente eso puede cambiar de un proyecto a otro. En nuestro caso aunque el proyecto tiene un tamaño considerable tampoco es enorme ni tenía unos tiempos de compilación brutales (esto último puede ser un punto en contra importante porque al menos en Xcode 9 introducir Swift lo aumentará sensiblemente y conozco proyectos en los que esto ya es un problema de entrada).

    Al final la respuesta vendrá en función de las características del proyecto en particular. En nuestro caso tras probar a añadir el primer fichero en Swift al proyecto vimos que no nos incrementaba demasiado el tiempo de compilación y que no generaba ningún otro efecto colateral que impidiese al proyecto compilar. A medida que hemos ido incorporando ficheros de Swift se ha penalizado un poco mas el tiempo de build pero nada insalvable ni mucho menos.

    Y por último, al final todo depende de las preferencias del equipo, nosotros estábamos a gusto con solo Objective-C, pero creíamos que la sintaxis de Swift y las nuevas características del lenguaje nos podían ser bastante útiles en el día a día. Si el equipo no se siente a gusto con el lenguaje, cree que le entorpecerá el desarrollo, o cualquier otro motivo entoces la respuesta esta clara.

    4. Añadir soporte para Swift al proyecto

    Xcode se encargará de configurar el soporte para Swift en el proyecto en cuanto se añada el primer fichero Swift al mismo. También te preguntará si deseas generar un "Bridging header". En él se podrá importar los ficheros Objective-C del proyecto que quieras exponer al código en Swift.

    De forma análoga, también se puede exponer código Swift a Objective-C (se hablará más a fondo sobre esto un poco más adelante). Para ello Xcode generará una cabecera Objective-C con el nombre "<targetName>-Swift.h". Hay que tener en cuenta que este header es autogenerado al compilar el código Swift, por lo que muchas veces el completado de código expuesto en Swift puede no funcionar en Objective-C hasta que no se compile.

    Ambos ficheros pueden cambiarse desde la configuración del proyecto.

    "Settings"

    5. Consejos, problemas, y cosas a tener en cuenta

    Hay que tener en cuenta que Swift está pensado para ser interoperable con Objective-C, pero desde nuestra experiencia en sentido contrario la cosa se puede complicar.

    Para proyectos nuevos hechos en Swift directamente lo normal es que en algún momento se necesite utilizar código Objective-C (por ejemplo, de alguna librería desarrollada en Swift), pero en nuestro caso es justo al contrario, partimos de la totalidad del código en Objective-C para ir progresivamente introduciendo nuevas funcionalidades en Swift e ir refactorizando las mas antiguas.

    • Introducir Swift progresivamente, para las nuevas funcionalidades de forma que pueda convivir con el código en Objective-C.

    • Se pueden heredar o extender clases de Objective-C desde Swift, pero no al contrario. Hay que tenerlo en cuenta si se pretende refactorizar una clase base en Swift.

    • De cara a extender la funcionalidad de una clase en Objective-C, siempre se puede crear una extensión de Swift para hacerlo (y de paso cumplir la O, Open-Closed principle, de SOLID 😉)

    • "Si hay que forzarlo no puede ser bueno…" En general no es buena idea hacer force unwrap o forced cast en Swift (al contrario que en Objective-C, normalmente acaba en un crash de la App). Procura evitarlos salvo que estés mínimo al 99% seguro de que no será nil . Nuestro consejo es utilizar las secuencias de control del lenguaje if let, guard let, etc para asegurarse.

      "Esta vez, sin resurrecciones" — Thanos

    • Si se pretende utilizar código Objective-C en Swift, debes importarlo en el Bridging Header. Si se importa una clase con dependencias, también se deben importar aquellos ficheros con la definición de aquellas dependencias que se deseen utilizar desde código Swift. Muchas veces puede ocurrir que importemos Foo.h que contiene un atributo de tipo bar de Bar y el compilador nos diga que Foo no tiene un atributo bar porque no hemos importado el fichero Bar.h.

    • Mucho cuidado con los casting de tipos de Objective-C a Swift, especialmente con NSArray y NSDictionary (hemos llegado a ver crashes en runtime con errores indescifrables en Swift por un casting por error de por ejemplo un NSDictionary<NSNumber*,NSString*> a [String: String])

    • Hay restricciones en las declaraciones de Swift que pueden ser representadas en Objective-C. No se pueden utilizar structs, clases anidadas, enums (genéricos), o tipos genéricos desde Objective-C. Tampoco podrás exponer opcionales de ciertos tipos (por ejemplo, Int, Double o Bool, que en Objective-C se expondrán como NSInteger, double y BOOL respectivamente). Para poder es necesario que una sea una subclase de NSObject.

    • Se pueden exponer a Objective-C, se puede utilizar el atributo @objc(name). El nombre es opcional, si no se indica se generará uno (ver el siguiente punto). También se puede utilizar el atributo @objcMembers que expondrá todas las declaraciones representables en Objective-C (personalmente lo desaconsejo, porque también expondrá subclases que puede que no queramos exponer). Si se intenta marcar algo que no sea representable en Objective-C, el compilador te lo dejará bien claro indicándote que no puede.

    • Cuando se expone código Objective-C a Swift y viceversa, hay que tener en cuenta que el compilador realiza una adaptación a los lenguajes de forma que sigan el estilo de cada uno, un método en Swift "@objc func circle(color: UIColor), size: CGSize) : Shape" se traducirá a Objective-C como "-(Shape*) circleWithColor:(UIColor*)color size:(CGSize)size;" y viceversa. Se pueden controlar con qué nombre se expondrán, en el caso de Swift "@objc(coolCircleWithCoolColor:size:" y en el caso de Objective-C con la macro NS_SWIFT_NAME (aunque de momento no permite renombrar métodos de instancia, solo de clase).

    • Se puede ver la interfaz generada por una clase Objective-C desde el asistente de Xcode. Puede llevar a errores porque no tiene en cuenta si está importada en el Bridging header "GeneratedInterface"

    • Se pueden exponer enums de Swift a Objective-C se le debe asignar el tipo Int, por ejemplo, este código expondrá los símbolos MyCoolEnumA y MyCoolEnumB a Objective-C.

      @objc enum MyCoolEnum: Int {
          case a
          case b
      }
    • Se puede jugar con las variables calculadas para exponer tipos a Objective-C que no pueden exponerse directamente, por ejemplo, para una variable readonly desde Objective-C. Puede que no sea muy elegante, pero a veces no queda otra si se tiene que convivir con Objective-C. Viendo el código también se puede pensar, "¿por qué no utilizar directamente un NSNumber y no complicarse?". Bueno, por poner un ejemplo, puede que se tenga una dependencia con una clase Objective-C pero 4 con código en Swift, y que ademas se tenga la intención de eliminar esa dependencia eventualmente, en ese caso puede que tenga mas sentido trabajar con un Int que con un NSNumber en el código Swift.

      class MyCoolClass : NSObject {
          var optInt : Int?
          @objc(optInt) var objcOptInt : NSNumber? {
              if let int = optInt {
                  return NSNumber(value: int)
              }
              return nil
          }
      }

    6. Conclusiones

    En nuestra experiencia, tras varios meses de haber añadido nuestro primera funcionalidad en Swift al proyecto, no nos arrepentimos de la decisión tomada. De forma muy progresiva hemos ido desarrollando las nuevas funcionalidades en Swift, y por el camino hemos ido refactorizando algunos módulos para hacer limpieza.

    A día de hoy no nos arrepentimos de la decisión tomada, poco a poco ha ido ganando terreno y actualmente aproximadamente el 60% del proyecto ha sido convertido a Swift.

    ¿Qué ventajas nos ha proporcionado? Pues bueno, tampoco es que nuestra productividad se haya multiplicado por diez, pero en líneas generales nos ha facilitado un poco la mantenibilidad y legibilidad del código. Por poner un ejemplo, antes hacíamos un uso intensivo de BlocksKit para intentar hacer la programación algo más "funcional" (nótense las comillas), y ahora las tenemos de forma nativa en el lenguaje.

    Con la llegada de los Codables también nos ha permitido quitarnos la dependencia con Mantle para el mapeo de objetos JSON.

    También la necesidad de declarar explícitamente la nullabilidad en Swift aunque por un lado es un dolor de cabeza por otra parte ha sido de ayuda para descubrir problemas en la aplicación derivados de situaciones teóricamente imposibles con las respuestas de los servicios que ni siquiera teníamos contempladas y que el patrón Null Object de Objective-C camuflaba.

    Y de rebote, y aunque esto no tiene nada que ver con el desarrollo en sí, a los programadores de la versión Android de la App, que a su vez están haciendo lo mismo con Kotlin, también les resulta mucho más fácil poder echar un ojo al código Swift para comparar la lógica de una y otra dada las similitudes que guardan ambos lenguajes.

    Gestión del estado en Angular con Akita

    $
    0
    0

    Índice de contenidos

    1. Entorno

    Este tutorial está escrito usando el siguiente entorno:

    • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
    • Sistema Operativo: LUbuntu 18.04
    • Angular 6
    • Akita 1.9.1

    2. Introducción

    Actualmente lo más complicado de gestionar en una aplicación completamente front hecha con Angular, Vue, ReactJS… es la gestión del estado. Es decir, mantener cierta información de la aplicación durante todo el ciclo de vida de la aplicación y que todos los componentes tengan acceso puntual a esta información. Podemos distinguir dos tipos de estado: de dominio, que representan el estado del servidor, por ejemplo, los valores de una determinada entidad y el estado de interfaz, por ejemplo, si un determinado combo tiene que aparecer desplegado o cuál es la pestaña que está activa.

    Cuando la aplicación no es grande y la información a almacenar no es sensible podemos hacer uso del LocalStorage para mantener el estado, pero cuando la aplicación se hace cada vez más grande está solución no es viable.

    Dentro del ecosistema de Angular ya hablamos en su momento del Angular Model Pattern pero también se hace pesado para aplicaciones realmente grandes, por lo que en estos casos se recomienda pasar directamente a NgRx.

    El problema con NgRx es que estamos más acostumbrados a la programación orientada a objetos que a la programación funcional, y este cambio de paradigma a muchos se nos hace muy cuesta arriba y hace que tengamos que aprender un framework que muchas veces no parece Angular.

    Pues bien ahora tenemos una nueva opción antes de NgRx para aplicaciones grandes, Akita que pretende hacer la gestión del estado de aplicaciones grandes desde un enfoque de programación orientada a objetos, como muestra su arquitectura.

    En contraposición al siguiente esquema de NgRx donde vemos que todo pasa por un proceso funcional de reducer con funciones puras que actualizan el estado creando un estado nuevo cada vez.

    Ambas soluciones se implementan sobre las funcionalidades de la programación reactiva (RxJS)

    Akita maneja 4 conceptos principales:

    • Store: es la “fuente de la verdad” del estado de nuestra aplicación. Es el elemento al que se va a consultar y modificar el estado de la aplicación. Se podría asemejar con una base de datos.
    • Model: es la representación del store. Contiene todos los objetos con sus atributos que queremos almacenar en el estado de la aplicación. Se podría asemejar con las tablas de una base de datos.
    • Service: es recomendable crear servicios de creación, actualización y borrado de los elementos del estado, en vez de trabajar directamente con el Store.
    • Query: son clases que tienen la responsabilidad de consultar el Store. Las podemos considerar como las queries que lanzamos contra una base de datos.

    3. Vamos al lío

    La instalación se hace de la forma habitual:

    $> npm install @datorama/akita --save

    Para este ejemplo vamos a hacer una aplicación con dos componentes: el primero “set-name” se va a encargar de establecer un nombre en el estado de la aplicación; y el segundo “get-name” se va encargar de consultar esa parte del estado para mostrarlo actualizado por pantalla.

    Vamos a crear el estado de la aplicación, para estructurarlo un poco vamos a crear la carpeta “state” dentro de “app”.

    Dentro de la carpeta “state” vamos a crear el fichero name.store.ts con el siguiente contenido:

    import { Injectable } from '@angular/core';
    import { Store, StoreConfig } from '@datorama/akita';
    
    export interface NameState {
      name: string | null;
    }
    
    export function createInitialName(): NameState {
      return {name: ''};
    }
    
    @Injectable({ providedIn: 'root' })
    @StoreConfig({ name: 'name' })
    export class NameStore extends Store {
    
      constructor() {
        super(createInitialNameState());
      }
    
      setName(name: string) {
        this.update({name});
      }
    
      resetName() {
        this.update(createInitialNameState());
      }
    
    }

    En esta clase definimos el “Model” de nuestro estado, que solo va a ser un objeto con un atributo “name” de tipo string y creamos el “Store” asociado extendiendo de la clase “Store” de Akita con el tipo definido para el “Model”. Además definimos dos métodos para cambiar el estado: setName para establecer un nuevo nombre y resetName para volver al nombre inicial que es cadena vacía.

    El siguiente paso sería crear el servicio para acceder a la funcionalidad del Store sin hacerlo directamente, en esta clase pondríamos las llamadas a un servicio proxy de comunicación con el servidor para la recuperación y actualización de datos. Para nuestro caso, este podría ser un posible contenido:

    import { Injectable } from '@angular/core';
    import { NameStore } from './name.store';
    
    @Injectable({
      providedIn: 'root'
    })
    export class NameService {
    
      constructor(private nameStore: nameStore) {
      }
    
      setName(name: string) {
        this.nameStore.setName(name);
      }
    
      resetName() {
        this.nameStore.resetName();
      }
    
    }

    Ahora necesitamos una clase que nos proporcione acceso de lectura al “Store”. Para ello creamos una clase que extiende de Query pasándole el tipo NameState. Este sería el contenido:

    import { Injectable } from '@angular/core';
    import { Query } from '@datorama/akita';
    import { NameState, NameStore } from './name.store';
    
    @Injectable({
      providedIn: 'root'
    })
    export class NameQuery extends Query {
    
      getName$ = this.select(state => state.name);
    
      constructor(protected store: NameStore) {
        super(store);
      }
    
    }

    La clase define el atributo getName$ de tipo Observable de string, que va a ser al que tenemos que suscribirnos para recibir la actualización del nombre en el estado de la aplicación.

    Ahora solo resta crear dos componentes: uno set-name que se va a encargar de establecer el nombre en el estado de la aplicación y otro get-name que se va a encargar de leer el nombre almacenado en el estado. Para crearlos hacemos uso del CLI de Angular de esta forma:

    $> npm run ng -- generate component set-name
    $> npm run ng -- generate component get-name

    Primero vamos a implementar el componente set-name. Para ello editamos el fichero set-name.component.ts y haciendo uso del servicio creamos métodos para poder establecer y resetear el nombre. Este sería el contenido:

    import { Component, OnInit } from '@angular/core';
    import { NameService } from '../state/name.service';
    
    @Component({
      selector: 'app-set-name',
      templateUrl: './set-name.component.html',
      styleUrls: ['./set-name.component.css']
    })
    export class SetNameComponent implements OnInit {
    
      constructor(
        private nameService: NameService
      ) { }
    
      ngOnInit() {
      }
    
      setName(user: string) {
        this.nameService.setName(user);
      }
    
      resetName() {
        this.nameService.resetName();
      }
    
    }

    En el template asociado (set-name.component.html) implementamos un campo de texto con dos botones: uno con la funcionalidad de establecer el nombre que pongamos en el campo de texto y otro para resetear el valor del componente. Este sería el contenido:

    Name:
    
    
    

    Ahora vamos a implementar el componente get-name, encargado de mostrar por pantalla el nombre que tenemos en el estado de la aplicación y si cambia, automáticamente mostrar el cambio. Para ello editamos el fichero get-name.component.ts donde haciendo uso del servicio “NameQuery” obtenemos el valor del nombre. Este sería el contenido:

    import { Component, OnInit } from '@angular/core';
    import { Observable } from 'rxjs';
    import { NameQuery } from '../state/name.query';
    
    @Component({
      selector: 'app-get-name',
      templateUrl: './get-name.component.html',
      styleUrls: ['./get-name.component.css']
    })
    export class GetNameComponent implements OnInit {
    
      name$: Observable;
    
      constructor(
        private nameQuery: NameQuery
      ) { }
    
      ngOnInit() {
        this.name$ = this.nameQuery.getName$;
      }
    
    }

    Y en el template asociado hacemos uso del pipe async para suscribirnos y desuscribirnos automáticamente al observable y mostrar el valor. Este sería el contenido:

    Name: {{name$ | async}}

    De esta forma colocando los dos componentes por pantalla, vemos que al poner el nombre en el campo de texto y pulsar en “Set Name” el otro componente muestra el valor introducido de forma automática.

    Pero ahora qué pasa si refrescamos el navegador, entonces todo el estado se pierde y dejamos de mostrar el nombre. Para evitar esto, Akita tiene el método persistState() que nos permite persistir el estado de la aplicación en el LocalStorage, de forma que sin hacer nada, al refrescar el navegador vemos que el nombre se sigue mostrando. El lugar indicado para llamar a la función persistState() es el fichero main.ts de esta forma:

    import { enableProdMode } from '@angular/core';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    import { persistState } from '@datorama/akita';
    import { AppModule } from './app/app.module';
    import { environment } from './environments/environment';
    
    
    if (environment.production) {
      enableProdMode();
    }
    
    persistState();
    
    platformBrowserDynamic().bootstrapModule(AppModule)
      .catch(err => console.log(err));

    Nota: cuidado con esta función ya que todo el estado se mostrará en claro en el LocalStorage; así que nada de almacenar claves propias en el estado y hacer uso de esta función 😉

    A poco que hayas visto algo sobre NgRx o Redux te habrá llamado la atención el plugin de Chrome que te permite navegar por los cambios del estado. Pues esto lo podemos conseguir en Akita simplemente instalando esta dependencia.

    $> npm install @datorama/akita-ngdevtools --save-dev

    Y estableciendo esta forma de importar el módulo en el módulo principal de la aplicación a fin de que solo esté disponible cuando estemos en modo de desarrollo.

    import { AkitaNgDevtools } from '@datorama/akita-ngdevtools';
    ​
    @NgModule({
      imports: [environment.production ? [] : AkitaNgDevtools.forRoot()]
      bootstrap: [AppComponent]
    })
    export class AppModule {}

    4. Conclusiones

    En este tutorial hemos visto lo más básico de Akita pero ya deja ver que es una forma mucho más “natural” de manejar el estado de nuestras grandes aplicaciones Angular.

    Cualquier duda o sugerencia en la zona de comentarios.

    Saludos.

    Haz más con menos. Aplicando DRY a Jenkins

    $
    0
    0

    Índice de contenidos

    1. Introducción

    DevOps ha venido para quedarse. Y con ello todas las tareas de administración asociadas. Cada día aparecen nuevas herramientas o se potencian las existentes, pero un fiel compañero siempre ha sido Jenkins. Hoy vamos a comprobar cómo podemos aplicar el principio DRY (don’t repeat yourself) a nuestros jobs de Jenkins.

    Digamos que tenemos nuestras tareas de administración automatizadas y nos damos cuenta de que estamos empezando a repetir un patrón de actuación: preparar entorno, ejecutar acción, validar estado.

    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro 17′ (2’5 Ghz i7, 16GB DDR3).
    • Sistema Operativo: Mac OS High Sierra 10.13
    • Entorno de desarrollo: JetBrains IntelliJ
    • Jenkins 2.60.3 con los plugins de pipeline instalados

    3. Crear una shared library en Jenkins

    3.1. Definiendo el pipeline común

    Lo primero que necesitamos a la hora de definir una librería es conocer qué queremos hacer. En nuestro caso vamos a definir un pipeline muy sencillo que únicamente haga un echo de los parámetros recibidos.

    TestPipeline.goovy
    import org.jenkinsci.plugins.*
    
    String parm1
    Boolean parm2
    String parm3
    
    TestPipeline withParm1(String parm1) {
        this.parm1 = parm1
        return this
    }
    
    TestPipeline withParm2(Boolean parm2) {
        this.parm2 = parm2
        return this
    }
    
    TestPipeline withParm3(String parm3) {
        this.parm3 = parm3
        return this
    }
    
    def execute() {
        pipeline {
            stage("Setup environment") {
                echo "Parameter: ${parm1}"
            }
            stage("Should execute action: ${parm2}") {
                if (parm2) {
                    echo "Executing action as stated by parm2: ${parm2}"
                }
            }
            stage("Verify state") {
                echo "Executing ${parm3}"
            }
        }
    }
    
    return this

    Es muy importante que la última sentencia del fichero sea return this. Si no, Jenkins no será capaz de cargar correctamente el pipeline y nos fallará.

    Para hacer uso de las librerías, estas se deben importar desde un sistema de control de versiones (GitLab, GitHub, etc). Además, el repositorio debe seguir una estructura determinada. Queda bastante bien explicado en la página oficial de documentación:

    (root)
        +- src                     # Groovy source files
        |   +- org
        |       +- foo
        |           +- Bar.groovy  # for org.foo.Bar class
        +- vars
        |   +- foo.groovy          # for global 'foo' variable
        |   +- foo.txt             # help for 'foo' variable
        +- resources               # resource files (external libraries only)
        |   +- org
        |       +- foo
        |           +- bar.json    # static helper data for org.foo.Bar

    Esta estructura nos da la potencia suficiente como para organizar nuestras librerías siguiendo una jerarquía que permita reutilización. Por ejemplo, podemos definir una serie de métodos dentro de una clase Maven.groovy que nos faciliten las tareas de compilación, generación de release, análisis con SonarQube, etc. De esta forma, cualquier pipeline que quiera utilizarlas podrá hacerlo sin tener que definir todas las opciones necesarias y sólo las imprescindibles.

    3.2. Definir la shared library en Jenkins

    Para definirla de forma efectiva en Jenkins, nos dirigiremos a la sección de administración, configurar el sistema y a la sección Global Pipeline Libraries. Añadiremos nuestra ruta al repositorio Git y le daremos un nombre por el que la identificaremos. Es importante definir como versión por defecto master, ya que como veremos después se puede sobreescribir, pero lo lógico es que sea master la que nos interese siempre.

    Jenkins-Shared-Libraries

    Definición de Jenkins Shared Libraries

    4. Creando nuestro Jenkinsfile

    Tomemos como ejemplo un Jenkinsfile mínimo:

    Jenkinsfile
    #!/usr/bin/env groovy
    
    @Library("Test") _
    import com.autentia.test.TestPipeline
    
    new TestPipeline()
        .withParm1(params.environment)
        .withParm2(params.dryRun)
        .withParm3(params.verifier)
        .execute()

    Con la directiva @Library estamos importando la librería definida con ese nombre. El guión bajo (_) hace referencia a la versión, en este caso a la por defecto, ¿recordáis haberla indicado como master?, pues era para simplificar este punto. Además, si tenemos en cuenta que cada vez que hacemos referencia a params son parámetros del Job de Jenkins, acabamos de construir un Jenkinsfile genérico para todos nuestros Jobs. Lo único que tendrán que hacer es fijar los parámetros necesarios, pero el flujo de ejecución será común para todos. ¿No es más sencillo de mantener? ¿Y de entender? ¿Y de mantener ;)?

    5. Conclusiones

    Con un poco de cariño podemos empezar a construir una serie de librerías con funcionalidad común que nos faciliten la vida, todo manteniendo una coherencia entre los jobs que lo utilicen. El mantenimiento de las librerías está centralizado, por lo que será más sencillo y tendremos menos errores. Una de las grandes ventajas que tienen es que al actualizar el código de la librería, todo job que la utilice refrescará inmediatamente los cambios.

    Estamos acostumbrados a aplicar patrones y principios de diseño a nuestro código, y cuando nos enfrentamos a un pipeline descuidamos lo aprendido. No nos olvidemos que tenemos que aplicar el mismo mimo al ciclo de vida.

    6. Referencias

    Desplegando nuestra aplicación en Google Cloud

    $
    0
    0

    Índice de contenidos

    1. Introducción

    El propósito es quitarnos el miedo a desplegar nuestra aplicación en un cloud. En este caso en Google Cloud, y ver como en poco minutos podemos tener nuestra aplicación disponible.

    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Entorno de desarrollo: Intellij Idea Ultimate
    • Java 1.8
    • Apache Maven 3.5.0
    • Navegador Google Chrome

    3. Cuenta en Google Cloud

    Google Cloud ofrece una prueba de evaluación gratuita para iniciarte, así que no tendrá ningún coste.

    Una vez creada la cuenta, seleccionamos nuestro proyecto. En el menú de la izquierda vienen un montón de opciones:

    • Productos
    • Compute
    • Almacenamiento

    Con un montón de opciones cada uno. Poco a poco. Nos vamos a quedar con App Engine de momento.
    En esta pantalla, en la parte suprerior indicará el proyecto en el que estamos trabajando, y, en el menú de la izquierda, se pueden ver
    otras opciones

    • Panel
    • Servicios
    • Versiones

    Poco a poco. La que nos interesa ahora es la consola de shell de Google cloud.

    4. Conociendo la consola de Google Cloud

    Arriba a la derecha hay un icono de consola. Al pulsarlo se abre en la parte inferior de nuestra pantalla con nuestro usuario y el nombre del proyecto.

    Sin miedo, es una shell: ls; pwd; mkdir myproject; …

    Vamos a traer un proyecto de ejemplo de github y a desplegarlo.

    git clone https://github.com/stoledano/GoogleCloudHelloWorld.git

    ¡Y funciona! Parece que tiene ya tiene git.

    cd GoogleCloudHelloWorld;
       mvn clean install;

    ¡Y maven también funciona! Poco a poco, pero ya tenemos un jar ejecutable en la shell.
    Vamos a por nuestro ejecutable

    cd target;
       java -jar helloworld-0.0.1-SNAPSHOT.jar;

    ¡Y funciona! Parece que todo va bien … vamos a comprobarlo:
    En la parte superior derecha de la shell tenemos varios iconos. Un cuadrado con un rombo en su interior nos permite ver nuestra aplicación desplegada sin publicar aún, en el puerto que le digamos. En este caso en el puerto 8080.
    Se abre …
    ¡Y funciona! Parece que devuelve un magnífico "Hi from Google Cloud" .

    5. Publicando nuestra app

    Docker se integra muy bien con app engine. Y con docker podemos desplegar cualquier código en cualquier lenguaje. Vamos a probar con nuestra aplicación.
    Sólo necesitamos Dockerfile que defina nuestro contenedor y el fichero app.yml que define las opciones de configuración de AppEngine.

    runtime: custom
      env: flex

    No se necesita demasiado para una prueba rápida. con "runtime" "custom" infica que se define en el Dockerfile, y el entorno flexible de AppEngine.

    Volvemos a la consola. Vamos al directorio donde están los ficheros "app.yml" y el "Dockerfile". (en nuestro caso el raíz de la aplicación) Si se ejecuta "gcloud app deploy", en unos minutos publicará la aplicación, y será accesible desde your-project.appspot.com

    ¡Y funciona! Así de fácil.

    6. Conclusiones

    El cloud de los proveedores puede complicarse hasta donde queramos y necesitemos, pero para empezar no necesitamos saber mucho más.

    7. Referencias

    La entrada Desplegando nuestra aplicación en Google Cloud se publicó primero en Adictos al trabajo.


    15 años de Adictos al trabajo

    $
    0
    0

    Adictos al Trabajo ha superado los 15 años de vida. Iba siendo el momento de darle un lavado de cara, espero que os guste.

    Os voy a contar sus inicios, el porqué de este portal y nombre. Os avanzo que va muy ligado a mi evolución, traumas y frustraciones.

    Estudié Telecomunicaciones (principios de los 90) y descubrí durante la carrera la programación en C/C++. La relación fue tan adictiva que dedicaba casi todas las horas del día a aprender cosas nuevas. Además, siendo un «intruso» en el sector sentía la obligación de formarme incansablemente. Como curiosidad, mi padre harto de que no atendiera las llamadas a la cena decidió avisarme cada noche solo una vez y, si no acudía de inmediato cortaba la luz. Mano de santo, porque o iba al primer aviso o perdía todo el trabajo no guardado.

    A los casi 10 años trabajando en Informática cumplía muchos de los estereotipos del sector que podemos encontrar ahora en muchos arquitectos de grandes empresas: amor por el trabajo técnico (aporte personal) y estudio continuo (inseguridad por no estar a la última), búsqueda de la calidad (y frustración por el exceso de demanda que la impide), hastío hacia  la gente que no le gustaba lo que a mí (era un rollo en las fiestas con gente no técnica), escasez de comunicación con los jefes (demasiados detalles), etc.

    Trabajaba en un banco de Responsable de Desarrollo y de repente, un efecto mariposa: los atentados del 11-S. Caída de la Economía y decisión de cerrar la unidad de negocio en la que yo estaba (banca personal para clientes de más de 100K euros). Nos dieron cuatro meses para reorganizar la vida.

    Entonces me puse a pensar ¿qué ha quedado de todo lo que he construido y aprendido en todos estos últimos años? La respuesta fue «casi nada». O era confidencial o formaba parte de un sistema mucho mayor o había sido sustituido por otro. Por tanto, legado a la posteridad: cero.

    Pensé que a partir de ese momento empezaría a documentar parte de lo que aprendía o repasaba. Por tanto, necesitaba un lugar donde publicarlo porque así nunca lo perdería y, además, la gente cuestionaría y complementaría mi formación. Y por supuesto, el EGO. Podría ver al cabo de los años un tangible.

    Por otra parte, yo no percibía mucho problema para encontrar trabajo pero tenía decenas de compañeros en el banco, en un sector no tan privilegiado en crisis, y se me ocurrió una idea sencilla: crear una página web, meter todos los currículums de todo el que quisiera y contactar con otros bancos. Se había seleccionado y contratado hacía meses a mucha gente muy buena y si estaban todos disponibles en un portal serían fáciles de reclutar.

    Inicialmente el portal tendría una parte para introducir el currículum y otra para publicar artículos y tutoriales.

    A la hora de elegir nombre, que ya sabéis que es siempre difícil, se me ocurrió www.adictosaltrabajo.com. Hace tiempo escuché una frase que para ser preciso he tenido que buscar y he encontrado en https://www.frasesde.org/frases-de-mente.php

    «La prueba de una inteligencia de primera categoría es la capacidad de mantener dos ideas opuestas en la mente al mismo tiempo, y todavía conservar la capacidad de funcionar.»

    (F. Scott Fitzgerald)

    Un adicto al trabajo (que yo lo era) tiene un comportamiento extremo que puede causarle mucho mal (sobre todo de relación con familia y amigos), pero también es verdad que si quieres ser el mejor en algo, tienes que dedicar la vida a ello: el equilibrio no se lleva bien con la genialidad.

    Por tanto estaba preocupado y orgulloso al mismo tiempo.

    Al cabo de los años, han colaborado decenas de personas de muchos modos distintos y tenemos que sentirnos orgullosos de la supervivencia del portal durante tanto tiempo.

    En una época donde la marca personal es muy importante pienso que sigue siendo un buen escaparate para colaborar. Así que os animo a todo el que queráis.

    Por mi parte, intentaré que siga vivo y aportar directamente y a través de Autentia. Es gratificante recibir de vez en cuando el agradecimiento de la gente por lo que www.adictosaltrabajo.com les ha aportado.

    La entrada 15 años de Adictos al trabajo se publicó primero en Adictos al trabajo.

    Liferay Symposium 2018

    $
    0
    0

    Introducción

    Al igual que el año pasado, en este hemos asistido al Liferay Symposium España, ya en su novena edición, que tuvo lugar los pasados 17 y 18 de octubre en el Teatro Goya en Madrid.

    Entre todas las novedades que presentaron, el foco se puso en Liferay DXP 7.1 y sus nuevos productos: Liferay Cloud, Liferay Commerce y Liferay Analytics Cloud. Además, dejaron claro la importancia de la transformación digital y cómo Liferay ayuda a las empresas a conseguirlo.

    El Symposium tuvo sesiones plenarias y dos tracks: el de tecnología y el de negocio. Nosotros acudimos a las plenarias y las tecnológicas, de las que os traemos un resumen.

    Transformación digital

    Bienvenida

    Carolina Moreno, vicepresidenta de Ventas de Liferay en la zona EMEA, dio la bienvenida y comentó que este Symposium iba a ser diferente a los anteriores por el hecho de que se iban a presentar nuevos productos y, por tanto, Liferay iba a pasar de ser compañía de un único producto a serlo de varios.

    Opening Keynote

    Bryan Cheung, CEO de Liferay, quiso expresar la importancia de la innovación a través de diversas historias. Nos habló de Amazon y su estrategia sacando Amazon Go y Amazon Whole Foods Market. Nos recordó cómo, cuando se empezó a extender el uso de la electricidad en los procesos industriales, muchas compañías fueron indiferentes a ello y siguieron durante décadas con sus rudimentarios métodos, consiguiendo que la competencia les superase.

    Con todo esto, Bryan quiso recalcar la importancia de la transformación digital y nos recordó que desarrollo y negocio tienen que trabajar juntos, que es necesario entender bien los problemas para saber cuáles deben ser las mejores soluciones a crear.

    Terminó mencionando que podemos aprender a utilizar el ecosistema de Liferay a través de la documentación y de Liferay University, plataforma de vídeotutoriales, algunos de ellos gratis.

    Nuevos productos de experiencia digital para el ciclo de vida el cliente

    Jorge Ferrer, vicepresidente de Ingeniería de Liferay, comentó la importancia de conseguir atraer nuevos clientes. Explicó que estos pasan por tres estados:

    1. Candidatos a ser clientes.
    2. Clientes.
    3. Defensores del producto.

    Y, una vez son defensores del producto, pueden volver a ser candidatos a cliente de nuevos productos de la compañía. Dicho esto, admitió que Liferay había estado un poco flojo a la hora de conseguir hacer los pases entre estos estados, y que, teniendo esta debilidad en mente, han estado trabajando en solucionarla.

    Después de esto, Jorge pasó a dar unas pinceladas de los nuevos productos que se iban a presentar a lo largo del Symposium.

    Liferay DXP 7.1

    Presentación de producto: Liferay DXP 7.1

    Rafael Lluis, consultor en Liferay, fue el encargado de presentar las novedades que incluye la nueva versión de Liferay DXP, la 7.1, disponible desde julio de 2018. Además, hizo una demo en directo para que las viésemos en funcionamiento. Entre ellas, destacó las mejoras en:

    • La usabilidad del menú de producto y las herramientas del sistema.
    • La experiencia de usuario a través del lenguaje de diseño de componentes de Liferay, Lexicon en su versión 2.0, y su implementación, Clay. Se ha hecho una revisión de la interfaz de todos los componentes.
    • El desarrollo frontend con el uso de Bootstrap 4, en lugar de 3, los nuevos componentes de gráficas y el hecho de poder utilizar frameworks modernos como Angular, React, Vue.js…
    • La aceptación de la RGPD, que explicó Sergio Sánchez en una charla posterior.
    • La autorización con el uso de OAuth 2.
    • La búsqueda. Se han actualizado a ElasticSearch 6.1 y ofrecen un panel de control para la gestión de índices de búsqueda.
    • La creación de páginas. Como explicaron posteriormente Pavel e Ianire, ahora se pueden crear páginas de contenidos.
    • Los menús múltiples, desacoplados de la jerarquía de páginas, con drag & drop.
    • Los blogs, que ya se repensaron en la versión 7.0 y ahora introducen mejoras de SEO, una vista de tipo tarjeta superior y se integran con servivios externos.
    • Los foros. Se añade drag & drop para los adjuntos, gestión de notificaciones se rediseñan los comentarios.
    • Los formularios. Han avanzado muchísimo y nos dan nuevas posibilidades gracias, en gran medida, al uso de reglas condicionales, detalladas posteriormente por Javier Ahedo.
    • El multidioma. Hasta ahora era necesario clonar los componentes para que cada uno tuviese un idioma distinto. Ya no, ya lo tenemos de serie.
    • El desarrollo. Han sacado una nueva generación de APIs y un plugin para el IDE favorito de muchos: IntelliJ.

    Podemos encontrar un resumen de todas estas novedades en la documentación oficial.

    Modern Site Building: Conoce cómo construir sites de nueva generación

    Pavel Savinov, ingeniero de software en Liferay, e Ianire Cobeaga, analista de negocio en Liferay, presentaron el nuevo tipo de páginas que trae Lfieray DXP 7.1: las páginas de contenido. Estas se unen a las ya existentes páginas de widgets.

    Las páginas de contenido son ideales para añadir contenido no estructurado. En el Symposium pudimos ver una demostración de cómo un usuario puede crear muy rápidamente una landing y editar sus contenidos sobre ella misma, de tal modo que es posible ver, a medida que editas el contenido, cómo está quedando la página.

    El hecho de que se puedan crear rápidamente viene dado porque están formadas por fragmentos que son independientes, reutilizables y personalizables. Y esto aporta flexibilidad, funcionalidad y desacoplamiento.

    Esta independencia permite que los desarrolladores y diseñadores creen los fragmentos y que los marketers se encarguen de crear las páginas a partir de ellos, de una manera ágil y simplificada, con el objetivo de empoderarlos y mejorar su experiencia a la hora de construir y editar páginas.

    Por último, Pavel e Ianire nos mostraron que se ha rediseñado el menú y nos comentaron que los cambios mostrados pertenecen a una línea de desarrollo que continúa y que traerá más novedades cuando Liferay DXP 7.2 se materialice.

    Liferay Forms 7.1: Nuevas capacidades y extensibilidad

    Javier Ahedo, consultor en Liferay, detalló las mejoras que han experimentado los formularios y que Rafael Lluis ya nos adelantó.

    Javier explicó las siguientes nuevas funcionalidades:

    • Multidioma en todos los campos y textos de la misma forma que se hace en los idiomas soportados por Liferay DXP. Con un único formulario, sin tener que clonar.
    • Nuevos campos y propiedades.
    • Autoguardado. Pudimos ver cómo, mientras dábamos forma a nuestra página de contenido, esta se iba guardando automáticamente. Por defecto lo hace cada minuto pero es configurable.
    • Duplicado de campos y duplicado de formularios.
    • Validador de textos con soporte a expresiones regulares.
    • Campos personalizados.

    Además, hizo una demo en vivo en la que pudimos ver el potencial de:

    • Las reglas condicionales. Es posible establecer acciones a realizar automáticamente dadas unas condiciones. Por ejemplo, Javier nos mostró una página con el típico proceso en pasos, como el de compra, en la que dependiendo de la opción que tuvieses marcada, la página te enviaba a un paso u otro.
    • Los combos dependientes, con el ejemplo de países y provincias: tras elegir un país, el combo de provincias se pobla con únicamente las provincias de dicho país.
    • Los campos calculados, que obtienen un valor automáticamente en función del valor de otros campos. Javier puso el ejemplo de un formulario de compra de entradas en el que elegías el número de personas y automáticamente se calculaba el precio.
    • Los conjuntos de elementos. Es posible crear estos conjuntos, por ejemplo un conjunto «datos de persona», y, al crear un formulario, arrastrar este conjunto al mismo y pasar a tener en él todos sus elementos.

    Securizando tu proyecto Liferay: Herramientas y técnicas

    Carlos Sierra, arquitecto de software en Liferay, vino a hablar de la importancia de la seguridad, dando una clase magistral de cómo conseguirla, y cómo Liferay DXP es «seguro por defecto».

    Expresó que la seguridad es algo que normalmente se deja para el final y que no debería ser así, que debería estar siempre presente, desde el comienzo de los proyectos. Vamos con su clase magistral.

    Carlos comenzó contando cierta teoría sobre seguridad:

    • Definió la «superficie de ataque» como la suma de los diferentes puntos a través de los cuales un usuario sin autorización podría introducir datos o extraerlos de un entorno.
    • Recomendó:
      • La defensa en profundidad, consistente en defender un sistema contra cualquier ataque usando varios métodos independientes.
      • El principio de menor privilegio, por el cual los usuarios, procesos o programas tienen que poder acceder solamente a la mínima información necesaria para efectuar su función.
      • Usar bibliotecas y frameworks que ya han sido probados por multitud de personas y están diseñados para evitar problemas de seguridad.
      • Usar diferentes técnicas y pruebas para encontrar y prevenir debilidades.
      • Repetir las mismas pruebas al cabo de un tiempo aunque pensemos que no van a fallar porque ya las realizamos en su día.
      • Establecer y mantener el control sobre todas las entradas y salidas y sobre el entorno.
      • Asumir que nuestro código va a poder ser leído por cualquiera. Tener especial cuidado, por tanto, en el desarrollo frontend y pensar que las primeras cosas que se hacen para hackear es decompilar las aplicaciones.

    Tras esto, comentó que Liferay DXP es seguro por defecto (más información en la documentación oficial) pero que es posible introducir brechas de seguridad a través de nuestras configuraciones y personalizaciones.

    Pasó a enumerar aspectos de la seguridad:

    • A nivel de sistema operativo:
      • Usar una cuenta diferente de la de administrador para ejecutar Liferay.
      • Usar una ubicación temporal específica en lugar de una común.
      • Permitir la escritura a «/deploy» únicamente durante los despliegues.
      • Ejecutar los despliegues y los backups con un usuario diferente.
      • No dar acceso de lectura ni de escritura de backups a la cuenta que ejecuta Liferay.
    • Por capas:
      • Cada capa solo debería tener visibilidad sobre lo siguiente.
      • No mezclar entornos.
      • Desactivar las funcionalidades que no usemos, tales como componentes y puertos.
      • Listar explícitamente la lista de interlocutores válidos, tanto de entrada como de salida.
      • Emplear solo contenidos estáticos, ni CGI ni PHP…
      • Dar acceso a las interfaces de gestión únicamente desde la red interna.
    • Al configurar Liferay:
      • Desactivar las funciones y módulos que no se utilicen.
      • Desactivar las páginas privadas si no son necesarias.
      • Usar staging remoto.
    • De contenido y permisos:
      • Validar todas las entradas del usuario, tanto en cliente como en servidor, y escapar todo su contenido.
      • Utilizar controles de acceso basados en roles, pero no preguntando si un usuario tiene cierto rol, sino si un usuario puede ejecutar cierta acción sobre cierto recurso.
      • No abusar de las cuentas de administrador.
      • Evitar los roles por defecto de Liferay, que vienen muy bien para comenzar a jugar con la plataforma pero que, al estar por defecto, ya se sabe qué roles existen y qué permisos posee cada uno.
    • De servicios personalizados:
      • Comprobar los permisos en los servicios remotos y delegar la lógica a los servicios locales.
      • Establecer protecciones para los servicios web JSON, como OAuth 2.
      • Usar JAXB o Jackson para tratar con XML y JSON en lugar de crear lógica a mano.
    • En el diseño y desarrollo:
      • Revisar la solución con todos los equipos involucrados y de manera holística, pues la seguridad es responsabilidad de toda la cadena.
      • Utilizar la herramienta de parcheo de Liferay para mantenerlo al día.
      • Emplear antivirus. La integración con clamAV viene por defecto.
      • Realizar tests de todo tipo, incluidos los de penetración, y ejecutarlos periódicamente, sin olvidar probar los roles y permisos.
    • En producción:
      • Poner en funcionamiento un sistema de detección de intrusiones (IDS), un firewall de aplicaciones web (WAF) y una aplicación de gestión del rendimiento (APM).
      • Realizar registros del servidor web, del servidor de aplicaciones, de los clicks realizados (con Google Analytics, por ejemplo) y de la sesión para poder realizar un seguimiento de la actividad de un determinado usuario.
      • Emplear el portlet de auditoría.
      • Implementar un portlet que compruebe el estado del servidor (health check) y tener así un panel de mandos.
      • Realizar regularmente informes de métricas de interés, con Jasper por ejemplo.
      • Monitorizar, en general.

    La seguridad debería ser como la higiene personal: no tenemos que descuidarla.

    10 agradables sorpresas que encontrarás en tu proceso de upgrade a Liferay DXP 7.1

    Alberto Chaparro, ingeniero de soporte/consultoría en Liferay, nos comentó las mejoras que ha experimentado el proceso de actualización entre versiones de Liferay, con el objetivo de instar a todo el mundo a actualizar a la nueva versión de Liferay DXP, la 7.1.

    1. Auditoría del proceso. Podemos ver un registro del tiempo que toma cada pequeño proceso, cuál es el nivel de parcheado al inicio de cada uno y se muestra más información con las trazas a DEBUG. En fin, medidas para facilitar el análisis del rendimiento y de los posibles problemas.
    2. Scripts. Ahora tenemos un par de scripts llamados «db_upgrade» —uno para Unix y otro para Windows— para simplificar el proceso de actualización. Además, si estamos utilizando SSH para actualizar y se pierde esta conexión, la actualización ya no se ve interrumpida. Por último, al ejecutar ahora un proceso de actualización, se verifica que no esté ya ejecutándose.
    3. Uso de un esquema de versiones en formato «mayor.minor.micro» (1.0.0) para definir el estado de las tablas y los datos asociados a un módulo en un momento dado.
    4. Modularidad del framework en el núcleo. Se siguen los mismos principios que los existentes para los módulos: división en pequeños procesos, cada uno con un esquema de versiones registrado en base de datos; y se simplifica el proceso de actualización, pues ya no hay más propiedades «upgrade.processes*».
    5. Posibilidad de relanzar la actualización en caso de fallo sin necesidad de tener que recuperar copias de seguridad.
    6. Los índices se modifican y regeneran automáticamente tras un proceso de actualización.
    7. Se definen tablas independientes para campos traducibles. Un proceso de actualización convertirá los XML de dichos campos en valores de la tabla de traducciones.
    8. Librería documental como único repositorio de imágenes.
    9. Cambios en el desarrollo:
      • Se implementa el estándar de portlets 3.0.
      • Se elimina el soporte a Velocity en temas y se usa Bootstrap 4.
      • Se extraen algunas portal.properties a la configuración de OSGi y clases del núcleo (portal-kernel) a módulos (Petra).
      • Se añaden nuevos comandos Gogo de diagnóstico en tiempo de ejecución: ds:softCircularDependency, ds:unsatisfied y system:check.
      • Se mejora el desarrollo en IntelliJ gracias al plugin oficial para Liferay.
    10. Mejoras del rendimiento de la actualización que han permitido que, por ejemplo, procesos de actualización con MySQL vayan un 78 % más rápido. De esta manera, la actualización de Liferay 6.2 a 7.1 es más liviana que de 6.2 a 7.0 (se pueden realizar actualizaciones directas a partir de Liferay 6.1.30).

    Desarrollos frontend modernos (React, Angular…) en Liferay DXP

    Iván Zaera, ingeniero de Software senior en Liferay, trajo las novedades del desarrollo frontend en Liferay.

    Hasta ahora los portlets tenían que ser desarrollados con npm y Gradle, los paquetes npm tenían que ser desplegados en Liferay y había integración y cooperación entre distintos portlets. Estos tres factores implicaban ciertas limitaciones: el desarrollo requería conocimiento específico de Liferay, el proceso de construcción tenía una configuración compleja y, a la hora de integrar, existía una desduplicación agresiva y no configurable de los módulos npm.

    Actualmente, tales limitaciones se han solucionado gracias a:

    • El bundler 2.0, en el que las dependencias por defecto son locales —tipo Webpack— y es posible configurar la estrategia de resolución de las mismas.
    • La compartición de módulos configurable gracias a los imports.
    • El desarrollo de portlets usando herramientas 100 % JavaScript. Como gestor de paquetes se utiliza npm, Yeoman para generar los proyectos y el bundler para empaquetar los artefactos. De esta manera ya no es necesario ni Gradle, ni Blade, ni Java.

    Podemos encontrar más información en el repositorio liferay-npm-build-tools. Su wiki es un buen punto de partida y podemos crear issues para preguntar y dar de alta bugs.

    Cómo Liferay DXP te ayuda en el cumplimiento de GDPR

    Sergio Sánchez, ingeniero de Soporte en Liferay, nos resumió las implicaciones del Reglamento General de Protección de Datos (RGPD, GDPR en inglés) y cómo Liferay DXP ayuda a cumplirlo.

    Sergio explicó que el GPDR consiste en:

    • Conocer los datos de carácter personal que procesamos, que son aquellos relativos a una persona física viva identificada o identificable, como el nombre, el domicilio, el teléfono, el número de tarjeta de débito, historial clínico, etc.
    • Cumplir las obligaciones basadas en la gestión de riesgos.
    • Cumplir las obligaciones que protegen los derechos individuales.

    Nos contó que la protección de datos se debe realizar desde las primeras fases del desarrollo, desde el diseño, y que por defecto hay que utilizar la configuración más adecuada a la privacidad. Liferay DXP ayuda a acelerar la implementación del RGPD porque incorpora de serie la exportación de datos, su anonimización y su borrado, además de que cifra las contraseñas o, directamente, no las almacena en base de datos.

    Con el RGPD, los usuarios tenemos diversos derechos:

    • Saber quién trata nuestros datos, con qué fin y cómo retirar nuestro consentimiento. Liferay facilita la implementación de los términos de uso y su consentimiento gracias a la página de términos de uso y del formulario de consentimiento con checkboxes para un marcado explícito (recordemos que, por ley, no pueden venir previamente marcados).
    • Solicitar el acceso a los datos personales de forma gratuita, y la copia dada debe tener un formato accesible. Liferay, desde el apartado «Usuarios», nos da de serie una opción para exportar los datos personales de cada usuario.
    • Pedir borrar nuestros datos. Aquí Liferay proporciona un asistente para el borrado total de un usuario, así como para poder anonimizar todos sus datos. Sergio nos recuerda aquí que esto se da para los datos de Liferay DXP y de los desarrollos con Service Builder y que, si tenemos datos de sistemas externos, ahí tendremos que hacernos cargo nosotros.
    • Poder corregir los errores en los datos y oponernos a su tratamiento para un determinado uso.

    Sergio concluyó su presentación reiterando la importancia del RGPD y que este no es únicamente cuestión de firmar un contrato, que tenemos, como tratantes de datos, obligaciones que cumplir.

    Liferay as a Headless Platform: From building custom front-ends to omnichannel experiences using Liferay APIs

    Pablo Agulla, Product Manager en Liferay, y José Manuel Navarro, ingeniero de Software senior en Liferay, nos hablaron de la estrategia de diseño de las APIs de Liferay.

    Antiguamente se empleaba Liferay con un único frontend, pero hoy día se busca la omnicanalidad con diferentes fronts como pueden ser páginas web o aplicaciones móviles. Esto aumenta la importancia de que nuestro backend exponga una buena API. Se utilizaban los servicios web JSON, pero ahora se quiere exponer funcionalidad de negocio en lugar de servicios, en diferentes paquetes con varias APIs:

    • Content Delivery API.
    • Content Management API.
    • Content Participation API.
    • Platform Administration API.
    • Site Management API.
    • Users & Permissions API.

    Las APIs siguen, además, una estructura de capas: API pública, API de colaborador, API privada, plataforma.

    Los planes para esta plataforma headless son:

    • Para la versión 7.2, ofrecer gestión y participación de contenido y aumentar las capacidades de filtrado, ordenación y y entrega de contenido.
    • Apostar por GraphQL para la entrega de contenido.
    • Crear una API de gestión de contenido avanzada.

    Claves para mejorar la experiencia de usuario con Liferay DXP 7.1

    Víctor Valle y Juan Antón, diseñadores UX en Liferay, presentaron los avances de Liferay para mejorar la experiencia de usuario.

    Nos hablaron de Lexicon, lenguaje de experiencia de usuario, y de Clay, su implementación para Liferay DXP.

    Comentaron que para Liferay DXP 7.1 se rediseñaron todos los widgets y se mejoró la paleta de colores, paleta que, por cierto, podemos cambiar para adecuarnos a la marca de nuestro cliente, pero que, según Víctor y Juan, deberíamos respetar al menos en lo tocante a los colores para las alertas de error, información, etc. que nos aparecen durante el uso del portal.

    Para la versión 7.2, tienen pensado incorporar lo siguiente:

    • Barras de filtro.
    • Mejoras en los menús contextuales.
    • Nuevas tarjetas.
    • Nuevas tablas con acciones rápidas, interacciones sobre las filas, atajos de teclado, cabeceras fijas, reordenamiento de las columnas y posibilidad de mostrarlas y ocultarlas.

    Más allá de la 7.2, una de las cosas que posiblemente modifiquen sea el panel lateral izquierdo, pues contienen muchísimas opciones.

    Liferay DXP Cloud

    Eduardo Lundgren, CTO de Liferay Cloud, presentó Liferay DXP Cloud, una solución PaaS que proporciona autoescalado, herramientas de desarrollo, entornos y monitorización.

    Eduardo siguió de nuevo haciendo énfasis en el mensaje general de este Symposium, en la importancia de la transformación digital. Como pequeña empresa, ¿qué podemos hacer para competir con las grandes?, ¿cómo seguir innovando y transformarse digitalmente? El ecosistema Liferay ayuda a ello y Liferay DXP Cloud es una parte importante.

    El señor Lundgren nos mostró en gráficas la creciente recopilación de datos que se está dando durante estos años y cómo en un futuro cercano las cifras se van a multiplicar asombrosamente. En este contexto, Liferay DXP Cloud nos permite llevar nuestra infraestructura a la nube, procesar todos los datos que se generan y aportar seguridad a los procesos, y es que cada entorno que tengamos en Cloud (staging, producción…) está en una red privada distinta, siendo accesibles a través de VPN.

    Entre las características notables de Liferay DXP Cloud, encontramos la automatización y la monitorización 24/7, que nos da:

    • Seguridad.
    • Alertas. Avisan, por ejemplo, de que la memoria se está quedando corta.
    • Escalado automático o manual en situaciones como las del punto anterior.
    • Colaboración. Es posible crear diferentes equipos, con diferentes personas, para cada entorno, y gestionar permisos, quién puede ver cada entorno y ejecutar comandos.
    • Trazabilidad de la actividad de los usuarios.

    La página oficial la encontramos aquí.

    Liferay Commerce

    Marco Leo, arquitecto de software en Liferay, dio un par de charlas para presentar Liferay Commerce y ofrecer una demo de este nuevo producto lanzado en junio de 2018.

    Marco empezó insistiendo en la necesidad de invertir en el comercio digital para ahorrar costes —los canales tradicionales son costosos— y para obtener más herramientas de ventas. Para ello podemos utilizar Liferay Commerce, desarrollado con un portal Liferay por debajo para así aprovechar sus ventajas: la unión de la gestión del contenido y el comercio, la colaboración gracias a la base de conocimiento del portal y el uso de widgets.

    Las funcionalidades y características de Liferay Commerce se pueden resumir en las siguientes:

    • Fácil gestión del catálogo gracias al multidioma y la clasificación de los productos.
    • Buena experiencia web por el uso de temas, plantillas y ser multidispositivo.
    • Búsqueda con ElasticSearch y filtros.
    • Implementación de cuestiones de comercio como ofrecer descuentos a diferentes tipos de usuarios, promociones, tasas, carrito o pago con plataformas como PayPal.
    • Gestión de pedidos con stock en tiempo real.
    • Soporte para los clientes de nuestro comercio.
    • Uso de aceleradores. Se integra con Talend para, por ejemplo, exportar datos a hojas de cálculo.
    • Interfaz limpia con diferentes vistas para el catálogo de productos: en lista y en cuadrícula.

    Actualmente existe una versión para la comunidad cuyo nombre, temporal, es Emporio.

    Marco terminó comentando los planes para Liferay Commerce:

    • En el primer cuatrimestre de 2019 lanzarán suscripción de productos y se podrán planificar en el tiempo los jobs de Talend.
    • En el tercer cuatrimestre de 2019 se podrá añadir un sistema de puntos para los clientes, lo cual es una buena forma de incentivar la compra en nuestro comercio, un conector con MuleSoft y la posibilidad de gestionar la devolución de productos.

    La página oficial la encontramos aquí.

    Liferay Analytics Cloud

    Rafael Lluis, consultor en Liferay, presentó, en su segunda charla de este Symposium, el último producto de Liferay: Analytics Cloud, disponible a finales de octubre de 2018.

    Esta herramienta de análisis nos permite medir y visualizar los datos de:

    • Las personas individualmente, a nivel de organización y por segmentos de usuarios.
    • Las interacciones, tanto a nivel de página como a nivel de asset.
    • Los orígenes de los datos.

    Rafa resumió los beneficios que nos aporta esta herramienta:

    • Poder conocer el rendimiento, el ROI de los contenidos. Con ello podemos decidir invertir en aquellos que más beneficio nos están reportanto o, al contrario, mejorar aquellos que no funcionan tan bien. Un claro ejemplo de esto es poder saber por qué los usuarios abandonan un formulario, saber en qué campo se encontraban cuando lo hicieron. Otro ejemplo sería conocer los puntos de entrada, cómo llega el público a nuestro contenido, cuál es su navegación a través de las diferentes páginas.
    • Ver gráficas y estadísticas a nivel de cliente, saber cómo se comportan nuestros usuarios, cuáles son sus intereses comunes. Esta información se puede enriquecer con datos externos que tengamos en formato CSV (en el futuro se aceptarán otros orígenes de datos).
    • En fin, convertir el conocimiento del cliente en una ventaja frente a la competencia.

    El roadmap para este producto es el siguiente:

    • A finales de 2018:
      • Cumplimiento con el RGPD.
      • Integración con Liferay 6.2, sitios que no estén construidos con Liferay y otras aplicaciones.
      • Interconexión Salesforce directa para SCV.
      • Cuentas, B2B y ABM.
    • A un año vista:
      • Capacidad de actuación.
      • Disponibilidad de APIs.
      • Integración de más canales sin tener que desarrollarlos a medida.
      • Creación y análisis en base a eventos personalizados.

    La página oficial la encontramos aquí.

    Charlas cortas

    Además de las presentaciones dadas por los propios empleados de Liferay, pudimos disfrutar de tres charlas cortas impartidas por colaboradores de Liferay:

    • Progressive Wep Applications en Liferay, por Salvador Tejero, consultor senior en Minsait.
    • Conectividad entre diferentes actores mediante el uso de tecnología VC con Liferay DXP, por Javier Lora, arquitecto de software en Ricoh.
    • Headless Liferay with SAP & Hybris Integration, por Adolfo Carlos Benítez y Juan Carlos Rivera, ingenieros de Software en Mimacom.

    Conclusión

    Tras estas dos jornadas, los chicos de Liferay nos dejaron claro en este Symposium que apuestan por la creación de productos que permitan la innovación y transformación digital de las empresas para obtener ventajas competitivas mediante la mejora de la experiencia del cliente. Es más, ellos mismos aplican esta idea al ecosistema Liferay para conseguir experimentar un mejor uso del mismo.

    Y, como decía Carolina Moreno, lo más interesante es que Liferay es ahora empresa de varios productos, aunque el portal sigue siendo su producto estrella.

    La entrada Liferay Symposium 2018 se publicó primero en Adictos al trabajo.

    Testing en componentes de Vue.js

    $
    0
    0

    Índice de contenidos

    1. Introducción

    En este tutorial aplicaremos los principios de TDD al desarrollo de componentes en Vue.js, asumiendo un conocimiento intermedio de Vue.js y un entendimiento básico de tests unitarios.

    Vamos a usar las librerías de testing Vue-Test-Utils y Jest, y Typescript para el tipado estático.

    Vue Test Utils es la librería de testing oficial para Vue.js. Nos da mucha facilidad a la hora de montar componentes, simular eventos de usuario, renderizado superficial, modificar el estado y los props de componentes y más.

    Jest es el motor de tests mantenido y usado por Facebook. La mayor ventaja de Jest sobre otros frameworks de tests es la velocidad y que no es necesario configurar casi nada para usarlo.

    Typescript es un superset de Javascript de Microsoft que introduce muchas características nuevas al lenguaje. Puede que la más notable sea el fuerte tipado estático.

    TDD o Test-driven development es un proceso de desarrollo de software que sigue un ciclo muy corto:

    1. Se definen los requerimientos del software.
    2. Se crean tests para cubrir esos requerimientos.
    3. Se implementa el software para pasar los tests.

    Si se necesita ampliar el software, se repite el mismo proceso. No se puede crear software que no tenga tests cubriéndolo.

    2. Entorno

    El tutorial está escrito usando el siguiente entorno:

    • Hardware: MacBook Pro 15’ (2,5 GHz Intel Core i7, 16GB DDR3)
    • Sistema operativo: macOS Sierra 10.12.6
    • Entorno de desarrollo: Visual Studio Code
    • Versión de Vue: 2.5.17

    3. Creación del proyecto.

    Vamos a usar Vue CLI 3, una herramienta de terminal que nos ayuda a la hora de
    crear la estructura de carpetas del proyecto.

    Para instalarlo de forma global es tan sencillo como:

    npm install -g @vue/cli
    # o
    yarn global add @vue/cli

    Ahora que tenemos el CLI instalado, abrimos el terminal en nuestra carpeta de proyectos y lo ejecutamos:

    vue create [nombre de proyecto]

    O si no queremos instalar el CLI globalmente:

    npx @vue/cli create [nombre de proyecto]

    El CLI nos preguntará si queremos crear un proyecto predeterminado o queremos personalizarlo. Nosotros vamos a elegir manualmente.

    Estas son las opciones que he elegido para este proyecto:

    El CLI nos pedirá algunos detalles más sobre la configuración que hemos elegido:

    Lo más importante a destacar en esta parte es que vamos a usar Jest.

    4. Definición de los requerimientos.

    Antes de ponernos a picar código como locos, debemos definir unos requerimientos claros.

    A riesgo de ser cliché, vamos a desarrollar una aplicación de tareas, o la infame Todo List. Estos son los requerimientos de la aplicación.

    • Debe tener una lista de items. Cada item tendrá:
      • Un título
      • Un botón con texto ‘complete’. Cuando sea clicado:
        • El título debe ser tachado.
        • El botón debe ser deshabilitado y su texto cambiar a ‘completed’.
    • Debe tener un campo de entrada de texto.
    • Debe tener un botón.
      • Si la entrada de texto está vacía, el botón debe estar deshabilitado.
      • Si la entrada de texto no está vacía, cuando sea clicado:
        • Debe añadir un nuevo item con el título de la entrada de texto.
        • Debe vaciar la entrada de texto.

    Personalmente, en esta etapa me suele ayudar mucho dibujar la aplicación en papel o en una herramienta de prototipado de interfaces, para ver claramente los componentes que voy a necesitar.

    De la especificación podemos deducir que vamos a necesitar dos componentes:

    • TodoList: un contenedor que guardará y modificará el estado de los hijos.
    • TodoItem: un componente que recibirá por props lo que debe renderizar.

    5. Las bases de Testing en componentes.

    A la hora de testear componentes, no necesitamos hacer testing de características de Vue, asumimos que estas va a funcionar normalmente.

    Nuestro objetivo es asegurar que la lógica que nosotros introducimos sigue el comportamiento especificado. Debemos testear:

    • Lifecycle hooks: comprobar que una función es llamada cuando el componente se monta, se destruye…
    • Métodos: comprobar que el retorno es el esperado o que se ha cambiado el estado correctamente.
    • Watchers: cuando se modifica un prop o método, asegurar que el watcher es invocado.
    • Propiedades computadas: comprobar que el retorno es el esperado.

    Snapshots

    Los snapshots son ‘fotos’ de nuestro componente renderizado. Cada vez que pasemos los tests de un componente, una nueva ‘foto’ es sacada.

    El motor de tests nos avisará si la nueva foto coincide con la anterior. Si no es así, nos avisará.

    Esto nos sirve para asegurarnos de que los cambios que hacemos que hacemos “por debajo” de un componente no afectan a la forma en la que este se renderiza. Son “gratis” (muy sencillos de implementar) y está bien tenerlos en todos los componentes que tengan algo de HTML a renderizar.

    Mount y ShallowMount

    mount() y shallowMount() son las funciones que nos permiten montar nuestro componente dentro de los tests.

    Mount nos montará el componente y todos los componentes hijos mientras que shallowMount solo montará el componente en cuestión.

    Por lo general es recomendable usar shallowMount antes que mount porque mantiene los tests aislados y unitarios y se tarda menos en ejecutar el test.

    Usa mount cuando quieras probar la integración entre distintos componentes.

    Triggers

    Para simular la interacción con el usuario, podemos disparar eventos de muchos tipos con trigger().

    6. Desarrollo de un componente. TodoItem.

    Testing.

    Este es un componente muy tonto, no tiene lógica de negocio, solo de vista.

    Vamos a comprobar:

    • El componente renderiza el mismo ‘snapshot’ que la última vez.
    • Si ‘isCompleted’ es ‘false’, el texto del botón es ‘complete’.
    • Si ‘isCompleted’ es ‘true’, el texto del botón es ‘completed’.
    • Si ‘isCompleted’ es ‘true’, el botón debería deshabilitarse.

    Creamos el fichero TodoItem.spec.ts en la carpeta de tests y nos ponemos a ello.

    import { shallowMount, Wrapper } from "@vue/test-utils";
      import TodoItem from "@/components/TodoItem.vue";
    
      describe("TodoItem.vue", () => {
        let id: number;
        let title: string;
        let isCompleted: boolean;
        let onClick;
        let wrapper: Wrapper<TodoItem>;
    
        // Montamos el componente con los props necesarios antes de cada test.
        beforeEach(() => {
          id = 1;
          title = "Test Title";
          isCompleted = false;
          onClick = () => {};
    
          wrapper = shallowMount(TodoItem, {
            propsData: {
              id,
              title,
              isCompleted,
              onClick
            }
          });
        });
    
        // Nos aseguramos que nadie ha modificado el componente sin modificar los tests.
        it("should match snapshot", () => {
          expect(wrapper).toMatchSnapshot();
        });
    
        // Si 'isCompleted' es 'false', el texto del botón es 'complete'.
        it("should change button text to 'complete' when 'isCompleted' is false", () => {
          // Cuando 'isCompleted' es falso.
          wrapper.setProps({ isCompleted: false });
    
          // Nos aseguramos de que el texto del botón cambia a 'complete'.
          expect(wrapper.find("button").text()).toBe("complete");
        });
    
        // Si 'isCompleted' es 'true', el texto del botón es 'completed'.
        it("should change button text to 'completed' when 'isCompleted' is true", () => {
          // Cuando 'isCompleted' es verdadero.
          wrapper.setProps({ isCompleted: true });
    
          // Nos aseguramos de que el texto del botón cambia a 'completed'.
          expect(wrapper.find("button").text()).toBe("completed");
        });
    
        // Si 'isCompleted' es 'true', el botón debería deshabilitarse.
        it("should disable button when 'isCompleted' is true", () => {
          // Cuando 'isCompleted' es verdadero.
          wrapper.setProps({ isCompleted: true });
    
          // Nos aseguramos que el botón es deshabilitado.
          expect(wrapper.find("button").attributes("disabled")).toMatch("disabled");
        });
      });

    Implementación.

    Ahora que nuestros tests cubren todo el comportamiento del componente, podemos empezar la implementación.

    <template>
      <div class="todo-item">
        <span :class="{ completed: isCompleted }">{{ title }}</span>
        <button @click="onClick(id)" :disabled="isCompleted">{{ buttonText }}</button>
      </div>
    </template>
    
    <script lang="ts">
    import Vue from "vue";
    
    export default Vue.extend({
      name: "TodoItem",
      props: {
        id: {
          type: Number,
          required: true
        },
        title: {
          type: String,
          required: true
        },
        isCompleted: {
          type: Boolean,
          required: true
        },
        onClick: {
          type: Function,
          required: true
        }
      },
      computed: {
        buttonText(): String {
          return this.isCompleted ? "completed" : "complete";
        }
      }
    });
    </script>
    
    <style scoped>
    .completed {
      text-decoration: line-through;
    }
    
    .todo-item {
      margin: 10px;
    }
    </style>

     

    Para probar que nuestra implementación pasa los tests, usamos:

    npm run test:unit
    # o
    yarn test:unit

    7. Desarrollo de un contenedor. TodoList.

    Testing.

    Este es un contenedor con algo de lógica y que contiene todos los TodoItems a renderizar.

    Vamos a comprobar:

    • El componente renderiza el mismo ‘snapshot’ que la última vez.
    • Si la entrada de texto está vacía, el botón debe estar deshabilitado.
    • Si la entrada de texto no está vacía, cuando el botón sea clicado:
      • Debe añadir un nuevo TodoItem con el título de la entrada de texto.
      • Debe vaciar la entrada de texto.

    Creamos el fichero TodoItem.spec.ts en la carpeta de tests y nos ponemos a ello.

    import { Wrapper, shallowMount, mount } from "@vue/test-utils";
    import TodoList from "@/containers/TodoList.vue";
    
    describe("TodoList.vue", () => {
      let wrapper: Wrapper;
    
      beforeEach(() => {
        wrapper = mount(TodoList);
      });
    
      // Nos aseguramos de que la imagen del componente es la misma.
      it("should match snapshot", () => {
        expect(wrapper).toMatchSnapshot();
      });
    
      // Si la entrada de texto está vacía, el botón debe estar deshabilitado.
      it("should disable the button if input text is empty", () => {
        // Cuando el input text esté vacío.
        wrapper.vm.$data.newTodo = "";
    
        // Aseguramos que el botón está deshabilitado.
        expect(wrapper.find("button").attributes("disabled")).toMatch("disabled");
      });
    
      // Clicar el botón añadirá un nuevo TodoItem a la lista, con el título adecuado.
      it("should add a new todo to the array and update the view when the button is clicked and the text input is not empty", () => {
        // Le damos valor al input text.
        wrapper.vm.$data.newTodo = "new todo title test";
        // Simulamos un click del usuario
        wrapper.find("button").trigger("click");
    
        // Aseguramos que el nuevo 'TodoItem' tiene como título el que nosotros le hemos dado.
        expect(wrapper.find(".todo-item").text()).toMatch("new todo title test");
      });
    
      // Clicar el botón vaciará el campo de texto si la entrada de texto no está vacía.
      it("should empty the text input when the button is clicked and the text input is not empty", () => {
        // Le damos valor al input text.
        wrapper.vm.$data.newTodo = "new todo title test";
        // Simulamos un click del usuario
        wrapper.find("button").trigger("click");
    
        // Aseguramos que el campo de texto está vacío.
        expect(wrapper.vm.$data.newTodo).toBe("");
      });
    
      // Clicar en el botón de un 'TodoItem', le dará true al valor 'isCompleted'.
      it("should update 'TodoItem' 'isCompleted' to true when its button is clicked", () => {
        wrapper.vm.$data.todos.push({ title: "title test", isCompleted: false });
        wrapper.find(".todo-item button").trigger("click");
        expect(wrapper.vm.$data.todos[0].isCompleted).toBe(true);
      });
    
      // La propiedad computada 'isButtonEnabled' deberá funcionar como previsto.
      it("should return a valid 'isButtonEnabled' computed property", () => {
        wrapper.vm.$data.newTodo = "";
        expect(wrapper.vm.isButtonEnabled).toBe(false);
        wrapper.vm.$data.newTodo = "test";
        expect(wrapper.vm.isButtonEnabled).toBe(true);
      });
    });

    Implementación.

    Ahora que los tests cubren todo el comportamiento del contenedor, podemos empezar la implementación.

    <template>
    <div class="todo-list">
      <input type="text" placeholder="New task..." v-model="newTodo"/>
      <button @click="addNewTodo" :disabled="!isButtonEnabled">+</button>
      <TodoItem v-for="(todo, key) in todos"
      :id="key"
      :key="key"
      :title="todo.title" 
      :isCompleted="todo.isCompleted" 
      :onClick="setTodoCompleted">
      </TodoItem>
    </div>
    </template>
    
    <script lang="ts">
    import Vue from "vue";
    import TodoItem from "@/components/TodoItem.vue";
    
    interface ITodo {
    title: string;
    isCompleted: boolean;
    }
    
    export default Vue.extend({
    name: "TodoList",
    components: {
      TodoItem
    },
    data: (): {
      todos: ITodo[];
      newTodo: string;
    } => ({
      todos: [],
      newTodo: ""
    }),
    computed: {
      isButtonEnabled(): boolean {
        return this.newTodo !== "";
      }
    },
    methods: {
      addNewTodo() {
        this.todos.push({ title: this.newTodo, isCompleted: false });
        this.newTodo = "";
      },
      setTodoCompleted(index: number) {
        this.todos[index].isCompleted = true;
      }
    }
    });
    </script>
     

    Para probar que nuestra implementación pasa los tests, usamos de nuevo:

    npm run test:unit
    # o
    yarn test:unit

    8. Conclusiones.

    Vue Test Utils nos permite testear la funcionalidad de una aplicación Vue de principio a fin.

    En este tutorial nos hemos centrado en el testing de componentes, pero también se puede testear otras partes de Vue como Vue-Router o Vuex.

    Échale un vistazo!

    El testeo del software es tan importante en el backend como en el frontend, no dejes mal a tus colegas ‘fronteros’ y testea como el que más!

    Puedes clonar y probar el proyecto con:

    git clone https://github.com/ArturoRodriguezRomero/Vue-Component-Testing-Example
    
    cd Vue-Component-Testing-Example
    
    yarn install
    
    yarn serve

    O descargarlo desde Github:

    Proyecto en Github

    9. Referencias.

    La entrada Testing en componentes de Vue.js se publicó primero en Adictos al trabajo.

    Nexus en HTTPS con Let’s Encrypt

    $
    0
    0

    Índice de contenidos

    1. Entorno

    Este tutorial está escrito usando el siguiente entorno:

    • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
    • Sistema Operativo: LUbuntu 18.04

    2. Introducción

    En el momento en el que nuestras herramientas de desarrollo las movemos a la nube, quedamos expuestos a cualquiera que acierte con nuestra URL por lo que se hace imprescindible que todo nuestro tráfico vaya a través del protocolo seguro HTTPS y Nexus no puede ser una excepción; más si cabe cuando lo utilizamos como repositorio privado de Docker que te obliga a que las imágenes sean servidas a través de protocolo seguro.

    Nota: Para seguir el tutorial es necesario que ya tengas generados certificados válidos en un dominio real, yo lo voy a hacer con los mios de Let’s Encrypt.

    3. Vamos al lío

    Lo primero que tenemos que hacer es acceder por SSH al servidor donde esté corriendo Nexus y localizar los siguientes paths:

    • $install-dir: se trata del directorio donde está instalado nexus, típicamente /opt/nexus pero depende de la instalación que se haya hecho.
    • $data-dir: se trata del directorio sonatype-work que suele estar como hermano del anterior.

    Hay que tener en cuenta que Nexus se levanta en un servidor Jetty y que lo que vamos a hacer es configurar este servidor para servir HTTPS.

    Entonces el primer paso es crear el fichero $install-dir/etc/ssl/keystore.jks que va a contener nuestro certificado. Si contamos con certificados de Let’s Encrypt tendremos que tener los ficheros: fullchain.pem y privkey.pem los cuales vamos a tener que convertir con la herramienta openssl al formato de keystore necesario:

    $> openssl pkcs12 -export -out keystore.pkcs12 -in fullchain.pem -inkey privkey.pem

    Nota: cuando nos lo pida establemos una contraseña que tendremos que recordar para más adelante.

    Ahora importamos el certificado creando el fichero keystore.jks

    $> keytool -importkeystore -srckeystore keystore.pkcs12 -srcstoretype PKCS12 -destkeystore keystore.jks

    Nota: establecemos la misma contraseña que en la ejecución anterior.

    Con esto ya tenemos creado nuestro fichero $install-dir/etc/ssl/keystore.jks que va a ser el que lea Nexus en su arranque.

    Ahora vamos a editar el fichero $data-dir/etc/nexus.properties donde vamos a añadir la línea “application-port-ssl=8443” y vamos a descomentar la línea nexus-args para añadir un nuevo valor “${jetty.etc}/jetty-https.xml” de modo que la línea quedaría de esta forma:

    nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-http.xml,${jetty.etc}/jetty-https.xml,${jetty.etc}/jetty-requestlog.xml,${jetty.etc}/jetty-http-redirect-to-https.xml

    Guardamos este fichero y editamos el fichero “$install-dir/etc/jetty/jetty-https.xml” donde modificamos los tres puntos en los que se establece la palabra “password” por la contraseña que hayamos puesto anteriormente.

    Ahora solo tenemos que reniciar Nexus con el comando:

    $> sudo systemctl restart nexus

    Para depurar cualquier tipo de error en el arranque lo más útil es mirar el log:

    $> tail -1000f $install-dir/log/nexus.log

    Si todo es correcto podrás acceder a Nexus a través de la URL: https://tudominio.org:8443 y ten en cuenta que cualquier referencia al repositorio ahora solo será válida con esta URL, por lo que tendrás que actualizarla donde se esté utilizando.

    4. Conclusiones

    Como ves no es complicado tener nuestro repositorio para todo sirviendo el contenido a través de protocolo seguro y dándonos un plus de seguridad en nuestras operaciones en la nube.

    Cualquier duda o sugerencia en la zona de comentarios.

    Saludos.

    La entrada Nexus en HTTPS con Let’s Encrypt se publicó primero en Adictos al trabajo.

    Gestión de versiones de Python con Pipenv

    $
    0
    0

    En este tutorial veremos cómo gestionar tu entorno local para utilizar múltiples versiones de Python.

    Índice de contenidos

    1. Introducción
    2. Gestión de versiones
      2.1 No tocar la instalación de Python del sistema operativo
      2.2 Instalar la última versión de Python con HomeBrew
      2.3 Hacer uso de los entornos virtuales de Python
      2.3.1 Virtualenv
      2.3.2 Pipenv
    3. Conclusión
    4. Referencias

    1. Introducción

    Gestionar diferentes proyectos en local puede ocasionar que descuidemos nuestro entorno local de forma que no sepamos qué versión tenemos de una determinada herramienta.

    Esto es algo que a mi me ha pasado con Python, y quería compartir una solución para gestionar versiones en varios proyectos.

    2. Gestión de versiones

    El proceso que he seguido para dejar un entorno local limpio en el que pueda instalar diferentes versiones de Python por proyecto pasa por:

    • No tocar la instalación de Python del sistema operativo.
    • Instalar la última versión de Python con HomeBrew.
    • Hacer uso de los entornos virtuales de Python.

    A continuación explicaremos cada uno de estos puntos en detalle 😀

    2.1 No tocar la instalación de Python del sistema operativo

    En MacOS, Python viene instalado por defecto. Este binario está localizado en /usr/bin.

    Yo prefiero no utilizarlo ya que:

    • Es el binario que viene instalado con el sistema, por lo que cambiar las versiones que pueda gestionar quizá puede tener efectos no deseados.
    • En general me gusta tener entornos que son replicables, y esto incluye nuestro entorno local. Al instalar Python por ti mismo, puedes hacerlo en un script pudiendo replicar tu entorno local de forma sencilla, dándote control también de la versión de Python que quieres usar.

    2.2 Instalar la última versión de Python con HomeBrew

    Por los motivos dichos anteriormente, procederemos a instalar Python, para lo cual utilizaremos el gestor de paquetes de HomeBrew. Mi compañero David ha escrito un tutorial sobre él que os dejo aquí en el que explica su uso e instalación.

    Instalación

    Para instalar Python con Homebrew ejecutamos:

    $ brew install python

    Esto nos instalará la última versión de Python disponible en el gestor de paquetes.
    Esta instalación nos habrá generado el enlace simbólico al binario python3 en /usr/local/bin haciendo referencia al Python instalado por HomeBrew.

    Como vemos, HomeBrew instala Python de forma que no entra en conflicto con el Python del sistema. A parte del propio Python, también nos instala ciertas utilidades como el gestor de paquetes de Python: PIP.

    2.3 Hacer uso de los entornos virtuales de Python

    Para usar los entornos virtuales de Python, tenemos varias alternativas, de las cuales veremos dos en este tutorial.

    2.3.1 Virtualenv

    Todas las alternativas están basadas en virtualenv, una herramienta para crear entornos aislados de Python. Funciona creando un directorio en el cual añade todos los ejecutables necesarios para usar los paquetes que puede necesitar un proyecto de Python.

    Gracias a esta herramienta, podremos crear un entorno virtual para cada uno de los proyectos que tengamos de forma que las dependencias entre proyectos quedan aisladas en cada uno.

    Para instalar virtualenv en nuestro entorno, basta con usar nuestro recién instalado pip y ejecutar:

    $ pip3 install virtualenv

    Comprobamos que está instalado ejecutando:

    $ virtualenv --version

    Para usarlo, basta con ejecutar:

    $ cd project $ virtualenv venv-2.7 -p python2.7

    Esto nos creará un directorio venv-2.7 en el que estarán todos los ejecutables necesarios. En este caso estamos instalando la versión de Python 2.7

    Por último, para usar nuestro entorno virtual ejecutamos:

    $ source venv-2.7/bin/activate

    El nombre del entorno virtual aparecerá ahora en la parte izquierda del prompt del sistema. En nuestro caso tendría esta pinta:

    $ (venv-2.7) prompt

    Podemos ahora por ejemplo, instalar Ansible con pip ejecutando:

    $ pip install ansible

    Ya que ahora tanto el comando python como pip hacen referencia a las versiones del entorno virtual. Esto podemos comprobarlo con el comando which pip en el que vemos que la ruta de ambos binarios no es la del sistema por defecto, sino la del directorio que acabamos de crear.

    Cuando quieres salir del entorno virtual, ejecutamos:

    $ deactivate

    Si quieres eliminar este entorno virtual, basta con eliminar el directorio que contiene el entorno virtual.

    Sin embargo este proceso tiene varios inconvenientes:

    • Crear un entorno virtual es tedioso cuando llevas muchos.
    • Tienes que recordar dónde has creado el entorno virtual para un proyecto específico. En principio puedes optar por dejarlo en el raíz del proyecto de git que estés usando, ignorando el directorio en tu .gitignore pero esto hace que si eliminas repositorio tengas que volver a crear el entorno virtual, reinstalando todas las dependencias del mismo.

    Para solucionar estos problemas usaremos pipenv, un paquete que te facilita el uso de entornos virtuales de Python.

    2.3.2 Pipenv

    Pipenv es una herramienta que nos permitirá facilitar el uso de los entornos virtuales. Vamos a ver cómo sería el flujo anterior y qué ventajas aporta.

    La instalación es tan sencilla como la anterior, ejecutando:

    $ pip3 install pipenv

    Si después de la instalación comprobamos con pip3 list veremos que hemos instalado los paquetes pipenv, virtualenv, virtualenv-clone y certifi, con lo que confirmamos que lo que hace es basarse en virtualenv pero haciéndonos la vida un poco más sencilla.

    Si queremos, como en el caso anterior, instalar Ansible con Python, accedemos al repositorio donde quieres tener tu entorno virtual y ejecutamos el comando:

    $ cd project $ pipenv --python 2.7 install ansible

    Este comando ejecutará:

    • Creación de un virtualenv: Al final necesitamos crear este entorno virtual para usar la versión de Python deseada (por defecto la última), pero este pasará a estar gestionado por el propio pipenv, y creado en el directorio ~/.local.
    • Creación de un Pipfile: Este Pipfile es el fichero encargado de gestionar todas las dependencias de este repositorio, por lo que no necesitas generar después este fichero con pip freeze > requirements.txt ya que por instalar utilizando pipenv él se encarga de actualizarlo.
    • Instalación del paquete: en este caso Ansible.
    • Actualización de pipfile.

    Y eso es todo.

    Si ahora quisiéramos usar nuestra versión de Ansible instalada, necesitaríamos activar nuestro entorno virtual. Para esto ejecutamos el comando pipenv shell el cual nos activará el entorno virtual.

    Para salir del entorno virtual basta con salir de esa shell con el comando exit.

    Si quieres eliminar el entorno virtual, basta con ejecutar pipenv –rm en el directorio del proyecto. Este comando no eliminará tu Pipfile por lo que todavía tendrás el listado de dependencias necesarias para tu proyecto.

    Las ventajas para usar pipenv son:

    • Te desentiendes de la gestión de entornos virtuales, y de la localización del mismo.
    • Te genera automaticamente un fichero para gestionar las dependencias de Python por proyecto.
    • Todo se hace con el mismo comando, el cual tiene un gran menú de ayuda pipenv –help. Ya no tienes que recordar comandos distintos para crear el entorno (virtualenv), habilitarlo (source), gestionar dependencias (pip) y salir del entorno (deactivate).

    Conclusión

    Al final tu entorno local queda más limpio y sin arrastrar configuración de versiones de python y paquetes de otros proyectos siguiendo este alcance en el que para resumir:

    • No utilizamos Python que nos provee el sistema por defecto.
    • Instalamos Python usando el gestor de paquetes de HomeBrew.
    • En este Python para uso local lo único que instalamos es pipenv, para poder gestionar los diferentes proyectos.
    • Toda la configuración relativa a un proyecto determinado irá dentro de su respectivo entorno virtual, que a su vez nos dejará un registro de qué dependencias y con qué versiones están instalados en cada uno, de forma que facilita la réplica del entorno si el proyecto es colaborativo.

    4. Referencias

    La entrada Gestión de versiones de Python con Pipenv se publicó primero en Adictos al trabajo.

    Viewing all 991 articles
    Browse latest View live