One place for hosting & domains

      How to Install and Secure phpMyAdmin with Nginx on an Ubuntu 18.04 server


      Introduction

      While many users need the functionality of a database system like MySQL, interacting with the system solely from the MySQL command-line client requires familiarity with the SQL language, so it may not be the preferred interface for some.

      phpMyAdmin was created so that users can interact with MySQL through an intuitive web interface, running alongside a PHP development environment. In this guide, we’ll discuss how to install phpMyAdmin on top of an Nginx server, and how to configure the server for increased security.

      Note: There are important security considerations when using software like phpMyAdmin, since it runs on the database server, it deals with database credentials, and it enables a user to easily execute arbitrary SQL queries into your database. Because phpMyAdmin is a widely-deployed PHP application, it is frequently targeted for attack. We will go over some security measures you can take in this tutorial so that you can make informed decisions.

      Prerequisites

      Before you get started with this guide, you’ll need the following available to you:

      Because phpMyAdmin handles authentication using MySQL credentials, it is strongly advisable to install an SSL/TLS certificate to enable encrypted traffic between server and client. If you do not have an existing domain configured with a valid certificate, you can follow this guide on securing Nginx with Let’s Encrypt on Ubuntu 18.04.

      Warning: If you don’t have an SSL/TLS certificate installed on the server and you still want to proceed, please consider enforcing access via SSH Tunnels as explained in Step 5 of this guide.

      Once you have met these prerequisites, you can go ahead with the rest of the guide.

      Step 1 — Installing phpMyAdmin

      The first thing we need to do is install phpMyAdmin on the LEMP server. We’re going to use the default Ubuntu repositories to achieve this goal.

      Let’s start by updating the server’s package index with:

      Now you can install phpMyAdmin with:

      • sudo apt install phpmyadmin

      During the installation process, you will be prompted to choose the web server (either Apache or Lighthttp) to configure. Because we are using Nginx as web server, we shouldn't make a choice here. Press tab and then OK to advance to the next step.

      Next, you’ll be prompted whether to use dbconfig-common for configuring the application database. Select Yes. This will set up the internal database and administrative user for phpMyAdmin. You will be asked to define a new password for the phpmyadmin MySQL user. You can also leave it blank and let phpMyAdmin randomly create a password.

      The installation will now finish. For the Nginx web server to find and serve the phpMyAdmin files correctly, we’ll need to create a symbolic link from the installation files to Nginx's document root directory:

      • sudo ln -s /usr/share/phpmyadmin /var/www/html

      Your phpMyAdmin installation is now operational. To access the interface, go to your server's domain name or public IP address followed by /phpmyadmin in your web browser:

      https://server_domain_or_IP/phpmyadmin
      

      phpMyAdmin login screen

      As mentioned before, phpMyAdmin handles authentication using MySQL credentials, which means you should use the same username and password you would normally use to connect to the database via console or via an API. If you need help creating MySQL users, check this guide on How To Manage an SQL Database.

      Note: Logging into phpMyAdmin as the root MySQL user is discouraged because it represents a significant security risk. We'll see how to disable root login in a subsequent step of this guide.

      Your phpMyAdmin installation should be completely functional at this point. However, by installing a web interface, we've exposed our MySQL database server to the outside world. Because of phpMyAdmin's popularity, and the large amounts of data it may provide access to, installations like these are common targets for attacks. In the following sections of this guide, we'll see a few different ways in which we can make our phpMyAdmin installation more secure.

      Step 2 — Changing phpMyAdmin's Default Location

      One of the most basic ways to protect your phpMyAdmin installation is by making it harder to find. Bots will scan for common paths, like phpmyadmin, pma, admin, mysql and such. Changing the interface's URL from /phpmyadmin to something non-standard will make it much harder for automated scripts to find your phpMyAdmin installation and attempt brute-force attacks.

      With our phpMyAdmin installation, we've created a symbolic link pointing to /usr/share/phpmyadmin, where the actual application files are located. To change phpMyAdmin's interface URL, we will rename this symbolic link.

      First, let's navigate to the Nginx document root directory and list the files it contains to get a better sense of the change we'll make:

      You’ll receive the following output:

      Output

      total 8 -rw-r--r-- 1 root root 612 Apr 8 13:30 index.nginx-debian.html lrwxrwxrwx 1 root root 21 Apr 8 15:36 phpmyadmin -> /usr/share/phpmyadmin

      The output shows that we have a symbolic link called phpmyadmin in this directory. We can change this link name to whatever we'd like. This will in turn change phpMyAdmin's access URL, which can help obscure the endpoint from bots hardcoded to search common endpoint names.

      Choose a name that obscures the purpose of the endpoint. In this guide, we'll name our endpoint /nothingtosee, but you should choose an alternate name. To accomplish this, we'll rename the link:

      • sudo mv phpmyadmin nothingtosee
      • ls -l

      After running the above commands, you’ll receive this output:

      Output

      total 8 -rw-r--r-- 1 root root 612 Apr 8 13:30 index.nginx-debian.html lrwxrwxrwx 1 root root 21 Apr 8 15:36 nothingtosee -> /usr/share/phpmyadmin

      Now, if you go to the old URL, you'll get a 404 error:

      https://server_domain_or_IP/phpmyadmin
      

      phpMyAdmin 404 error

      Your phpMyAdmin interface will now be available at the new URL we just configured:

      https://server_domain_or_IP/nothingtosee
      

      phpMyAdmin login screen

      By obfuscating phpMyAdmin's real location on the server, you're securing its interface against automated scans and manual brute-force attempts.

      Step 3 — Disabling Root Login

      On MySQL as well as within regular Linux systems, the root account is a special administrative account with unrestricted access to the system. In addition to being a privileged account, it's a known login name, which makes it an obvious target for brute-force attacks. To minimize risks, we'll configure phpMyAdmin to deny any login attempts coming from the user root. This way, even if you provide valid credentials for the user root, you'll still get an "access denied" error and won't be allowed to log in.

      Because we chose to use dbconfig-common to configure and store phpMyAdmin settings, the default configuration is currently stored in the database. We'll need to create a new config.inc.php file to define our custom settings.

      Even though the PHP files for phpMyAdmin are located inside /usr/share/phpmyadmin, the application uses configuration files located at /etc/phpmyadmin. We will create a new custom settings file inside /etc/phpmyadmin/conf.d, and name it pma_secure.php:

      • sudo nano /etc/phpmyadmin/conf.d/pma_secure.php

      The following configuration file contains the necessary settings to disable passwordless logins (AllowNoPassword set to false) and root login (AllowRoot set to false):

      /etc/phpmyadmin/conf.d/pma_secure.php

      <?php
      
      # PhpMyAdmin Settings
      # This should be set to a random string of at least 32 chars
      $cfg['blowfish_secret'] = '3!#32@3sa(+=_4?),5XP_:U%%834sdfSdg43yH#{o';
      
      $i=0;
      $i++;
      
      $cfg['Servers'][$i]['auth_type'] = 'cookie';
      $cfg['Servers'][$i]['AllowNoPassword'] = false;
      $cfg['Servers'][$i]['AllowRoot'] = false;
      
      ?>
      

      Save the file when you're done editing by pressing CTRL + X then y to confirm changes and ENTER. The changes will apply automatically. If you reload the login page now and try to log in as root, you will get an Access Denied error:

      access denied

      Root login is now prohibited on your phpMyAdmin installation. This security measure will block brute-force scripts from trying to guess the root database password on your server. Moreover, it will enforce the usage of less-privileged MySQL accounts for accessing phpMyAdmin's web interface, which by itself is an important security practice.

      Step 4 — Creating an Authentication Gateway

      Hiding your phpMyAdmin installation on an unusual location might sidestep some automated bots scanning the network, but it's useless against targeted attacks. To better protect a web application with restricted access, it's generally more effective to stop attackers before they can even reach the application. This way, they'll be unable to use generic exploits and brute-force attacks to guess access credentials.

      In the specific case of phpMyAdmin, it's even more important to keep the login interface locked away. By keeping it open to the world, you're offering a brute-force platform for attackers to guess your database credentials.

      Adding an extra layer of authentication to your phpMyAdmin installation enables you to increase security. Users will be required to pass through an HTTP authentication prompt before ever seeing the phpMyAdmin login screen. Most web servers, including Nginx, provide this capability natively.

      To set this up, we first need to create a password file to store the authentication credentials. Nginx requires that passwords be encrypted using the crypt() function. The OpenSSL suite, which should already be installed on your server, includes this functionality.

      To create an encrypted password, type:

      You will be prompted to enter and confirm the password that you wish to use. The utility will then display an encrypted version of the password that will look something like this:

      Output

      O5az.RSPzd.HE

      Copy this value, as you will need to paste it into the authentication file we'll be creating.

      Now, create an authentication file. We'll call this file pma_pass and place it in the Nginx configuration directory:

      • sudo nano /etc/nginx/pma_pass

      In this file, you’ll specify the username you would like to use, followed by a colon (:), followed by the encrypted version of the password you received from the openssl passwd utility.

      We are going to name our user sammy, but you should choose a different username. The file should look like this:

      /etc/nginx/pma_pass

      sammy:O5az.RSPzd.HE
      

      Save and close the file when you're done.

      Now we're ready to modify the Nginx configuration file. For this guide, we'll use the configuration file located at /etc/nginx/sites-available/example.com. You should use the relevant Nginx configuration file for the web location where phpMyAdmin is currently hosted. Open this file in your text editor to get started:

      • sudo nano /etc/nginx/sites-available/example.com

      Locate the server block, and the location / section within it. We need to create a new location section within this block to match phpMyAdmin's current path on the server. In this guide, phpMyAdmin's location relative to the web root is /nothingtosee:

      /etc/nginx/sites-available/default

      server {
          . . .
      
              location / {
                      try_files $uri $uri/ =404;
              }
      
              location /nothingtosee {
                      # Settings for phpMyAdmin will go here
              }
      
          . . .
      }
      

      Within this block, we'll need to set up two different directives: auth_basic, which defines the message that will be displayed on the authentication prompt, and auth_basic_user_file, pointing to the file we just created. This is how your configuration file should look like when you're finished:

      /etc/nginx/sites-available/default

      server {
          . . .
      
              location /nothingtosee {
                      auth_basic "Admin Login";
                      auth_basic_user_file /etc/nginx/pma_pass;
              }
      
      
          . . .
      }
      

      Save and close the file when you're done. To check if the configuration file is valid, you can run:

      The following output is expected:

      Output

      nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful

      To activate the new authentication gate, you must reload the web server:

      • sudo systemctl reload nginx

      Now, if you visit the phpMyAdmin URL in your web browser, you should be prompted for the username and password you added to the pma_pass file:

      https://server_domain_or_IP/nothingtosee
      

      Nginx authentication page

      Once you enter your credentials, you'll be taken to the standard phpMyAdmin login page.

      Note: If refreshing the page does not work, you may have to clear your cache or use a different browser session if you've already been using phpMyAdmin.

      In addition to providing an extra layer of security, this gateway will help keep your MySQL logs clean of spammy authentication attempts.

      Step 5 — Setting Up Access via Encrypted Tunnels (Optional)

      For increased security, it is possible to lock down your phpMyAdmin installation to authorized hosts only. You can whitelist authorized hosts in your Nginx configuration file, so that any request coming from an IP address that is not on the list will be denied.

      Even though this feature alone can be enough in some use cases, it's not always the best long-term solution, mainly due to the fact that most people don't access the Internet from static IP addresses. As soon as you get a new IP address from your Internet provider, you'll be unable to get to the phpMyAdmin interface until you update the Nginx configuration file with your new IP address.

      For a more robust long-term solution, you can use IP-based access control to create a setup in which users will only have access to your phpMyAdmin interface if they're accessing from either an authorized IP address or localhost via SSH tunneling. We'll see how to set this up in the sections below.

      Combining IP-based access control with SSH tunneling greatly increases security because it fully blocks access coming from the public internet (except for authorized IPs), in addition to providing a secure channel between user and server through the use of encrypted tunnels.

      Setting Up IP-Based Access Control on Nginx

      On Nginx, IP-based access control can be defined in the corresponding location block of a given site, using the directives allow and deny. For instance, if we want to only allow requests coming from a given host, we should include the following two lines, in this order, inside the relevant location block for the site we would like to protect:

      allow hostname_or_IP;
      deny all;
      

      You can allow as many hosts as you want, you only need to include one allow line for each authorized host/IP inside the respective location block for the site you're protecting. The directives will be evaluated in the same order as they are listed, until a match is found or the request is finally denied due to the deny all directive.

      We'll now configure Nginx to only allow requests coming from localhost or your current IP address. First, you'll need to know the current public IP address your local machine is using to connect to the Internet. There are various ways to obtain this information; for simplicity, we're going to use the service provided by ipinfo.io. You can either open the URL https://ipinfo.io/ip in your browser, or run the following command from your local machine:

      • curl https://ipinfo.io/ip

      You should get a simple IP address as output, like this:

      Output

      203.0.113.111

      That is your current public IP address. We'll configure phpMyAdmin's location block to only allow requests coming from that IP, in addition to localhost. We'll need to edit once again the configuration block for phpMyAdmin inside /etc/nginx/sites-available/example.com.

      Open the Nginx configuration file using your command-line editor of choice:

      • sudo nano /etc/nginx/sites-available/example.com

      Because we already have an access rule within our current configuration, we need to combine it with IP-based access control using the directive satisfy all. This way, we can keep the current HTTP authentication prompt for increased security.

      This is how your phpMyAdmin Nginx configuration should look like after you're done editing:

      /etc/nginx/sites-available/example.com

      server {
          . . .
      
          location /nothingtosee {
              satisfy all; #requires both conditions
      
              allow 203.0.113.111; #allow your IP
              allow 127.0.0.1; #allow localhost via SSH tunnels
              deny all; #deny all other sources
      
              auth_basic "Admin Login";
              auth_basic_user_file /etc/nginx/pma_pass;
          }
      
          . . .
      }
      

      Remember to replace nothingtosee with the actual path where phpMyAdmin can be found, and the highlighted IP address with your current public IP address.

      Save and close the file when you're done. To check if the configuration file is valid, you can run:

      The following output is expected:

      Output

      nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful

      Now reload the web server so the changes take effect:

      • sudo systemctl reload nginx

      Because your IP address is explicitly listed as an authorized host, your access shouldn't be disturbed. Anyone else trying to access your phpMyAdmin installation will now get a 403 error (Forbidden):

      https://server_domain_or_IP/nothingtosee
      

      403 error

      In the next section, we'll see how to use SSH tunneling to access the web server through local requests. This way, you'll still be able to access phpMyAdmin's interface even when your IP address changes.

      Accessing phpMyAdmin Through an Encrypted Tunnel

      SSH tunneling works as a way of redirecting network traffic through encrypted channels. By running an ssh command similar to what you would use to log into a server, you can create a secure "tunnel" between your local machine and that server. All traffic coming in on a given local port can now be redirected through the encrypted tunnel and use the remote server as a proxy, before reaching out to the internet. It's similar to what happens when you use a VPN (Virtual Private Network), however SSH tunneling is much simpler to set up.

      We'll use SSH tunneling to proxy our requests to the remote web server running phpMyAdmin. By creating a tunnel between your local machine and the server where phpMyAdmin is installed, you can redirect local requests to the remote web server, and what's more important, traffic will be encrypted and requests will reach Nginx as if they're coming from localhost. This way, no matter what IP address you're connecting from, you'll be able to securely access phpMyAdmin's interface.

      Because the traffic between your local machine and the remote web server will be encrypted, this is a safe alternative for situations where you can't have an SSL/TLS certificate installed on the web server running phpMyAdmin.

      From your local machine, run this command whenever you need access to phpMyAdmin:

      • ssh user@server_domain_or_IP -L 8000:localhost:80 -L 8443:localhost:443 -N

      Let's examine each part of the command:

      • user: SSH user to connect to the server where phpMyAdmin is running
      • hostname_or_IP: SSH host where phpMyAdmin is running
      • -L 8000:localhost:80 redirects HTTP traffic on port 8000
      • -L 8443:localhost:443 redirects HTTPS traffic on port 8443
      • -N: do not execute remote commands

      Note: This command will block the terminal until interrupted with a CTRL+C, in which case it will end the SSH connection and stop the packet redirection. If you'd prefer to run this command in background mode, you can use the SSH option -f.

      Now, go to your browser and replace server_domain_or_IP with localhost:PORT, where PORT is either 8000 for HTTP or 8443 for HTTPS:

      http://localhost:8000/nothingtosee
      
      https://localhost:443/nothingtosee
      

      phpMyAdmin login screen

      Note: If you're accessing phpMyAdmin via https, you might get an alert message questioning the security of the SSL certificate. This happens because the domain name you're using (localhost) doesn't match the address registered within the certificate (domain where phpMyAdmin is actually being served). It is safe to proceed.

      All requests on localhost:8000 (HTTP) and localhost:8443 (HTTPS) are now being redirected through a secure tunnel to your remote phpMyAdmin application. Not only have you increased security by disabling public access to your phpMyAdmin, you also protected all traffic between your local computer and the remote server by using an encrypted tunnel to send and receive data.

      If you'd like to enforce the usage of SSH tunneling to anyone who wants access to your phpMyAdmin interface (including you), you can do that by removing any other authorized IPs from the Nginx configuration file, leaving 127.0.0.1 as the only allowed host to access that location. Considering nobody will be able to make direct requests to phpMyAdmin, it is safe to remove HTTP authentication in order to simplify your setup. This is how your configuration file would look like in such a scenario:

      /etc/nginx/sites-available/example.com

      server {
          . . .
      
          location /nothingtosee { 
              allow 127.0.0.1; #allow localhost only
              deny all; #deny all other sources
          }
      
          . . .
      }
      

      Once you reload Nginx's configuration with sudo systemctl reload nginx, your phpMyAdmin installation will be locked down and users will be required to use SSH tunnels in order to access phpMyAdmin's interface via redirected requests.

      Conclusion

      In this tutorial, we saw how to install phpMyAdmin on Ubuntu 18.04 running Nginx as the web server. We also covered advanced methods to secure a phpMyAdmin installation on Ubuntu, such as disabling root login, creating an extra layer of authentication, and using SSH tunneling to access a phpMyAdmin installation via local requests only.

      After completing this tutorial, you should be able to manage your MySQL databases from a reasonably secure web interface. This user interface exposes most of the functionality available via the MySQL command line. You can browse databases and schema, execute queries, and create new data sets and structures.



      Source link

      How To Set Up a Private Docker Registry on Top of DigitalOcean Spaces and Use It with DigitalOcean Kubernetes


      The author selected the Free and Open Source Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      A Docker registry is a storage and content delivery system for named Docker images, which are the industry standard for containerized applications. A private Docker registry allows you to securely share your images within your team or organization with more flexibility and control when compared to public ones. By hosting your private Docker registry directly in your Kubernetes cluster, you can achieve higher speeds, lower latency, and better availability, all while having control over the registry.

      The underlying registry storage is delegated to external drivers. The default storage system is the local filesystem, but you can swap this for a cloud-based storage driver. DigitalOcean Spaces is an S3-compatible object storage designed for developer teams and businesses that want a scalable, simple, and affordable way to store and serve vast amounts of data, and is very suitable for storing Docker images. It has a built-in CDN network, which can greatly reduce latency when frequently accessing images.

      In this tutorial, you’ll deploy your private Docker registry to your DigitalOcean Kubernetes cluster using Helm, backed up by DigitalOcean Spaces for storing data. You’ll create API keys for your designated Space, install the Docker registry to your cluster with custom configuration, configure Kubernetes to properly authenticate with it, and test it by running a sample deployment on the cluster. At the end of this tutorial, you’ll have a secure, private Docker registry installed on your DigitalOcean Kubernetes cluster.

      Prerequisites

      Before you begin this tutorial, you’ll need:

      • Docker installed on the machine that you’ll access your cluster from. For Ubuntu 18.04 visit How To Install and Use Docker on Ubuntu 18.04. You only need to complete the first step. Otherwise visit Docker’s website for other distributions.

      • A DigitalOcean Kubernetes cluster with your connection configuration configured as the kubectl default. Instructions on how to configure kubectl are shown under the Connect to your Cluster step shown when you create your cluster. To learn how to create a Kubernetes cluster on DigitalOcean, see Kubernetes Quickstart.

      • A DigitalOcean Space with API keys (access and secret). To learn how to create a DigitalOcean Space and API keys, see How To Create a DigitalOcean Space and API Key.

      • The Helm package manager installed on your local machine, and Tiller installed on your cluster. Complete steps 1 and 2 of the How To Install Software on Kubernetes Clusters with the Helm Package Manager. You only need to complete the first two steps.

      • The Nginx Ingress Controller and Cert-Manager installed on the cluster. For a guide on how to do this, see How to Set Up an Nginx Ingress with Cert-Manager on DigitalOcean Kubernetes.

      • A domain name with two DNS A records pointed to the DigitalOcean Load Balancer used by the Ingress. If you are using DigitalOcean to manage your domain’s DNS records, consult How to Manage DNS Records to create A records. In this tutorial, we’ll refer to the A records as registry.example.com and k8s-test.example.com.

      Step 1 — Configuring and Installing the Docker Registry

      In this step, you will create a configuration file for the registry deployment and install the Docker registry to your cluster with the given config using the Helm package manager.

      During the course of this tutorial, you will use a configuration file called chart_values.yaml to override some of the default settings for the Docker registry Helm chart. Helm calls its packages, charts; these are sets of files that outline a related selection of Kubernetes resources. You’ll edit the settings to specify DigitalOcean Spaces as the underlying storage system and enable HTTPS access by wiring up Let’s Encrypt TLS certificates.

      As part of the prerequisite, you would have created the echo1 and echo2 services and an echo_ingress ingress for testing purposes; you will not need these in this tutorial, so you can now delete them.

      Start off by deleting the ingress by running the following command:

      • kubectl delete -f echo_ingress.yaml

      Then, delete the two test services:

      • kubectl delete -f echo1.yaml && kubectl delete -f echo2.yaml

      The kubectl delete command accepts the file to delete when passed the -f parameter.

      Create a folder that will serve as your workspace:

      Navigate to it by running:

      Now, using your text editor, create your chart_values.yaml file:

      Add the following lines, ensuring you replace the highlighted lines with your details:

      chart_values.yaml

      ingress:
        enabled: true
        hosts:
          - registry.example.com
        annotations:
          kubernetes.io/ingress.class: nginx
          certmanager.k8s.io/cluster-issuer: letsencrypt-prod
          nginx.ingress.kubernetes.io/proxy-body-size: "30720m"
        tls:
          - secretName: letsencrypt-prod
            hosts:
              - registry.example.com
      
      storage: s3
      
      secrets:
        htpasswd: ""
        s3:
          accessKey: "your_space_access_key"
          secretKey: "your_space_secret_key"
      
      s3:
        region: your_space_region
        regionEndpoint: your_space_region.digitaloceanspaces.com
        secure: true
        bucket: your_space_name
      

      The first block, ingress, configures the Kubernetes Ingress that will be created as a part of the Helm chart deployment. The Ingress object makes outside HTTP/HTTPS routes point to internal services in the cluster, thus allowing communication from the outside. The overridden values are:

      • enabled: set to true to enable the Ingress.
      • hosts: a list of hosts from which the Ingress will accept traffic.
      • annotations: a list of metadata that provides further direction to other parts of Kubernetes on how to treat the Ingress. You set the Ingress Controller to nginx, the Let's Encrypt cluster issuer to the production variant (letsencrypt-prod), and tell the nginx controller to accept files with a max size of 30 GB, which is a sensible limit for even the largest Docker images.
      • tls: this subcategory configures Let's Encrypt HTTPS. You populate the hosts list that defines from which secure hosts this Ingress will accept HTTPS traffic with our example domain name.

      Then, you set the file system storage to s3 — the other available option would be filesystem. Here s3 indicates using a remote storage system compatible with the industry-standard Amazon S3 API, which DigitalOcean Spaces fulfills.

      In the next block, secrets, you configure keys for accessing your DigitalOcean Space under the s3 subcategory. Finally, in the s3 block, you configure the parameters specifying your Space.

      Save and close your file.

      Now, if you haven't already done so, set up your A records to point to the Load Balancer you created as part of the Nginx Ingress Controller installation in the prerequisite tutorial. To see how to set your DNS on DigitalOcean, see How to Manage DNS Records.

      Next, ensure your Space isn't empty. The Docker registry won't run at all if you don't have any files in your Space. To get around this, upload a file. Navigate to the Spaces tab, find your Space, click the Upload File button, and upload any file you'd like. You could upload the configuration file you just created.

      Empty file uploaded to empty Space

      Before installing anything via Helm, you need to refresh its cache. This will update the latest information about your chart repository. To do this run the following command:

      Now, you'll deploy the Docker registry chart with this custom configuration via Helm by running:

      • helm install stable/docker-registry -f chart_values.yaml --name docker-registry

      You'll see the following output:

      Output

      NAME: docker-registry ... NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/ConfigMap NAME DATA AGE docker-registry-config 1 1s ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE docker-registry-54df68fd64-l26fb 0/1 ContainerCreating 0 1s ==> v1/Secret NAME TYPE DATA AGE docker-registry-secret Opaque 3 1s ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE docker-registry ClusterIP 10.245.131.143 <none> 5000/TCP 1s ==> v1beta1/Deployment NAME READY UP-TO-DATE AVAILABLE AGE docker-registry 0/1 1 0 1s ==> v1beta1/Ingress NAME HOSTS ADDRESS PORTS AGE docker-registry registry.example.com 80, 443 1s NOTES: 1. Get the application URL by running these commands: https://registry.example.com/

      Helm lists all the resources it created as a result of the Docker registry chart deployment. The registry is now accessible from the domain name you specified earlier.

      You've configured and deployed a Docker registry on your Kubernetes cluster. Next, you will test the availability of the newly deployed Docker registry.

      Step 2 — Testing Pushing and Pulling

      In this step, you'll test your newly deployed Docker registry by pushing and pulling images to and from it. Currently, the registry is empty. To have something to push, you need to have an image available on the machine you're working from. Let's use the mysql Docker image.

      Start off by pulling mysql from the Docker Hub:

      Your output will look like this:

      Output

      Using default tag: latest latest: Pulling from library/mysql 27833a3ba0a5: Pull complete ... e906385f419d: Pull complete Digest: sha256:a7cf659a764732a27963429a87eccc8457e6d4af0ee9d5140a3b56e74986eed7 Status: Downloaded newer image for mysql:latest

      You now have the image available locally. To inform Docker where to push it, you'll need to tag it with the host name, like so:

      • sudo docker tag mysql registry.example.com/mysql

      Then, push the image to the new registry:

      • sudo docker push registry.example.com/mysql

      This command will run successfully and indicate that your new registry is properly configured and accepting traffic — including pushing new images. If you see an error, double check your steps against steps 1 and 2.

      To test pulling from the registry cleanly, first delete the local mysql images with the following command:

      • sudo docker rmi registry.example.com/mysql && sudo docker rmi mysql

      Then, pull it from the registry:

      • sudo docker pull registry.example.com/mysql

      This command will take a few seconds to complete. If it runs successfully, that means your registry is working correctly. If it shows an error, double check what you have entered against the previous commands.

      You can list Docker images available locally by running the following command:

      You'll see output listing the images available on your local machine, along with their ID and date of creation.

      Your Docker registry is configured. You've pushed an image to it and verified you can pull it down. Now let's add authentication so only certain people can access the code.

      Step 3 — Adding Account Authentication and Configuring Kubernetes Access

      In this step, you'll set up username and password authentication for the registry using the htpasswd utility.

      The htpasswd utility comes from the Apache webserver, which you can use for creating files that store usernames and passwords for basic authentication of HTTP users. The format of htpasswd files is username:hashed_password (one per line), which is portable enough to allow other programs to use it as well.

      To make htpasswd available on the system, you'll need to install it by running:

      • sudo apt install apache2-utils -y

      Note:
      If you're running this tutorial from a Mac, you'll need to use the following command to make htpasswd available on your machine:

      • docker run --rm -v ${PWD}:/app -it httpd htpasswd -b -c /app/htpasswd_file sammy password

      Create it by executing the following command:

      Add a username and password combination to htpasswd_file:

      • htpasswd -B htpasswd_file username

      Docker requires the password to be hashed using the bcrypt algorithm, which is why we pass the -B parameter. The bcrypt algorithm is a password hashing function based on Blowfish block cipher, with a work factor parameter, which specifies how expensive the hash function will be.

      Remember to replace username with your desired username. When run, htpasswd will ask you for the accompanying password and add the combination to htpasswd_file. You can repeat this command for as many users as you wish to add.

      Now, show the contents of htpasswd_file by running the following command:

      Select and copy the contents shown.

      To add authentication to your Docker registry, you'll need to edit chart_values.yaml and add the contents of htpasswd_file in the htpasswd variable.

      Open chart_values.yaml for editing:

      Find the line that looks like this:

      chart_values.yaml

        htpasswd: ""
      

      Edit it to match the following, replacing htpasswd_file_contents with the contents you copied from the htpasswd_file:

      chart_values.yaml

        htpasswd: |-
          htpasswd_file_contents
      

      Be careful with the indentation, each line of the file contents must have four spaces before it.

      Once you've added your contents, save and close the file.

      To propagate the changes to your cluster, run the following command:

      • helm upgrade docker-registry stable/docker-registry -f chart_values.yaml

      The output will be similar to that shown when you first deployed your Docker registry:

      Output

      Release "docker-registry" has been upgraded. Happy Helming! LAST DEPLOYED: ... NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/ConfigMap NAME DATA AGE docker-registry-config 1 3m8s ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE docker-registry-6c5bb7ffbf-ltnjv 1/1 Running 0 3m7s ==> v1/Secret NAME TYPE DATA AGE docker-registry-secret Opaque 4 3m8s ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE docker-registry ClusterIP 10.245.128.245 <none> 5000/TCP 3m8s ==> v1beta1/Deployment NAME READY UP-TO-DATE AVAILABLE AGE docker-registry 1/1 1 1 3m8s ==> v1beta1/Ingress NAME HOSTS ADDRESS PORTS AGE docker-registry registry.example.com 159.89.215.50 80, 443 3m8s NOTES: 1. Get the application URL by running these commands: https://registry.example.com/

      This command calls Helm and instructs it to upgrade an existing release, in your case docker-registry, with its chart defined in stable/docker-registry in the chart repository, after applying the chart_values.yaml file.

      Now, you'll try pulling an image from the registry again:

      • sudo docker pull registry.example.com/mysql

      The output will look like the following:

      Output

      Using default tag: latest Error response from daemon: Get https://registry.example.com/v2/mysql/manifests/latest: no basic auth credentials

      It correctly failed because you provided no credentials. This means that your Docker registry authorizes requests correctly.

      To log in to the registry, run the following command:

      • sudo docker login registry.example.com

      Remember to replace registry.example.com with your domain address. It will prompt you for a username and password. If it shows an error, double check what your htpasswd_file contains. You must define the username and password combination in the htpasswd_file, which you created earlier in this step.

      To test the login, you can try to pull again by running the following command:

      • sudo docker pull registry.example.com/mysql

      The output will look similar to the following:

      Output

      Using default tag: latest latest: Pulling from mysql Digest: sha256:f2dc118ca6fa4c88cde5889808c486dfe94bccecd01ca626b002a010bb66bcbe Status: Image is up to date for registry.example.com/mysql:latest

      You've now configured Docker and can log in securely. To configure Kubernetes to log in to your registry, run the following command:

      • sudo kubectl create secret generic regcred --from-file=.dockerconfigjson=/home/sammy/.docker/config.json --type=kubernetes.io/dockerconfigjson

      You will see the following output:

      Output

      secret/regcred created

      This command creates a secret in your cluster with the name regcred, takes the contents of the JSON file where Docker stores the credentials, and parses it as dockerconfigjson, which defines a registry credential in Kubernetes.

      You've used htpasswd to create a login config file, configured the registry to authenticate requests, and created a Kubernetes secret containing the login credentials. Next, you will test the integration between your Kubernetes cluster and registry.

      Step 4 — Testing Kubernetes Integration by Running a Sample Deployment

      In this step, you'll run a sample deployment with an image stored in the in-cluster registry to test the connection between your Kubernetes cluster and registry.

      In the last step, you created a secret, called regcred, containing login credentials for your private registry. It may contain login credentials for multiple registries, in which case you'll have to update the Secret accordingly.

      You can specify which secret Kubernetes should use when pulling containers in the pod definition by specifying imagePullSecrets. This step is necessary when the Docker registry requires authentication.

      You'll now deploy a sample Hello World image from your private Docker registry to your cluster. First, in order to push it, you'll pull it to your machine by running the following command:

      • sudo docker pull paulbouwer/hello-kubernetes:1.5

      Then, tag it by running:

      • sudo docker tag paulbouwer/hello-kubernetes:1.5 registry.example.com/paulbouwer/hello-kubernetes:1.5

      Finally, push it to your registry:

      • sudo docker push registry.example.com/paulbouwer/hello-kubernetes:1.5

      Delete it from your machine as you no longer need it locally:

      • sudo docker rmi registry.example.com/paulbouwer/hello-kubernetes:1.5

      Now, you'll deploy the sample Hello World application. First, create a new file, hello-world.yaml, using your text editor:

      Next, you'll define a Service and an Ingress to make the app accessible to outside of the cluster. Add the following lines, replacing the highlighted lines with your domains:

      hello-world.yaml

      apiVersion: extensions/v1beta1
      kind: Ingress
      metadata:
        name: hello-kubernetes-ingress
        annotations:
          kubernetes.io/ingress.class: nginx
          nginx.ingress.kubernetes.io/rewrite-target: /
      spec:
        rules:
        - host: k8s-test.example.com
          http:
            paths:
            - path: /
              backend:
                serviceName: hello-kubernetes
                servicePort: 80
      ---
      apiVersion: v1
      kind: Service
      metadata:
        name: hello-kubernetes
      spec:
        type: NodePort
        ports:
        - port: 80
          targetPort: 8080
        selector:
          app: hello-kubernetes
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: hello-kubernetes
      spec:
        replicas: 3
        selector:
          matchLabels:
            app: hello-kubernetes
        template:
          metadata:
            labels:
              app: hello-kubernetes
          spec:
            containers:
            - name: hello-kubernetes
              image: registry.example.com/paulbouwer/hello-kubernetes:1.5
              ports:
              - containerPort: 8080
            imagePullSecrets:
            - name: regcred
      

      First, you define the Ingress for the Hello World deployment, which you will route through the Load Balancer that the Nginx Ingress Controller owns. Then, you define a service that can access the pods created in the deployment. In the actual deployment spec, you specify the image as the one located in your registry and set imagePullSecrets to regcred, which you created in the previous step.

      Save and close the file. To deploy this to your cluster, run the following command:

      • kubectl apply -f hello-world.yaml

      You'll see the following output:

      Output

      ingress.extensions/hello-kubernetes-ingress created service/hello-kubernetes created deployment.apps/hello-kubernetes created

      You can now navigate to your test domain — the second A record, k8s-test.example.com in this tutorial. You will see the Kubernetes Hello world! page.

      Hello World page

      The Hello World page lists some environment information, like the Linux kernel version and the internal ID of the pod the request was served from. You can also access your Space via the web interface to see the images you've worked with in this tutorial.

      If you want to delete this Hello World deployment after testing, run the following command:

      • kubectl delete -f hello-world.yaml

      You've created a sample Hello World deployment to test if Kubernetes is properly pulling images from your private registry.

      Conclusion

      You have now successfully deployed your own private Docker registry on your DigitalOcean Kubernetes cluster, using DigitalOcean Spaces as the storage layer underneath. There is no limit to how many images you can store, Spaces can extend infinitely, while at the same time providing the same security and robustness. In production, though, you should always strive to optimize your Docker images as much as possible, take a look at the How To Optimize Docker Images for Production tutorial.



      Source link

      How To Build a Search Bar with RxJS


      The author selected Mozilla Foundation to receive a donation as part of the Write for DOnations program.

      Introduction

      Reactive Programming is a paradigm concerned with asynchronous data streams, in which the programming model considers everything to be a stream of data spread over time. This includes keystrokes, HTTP requests, files to be printed, and even elements of an array, which can be considered to be timed over very small intervals. This makes it a perfect fit for JavaScript as asynchronous data is common in the language.

      RxJS is a popular library for reactive programming in JavaScript. ReactiveX, the umbrella under which RxJS lies, has its extensions in many other languages like Java, Python, C++, Swift, and Dart. RxJS is also widely used by libraries like Angular and React.

      RxJS’s implementation is based on chained functions that are aware and capable of handling data over a range of time. This means that one could implement virtually every aspect of RxJS with nothing more than functions that receive a list of arguments and callbacks, and then execute them when signaled to do so. The community around RxJS has done this heavy lifting, and the result is an API that you can directly use in any application to write clean and maintainable code.

      In this tutorial, you will use RxJS to build a feature-rich search bar that returns real-time results to users. You will also use HTML and CSS to format the search bar. The end result will look this this:

      Demonstration of Search Bar

      Something as common and seemingly simple as a search bar needs to have various checks in place. This tutorial will show you how RxJS can turn a fairly complex set of requirements into code that is manageable and easy to understand.

      Prerequisites

      Before you begin this tutorial you’ll need the following:

      The full code for the tutorial is available on Github.

      In this step, you will create and style the search bar with HTML and CSS. The code will use a few common elements from Bootstrap to speed up the process of structuring and styling the page so you can focus on adding custom elements. Bootstrap is a CSS framework that contains templates for common elements like typography, forms, buttons, navigation, grids, and other interface components. Your application will also use Animate.css to add animation to the search bar.

      You will start start by creating a file named search-bar.html with nano or your favorite text editor:

      Next, create the basic structure for your application. Add the following HTML to the new file:

      search-bar.html

      <!DOCTYPE html>
      <html>
      
        <head>
          <title>RxJS Tutorial</title>
          <!-- Load CSS -->
      
          <!-- Load Rubik font -->
      
          <!-- Add Custom inline CSS -->
      
        </head>
      
        <body>
            <!-- Content -->
      
            <!-- Page Header and Search Bar -->
      
            <!-- Results -->
      
            <!-- Load External RxJS -->
      
            <!-- Add custom inline JavaScript -->
            <script>
      
            </script>
        </body>
      
      </html>
      

      As you need CSS from the entire Bootstrap library, go ahead and load the CSS for Bootstrap and Animate.css.

      Add the following code under the Load CSS comment:

      search-bar.html

      ...
      <!-- Load CSS -->
          <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
          <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" />
      ...
      

      This tutorial will use a custom font called Rubik from the Google Fonts library to style the search bar. Load the font by adding the highlighted code under the Load Rubik font comment:

      search-bar.html

      ...
      <!-- Load Rubik font -->
          <link href="https://fonts.googleapis.com/css?family=Rubik" rel="stylesheet">
      ...
      

      Next, add the custom CSS to the page under the Add Custom inline CSS comment. This will make sure that the headings, search bar, and the results on the page are easy to read and use.

      search-bar.html

      ...
      <!-- Add Custom inline CSS -->
          <style>
            body {
              background-color: #f5f5f5;
              font-family: "Rubik", sans-serif;
            }
      
            .search-container {
              margin-top: 50px;
            }
            .search-container .search-heading {
              display: block;
              margin-bottom: 50px;
            }
            .search-container input,
            .search-container input:focus {
              padding: 16px 16px 16px;
              border: none;
              background: rgb(255, 255, 255);
              box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1) !important;
            }
      
            .results-container {
              margin-top: 50px;
            }
            .results-container .list-group .list-group-item {
              background-color: transparent;
              border-top: none !important;
              border-bottom: 1px solid rgba(236, 229, 229, 0.64);
            }
      
            .float-bottom-right {
              position: fixed;
              bottom: 20px;
              left: 20px;
              font-size: 20px;
              font-weight: 700;
              z-index: 1000;
            }
            .float-bottom-right .info-container .card {
              display: none;
            }
            .float-bottom-right .info-container:hover .card,
            .float-bottom-right .info-container .card:hover {
              display: block;
            }
          </style>
      ...
      

      Now that you have all of the styles in place, add the HTML that will define the header and the input bar under the Page Header and Search Bar comment:

      search-bar.html

      ...
      <!-- Content -->
      <!-- Page Header and Search Bar -->
            <div class="container search-container">
              <div class="row justify-content-center">
                <div class="col-md-auto">
                  <div class="search-heading">
                    <h2>Search for Materials Published by Author Name</h2>
                    <p class="text-right">powered by <a href="https://www.crossref.org/">Crossref</a></p>
                  </div>
                </div>
              </div>
              <div class="row justify-content-center">
                <div class="col-sm-8">
                  <div class="input-group input-group-md">
                    <input id="search-input" type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus>
                  </div>
                </div>
              </div>
            </div>
      ...
      

      This uses the grid system from Bootstrap to structure the page header and the search bar. You have assigned a search-input identifier to the search bar, which you will use to bind to a listener later in the tutorial.

      Next, you will create a location to display the results of the search. Under the Results comment, create a div with the response-list identifier to add the results later in the tutorial:

      search-bar.html

      ...
      <!-- Results -->
            <div class="container results-container">
              <div class="row justify-content-center">
                <div class="col-sm-8">
                  <ul id="response-list" class="list-group list-group-flush"></ul>
                </div>
              </div>
            </div>
      ...
      

      At this point, the search-bar.html file will look like this:

      search-bar.html

      <!DOCTYPE html>
      <html>
      
        <head>
          <title>RxJS Tutorial</title>
          <!-- Load CSS -->
          <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
          <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" />
      
          <!-- Load Rubik font -->
          <link href="https://fonts.googleapis.com/css?family=Rubik" rel="stylesheet">
      
          <!-- Add Custom inline CSS -->
          <style>
            body {
              background-color: #f5f5f5;
              font-family: "Rubik", sans-serif;
            }
      
            .search-container {
              margin-top: 50px;
            }
            .search-container .search-heading {
              display: block;
              margin-bottom: 50px;
            }
            .search-container input,
            .search-container input:focus {
              padding: 16px 16px 16px;
              border: none;
              background: rgb(255, 255, 255);
              box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1) !important;
            }
      
            .results-container {
              margin-top: 50px;
            }
            .results-container .list-group .list-group-item {
              background-color: transparent;
              border-top: none !important;
              border-bottom: 1px solid rgba(236, 229, 229, 0.64);
            }
      
            .float-bottom-right {
              position: fixed;
              bottom: 20px;
              left: 20px;
              font-size: 20px;
              font-weight: 700;
              z-index: 1000;
            }
            .float-bottom-right .info-container .card {
              display: none;
            }
            .float-bottom-right .info-container:hover .card,
            .float-bottom-right .info-container .card:hover {
              display: block;
            }
          </style>
        </head>
      
        <body>
            <!-- Content -->
            <!-- Page Header and Search Bar -->
            <div class="container search-container">
              <div class="row justify-content-center">
                <div class="col-md-auto">
                  <div class="search-heading">
                    <h2>Search for Materials Published by Author Name</h2>
                    <p class="text-right">powered by <a href="https://www.crossref.org/">Crossref</a></p>
                  </div>
                </div>
              </div>
              <div class="row justify-content-center">
                <div class="col-sm-8">
                  <div class="input-group input-group-md">
                    <input id="search-input" type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus>
                  </div>
                </div>
              </div>
            </div>
      
            <!-- Results -->
            <div class="container results-container">
              <div class="row justify-content-center">
                <div class="col-sm-8">
                  <ul id="response-list" class="list-group list-group-flush"></ul>
                </div>
              </div>
            </div>
      
            <!-- Load RxJS -->
      
            <!-- Add custom inline JavaScript -->
            <script>
      
            </script>
        </body>
      
      </html>
      

      In this step, you've laid out the basic structure for your search bar with HTML and CSS. In the next step, you will write a JavaScript function that will accept search terms and return results.

      Step 2 — Writing the JavaScript

      Now that you have the search bar formatted, you are ready to write the JavaScript code that will act as a foundation for the RxJS code that you'll write later in this tutorial. This code will work with RxJS to accept search terms and return results.

      Since you won't need the functionalities that Bootstrap and JavaScript provide in this tutorial, you aren't going to load them. However, you will be using RxJS. Load the RxJS library by adding the following under the Load RxJS comment:

      search-bar.html

      ...
      <!-- Load RxJS -->
          <script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js"></script>
      ...
      

      Now you will store references of the div from the HTML to which the results will be added. Add the highlighted JavaScript code in the <script> tag under the Add custom inline JavaScript comment:

      search-bar.html

      ...
      <!-- Add custom inline JavaScript -->
      <script>
              const output = document.getElementById("response-list");
      
      </script>
      ...
      

      Next, add the code to convert the JSON response from the API into the HTML elements to display on the page. This code will first clear the contents of the search bar and then set a delay for the search result animation.

      Add the highlighted function between the <script> tags:

      search-bar.html

      ...
      <!-- Add custom inline JavaScript -->
      <script>
          const output = document.getElementById("response-list");
      
              function showResults(resp) {
              var items = resp['message']['items']
              output.innerHTML = "";
              animationDelay = 0;
              if (items.length == 0) {
                output.innerHTML = "Could not find any :(";
              } else {
                items.forEach(item => {
                  resultItem = `
                  <div class="list-group-item animated fadeInUp" style="animation-delay: ${animationDelay}s;">
                    <div class="d-flex w-100 justify-content-between">
      <^>                <h5 class="mb-1">${(item['title'] && item['title'][0]) || "&lt;Title not available&gt;"}</h5>
                    </div>
                    <p class="mb-1">${(item['container-title'] && item['container-title'][0]) || ""}</p>
                    <small class="text-muted"><a href="${item['URL']}" target="_blank">${item['URL']}</a></small>
                    <div> 
                      <p class="badge badge-primary badge-pill">${item['publisher'] || ''}</p>
                      <p class="badge badge-primary badge-pill">${item['type'] || ''}</p> 
                   </div>
                  </div>
                  `;
                  output.insertAdjacentHTML("beforeend", resultItem);
                  animationDelay += 0.1; 
      
                });
              }
            }
      
      </script>
      ...
      

      The code block starting with if is a conditional loop that checks for search results, and displays a message if no results were found. If results are found, then the forEach loop will provide the results with an animation to the user.

      In this step, you laid out the base for the RxJS by writing out a function that can accept results and return it on the page. In the next step, you will make the search bar functional.

      Step 3 — Setting Up a Listener

      RxJS is concerned with data streams, which in this project is a series of characters that the user enters in to the input element, or search bar. In this step, you will add a listener on the input element to listen for updates.

      First, take note of the search-input identifier that you added earlier in the tutorial:

      search-bar.html

      ...
      <input id="search-input" type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus>
      ...
      

      Next, create a variable that will hold references for the search-input element. This will become the Observable that the code will use to listen for input events. Observables are a collection of future values or events that an Observer listens to, and are also known as callback functions.

      Add the highlighted line in the <script> tag under the JavaScript from the previous step:

      search-bar.html

      ...
            output.insertAdjacentHTML("beforeend", resultItem);
            animationDelay += 0.1; 
      
          });
        }
      }
      
      
            let searchInput = document.getElementById("search-input");
      ...
      

      Now that you've added a variable to reference input, you will use the fromEvent operator to listen for events. This will add a listener on a DOM, or Document Object Model, element for a certain kind of event. A DOM element could be a html, body, div, or img element on a page. In this case, your DOM element is the search bar.

      Add the following highlighted line under your searchInput variable to pass your parameters to fromEvent. Your searchInput DOM element is the first parameter. This is followed by the input event as the second parameter, which is the event type the code will listen for.

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
      ...
      

      Now that your listener is set up, your code will receive a notification whenever any updates take place on the input element. In the next step you will use operators to take action on such events.

      Step 4 — Adding Operators

      Operators are pure functions with one task—to perform an operation on data. In this step, you will use operators to perform various tasks such as buffering the input parameter, making HTTP requests, and filtering results.

      You will first make sure that the results update in real-time as the user enters queries. To achieve this, you will use the DOM input event from the previous step. The DOM input event contains various details, but for this tutorial you are interested in values typed into the target element. Add the following code to use the pluck operator to take an object and return the value at the specified key:

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
              .pluck('target', 'value')
      ...
      

      Now that the events are in the necessary format, you will set the search-term minimum to three characters. In many cases, anything less than three characters will not yield relevant results, or the user may still be in the process of typing.

      You will use the filter operator to set the minimum. It will pass the data further down the stream if it satisfies the specified condition. Set the length condition to greater than 2 to require at least three characters.

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
              .pluck('target', 'value')
              .filter(searchTerm => searchTerm.length > 2)
      ...
      

      You will also make sure that requests are only sent in at 500ms intervals to ease up the load on the API server. To do this, you will use the debounceTime operator to maintain a minimum specified interval between each event that it passes through the stream. Add the highlighted code under the filter operator:

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
              .pluck('target', 'value')
              .filter(searchTerm => searchTerm.length > 2)
              .debounceTime(500)
      ...
      

      The application should also ignore the search term if there have been no changes since the last API call. This will optimize the application by further reducing the number of sent API calls.

      As an example, a user may type super cars, delete the last character (making the term super car), and then add the deleted character back to revert the term back to super cars. As a result, the term did not change, and therefore the search results should not change. In such cases it makes sense to not perform any operations.

      You will use the distinctUntilChanged operator to configure this. This operator remembers the previous data that was passed through the stream and passes another only if it is different.

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
              .pluck('target', 'value')
              .filter(searchTerm => searchTerm.length > 2)
              .debounceTime(500)
              .distinctUntilChanged()
      ...
      

      Now that you have regulated the inputs from the user, you will add the code that will query the API with the search term. To do this, you will use the RxJS implementation of AJAX. AJAX makes API calls asynchronously in the background on a loaded page. AJAX will allow you to avoid reloading the page with results for new search terms and also update the results on the page by fetching the data from the server.

      Next, add the code to use switchMap to chain AJAX to your application. You will also use map to map the input to an output. This code will apply the function passed to it to every item emitted by an Observable.

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
              .pluck('target', 'value')
              .filter(searchTerm => searchTerm.length > 2)
              .debounceTime(500)
              .distinctUntilChanged()
              .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`)
                .map(resp => ({
                    "status" : resp["status"] == 200,
                    "details" : resp["status"] == 200 ? resp["response"] : [],
                    "result_hash": Date.now()
                  })
                )
              )
      ...
      

      This code breaks the API response into three parts:

      • status: The HTTP status code returned by the API server. This code will only accept 200, or successful, responses.
      • details: The actual response data received. This will contain the results for the queried search term.
      • result_hash: A hash value of the responses returned by the API server, which for the purpose of this tutorial is a UNIX time-stamp. This is a hash of results that changes when the results change. The unique hash value will allow the application to determine if the results have changed and should be updated.

      Systems fail and your code should be prepared to handle errors. To handle errors that may happen in the API call, use the filter operator to only accept successful responses:

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
              .pluck('target', 'value')
              .filter(searchTerm => searchTerm.length > 2)
              .debounceTime(500)
              .distinctUntilChanged()
              .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`)
                .map(resp => ({
                    "status" : resp["status"] == 200,
                    "details" : resp["status"] == 200 ? resp["response"] : [],
                    "result_hash": Date.now()
                  })
                )
              )
              .filter(resp => resp.status !== false)
      ...
      

      Next, you will add code to only update the DOM if changes are detected in the response. DOM updates can be a resource-heavy operation, so reducing the number of updates will have a positive impact on the application. Since the result_hash will only change when a response changes, you will use it to implement this functionality.

      To do this, use the distinctUntilChanged operator like before. The code will use it to only accept user input when the key has changed.

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
              .pluck('target', 'value')
              .filter(searchTerm => searchTerm.length > 2)
              .debounceTime(500)
              .distinctUntilChanged()
              .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`)
                .map(resp => ({
                    "status" : resp["status"] == 200,
                    "details" : resp["status"] == 200 ? resp["response"] : [],
                    "result_hash": Date.now()
                  })
                )
              )
              .filter(resp => resp.status !== false)
              .distinctUntilChanged((a, b) => a.result_hash === b.result_hash)
      ...
      

      You previously used the distinctUntilChanged operator to see if the entirety of the data had changed, but in this instance, you check for an updated key in the response. Comparing the entire response would be resource-costly when compared to identifying changes in a single key. Since the key hash is representative of the whole response, it can confidently be used to identify response changes.

      The function accepts two objects, the previous value that it had seen and the new value. We check the hash from these two objects and return True when these two values match, in which case the data is filtered out and not passed further in the pipeline.

      In this step, you created a pipeline that receives a search term entered by the user and then performs various checks on it. After the checks are complete, it makes an API call and returns the response in a format that displays results back to the user. You optimized the resource usage on both the client and server side by limiting API calls when necessary. In the next step, you will configure the application to start listening on the input element, and pass the results to the function that will render it on the page.

      Step 5 — Activating Everything with a Subscription

      subscribe is the final operator of the link that enables the observer to see data events emitted by the Observable. It implements the following three methods:

      • onNext: This specifies what to do when an event is received.
      • onError: This is responsible for handling errors. Calls to onNext and onCompleted will not be made once this method is called.
      • onCompleted: This method is called when onNext has been called for the final time. There would be no more data that will be passed in the pipeline.

      This signature of a subscriber is what enables one to achieve lazy execution, which is the ability to define an Observable pipeline and set it in motion only when you subscribe to it. You won't use this example in your code, but the following shows you how an Observable can be subscribed to:

      Next, subscribe to the Observable and route the data to the method that is responsible for rendering it in the UI.

      search-bar.html

      ...
            let searchInput = document.getElementById("search-input");
            Rx.Observable.fromEvent(searchInput, 'input')
              .pluck('target', 'value')
              .filter(searchTerm => searchTerm.length > 2)
              .debounceTime(500)
              .distinctUntilChanged()
              .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`)
                .map(resp => ({
                    "status" : resp["status"] == 200,
                    "details" : resp["status"] == 200 ? resp["response"] : [],
                    "result_hash": Date.now()
                  })
                )
              )
              .filter(resp => resp.status !== false)
              .distinctUntilChanged((a, b) => a.result_hash === b.result_hash)
              .subscribe(resp => showResults(resp.details));
      ...
      

      Save and close the file after making these changes.

      Now that you've completed writing the code, you are ready to view and test your search bar. Double-click the search-bar.html file to open it in your web browser. If the code was entered in correctly, you will see your search bar.

      The completed search bar

      Type content in your search bar to test it out.

      A gif of content being entered into the search bar, showing that two characters won't return any results.

      In this step, you subscribed to the Observable to activate your code. You now have a stylized and functioning search bar application.

      Conclusion

      In this tutorial, you created a feature-rich search bar with RxJS, CSS, and HTML that provides real-time results to users. The search bar requires a minimum of three characters, updates automatically, and is optimized for both the client and the API server.

      What could be considered a complex set of requirements was created with 18 lines of RxJS code. The code is not only reader-friendly, but it is also much cleaner than a standalone JavaScript implementation. This means that your code will be easier to understand, update, and maintain in the future.

      To read more about using RxJS, check out the official API documentation.



      Source link