One place for hosting & domains

      Cómo instalar y usar Linkerd con Kubernetes


      El autor seleccionó el Tech Education Fund para que recibiese una donación como parte del programa Write for DOnations.

      Introducción

      Una malla de servicios es una capa de infraestructura dedicada que ayuda a los administradores a gestionar la comunicación de servicio a servicio. Al ofrecer muchas herramientas potentes, estas mallas de servicios pueden hacer que su sistema sea más seguro, confiable y, también, más visible.

      Una malla de servicios como Linkerd, por ejemplo, puede cifrar automáticamente conexiones; gestionar reintentos de solicitudes y tiempos de espera; proporcionar información de telemetría, como tasas de éxito y latencias; y más.

      En este tutorial, instalará la malla de servicios Linkerd en su clúster de Kubernetes, implementará una aplicación de ejemplo y, luego, explorará el panel de Linkerd. Después de familiarizarse con cierta información de este panel, configurará Linkerd para aplicar políticas de timeout y retry para un pod de Kubernetes en particular.

      De forma alternativa, considere la opción de instalación de Linkerd/Kubernetes en un solo clic de DigitalOcean.

      Requisitos previos

      • Un clúster de Kubernetes 1.12+. En este tutorial, se usará un clúster de Kubernetes de DigitalOcean con tres nodos para la configuración, pero puede crear un clúster usando otro método.

      • La herramienta de línea de comandos kubectl instalada en un servidor de desarrollo y configurada para conectarse a su clúster. Puede encontrar más información sobre la instalación de kubectl en su documentación oficial.

      Paso 1: Implementar la aplicación

      Para ver Linkerd en acción, debe tener una aplicación en ejecución en su clúster. En este paso, implementará una aplicación denominada emojivoto, que el equipo de Linkerd creó para este propósito.

      En este repositorio, puede ver el código de los cuatro servicios que componen la aplicación, así como el archivo de manifiesto que usará para implementar estos servicios en su clúster de Kubernetes.

      Primero, guarde este archivo de manifiesto de forma local:

      • curl https://run.linkerd.io/emojivoto.yml --output manifest.yaml

      Utiliza curl para obtener el archivo y, luego, pasa la opción --output para indicarle dónde desea guardar el archivo. En este caso, está creando un archivo denominado manifest.yaml.

      Para comprender mejor lo que hace este archivo, inspeccione su contenido con cat o ábralo con su editor favorito:

      Presione la barra espaciadora para desplazarse por las directivas. Verá que manifest.yaml crea un espacio de nombres de Kubernetes denominado emojivoto, donde se ejecutará todo lo relacionado con esta aplicación, y algunos Deployments y Services de Kubernetes.

      A continuación, aplique este manifiesto en su clúster de Kubernetes:

      • kubectl apply -f manifest.yaml

      Una vez más, está usando kubectl apply con el indicador -f para asignar un archivo que quiere aplicar.

      Este comando generará una lista de todos los recursos que se crearon:

      Output

      namespace/emojivoto created serviceaccount/emoji created serviceaccount/voting created serviceaccount/web created service/emoji-svc created service/voting-svc created service/web-svc created deployment.apps/emoji created deployment.apps/vote-bot created deployment.apps/voting created deployment.apps/web created

      Ahora, compruebe que los servicios se estén ejecutando:

      • kubectl -n emojivoto get pods

      Utiliza kubectl para enumerar todos los pods que tiene en ejecución en su clúster y, luego, pasa el indicador -n para indicar los espacios de nombres que quiere usar. Pasa el espacio de nombre emojivoto porque allí es donde está ejecutando todos estos servicios.

      Estará listo cuando vea todos los pods con estado Running (En ejecución):

      Output

      NAME READY STATUS RESTARTS AGE emoji-566954596f-cw75b 1/1 Running 0 24s vote-bot-85c5f5699f-7dw5c 1/1 Running 0 24s voting-756995b6fc-czf8z 1/1 Running 0 24s web-7f7b69d467-2546n 1/1 Running 0 23s

      Por último, para ver la aplicación en ejecución en su navegador, usará la función integrada kubectl para reenviar solicitudes locales a su clúster remoto:

      • kubectl -n emojivoto port-forward svc/web-svc 8080:80

      Nota: Si no está ejecutando esto desde su máquina local, deberá añadir el indicador --address 0.0.0.0 para escuchar en todas las direcciones, no solo en localhost.

      Aquí, nuevamente, está usando kubectl en los espacios de nombres emojivoto, pero, ahora, está invocando el subcomando port-forward y le está indicando que reenvíe todas las solicitudes locales del puerto 8080 al servicio de Kubernetes web-svc, en el puerto 80. Esta es, simplemente, una forma conveniente de acceder a su aplicación sin necesidad de tener un equilibrador de carga adecuado establecido.

      Ahora, diríjase a http://localhost:8080 para ver la aplicación emojivoto.

      Aplicación emojivoto de ejemplo

      Presione CTRL + C en su terminal. Ahora que tiene una aplicación en ejecución en su clúster, está listo para instalar Linkerd y ver cómo funciona.

      Paso 2: Instalar Linkerd

      Ahora que tiene una aplicación en ejecución, instalaremos Linkerd. Para instalarlo en su clúster de Kubernetes, primero, necesita la CLI de Linkerd. Utilizará esta interfaz de línea de comandos para interactuar con Linkerd desde su máquina local. Luego, puede instalar Linkerd en su clúster.

      Primero, instalaremos la CLI con la secuencia de comandos proporcionada por el equipo Linkerd:

      • curl https://run.linkerd.io/install | sh

      Aquí, utiliza curl para descargar la secuencia de comandos de instalación y, luego, canaliza el resultado a sh, que la ejecuta de forma automática. De forma alternativa, puede descargar la CLI directamente de la página de lanzamientos de Linkerd.

      Si utiliza la secuencia de comandos, esta instalará Linkerd en ~/.linkerd2/bin. Ahora, confirme que la CLI esté funcionando correctamente:

      • ~/.linkerd2/bin/linkerd version

      El comando generará algo similar a esto:

      Output

      Client version: stable-2.7.1 Server version: unavailable

      Luego, para facilitar la ejecución de CLI, añada este directorio a su $PATH:

      • export PATH=$PATH:$HOME/.linkerd2/bin

      Ahora, puede ejecutar los comandos, como el anterior, de forma más directa:

      Por último, instalaremos Linkerd en su clúster de Kubernetes. El comando linkerd install se utiliza para generar todos los manifiestos yaml necesarios para ejecutar Linkerd, pero no los aplica a su clúster. Ejecute este comando para inspeccionar su resultado:

      Verá un resultado largo que enumera todos los manifiestos yaml de los recursos que Linkerd necesita para ejecutarse. Para aplicar estos manifiestos a su clúster, ejecute lo siguiente:

      • linkerd install | kubectl apply -f -

      La ejecución de linkerd install generará todos los manifiestos que vio anteriormente. Luego, | canaliza el resultado directamente a kubectl apply para que los aplique.

      Cuando ejecute este comando, kubectl apply generará una lista de todos los recursos que se crearon.

      Para confirmar que todo se esté ejecutando en su clúster, ejecute linkerd check:

      Con esto, se ejecutarán varias verificaciones de su clúster para confirmar que todos los componentes necesarios se estén ejecutando:

      Output

      kubernetes-api -------------- √ can initialize the client √ can query the Kubernetes API [...] control-plane-version --------------------- √ control plane is up-to-date √ control plane and cli versions match Status check results are √

      Por último, ejecute este comando para abrir el panel de Linkerd integrado en su navegador (recuerde que deberá proporcionar el indicador --address 0.0.0.0 si no está ejecutando el comando desde su máquina local):

      Panel de Linkerd

      Puede obtener la mayoría de la información que ve en el panel usando la CLI de Linkerd. Por ejemplo, ejecute este comando para ver implementaciones con estadísticas de alto nivel:

      • linkerd stat deployments -n linkerd

      Aquí, está indicando que quiere las estadísticas de las implementaciones que se están ejecutando en el espacio de nombres linkerd. Estos son los componentes propios de Linkerd y, curiosamente, se puede usar el mismo Linkerd para monitorearlos. Puede ver estadísticas como las solicitudes por segundo (RPS), la tasa de éxito, la latencia y más. También puede ver la columna Meshed, que indica la cantidad de pods que insertó Linkerd:

      Output

      NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN linkerd-controller 1/1 100.00% 0.4rps 1ms 87ms 98ms 5 linkerd-destination 1/1 100.00% 0.3rps 1ms 2ms 2ms 13 linkerd-grafana 1/1 100.00% 0.3rps 2ms 3ms 3ms 2 linkerd-identity 1/1 100.00% 0.3rps 1ms 2ms 2ms 10 linkerd-prometheus 1/1 100.00% 0.7rps 35ms 155ms 191ms 9 linkerd-proxy-injector 1/1 100.00% 0.3rps 2ms 3ms 3ms 2 linkerd-sp-validator 1/1 100.00% 0.3rps 1ms 5ms 5ms 2 linkerd-tap 1/1 100.00% 0.3rps 1ms 4ms 4ms 6 linkerd-web 1/1 100.00% 0.3rps 1ms 2ms 2ms 2

      Ahora, pruebe este comando en su espacio de nombres emojivoto:

      • linkerd stat deployments -n emojivoto

      Si bien puede ver los cuatro servicios, ninguna de las estadísticas que vio está disponible para estas implementaciones, y puede observar que el valor de la columna “Meshed” es 0/1:

      Output

      NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN emoji 0/1 - - - - - - vote-bot 0/1 - - - - - - voting 0/1 - - - - - - web 0/1 - - - - - -

      Este resultado indica que todavía no insertó Linkerd en la aplicación. Ese será su siguiente paso.

      Paso 3: Insertar Linkerd en su aplicación

      Ahora que tiene Linkerd activo en su clúster, está listo para insertarlo en su aplicación emojivoto.

      Linkerd funciona ejecutando un contenedor sidecar en sus pods de Kubernetes. Es decir, insertará un contenedor proxy linkerd en cada pod que tenga en ejecución. Cada solicitud que sus pods envíen o reciban, luego, pasará por este proxy muy ligero que recopila métricas (como la tasa de éxito, las solicitudes por segundo y la latencia) y aplica políticas (como tiempos de espera y reintentos).

      Puede insertar el proxy de Linkerd de forma manual con este comando:

      • kubectl get deployments -n emojivoto -o yaml | linkerd inject - | kubectl apply -f -

      En este comando, primero, utiliza kubectl get para obtener todas las deployments de Kubernetes que tiene activas en el espacio de nombres emojivoto y, luego, especifica que quiere el resultado en formato yaml. Luego, envía ese resultado al comando linkerd inject. Este comando lee el archivo yaml con los manifiestos que tiene en ejecución y lo modifica para que incluya el proxy linkerd en cada deployment.

      Por último, recibe este manifiesto modificado y lo aplica a su clúster con kubectl apply.

      Después de ejecutar este comando, verá un mensaje que indica que los cuatro servicios para emojivoto (emoji, vote-bot, voting y web) se insertaron correctamente.

      Si consulta las stats de emojivoto, verá que, ahora, todas sus deployments están interconectadas, y, después de unos segundos, empezará a ver las mismas estadísticas que vio para el espacio de nombres linkerd:

      • linkerd stat deployments -n emojivoto

      Aquí, puede ver las estadísticas de los cuatro servicios que componen la aplicación emojivoto, con sus solicitudes por segundo, su tasa de éxito y latencia respectivas, sin necesidad de escribir ni cambiar ningún código de la aplicación.

      Output

      NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN emoji 1/1 100.00% 1.9rps 1ms 2ms 2ms 2 vote-bot 1/1 - - - - - - voting 1/1 85.96% 0.9rps 1ms 1ms 1ms 2 web 1/1 93.04% 1.9rps 8ms 27ms 29ms 2

      No se muestra ninguna estadística del servicio vote-bot porque es solo un bot que envía solicitudes a otros servicios y, por lo tanto, no recibe nada de tráfico, que, en sí, es información valiosa.

      Ahora, veamos cómo puede proporcionar a Linkerd más información sobre sus servicios para personalizar su comportamiento.

      Paso 4: Definir un perfil de servicio

      Ahora que insertó Linkerd en su aplicación, puede comenzar a obtener información valiosa sobre el comportamiento de cada uno de sus servicios. Observe que lo hizo sin necesidad de escribir ninguna configuración personalizada ni de cambiar el código de su aplicación. Sin embargo, si proporciona a Linkerd un poco más de información puede aplicar numerosas políticas, como tiempos de espera y reintentos. También puede proporcionar métricas por ruta.

      Esta información se proporciona mediante un perfil de servicio, que es un recurso de Linkerd personalizado en el que puede describir las rutas de sus aplicaciones y cómo se comporta cada una de ellas.

      Puede ver el aspecto del manifiesto de un perfil de servicio en este ejemplo:

      example-service-profile.yaml

      apiVersion: linkerd.io/v1alpha2
      kind: ServiceProfile
      metadata:
        name: my-service.my-namespace.svc.cluster.local
      spec:
        routes:
        - name: My Route Name
          isRetryable: true # Define it's safe to retry this route
          timeout: 100ms # Define a timeout for this route
          condition:
            method: GET
            pathRegex: /my/route/path
      

      El perfil de servicio describe una lista de rutas y, luego, define el comportamiento que tendrán las solicitudes que coinciden con la condition especificada. En este ejemplo, indica que cada solicitud GET que se envíe a /my/route/path tendrá un tiempo de espera de 100 ms y, si presenta un error, se podrá reintentar.

      Ahora, crearemos un perfil de servicio para uno de sus servicios. Tomando voting-svc como ejemplo, primero, utilice la CLI de Linkerd para verificar las rutas que definió para este servicio:

      • linkerd routes svc/voting-svc -n emojivoto

      Aquí, utiliza el comando linkerd routes para enumerar todas las rutas del servicio voting-svc en el espacio de nombres emojiovoto:

      Output

      ROUTE SERVICE SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 [DEFAULT] voting-svc 83.05% 1.0rps 1ms 1ms 2ms

      Verá una sola ruta: [DEFAULT]. Aquí es donde se agrupan todas las solicitudes hasta que defina su perfil de servicio.

      Ahora, abra nano o su editor favorito para crear un archivo service-profile.yaml:

      • nano service-profile.yaml

      Añada la siguiente definición de perfil de servicio a este archivo:

      service-profile.yaml

      apiVersion: linkerd.io/v1alpha2
      kind: ServiceProfile
      metadata:
        name: voting-svc.emojivoto.svc.cluster.local
        namespace: emojivoto
      spec:
        routes:
        - name: VoteDoughnut
          isRetryable: true
          timeout: 100ms
          condition:
            method: POST
            pathRegex: /emojivoto.v1.VotingService/VoteDoughnut
      

      Ahora, guarde el archivo y cierre su editor.

      Aquí, declara un perfil de servicio para el servicio voting-svc en el espacio de nombres emojivoto. Definió una ruta, denominada VoteDoughnut, que hará coincidir toda solicitud POST con la ruta /emojivoto.v1.VotingService/VoteDoughnut.  Si una solicitud que coincida con estos criterios requiere más de 100 ms, Linkerd la cancelará, y el cliente recibirá una respuesta 504. También le indica a Linkerd que, si la solicitud presenta un error, se puede volver a intentar.

      Ahora, aplique este archivo a su clúster:

      • kubectl apply -f service-profile.yaml

      Después de algunos segundos, vuelva a revisar las rutas de este servicio:

      • linkerd routes svc/voting-svc -n emojivoto

      Ahora, verá la ruta de VoteDoughnut que acaba de definir:

      Output

      ROUTE SERVICE SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 VoteDoughnut voting-svc 0.00% 0.2rps 1ms 1ms 1ms [DEFAULT] voting-svc 100.00% 0.8rps 1ms 4ms 4ms

      Puede ver varias métricas personalizadas, como la tasa de éxito, las solicitudes por segundo y latencia, de esta ruta específica. Observe que el punto final VoteDoughnut se configuró intencionalmente para que siempre devuelva un error, y tiene una tasa de éxito del 0 %, mientras que la de la ruta [DEFAULT] es del 100 %.

      Ahora, después de proporcionar a Linkerd un poco más de información sobre su servicio, cuenta con métricas personalizadas por ruta y tiene dos políticas implementadas: tiempos de espera y reintentos.

      Conclusión

      En este artículo, instaló Linkerd en su clúster de Kubernetes y lo utilizó para monitorear una aplicación de ejemplo. Obtuvo información de telemetría útil, como la tasa de éxito, el rendimiento y latencia. También configuró un perfil de servicio de Linkerd para recopilar métricas por ruta e implementar dos políticas en la aplicación emojivoto.

      Si desea obtener más información sobre Linkerd, puede consultar su excelente página de documentación, en la que se indica cómo proteger sus servicios, configurar seguimiento distribuido, automatizar versiones controladas (canary) y mucho más.

      En este punto, también podría considerar probar Istio, que es otra malla de servicios con un conjunto diferente de funciones, ventajas y desventajas.



      Source link

      Cómo usar Go con MongoDB utilizando el controlador de Go de MongoDB


      El autor seleccionó la Free Software Foundation para recibir una donación como parte del programa Write for DOnations.

      Introducción

      Después de muchos años de basarse en soluciones desarrolladas por la comunidad, MongoDB anunció que estaban trabajando en un controlador oficial para Go.  Este nuevo controlador estuvo listo para la producción en marzo de 2019 con el lanzamiento de la versión v1.0.0 y, desde entonces, se ha estado actualizando de forma continua.

      Al igual que los demás controladores oficiales de MongoDB, el controlador de Go es característico del lenguaje de programación Go y ofrece una manera sencilla de utilizar MongoDB como solución de base de datos para programas de Go. Está completamente integrado con la API de MongoDB y presenta todas las funciones de consulta, indexación y agregación de la API, así como otras funciones avanzadas. A diferencia de las bibliotecas de terceros, los ingenieros de MongoDB lo respaldarán por completo, por lo que puede tener la seguridad de que se seguirá desarrollando y manteniendo.

      En este tutorial, empezará a utilizar el controlador de oficial de Go de MongoDB. Instalará el controlador, establecerá conexión con una base de datos de MongoDB y realizará varias operaciones CRUD. En el proceso, creará un programa de administración de tareas para gestionar tareas a través de la línea de comandos.

      Requisitos previos

      Para este tutorial, necesitará lo siguiente:

      • Go instalado en su máquina y un espacio de trabajo de Go configurado conforme a Cómo instalar Go y configurar un entorno de programación local. En este tutorial, el proyecto se denominará tasker. Necesitará tener instalada la versión 1.11 o superior de Go en su máquina y los módulos de Go habilitados.
      • MongoDB instalada para su sistema operativo conforme a Cómo instalar MongoDB. MongoDB 2.6 o superior, que es la versión mínima que admite el controlador de Go de MongoDB.

      Si utiliza Go v1.11 o 1.12, asegúrese de que los módulos de Go estén habilitados fijando la variable de entorno GO111MODULE en on como se indica a continuación:

      Para obtener más información sobre la implementación de variables de entorno, consulte el tutorial Cómo leer y establecer variables de entorno y shell.

      Los comandos y el código que se muestran en esta guía se probaron con Go v1.14.1 y MongoDB v3.6.3.

      Paso 1: Instalar el controlador de Go de MongoDB

      En este paso, instalará el paquete del controlador de Go para MongoDB y lo importará en su proyecto. También establecerá conexión con su base de datos de MongoDB y verificará el estado de la conexión.

      Proceda a crear un directorio nuevo para este tutorial en su sistema de archivos:

      Una vez que haya establecido el directorio de su proyecto, posiciónese en él con el siguiente comando:

      A continuación, inicie el proyecto de Go con un archivo go.mod. Este archivo define los requisitos del proyecto y bloquea las dependencias en sus versiones correctas:

      Si el directorio de su proyecto está fuera de $GOPATH, debe especificar la ruta de importación de su módulo de la siguiente manera:

      • go mod init github.com/<your_username>/tasker

      En este punto, su archivo go.mod tendrá el siguiente aspecto:

      go.mod

      module github.com/<your_username>/tasker
      
      go 1.14
      

      Agregue el controlador de Go de MongoDB como dependencia de su proyecto utilizando el siguiente comando:

      • go get go.mongodb.org/mongo-driver

      Verá un resultado como el siguiente:

      Output

      go: downloading go.mongodb.org/mongo-driver v1.3.2 go: go.mongodb.org/mongo-driver upgrade => v1.3.2

      En este punto, su archivo go.mod tendrá el siguiente aspecto:

      go.mod

      module github.com/<your_username>/tasker
      
      go 1.14
      
      require go.mongodb.org/mongo-driver v1.3.1 // indirect
      

      A continuación, cree un archivo main.go en el root de su proyecto y ábralo en su editor de texto:

      Para comenzar a usar el controlador, importe los siguientes paquetes en su archivo main.go:

      main.go

      package main
      
      import (
          "context"
          "log"
      
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      

      Aquí, añade los paquetes mongo y options, que proporciona el controlador de Go de MongoDB.

      A continuación, después de realizar sus importaciones, cree un cliente de MongoDB nuevo y establezca conexión con su servidor de MongoDB en ejecución:

      main.go

      . . .
      var collection *mongo.Collection
      var ctx = context.TODO()
      
      func init() {
          clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
          client, err := mongo.Connect(ctx, clientOptions)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      mongo.Connect() acepta los objetos Context y options.ClientOptions, que se utiliza para establecer la cadena de conexión y otros ajustes del controlador. Puede consultar las opciones de configuración disponibles en la documentación del paquete de opciones.

      El contexto es como un tiempo de espera o un plazo que indica cuándo una operación debe dejar de funcionar y reanudarse. Ayuda a prevenir la degradación del rendimiento en los sistemas de producción cuando determinadas operaciones se ejecutan con lentitud. En este código, está pasando context.TODO() para indicar que no está seguro de qué contexto usar en este momento, pero que planea añadir uno en el futuro.

      A continuación, vamos a asegurarnos de que su servidor de MongoDB se haya encontrado y conectado con éxito utilizando el método Ping.  Añada el siguiente código en la función init:

      main.go

      . . .
          log.Fatal(err)
        }
      
        err = client.Ping(ctx, nil)
        if err != nil {
          log.Fatal(err)
        }
      }
      

      Si hay algún error al establecer conexión con la base de datos, el programa debe bloquearse mientras intenta solucionar el problema, dado que no tiene sentido mantenerlo en ejecución sin una conexión activa con la base de datos.

      Añada el siguiente código para crear una base de datos:

      main.go

      . . .
        err = client.Ping(ctx, nil)
        if err != nil {
          log.Fatal(err)
        }
      
        collection = client.Database("tasker").Collection("tasks")
      }
      

      Creó una base de datos tasker y una colección task para almacenar las tareas que creará. También estableció collection como variable de nivel de paquete para poder reutilizar la conexión a la base de datos en todo el paquete.

      Guarde el archivo y ciérrelo.

      En este punto, todo main.go tiene la siguiente estructura:

      main.go

      package main
      
      import (
          "context"
          "log"
      
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      
      var collection *mongo.Collection
      var ctx = context.TODO()
      
      func init() {
          clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
          client, err := mongo.Connect(ctx, clientOptions)
          if err != nil {
              log.Fatal(err)
          }
      
          err = client.Ping(ctx, nil)
          if err != nil {
              log.Fatal(err)
          }
      
          collection = client.Database("tasker").Collection("tasks")
      }
      

      Configuró su programa para establecer conexión con su servidor de MongoDB usando el controlador de Go. En el siguiente paso, procederá a crear su programa de administración de tareas.

      Paso 2: Crear un programa de CLI

      En este paso, instalará el famoso paquete cli para ayudar a desarrollar su programa de administración de tareas. Ofrece una interfaz que puede aprovechar para crear rápidamente herramientas de línea de comandos modernas. Por ejemplo, este paquete proporciona la capacidad de definir subcomandos para su programa con el fin de obtener una experiencia de línea de comandos más similar a git.

      Ejecute el siguiente comando para añadir el paquete como dependencia:

      • go get github.com/urfave/cli/v2

      Luego, vuelva a abrir su archivo main.go:

      Añada el siguiente código resaltado a su archivo main.go:

      main.go

      package main
      
      import (
          "context"
          "log"
          "os"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Importó el paquete cli como se indicó. También importó el paquete os, que usará para pasar argumentos de línea de comandos a su programa:

      Añada el siguiente código después de la función init para crear su programa de CLI y hacer que su código se compile:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:     "tasker",
              Usage:    "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{},
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      Este fragmento crea un programa de CLI denominado tasker y añade una descripción de uso breve que se imprimirá al ejecutar el programa. El segmento de Commands es donde agregará comandos para su programa. El comando Run redistribuye el segmento de argumentos al comando correspondiente.

      Guarde y cierre su archivo.

      Este es el comando que necesita para compilar y ejecutar el programa:

      Verá el siguiente resultado:

      Output

      NAME: tasker - A simple CLI program to manage your tasks USAGE: main [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false)

      El programa ejecuta y muestra texto de ayuda, que es útil para aprender sobre lo que puede hacer el programa y cómo usarlo.

      En los siguientes pasos, mejorará la utilidad de su programa añadiendo subcomandos para ayudar a gestionar sus tareas en MongoDB.

      Paso 3: Crear una tarea

      En este paso, agregará un subcomando a su programa de CLI utilizando el paquete cli. Al final de esta sección, podrá añadir una tarea nueva a la base de datos de MongoDB utilizando un comando add nuevo en su programa de CLI.

      Comience por abrir su archivo main.go:

      Luego, importe los paquetes go.mongodb.org/mongo-driver/bson/primitive, time y errors:

      main.go

      package main
      
      import (
          "context"
          "errors"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Luego, cree una nueva estructura para representar una sola tarea en la base de datos e insértela inmediatamente antes de la función main:

      main.go

      . . .
      type Task struct {
          ID        primitive.ObjectID `bson:"_id"`
          CreatedAt time.Time          `bson:"created_at"`
          UpdatedAt time.Time          `bson:"updated_at"`
          Text      string             `bson:"text"`
          Completed bool               `bson:"completed"`
      }
      . . .
      

      Utilizó el paquete primitive para establecer el tipo de ID de cada tarea, dado que MongoDB utiliza ObjectID para el campo _id por defecto. Otro comportamiento predeterminado de MongoDB es que el nombre de campo en minúsculas se utiliza como la clave de cada campo exportado cuando se está serializado, pero esto se puede modificar utilizando las etiquetas de estructura bson.

      A continuación, cree una función que reciba una instancia de Task y la guarde en la base de datos. Añada este fragmento después de la función main:

      main.go

      . . .
      func createTask(task *Task) error {
          _, err := collection.InsertOne(ctx, task)
        return err
      }
      . . .
      

      El método collection.InsertOne() inserta la tarea proporcionada en la colección de la base de datos y devuelve la ID del documento que se insertó. Como no necesita esta ID, la descarta al asignar al operador de guion bajo.

      El siguiente paso es añadir un comando nuevo a su programa de administración de tareas para crear tareas nuevas. Lo llamaremos add:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      Todos los comandos nuevos que se añaden a su programa de CLI se colocan dentro del segmento de Commands. Cada uno consta de un nombre, una descripción de uso y una acción. Este es el código que se ejecutará al ejecutar el comando.

      Con este código, se recoge el primer argumento en add y se utiliza para establecer la propiedad Text de una nueva instancia de Task a la vez que se asignan los valores predeterminados correspondientes de las demás propiedades. La tarea nueva se pasa posteriormente a createTask, que inserta la tarea en la base de datos y devuelve nil si todo está bien, lo que provoca que el comando se cierre.

      Guarde y cierre su archivo.

      Pruébelo al añadir algunas tareas utilizando el comando add. Si todo es correcto, no verá errores en la pantalla:

      • go run main.go add "Learn Go"
      • go run main.go add "Read a book"

      Ahora que puede añadir tareas correctamente, implementaremos una manera de mostrar todas las tareas que añadió a la base de datos.

      Paso 4: Enumerar todas las tareas

      Los documentos de una colección se pueden enumerar utilizando el método collection.Find(), que espera un filtro y un indicador a un valor en el que se pueda decodificar el resultado.  El valor que devuelve es un cursor, que proporciona un flujo de documentos que se pueden iterar y decodificar de a uno a la vez. El cursor se cierra una vez que se agota.

      Abra su archivo main.go:

      Asegúrese de importar el paquete bson:

      main.go

      package main
      
      import (
          "context"
          "errors"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Luego, cree las siguientes funciones inmediatamente después de createTask:

      main.go

      . . .
      func getAll() ([]*Task, error) {
        // passing bson.D{{}} matches all documents in the collection
          filter := bson.D{{}}
          return filterTasks(filter)
      }
      
      func filterTasks(filter interface{}) ([]*Task, error) {
          // A slice of tasks for storing the decoded documents
          var tasks []*Task
      
          cur, err := collection.Find(ctx, filter)
          if err != nil {
              return tasks, err
          }
      
          for cur.Next(ctx) {
              var t Task
              err := cur.Decode(&t)
              if err != nil {
                  return tasks, err
              }
      
              tasks = append(tasks, &t)
          }
      
          if err := cur.Err(); err != nil {
              return tasks, err
          }
      
        // once exhausted, close the cursor
          cur.Close(ctx)
      
          if len(tasks) == 0 {
              return tasks, mongo.ErrNoDocuments
          }
      
          return tasks, nil
      }
      

      BSON (JSON con codificado binario) es la forma en que los documentos se representan en una base de datos de MongoDB, y el paquete bson es lo que nos ayuda a trabajar con los objetos de BSON en Go. El tipo bson.D que se utiliza en la función getAll() representa un documento de BSON y se utiliza cuando el orden de las propiedades es importante. Al pasar bson.D{{}} como su filtro a filterTasks(), indica que desea hacer coincidir todos los documentos de la colección.

      En la función filterTasks(), itera sobre el cursor que devuelve el método collection.Find() y decodifica cada documento en una instancia de Task. Luego, cada Task se anexa al segmento de tareas creadas al principio de la función. Una vez que el cursor se agota, se cierra y se devuelve el segmento de tasks.

      Antes de crear un comando para enumerar todas las tareas, crearemos una función de ayuda que tome un segmento de tasks e imprima en la salida estándar. Utilizará el paquete color para dar color al resultado.

      Para poder utilizar este paquete, instálelo con lo siguiente:

      • go get gopkg.in/gookit/color.v1

      Verá el siguiente resultado:

      Output

      go: downloading gopkg.in/gookit/color.v1 v1.1.6 go: gopkg.in/gookit/color.v1 upgrade => v1.1.6

      Impórtelo en su archivo main.go junto con el paquete fmt:

      main.go

      package main
      
      import (
          "context"
          "errors"
        "fmt"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
          "gopkg.in/gookit/color.v1"
      )
      . . .
      

      Luego, cree una función printTasks nueva después de su función main:

      main.go

      . . .
      func printTasks(tasks []*Task) {
          for i, v := range tasks {
              if v.Completed {
                  color.Green.Printf("%d: %sn", i+1, v.Text)
              } else {
                  color.Yellow.Printf("%d: %sn", i+1, v.Text)
              }
          }
      }
      . . .
      

      Esta función printTasks toma un segmento de tasks, itera sobre cada una de ellas y las imprime en la salida estándar utilizando el color verde para indicar las tareas completadas y el amarillo para las tareas incompletas.

      Proceda a agregar las siguientes líneas resaltadas para crear un comando all nuevo en el segmento de Commands. Este comando imprimirá todas las tareas añadidas a la salida estándar:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      
      . . .
      

      El comando all obtiene todas las tareas presentes en la base de datos y las imprime en la salida estándar. Si no hay tareas presentes, en su lugar, se imprime una línea de comandos para añadir una tarea nueva.

      Guarde y cierre su archivo.

      Compile y ejecute su programa con el comando all:

      Enumerará todas las tareas que añadió hasta ahora:

      Output

      1: Learn Go 2: Read a book

      Ahora que puede ver todas las tareas de la base de datos, añadiremos la capacidad de marcar una tarea como completa en el siguiente paso.

      Paso 5: Completar una tarea

      En este paso, creará un subcomando nuevo denominado done que le permitirá marcar una tarea existente en la base de datos como completada. Para marcar una tarea como completada, puede utilizar el método collection.FindOneAndUpdate(). Le permite localizar un documento en una colección y actualizar todas sus propiedades o algunas de ellas. Este método requiere un filtro para localizar el documento y un documento de actualización para describir la operación. Los dos se crean utilizando tipos bson.D.

      Comience por abrir su archivo main.go:

      A continuación, inserte el siguiente fragmento después de la función de filterTasks:

      main.go

      . . .
      func completeTask(text string) error {
          filter := bson.D{primitive.E{Key: "text", Value: text}}
      
          update := bson.D{primitive.E{Key: "$set", Value: bson.D{
              primitive.E{Key: "completed", Value: true},
          }}}
      
          t := &Task{}
          return collection.FindOneAndUpdate(ctx, filter, update).Decode(t)
      }
      . . .
      

      La función coincide con el primer documento en el que la propiedad de texto es igual al parámetro text. El documento update especifica que la propiedad completed se establezca en true. Si hay un error en la operación FindOneAndUpdate(), se devolverá mediante completeTask(). De lo contrario, se devuelve nil.

      A continuación, añadiremos un comando done a su programa de CLI que marca una tarea como completada:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      
      . . .
      

      Utiliza el argumento que se pasó al comando done para encontrar el primer documento cuya propiedad text coincida. Si se encuentra, la propiedad completed del documento se establece en true.

      Guarde y cierre su archivo.

      Luego, ejecute su programa con el comando done:

      • go run main.go done "Learn Go"

      Si vuelve a utilizar el comando all, observará que la tarea que se marcó como completada, ahora, se imprime en color verde.

      Captura de pantalla del resultado de la terminal después de completar una tarea

      A veces, solo desea ver las tareas que todavía no se completaron. Añadiremos esa función a continuación.

      Paso 6: Mostrar únicamente tareas pendientes

      En este paso, incorporará código para obtener las tareas pendientes de la base de datos utilizando el controlador de MongoDB. Las tareas pendientes son las que tienen la propiedad completed establecida en false.

      Vamos a añadir una función nueva para obtener las tareas que todavía no se completaron. Abra su archivo main.go:

      Luego, añada este fragmento después de la función completeTask:

      main.go

      . . .
      func getPending() ([]*Task, error) {
          filter := bson.D{
              primitive.E{Key: "completed", Value: false},
          }
      
          return filterTasks(filter)
      }
      . . .
      

      Creó un filtro utilizando los paquetes bson y primitive del controlador de MongoDB, que devolverá los documentos que tengan la propiedad completed establecida en false. Luego, el segmento de tareas pendientes se devuelve al autor de la llamada.

      En lugar de crear un comando nuevo para enumerar las tareas pendientes, vamos a hacer que sea la acción predeterminada cuando se ejecute el programa sin comandos. Para hacerlo, vamos a añadir una propiedad Action al programa de la siguiente manera:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
      . . .
      

      La propiedad Action realiza una acción predeterminada cuando el programa se ejecuta sin ningún subcomando. Aquí es donde se aplica la lógica para enumerar las tareas pendientes. Se invoca la función get getPending() y las tareas resultantes se imprimen en la salida estándar utilizando printTasks(). Si no hay tareas pendientes, se muestra un aviso en su lugar, que anima al usuario a añadir una nueva tarea usando el comando add.

      Guarde y cierre su archivo.

      Ahora, al ejecutar el programa sin añadir ningún comando, se enumerarán todas las tareas pendientes en la base de datos:

      Verá el siguiente resultado:

      Output

      1: Read a book

      Ahora que puede enumerar tareas incompletas, vamos a añadir otro comando que permite ver únicamente las tareas completadas.

      Paso 7: Mostrar tareas completadas

      En este paso, agregará un subcomando finished nuevo que obtiene las tareas completadas de la base de datos y las muestra en la pantalla. Esto incluye filtrar y devolver las tareas que tengan la propiedad completed establecida en true.

      Abra su archivo main.go:

      Luego, añada el siguiente código al final de su archivo:

      main.go

      . . .
      func getFinished() ([]*Task, error) {
          filter := bson.D{
              primitive.E{Key: "completed", Value: true},
          }
      
          return filterTasks(filter)
      }
      . . .
      

      De forma similar a lo que hizo con la función getPending(), añadió una función getFinished() que devuelve un segmento de tareas completadas. En este caso, el filtro tiene la propiedad completed establecida en true, por lo tanto, solo se devolverán los documentos que coincidan con esta condición.

      A continuación, cree un comando finished que imprima todas las tareas completadas:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
                  {
                      Name:    "finished",
                      Aliases: []string{"f"},
                      Usage:   "list completed tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getFinished()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `done 'task'` to complete a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
              }
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      . . .
      

      El comando finished obtiene las tareas que tienen la propiedad completed establecida en true a través de la función getFinished() creada aquí. Luego, las pasa a la función printTasks para que se impriman en la salida estándar.

      Guarde y cierre su archivo.

      Ejecute el siguiente comando:

      Verá el siguiente resultado:

      Output

      1: Learn Go

      En el paso final, proporcionará a los usuarios la opción de eliminar tareas de la base de datos.

      Paso 8: Eliminar una tarea

      En este paso, agregará un subcomando delete para permitir que los usuarios puedan eliminar tareas de la base de datos. Para eliminar una tarea, utilizará el método collection.DeleteOne() del controlador de MongoDB. También utiliza un filtro para buscar el documento que se eliminará.

      Vuelva a abrir su archivo main.go una vez más:

      Añada esta función deleteTask para eliminar tareas de la base de datos directamente después de su función getFinished:

      main.go

      . . .
      func deleteTask(text string) error {
          filter := bson.D{primitive.E{Key: "text", Value: text}}
      
          res, err := collection.DeleteOne(ctx, filter)
          if err != nil {
              return err
          }
      
          if res.DeletedCount == 0 {
              return errors.New("No tasks were deleted")
          }
      
          return nil
      }
      . . .
      

      Este método deleteTask toma un argumento de cadena que representa el elemento de tarea que se va a eliminar. Se crea un filtro para buscar el elemento de tarea que tenga la propiedad text establecida en el argumento de cadena. Pasa el filtro al método DeleteOne() que coincide con el elemento de la colección y lo elimina.

      Puede verificar la propiedad DeletedCount en el resultado del método DeleteOne para verificar si se eliminaron documentos. Si el filtro no encuentra un documento para eliminar, DeletedCount será cero y, en ese caso, puede devolver un error.

      Ahora, añada un comando rm nuevo como se indica en la sección resaltada:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
                  {
                      Name:    "finished",
                      Aliases: []string{"f"},
                      Usage:   "list completed tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getFinished()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `done 'task'` to complete a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:  "rm",
                      Usage: "deletes a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          err := deleteTask(text)
                          if err != nil {
                              return err
                          }
      
                          return nil
                      },
                  },
              }
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      . . .
      

      Al igual que con todos los demás subcomandos añadidos anteriormente, el comando rm utiliza su primer argumento para buscar una tarea en la base de datos y la elimina.

      Guarde y cierre su archivo.

      Puede enumerar tareas pendientes ejecutando su programa sin pasar ningún subcomando de la siguiente manera:

      Output

      1: Read a book

      Al ejecutar el subcomando rm en la tarea de "Read a book", se eliminará de la base de datos:

      • go run main.go rm "Read a book"

      Si vuelve a enumerar todas las tareas pendientes, observará que la tarea "Read a book" ya no aparece y, en su lugar, se muestra un aviso para añadir una nueva tarea:

      Output

      Nothing to see here Run `add 'task'` to add a task

      En este paso, añadió una función para eliminar tareas de la base de datos.

      Conclusión

      Creó correctamente un programa de línea de comandos de administración de tareas y aprendió los conceptos básicos del uso del controlador de Go de MongoDB en el proceso.

      Asegúrese de consultar la documentación completa del controlador de Go de MongoDB en GoDoc para obtener más información sobre las funciones que proporciona su utilización. La documentación que describe el uso de agregaciones o transacciones puede resultarle particularmente interesante.

      Puede ver el código final de este tutorial en este repositorio de GitHub.



      Source link

      Como Usar o Tipo de Dados MySQL BLOB para Armazenar Imagens com PHP no Ubuntu 18.04


      O autor selecionou Girls Who Code para receber uma doação como parte do programa Write for DOnations.

      Introdução

      Um Binary Large Object (BLOB) é um tipo de dados MySQL que pode armazenar dados binários como imagens, multimedia e arquivos PDF.

      Ao criar aplicações que requerem um banco de dados fortemente acoplado onde imagens devem estar sincronizadas com dados relacionados (por exemplo, um portal de funcionários, um banco de dados de estudantes, ou uma aplicação financeira), você pode achar que é conveniente armazenar imagens como fotos de passaporte e assinaturas de alunos em um banco de dados MySQL, juntamente com outras informações relacionadas.

      É aqui que o tipo de dados MySQL BLOB entra. Esta abordagem de programação elimina a necessidade de criar um sistema de arquivos separado para armazenar imagens. O esquema também centraliza o banco de dados, tornando-o mais portátil e seguro porque os dados estão isolados do sistema de arquivos. A criação de backups também é mais simples, pois você pode criar um único arquivo de dump do MySQL que contém todos os seus dados.

      A recuperação de dados é mais rápida e, ao criar registros, você pode garantir que as regras de validação de dados e a integridade referencial sejam mantidas especialmente ao utilizar as transações do MySQL.

      Neste tutorial, você usará o tipo de dados MySQL BLOB para armazenar imagens com PHP no Ubuntu 18.04.

      Pré-requisitos

      Para seguir com este guia, você precisará do seguinte:

      Passo 1 — Criando um Banco de Dados

      Você começará criando um banco de dados de exemplo para seu projeto. Para fazer isso, faça um SSH em seu servidor e, em seguida, execute o seguinte comando para fazer login no seu servidor MySQL como root:

      Digite a senha do root do seu banco de dados MySQL e clique em ENTER para continuar.

      Em seguida, execute o seguinte comando para criar um banco de dados. Neste tutorial, iremos nomeá-lo como test_company:

      • CREATE DATABASE test_company;

      Assim que o banco de dados for criado, você verá a seguinte saída:

      Output

      Query OK, 1 row affected (0.01 sec)

      Em seguida, crie uma conta test_user no servidor MySQL e lembre-se de substituir PASSWORD por uma senha forte:

      • CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'PASSWORD';

      Você verá o seguinte resultado:

      Output

      Query OK, 0 rows affected (0.01 sec)

      Para conceder ao test_user privilégios completos no banco de dados test_company, execute:

      • GRANT ALL PRIVILEGES ON test_company.* TO 'test_user'@'localhost';

      Certifique-se de obter a seguinte saída:

      Output

      Query OK, 0 rows affected (0.01 sec)

      Finalmente, libere a tabela de privilégios para que o MySQL recarregue as permissões:

      Certifique-se de ver a seguinte saída:

      Output

      Query OK, 0 rows affected (0.01 sec)

      Agora que o banco de dados test_company e o test_user estão prontos, você prosseguirá com a criação de uma tabela products para armazenar produtos de exemplo. Você usará esta tabela mais tarde para inserir e recuperar registros para demonstrar como o MySQL BLOB funciona.

      Faça log-off do servidor MySQL:

      Em seguida, faça login novamente com as credenciais do test_user que você criou:

      Quando solicitado, digite a senha para o test_user e tecle ENTER para continuar. Em seguida, alterne para o banco de dados test_company digitando o seguinte:

      Assim que o banco de dados test_company for selecionado, o MySQL exibirá:

      Output

      Database changed

      Em seguida, crie uma tabela products executando:

      • CREATE TABLE `products` (product_id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(50), price DOUBLE, product_image BLOB) ENGINE = InnoDB;

      Este comando cria uma tabela com nome products. A tabela tem quatro colunas:

      • product_id: esta coluna utiliza um tipo de dados BIGINT para acomodar uma grande lista de produtos até um máximo de 2⁶³-1 items. Você marcou a coluna como PRIMARY KEY para identificar unicamente os produtos. Para que o MySQL trate da geração de novos identificadores para as colunas inseridas, você usou a palavra-chave AUTO_INCREMENT.

      • product_name: esta coluna contém os nomes dos produtos. Você usou o tipo de dados VARCHAR já que este campo geralmente irá lidar com alfanuméricos com um máximo de 50 caracteres — o limite de 50 é apenas um valor hipotético usado para o propósito deste tutorial.

      • price: para fins de demonstração, sua tabela products contém a coluna de preço para armazenar o preço de varejo dos produtos. Como alguns produtos podem ter valores de ponto flutuante (por exemplo, 23.69, 45.36, 102.99), você utilizou o tipo de dados DOUBLE.

      • product_image: esta coluna utiliza um tipo de dados BLOB para armazenar os dados do binário real das imagens dos produtos.

      Você usou o InnoDB storage ENGINE para que a tabela suporte uma grande variedade de recursos, incluindo as transações do MySQL. Após executar isso para criar a tabela de products, você verá a seguinte saída:

      Output

      Query OK, 0 rows affected (0.03 sec)

      Saia do seu servidor MySQL:

      Você receberá a seguinte saída:

      Output

      Bye

      A tabela products agora está pronta para armazenar alguns registros, incluindo imagens de produtos e você a preencherá com alguns produtos no próximo passo.

      Neste passo, você criará um script PHP que se conectará ao banco de dados MySQL que você criou no Passo 1. O script preparará três produtos de exemplo e os inserirá na tabela products.

      Para criar o código PHP, abra um novo arquivo com seu editor de texto:

      • sudo nano /var/www/html/config.php

      Em seguida, digite as seguintes informações no arquivo e substitua PASSWORD pela senha do test_user que você criou no Passo 1:

      /var/www/html/config.php

      <?php
      
      define('DB_NAME', 'test_company');
      define('DB_USER', 'test_user');
      define('DB_PASSWORD', 'PASSWORD');
      define('DB_HOST', 'localhost');
      
      $pdo = new PDO("mysql:host=" . DB_HOST . "; dbname=" . DB_NAME, DB_USER, DB_PASSWORD);
      $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
      
      

      Salve e feche o arquivo.

      Neste arquivo, você usou quatro constantes PHP para se conectar ao banco de dados MySQL que você criou no Passo 1:

      • DB_NAME esta constante contém o nome do banco de dados test_company.

      • DB_USER: esta variável contém o nome de usuário test_user.

      • DB_PASSWORD : esta constante armazena a PASSWORD MySQL da conta do test_user.

      • DB_HOST: isso representa o servidor onde o banco de dados está. Neste caso, você está usando o servidor localhost.

      A seguinte linha em seu arquivo inicia um PHP Data Object (PDO) e se conecta ao banco de dados MySQL:

      ...
      $pdo = new PDO("mysql:host=" . DB_HOST . "; dbname=" . DB_NAME, DB_USER, DB_PASSWORD);
      ...
      

      Perto do final do arquivo, você definiu alguns atributos PDO:

      • ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION: este atributo instrui o PDO a lançar uma exceção que pode ser logada para fins de depuração.
      • ATTR_EMULATE_PREPARES, false: esta opção aumenta a segurança ao informar ao o mecanismo de banco de dados do MySQL para fazer a preparação ao invés do PDO.

      Você incluirá o arquivo /var/www/html/config.php em dois scripts PHP que você criará a seguir para inserir e recuperar registros, respectivamente.

      Primeiro, crie o script PHP /var/www/html/insert_products.php para inserir registros na tabela products:

      • sudo nano /var/www/html/insert_products.php

      Em seguida, adicione as seguintes informações no arquivo /var/www/html/insert_products.php

      /var/www/html/insert_products.php

      <?php
      
      require_once 'config.php';
      
      $products = [];
      
      $products[] = [
                    'product_name' => 'VIRTUAL SERVERS',
                    'price' => 5,
                    'product_image' => file_get_contents("https://i.imgur.com/VEIKbp0.png")
                    ];
      
      $products[] = [
                    'product_name' => 'MANAGED KUBERNETES',
                    'price' => 30,
                    'product_image' => file_get_contents("https://i.imgur.com/cCc9Gw9.png")
                    ];
      
      $products[] = [
                    'product_name' => 'MySQL DATABASES',
                    'price' => 15,
                    'product_image' => file_get_contents("https://i.imgur.com/UYcHkKD.png" )
                    ];
      
      $sql = "INSERT INTO products(product_name, price, product_image) VALUES (:product_name, :price, :product_image)";
      
      foreach ($products as $product) {
          $stmt = $pdo->prepare($sql);
          $stmt->execute($product);
      }
      
      echo "Records inserted successfully";
      

      Salve e feche o arquivo.

      No arquivo, você incluiu o arquivo config.php no topo. Este é o primeiro arquivo que você criou para definir as variáveis de banco de dados e se conectar ao banco de dados. O arquivo também inicia um objeto PDO e o armazena em uma variável $pdo.

      Em seguida, você criou uma matriz de dados dos produtos para serem inseridos no banco de dados. Além de product_name e price, que são preparados como strings e valores numéricos respectivamente, o script utiliza a função integrada do PHP file_get_contents para ler imagens de uma origem externa e as passar como strings para a coluna product_image.

      Em seguida, você preparou uma instrução SQL e usou a instrução PHP foreach{...} para inserir cada produto no banco de dados.

      Para executar o arquivo /var/www/html/insert_products.php execute-o na janela do seu navegador usando a seguinte URL. Lembre-se de substituir your-server-IP pelo endereço IP público do seu servidor:

      http://your-server-IP/insert_products.php
      

      Após executar o arquivo, você verá uma mensagem de sucesso em seu navegador, confirmando que os registros foram inseridos no banco de dados.

      Uma mensagem mostrando que os registros foram inseridos com sucesso no banco de dados

      Você inseriu com sucesso três registros contendo imagens de produtos na tabela products. No próximo passo, você criará um script PHP para recuperar esses registros e exibi-los no seu navegador.

      Passo 3 — Exibindo Informações de Produtos do Banco de Dados MySQL

      Com as informações e imagens dos produtos no banco de dados, você agora irá programar outro script PHP que consulta e exibe informações dos produtos em uma tabela HTML no seu navegador.

      Para criar o arquivo, digite o seguinte:

      • sudo nano /var/www/html/display_products.php

      Em seguida, digite as seguintes informações no arquivo:

      /var/www/html/display_products.php

      <html>
        <title>Using BLOB and MySQL</title>
        <body>
      
        <?php
      
        require_once 'config.php';
      
        $sql = "SELECT * FROM products";
        $stmt = $pdo->prepare($sql);
        $stmt->execute();
        ?>
      
        <table border="1" align = 'center'> <caption>Products Database</caption>
          <tr>
            <th>Product Id</th>
            <th>Product Name</th>
            <th>Price</th>
            <th>Product Image</th>
          </tr>
      
        <?php
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            echo '<tr>';
            echo '<td>' . $row['product_id'] . '</td>';
            echo '<td>' . $row['product_name'] . '</td>';
            echo '<td>' . $row['price'] . '</td>';
            echo '<td>' .
            '<img src = "data:image/png;base64,' . base64_encode($row['product_image']) . '" width = "50px" height = "50px"/>'
            . '</td>';
            echo '</tr>';
        }
        ?>
      
        </table>
        </body>
      </html>
      

      Salve as alterações no arquivo e feche-o.

      Aqui, você incluiu novamente o arquivo config.php para se conectar ao banco de dados. Em seguida, você preparou e executou uma instrução SQL usando o PDO para recuperar todos os itens da tabela products usando o comando SELECT * FROM products

      Depois, você criou uma tabela HTML e a preencheu com os dados dos produtos usando a instrução PHP while() {...}. A linha $row = $stmt->fetch(PDO::FETCH_ASSOC) consulta o banco de dados e armazena o resultado na variável $row como uma matriz multidimensional, que você então exibiu em uma coluna de tabela HTML usando a sintaxe $row['column_name'].

      As imagens da coluna product_image são incluídas dentro das tags <img src = "">. Você usou os atributos width e height para redimensionar as imagens para um tamanho menor que pode se encaixar na coluna HTML.

      Para converter os dados mantidos pelo tipo de dados BLOB de volta para imagens, você usou a função PHP integrada base64_encode e a seguinte sintaxe para o esquema Data URI:

      data:media_type;base64, base_64_encoded_data
      

      Neste caso, o image/png é o media_type e a string codificada em Base64 da coluna product_image é o base_64_encoded_data.

      Em seguida, execute o arquivo display_products.php em um navegador web digitando o seguinte endereço:

      http://your-server-IP/display_products.php
      

      Após executar o arquivo display_products.php em seu navegador, você verá uma tabela HTML com uma lista de produtos e imagens associadas.

      List of products from MySQL database

      Isso confirma que o script PHP para recuperar imagens do MySQL está funcionando como esperado.

      Conclusão

      Neste guia, você utilizou o tipo de dados MySQL BLOB para armazenar e exibir imagens com PHP no Ubuntu 18.04. Você também viu as vantagens básicas de armazenar imagens em um banco de dados, ao invés de armazená-las em um sistema de arquivos. Elas incluem a portabilidade, segurança e facilidade de backup. Se você estiver construindo uma aplicação como um portal de estudantes ou o banco de dados de funcionários que exige que informações e imagens relacionadas sejam armazenadas em conjunto, então essa tecnologia pode ser de grande uso para você.

      Para obter mais informações sobre os tipos de dados suportados no MySQL, siga o guia MySQL Data Types. Se você estiver interessado em mais conteúdos relacionados ao MySQL e ao PHP, verifique os seguintes tutoriais:



      Source link