Jenkins para Android con Docker

Hola a todos!

Trabajo como desarrollador de Android, y no hace mucho tiempo nos topamos con algunas tareas de rutina en nuestro proyecto que nos gustaría automatizar. Por ejemplo, tenemos 5 sabores diferentes, para cada uno de los cuales necesitamos cargar nuestra construcción en la tela, a veces para diferentes carros varias veces al día. Sí, esta tarea también se puede hacer usando la tarea de gradle, pero me gustaría no comenzar este proceso en la máquina del desarrollador, sino hacerlo de manera centralizada. O, por ejemplo, cargue automáticamente la compilación en Google Play a beta. Bueno, solo quería elegir el sistema CI. Lo que surgió de esto y cómo lo configuramos, por qué está Docker, más adelante en el artículo.



En mi opinión, toda la tarea se dividió en aproximadamente dos etapas:

  1. Instale y configure Jenkins con el SDK de Android
  2. Configurar tareas que ya están dentro de Jenkins

En este artículo, quiero tocar el primer punto, y si esto es de interés para alguien, en el próximo artículo describiré el proceso de configuración de tareas de ensamblaje en Jenkins.

Entonces, el primer punto es la instalación y configuración del sistema Jenkins


Habré ya tiene un artículo maravilloso sobre este tema, pero ya lleva un par de años y algunas cosas están un poco desactualizadas (por ejemplo, sdkmanager), aunque me ayudó mucho a descubrir qué y cómo hacer en las etapas iniciales.

Si observa la documentación oficial para instalar Jenkins, veremos tres formas diferentes de hacerlo: inicie una imagen acoplada lista para usar, descargue y ejecute un archivo war, y también simplemente instale jenkins en el sistema a la antigua usanza (por ejemplo, apt-get install jenkins usando ubuntu como ejemplo). La primera opción es la más correcta, ya que no lleva configuraciones innecesarias y dependencias a nuestro sistema host, y en cualquier momento, incluso si algo sale mal, es fácil y simple eliminar todo y comenzar de nuevo. Pero la imagen estándar de Docker para Jenkins contiene algunos de los datos que no necesitamos (por ejemplo, el complemento blueocean) y no contiene lo que definitivamente necesitaremos (por ejemplo, Android SDK). Se decidió crear nuestra propia imagen de acoplador que en su interior descargará y ejecutará el archivo war, descargará e instalará el sdk de Android, y configurará todas las demás configuraciones que necesitemos. Para iniciarlo más tarde, necesitamos un sistema host con docker instalado. Sugiero aquí no reinventar la rueda y usar DigitalOcean.

Crear y configurar una máquina virtual


Para empezar, si alguien más no está registrado allí, sugiero que se registre (aquí, al momento de escribir el artículo, había un enlace de referencia, pero después de leer las reglas, lo descarté). Después del registro, puede buscar en Google uno u otro código promocional en Internet y obtener unos 10 dólares para comenzar.

Después tenemos que conseguir una nueva gota. Seleccione el elemento Gotitas y luego Crear gotita .

imagen

El sistema host es Ubuntu 18.04. Puede elegir una imagen con Docker ya instalado y configurado, pero haremos todo por nuestra cuenta. Dado que el ensamblaje de las compilaciones de Android aún requiere muchos recursos, debemos elegir una configuración de al menos 20 dólares para que las compilaciones se recopilen normalmente y con relativa rapidez.

imagen

Elegiremos una ubicación más cercana (por ejemplo, en Alemania). Luego hay dos opciones sobre cómo nos conectaremos a nuestro servidor virtual. Podemos agregar una clave ssh o prescindir de ella. Si en este lugar no indicamos qué clave usar, la contraseña del usuario raíz se enviará a nuestro correo.

imagen

Aquí podemos cambiar el nombre del servidor y completar la creación haciendo clic en el botón Crear .

imagen

Ahora vamos al droplet creado y copiamos la dirección IP para nosotros, para una mayor conexión y configuración.

imagen

Necesitamos un cliente ssh. Si trabaja desde debajo de una amapola, entonces puede usar el terminal estándar, si desde debajo de Windows podemos usar masilla para trabajar o usar el subsistema Linux (solo para Windows 10). Yo personalmente uso la última opción, y me queda completamente bien.

Conéctese a nuestro servidor usando el siguiente comando

 ssh root@YOUR_IP_ADDRESS 

La consola le ofrecerá guardar la clave, estamos de acuerdo con esto. Después de conectarnos, crearemos un nuevo usuario para nosotros, lo agregaremos a los superusuarios (y le daremos la oportunidad de usar sudo sin contraseña), le copiaremos la clave de acceso a través de ssh y le diremos que es el propietario de estos archivos (de lo contrario, no funcionará). El nombre de usuario se cambia a cualquier conveniente para usted.

 useradd -m -s /bin/bash username \ && echo 'username ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ && mkdir /home/username/.ssh \ && cp /root/.ssh/authorized_keys /home/username/.ssh/authorized_keys \ && chown username:username -R /home/username/.ssh 

Desconectarse de la raíz con el comando

 exit 

Y ya nos volveremos a conectar con la ayuda del nuevo usuario creado.

 ssh username@YOUR_IP_ADDRESS 

Después de actualizar el sistema y reiniciar nuestro servidor (si durante el proceso de actualización el sistema le preguntará algo, es suficiente elegir siempre los valores predeterminados en este caso).

 sudo apt update && sudo apt full-upgrade -y && sudo apt autoremove -y && sudo reboot 

La configuración básica está completa. Desde el punto de vista del sistema de combate, no es muy seguro, pero dentro del marco de este artículo es completamente adecuado.

Instalar Docker.


Para instalar Docker en nuestro sistema, utilizaremos la documentación oficial . Dado que tenemos un sistema recién instalado, omitiremos este punto, y si tiene un sistema en el que algo se ha estado ejecutando durante mucho tiempo, por recomendación de los chicos de Docker, elimine las posibles versiones anteriores

 sudo apt-get remove docker docker-engine docker.io containerd runc 

No olvide conectarse primero a través de ssh a nuestro servidor. La instalación de Docker en sí se describe con gran detalle en la documentación, daré comandos generales para que sea más fácil para usted. Lo que hacen se puede leer allí. Primero agregue el repositorio.

 sudo apt update \ && sudo apt install -y apt-transport-https ca-certificates \ curl gnupg-agent software-properties-common \ && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - \ && sudo apt-key fingerprint 0EBFCD88 \ && sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 

Y luego instale Docker:

 sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io 

Para que en el futuro podamos llamar a los comandos de Docker sin el prefijo sudo, ejecute el siguiente comando (que también se describe cuidadosamente en las instrucciones ).

 sudo usermod -aG docker username 

Después de eso, debe volver a iniciar sesión (usando el comando de salida y reconectando al servidor) para que esto funcione.

Docker está instalado, lo que podemos verificar con el comando

 docker run hello-world 

Ella descarga la imagen de prueba, la ejecuta en el contenedor. El contenedor, después de comenzar, imprime un mensaje informativo y sale.

¡Felicitaciones, hemos terminado la etapa de preparación del servidor para el trabajo!

Crear tu imagen de Docker


Crearemos la imagen de Docker escribiendo nuestro propio Dockerfile. Ejemplos de cómo hacer esto correctamente en Internet, un vagón y un carrito pequeño, mostraré mi versión terminada e intentaré comentarla lo más posible. También hay un manual de instrucciones de Docker con ejemplos sobre la ortografía correcta y canónica de dockerfile.

Crea y abre tu Dockerfile para editar

 touch Dockerfile && nano Dockerfile 

En él, por ejemplo, colocaremos el contenido de mi Dockerfile

Todo mi dockerfile con comentarios
 #    . FROM ubuntu:18.04 #      LABEL author="osipovaleks" LABEL maintainer="osipov.aleks.kr@gmail.com" LABEL version="1.0" LABEL description="Docker image for Jenkins with Android SDK" #  ,  Jenkins    ENV TZ=Europe/Kiev RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # i386    ia32-libs RUN dpkg --add-architecture i386 #      RUN apt-get update && apt-get install -y git \ wget \ unzip \ sudo \ tzdata \ locales\ openjdk-8-jdk \ libncurses5:i386 \ libstdc++6:i386 \ zlib1g:i386 #  ,       RUN apt-get clean && rm -rf /var/lib/apt/lists /var/cache/apt #  RUN locale-gen en_US.UTF-8 ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 #   Android Sdk     ARG android_home_dir=/var/lib/android-sdk/ ARG sdk_tools_zip_file=sdk-tools-linux-4333796.zip RUN mkdir $android_home_dir RUN wget https://dl.google.com/android/repository/$sdk_tools_zip_file -P $android_home_dir -nv RUN unzip $android_home_dir$sdk_tools_zip_file -d $android_home_dir RUN rm $android_home_dir$sdk_tools_zip_file && chmod 777 -R $android_home_dir # environment    ENV ANDROID_HOME=$android_home_dir ENV PATH="${PATH}:$android_home_dir/tools/bin:$android_home_dir/platform-tools" ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/ #   Android SDK RUN yes | sdkmanager --licenses #    Jenkins ENV JENKINS_HOME=/var/lib/jenkins RUN mkdir $JENKINS_HOME && chmod 777 $JENKINS_HOME #     jenkins,   ,         RUN useradd -m jenkins && echo 'jenkins ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers USER jenkins WORKDIR /home/jenkins #   war     Jenkins RUN wget http://mirrors.jenkins.io/war-stable/latest/jenkins.war -nv CMD java -jar jenkins.war #      EXPOSE 8080/tcp 


Algunas aclaraciones:

  • Al principio, había un deseo de usar un alpine más ligero en lugar de ubuntu, pero no tiene soporte para ia32-libs , que es necesario para construir proyectos usando el SDK de Android.
  • Instalamos openjdk-8-jdk, no el openjdk-8-jdk-headless más liviano, porque algunas funciones de Jenkins necesitan un sistema completo (por ejemplo, mostrar resultados de pruebas unitarias).
  • Es necesario instalar configuraciones regionales, debido al hecho de que en algunos proyectos, sin ellos, el ensamblaje de gradle se bloquea sin errores y registros claros, y pasé varios días para llegar al fondo de esta razón (en ubuntu regular que no está en la ventana acoplable, todas las configuraciones regionales se llenan por defecto) .
  • Necesitamos aceptar de inmediato todas las licencias para el SDK de Android, de modo que durante el proceso de compilación Jenkins pueda instalar de forma independiente los componentes que necesita (por ejemplo, los SDK que necesita para diferentes versiones de la API). Si es necesario, más adelante dentro del contenedor docker será posible administrar el SDK usando sdkmanager, por ejemplo sdkmanager --list permite ver todos los componentes disponibles y todos instalados, y sdkmanager --install "platforms;android-26" instalará el SDK para la versión 26 de la API.
  • En general, era posible no iniciar el usuario jenkins y permanecer con el usuario raíz, pero de alguna manera no es del todo correcto, tampoco podría otorgarle derechos de superusuario, pero esto se hizo en términos de conveniencia, si es necesario instalar algo en la etapa de configuración y depuración.
  • El tamaño básico de la imagen resultó ser bastante grande (casi 800 mb), pero en general llegué a la conclusión de que para mí esto no es muy crítico, y es más fácil para mí descargarlo de esta forma que pasar tiempo buscando y eliminando paquetes que no necesito.

Después de escribir Dockerfile, debemos convertirlo en una imagen preparada para Docker, en función de qué contenedores se crearán. Esto lo hace simplemente un equipo

 docker build -t jenkins-image 

donde el parámetro -t jenkins-image es responsable del nombre de su imagen, y el punto al final del comando indica que debe buscar el Dockerfile para ensamblar dentro de este directorio. El proceso de compilación en sí toma algo de tiempo, y después de la compilación, la consola debería tener un mensaje similar.
Construido con éxito 9fd8f5545c27
Etiquetado correctamente jenkins-image: último
Lo que nos dice que nuestra imagen se ha ensamblado con éxito, y podemos proceder al siguiente paso, a saber, el lanzamiento de nuestro contenedor

Docker Hub e imágenes listas para usar


Sí, por supuesto, podemos usar nuestra imagen preparada para iniciar el contenedor, pero si necesitamos hacerlo en más de varios dispositivos, crear un Dockerfile cada vez y construir una imagen preparada a partir de él no será muy conveniente. Y si también actualizamos el contenido de nuestro Dockerfile, no será conveniente implementar los cambios en todos los nodos. Para estos fines, existe un repositorio público de imágenes de Docker Hub . Le permite no recopilar una imagen cada vez, en cada nodo, sino simplemente descargarla desde el repositorio público y usarla por igual en todas las máquinas. Por ejemplo, la imagen que sirvió como ejemplo para este artículo está disponible en el repositorio llamado osipovaleks / docker-jenkins-android , y más adelante en el artículo trabajaremos con ella.

Este artículo no implica un estudio detallado del Docker Hub, no entenderemos cómo cargar nuestras imágenes allí (aunque esto no es muy difícil) y qué se puede hacer con ellas allí, no entenderemos que todavía puede haber sus propios repositorios públicos o privados, En esto, todo se puede resolver de forma independiente si es necesario.

Lanzamiento de contenedores


Hay dos formas de iniciar un contenedor.

  1. La primera forma, simplemente usando el docker run , le permite hacer esto fácil y rápidamente de la siguiente manera

     docker run --name jenkins -d -it -v jenkins-data:/var/lib/jenkins -v jenkins-home:/home/jenkins -p 8080:8080 --restart unless-stopped osipovaleks/docker-jenkins-android 

    donde el comando de run tiene los siguientes parámetros

    • --name jenkins - nombre del futuro contenedor
    • -d - iniciar contenedor en segundo plano
    • -it - banderas para trabajar con STDIN y tty
    • -v jenkins-data:/var/lib/jenkins y -v jenkins-home:/home/jenkins - crea (si no se creó) y asigna archivos de volumen especiales a las secciones internas del contenedor que nos permitirán guardar nuestro Jenkins configurado incluso después de la reconstrucción contenedor
    • -p 8080:8080 el puerto del host al puerto del contenedor para que tengamos acceso a la interfaz web (sí, este es el puerto que especificamos en el Dockerfile)
    • --restart unless-stopped : la opción determina la política de ejecución automática del contenedor después de reiniciar el host (en este caso, inicio automático si el contenedor no se apagó manualmente)
    • osipovaleks/docker-jenkins-android : imagen para la implementación.

    A la salida de la consola de Docker, deberíamos obtener la identificación del contenedor creado y también mostrar información sobre cómo se carga la imagen en el sistema (por supuesto, si aún no está cargada), algo como esto
    No se puede encontrar la imagen 'osipovaleks / docker-jenkins-android: latest' localmente
    último: extracción de osipovaleks / docker-jenkins-android
    6cf436f81810: extracción completa
    987088a85b96: extracción completa
    b4624b3efe06: extracción completa
    d42beb8ded59: Pull complete
    b3896048bb8c: extracción completa
    8eeace4c3d64: extracción completa
    d9b74624442c: extracción completa
    36bb3b7da419: Pull complete
    31361bd508cb: extracción completa
    cee49ae4c825: extracción completa
    868ddf54d4c1: extracción completa
    361bd7573dd0: Pull complete
    bb7b15e36ae8: Pull complete
    97f19daace79: Pull complete
    1f5eb3850f3e: extracción completa
    651e7bbedad2: extracción completa
    a52705a2ded7: extracción completa
    Resumen: sha256: 321453e2f2142e433817cc9559443387e9f680bb091d6369bbcbc1e0201be1c5
    Estado: Imagen más reciente descargada para osipovaleks / docker-jenkins-android: latest
    ef9e5512581da66d66103d9f6ea6ccd74e5bdb3776747441ce6a88a98a12b5a4
  2. La segunda forma de comenzar implica escribir un archivo de composición especial, donde el comando de ejecución simplemente se describe usando el lenguaje YAML y se inicia usando Docker Compose.

    Para hacer esto, necesitamos instalarlo:

     sudo apt update && sudo apt install -y docker-compose 

    A continuación, cree un directorio para el proyecto (esto es importante si le importa cómo se llamarán los volúmenes creados automáticamente para el contenedor) y vaya a él

     mkdir jenkinsProject && cd jenkinsProject 

    y dentro creamos el archivo de composición en sí y pasamos al modo de edición

     touch docker-compose.yml && nano docker-compose.yml 

    y poner los siguientes contenidos en él

     version: '3' services: jenkins: container_name: jenkins image: osipovaleks/docker-jenkins-android ports: - "8080:8080" restart: unless-stopped volumes: - "jenkins-data:/var/lib/jenkins" - "jenkins-home:/home/jenkins" volumes: jenkins-data: jenkins-home: 

    En él, tal vez, solo la primera línea plantea preguntas ( version: '3' ) que indica la versión de las capacidades del archivo de composición, así como una sección con el bloque de volumes que enumera las utilizadas en este contenedor

    Ejecute su contenedor con el comando:

     docker-compose up -d 

    donde el indicador -d también indica que el contenedor se creará y se iniciará en segundo plano. Como resultado, Docker debería mostrar algo como lo siguiente:
    Creación del volumen "jenkinsproject_jenkins-data" con el controlador predeterminado
    Creación del volumen "jenkinsproject_jenkins-home" con el controlador predeterminado
    Tirando de jenkins (osipovaleks / docker-jenkins-android: latest) ...
    último: extracción de osipovaleks / docker-jenkins-android
    6cf436f81810: extracción completa
    987088a85b96: extracción completa
    b4624b3efe06: extracción completa
    d42beb8ded59: Pull complete
    b3896048bb8c: extracción completa
    8eeace4c3d64: extracción completa
    d9b74624442c: extracción completa
    36bb3b7da419: Pull complete
    31361bd508cb: extracción completa
    cee49ae4c825: extracción completa
    868ddf54d4c1: extracción completa
    361bd7573dd0: Pull complete
    bb7b15e36ae8: Pull complete
    97f19daace79: Pull complete
    1f5eb3850f3e: extracción completa
    651e7bbedad2: extracción completa
    a52705a2ded7: extracción completa
    Resumen: sha256: 321453e2f2142e433817cc9559443387e9f680bb091d6369bbcbc1e0201be1c5
    Estado: Imagen más reciente descargada para osipovaleks / docker-jenkins-android: latest
    Creando Jenkins ...
    Creando jenkins ... hecho
    ¿Recuerdas que dije que el nombre de los volúmenes creados dependerá del nombre del proyecto? Ejecute el comando:

     docker volume ls 

    y obtenemos tal salida
    NOMBRE DE VOLUMEN DEL CONDUCTOR
    jenkinsproject_jenkins-data local
    jenkinsproject_jenkins-home local
    donde veremos que a pesar del hecho de que el nombre del volumen fue elegido por jenkins-home , en realidad, un prefijo del nombre del proyecto se jenkinsproject _jenkins-home a él y el nombre del volumen resultó ser jenkinsproject _jenkins-home


¿Qué opción de inicio usar? Aquí puede elegir por sí mismo, se cree que Docker Compose es más una herramienta para lanzar varios contenedores a la vez, que están vinculados entre sí, y si necesita iniciar solo un contenedor, puede usar el docker run .

Ahora, después de estos pasos secundarios para iniciar y configurar el servidor, así como también para iniciar el contenedor con Jenkins, podemos proceder a su configuración inicial

Configuración inicial de Jenkins


Tome la dirección IP de nuestro servidor, agregue el puerto 8080 indicado por nosotros y siga este enlace en el navegador.

http://YOUR_IP_ADDRESS:8080/

Si antes de eso todo se configuró y comenzó correctamente, entonces aquí veremos la siguiente imagen



Para la primera configuración, necesitamos ingresar la contraseña que el sistema generó durante la instalación. Para hacer esto, solo necesitamos mirar el contenido del archivo /var/lib/jenkins/secrets/initialAdminPassword . Pero este archivo está dentro de nuestro contenedor en ejecución, y para leerlo, necesitamos conectarnos al contenedor usando el siguiente comando:

 docker exec -it jenkins /bin/bash 

donde la opción -it es similar a ejecutar docker run , jenkins es el nombre de nuestro contenedor y /bin/bash ejecutará /bin/bash para nosotros en el contenedor y le dará acceso. Después de eso, podemos ver la contraseña inicial para Jenkins:

 cat /var/lib/jenkins/secrets/initialAdminPassword 

lo siguiente aparece en la consola
91092b18d6ca4492a2759b1903241d2a
Esta es la contraseña.

El usuario ALexhha sugirió una opción más simple para leer esta contraseña, sin conectarse al contenedor en sí. El hecho es que al momento de lanzar Jenkins, esta contraseña se muestra en los registros. Resulta que todo lo que necesitamos es leer los registros del contenedor. En nuestro caso, esto se hace con el siguiente comando:

 docker logs jenkins 


donde jenkins nombre de nuestro contenedor, y en los registros puede ver lo siguiente:

***************************************************** ***********
***************************************************** ***********
******************************************** .... ***********

Se requiere la configuración inicial de Jenkins. Se ha creado un usuario administrador y se ha generado una contraseña.
Utilice la siguiente contraseña para proceder a la instalación:

91092b18d6ca4492a2759b1903241d2a

Esto también se puede encontrar en: / var / lib / jenkins / secrets / initialAdminPassword

******************************************** .... ***********
******************************************** .... ***********

******************************************** .... ***********

Esta opción es un poco más simple y rápida.

Cópielo, péguelo en el campo Contraseña de administrador en la interfaz web y haga clic en Continuar . En la siguiente pantalla, seleccione Instalar complementos sugeridos e instale un conjunto de complementos predeterminados.





Después de instalar los complementos, cree un usuario para nosotros y haga clic en Guardar y finalizar



Estamos de acuerdo con la sección Configuración de instancia, donde se nos pide que completemos la URL en la que trabajará Jenkins (en nuestro caso, deje todo como está)



Y en la siguiente pantalla, haga clic en el preciado Comenzar a usar Jenkins



¡Así que instalamos y lanzamos Jenkins!



Ya es bastante posible trabajar con él, pero para recopilar nuestras compilaciones de Android, deberá configurar algunos puntos más. La localización de Jenkins está relacionada con el idioma seleccionado de su navegador y, por supuesto, la traducción al ruso no está completamente terminada, y obtenemos una mezcla infernal de ruso e inglés. Si tuvo éxito exactamente de la misma manera, y eso lo enfurece, entonces puede usar el complemento especial y configurar el idioma predeterminado de la interfaz. Bueno, o cambie su navegador a la interfaz en inglés.

Vaya a la configuración de Jenkins y seleccione Configuración del sistema.



Verifique las variables de entorno e ingrese el nombre ANDROID_HOME en el campo y especifique / var / lib / android-sdk / en el campo (especificamos estos datos en el Dockerfile como el directorio de inicio para el SDK de Android).



Haga clic en el botón Guardar , salga de esta sección de configuración y vaya a la sección llamada Configuración de herramientas globales .



Configure la partición JDK (donde la variable JAVA_HOME también fue poblada por nosotros en el Dockerfile, y podemos usar su valor / usr / lib / jvm / java-8-openjdk-amd64 / here ).



También aquí todavía tenemos que completar la sección de Gradle . Seleccionamos e instalamos la versión de Gradle que se utiliza en los proyectos que creará con este sistema CI.Puedes tener varias versiones. Tampoco puede iniciar la variable Gradle si tiene gradlew en el repositorio, por ejemplo, y puede construirlo con él.



Con esto podemos terminar nuestra primera etapa. El sistema Jenkins está en pleno funcionamiento y podemos pasar a personalizar las tareas de compilación ellos mismos. Tenga en cuenta que el sistema se ajustó a nuestras necesidades y aquí puede no proporcionarle lo que necesita; por ejemplo, no hay emuladores de Android para pruebas y NDK.

Si este artículo le interesa a alguien, entonces continuaré en la segunda parte con un ejemplo de uno o dos problemas, describiré la integración de Jenkins y Bitbucket (es él, no Github, porque es más fácil con repositorios privados gratuitos y artículos en Internet sobre es más pequeño, pero quizás más divertido), te diré cómo hacer amigos con la clave ssh de nuestro contenedor con el repositorio, sobre notificaciones por correo electrónico, así como varios otros chips. En general, sobre todo lo que hemos configurado.

Te pido que no patees mucho, este es mi primer artículo sobre Habr. Bueno para todos!

Source: https://habr.com/ru/post/443606/


All Articles