<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://uace.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://uace.github.io/" rel="alternate" type="text/html"/><updated>2025-07-09T20:22:18+00:00</updated><id>https://uace.github.io/feed.xml</id><title type="html">UAce</title><subtitle>Yueh&apos;s personal website created with Jekyll and hosted on GitHub. It uses HTML5, SASS, Bootstrap 4, JavaScript and JQuery.</subtitle><entry><title type="html">How to Setup Ambassador Edge Stack’s Automatic TLS with ACME</title><link href="https://uace.github.io/tutorial/how-to-setup-ambassador-edge-stack-s-automatic-tls-with-acme" rel="alternate" type="text/html" title="How to Setup Ambassador Edge Stack’s Automatic TLS with ACME"/><published>2021-04-13T04:14:00+00:00</published><updated>2021-04-13T04:14:00+00:00</updated><id>https://uace.github.io/tutorial/how-to-setup-ambassador-edge-stack-s-automatic-tls-with-acme</id><content type="html" xml:base="https://uace.github.io/tutorial/how-to-setup-ambassador-edge-stack-s-automatic-tls-with-acme"><![CDATA[<p>At my current company Ormuco Inc., we use <strong>Ambassador Edge Stack</strong> (<a href="https://www.getambassador.io/">AES</a>) as our Kubernetes-native API Gateway with HTTPS enabled and TLS termination. Until recently, we had been generating certificates using <strong>Certbot</strong> and renewing them with automated scripts.</p> <p>In this article, I will walk you through a better way to manage certificates in Kubernetes using Ambassador Edge Stack’s automatic TLS with ACME.</p> <h2 id="prerequisites">Prerequisites</h2> <p>This tutorial requires you have the following:</p> <ul> <li>A Kubernetes cluster</li> <li>A domain name</li> <li>Ambassador Edge Stack deployed in your cluster (see <a href="https://www.getambassador.io/docs/edge-stack/latest/topics/install/#img-classos-logo-srcimageskubernetespng-install-via-kubernetes-yaml">here</a>)</li> <li>Your domain name pointing to the Ambassador LoadBalancer’s external IP</li> </ul> <h2 id="what-is-ambassador-automatic-tls">What is Ambassador Automatic TLS?</h2> <blockquote> <p>The <strong>Ambassador Edge Stack</strong> has simple and easy built-in support for automatically using ACME to create and renew TLS certificates; configured by the Host resource. However, it only supports ACME’s <strong>http-01 challenge</strong>; if you require more flexible certificate management (such as using ACME’s <strong>dns-01 challenge</strong>, or using a non-ACME certificate source), the Ambassador Edge stack also supports using external certificate management tools.</p> <p>One such tool is <strong>Jetstack</strong>’s cert-manager, which is a general-purpose tool for managing certificates in Kubernetes. Cert-manager will automatically create and renew TLS certificates and store them as Kubernetes secrets for easy use in a cluster. The Ambassador Edge Stack will automatically watch for secret changes and reload certificates upon renewal.</p> </blockquote> <p>Essentially, we can deploy Cert-Manager to manage certificates in Kubernetes for us. Ambassador only supports HTTP-01 challenge but it’s possible to perform DNS-01 challenge using Cert-Manager.</p> <p><em><strong>Note</strong>: We use GoDaddy domain names and it is not a supported DNS Provider (see <a href="https://cert-manager.io/docs/configuration/acme/dns01/#supported-dns01-providers">list of supported providers</a>). There are several Cert-Manager Godaddy Webhook implementations online but they don’t seem to be well maintained so I decided to stick with HTTP-01 challenge.</em></p> <h2 id="how-to-setup-automatic-tls-with-acme">How to Setup Automatic TLS with ACME?</h2> <p>For tutorial, I will be using an arbitrary email <ins>my-email@gmail.com</ins> and <strong>Let’s Encrypt</strong> to Issue a certificate for an arbitrary domain name <ins>dev.mydomain.com</ins>.</p> <h4 id="install-the-cert-manager-tool-with-kubectl">Install the Cert-Manager tool with kubectl</h4> <p>Let’s start by installing the Cert-Manager tool that will manage our certificates.</p> <pre><code class="language-Bash"># Install Custom Resource Definitions and Cert-Manager
kubectl apply -f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml
</code></pre> <p><em><strong>Note</strong>: You can also install Cert-Manager with Helm (see <a href="https://cert-manager.io/docs/installation/kubernetes/#installing-with-helm">here</a>)</em></p> <h4 id="create-a-clusterissuer-resource">Create a ClusterIssuer resource</h4> <blockquote> <p>An Issuer or ClusterIssuer identifies which Certificate Authority cert-manager will use to issue a certificate. Issuer is a namespaced resource allowing you to use different CAs in each namespace, a ClusterIssuer is used to issue certificates in any namespace. Configuration depends on which ACME challenge you are using.</p> </blockquote> <p>Once the Cert-manager deployments are completed, you can create a ClusterIssuer (global) or an Issuer (namespaced) resource. In this case, we are using <strong>Let’s Encrypt</strong>.</p> <pre><code class="language-YAML">---
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    # Replace this email address with your own.
    # Let's Encrypt will use this to contact you about expiring
    # certificates, and issues related to your account.
    email: my-email@gmail.com
    # ACME URL, you can use the URL for Staging environment to Issue untrusted certificates
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      # Secret resource that will be used to store the account's private key.
      name: issuer-account-private-key
    solvers:
    # Define the solver to perform HTTP-01 challenge
    - http01:
        ingress:
          class: nginx
      selector: {}
</code></pre> <h4 id="create-a-certificate-resource">Create a Certificate resource</h4> <blockquote> <p>A Certificate is a namespaced resource that specifies fields that are used to generated certificate signing requests which are then fulfilled by the issuer type you have referenced. Certificates specify which issuer they want to obtain the certificate from by specifying the certificate.spec.issuerRef field.</p> </blockquote> <p>Once the Issuer is ready, you can create a Certificate resource which will send a request to issue a new certificate.</p> <pre><code class="language-YAML">---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: dev.mydomain.com
  # Cert-manager will put the resulting Secret in the same Kubernetes 
  # namespace as the Certificate. You should create the certificate in 
  # whichever namespace you want to configure a Host.
spec:
  secretName: dev.mydomain.com
  issuerRef:
    # Name of ClusterIssuer
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - dev.mydomain.com
</code></pre> <p>After applying this template, you should see the following events:</p> <div class="language-console highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="gp">$</span><span class="w"> </span>kubectl get events <span class="nt">-n</span> default <span class="c"># The namespace in which you created your Certificate resource</span>
<span class="go">14m         Normal    cert-manager.io     certificaterequest/dev.mydomain.com-qrfxs               Certificate request has been approved by cert-manager.io
14m         Normal    Issuing             certificate/dev.mydomain.com                            Issuing certificate as Secret does not exist
14m         Normal    Generated           certificate/dev.mydomain.com                            Stored new private key in temporary Secret resource "dev.mydomain.com-lrdk6"
14m         Normal    Requested           certificate/dev.mydomain.com                            Created new CertificateRequest resource "dev.mydomain.com-qrfxs"
14m         Normal    Created             order/dev.mydomain.com-qrfxs-820390478                  Created Challenge resource "dev.mydomain.com-qrfxs-820390478-3681158932" for domain "dev.mydomain.com"
</span><span class="gp">&lt;unknown&gt;</span><span class="w">   </span>Normal    Scheduled           pod/cm-acme-http-solver-fbhcs                           Successfully assigned default/cm-acme-http-solver-fbhcs to the-name-of-some-node-1
<span class="go">14m         Normal    Presented           challenge/dev.mydomain.com-qrfxs-820390478-3681158932   Presented challenge using HTTP-01 challenge mechanism
14m         Normal    Started             challenge/dev.mydomain.com-qrfxs-820390478-3681158932   Challenge scheduled for processing
14m         Normal    Pulling             pod/cm-acme-http-solver-fbhcs                           Pulling image "quay.io/jetstack/cert-manager-acmesolver:v1.3.1"
13m         Normal    Pulled              pod/cm-acme-http-solver-fbhcs                           Successfully pulled image "quay.io/jetstack/cert-manager-acmesolver:v1.3.1"
13m         Normal    Started             pod/cm-acme-http-solver-fbhcs                           Started container acmesolver
13m         Normal    Created             pod/cm-acme-http-solver-fbhcs                           Created container acmesolver
</span></code></pre></div></div> <h4 id="create-a-mapping-and-service-resource-for-http-challenge">Create a Mapping and Service resource for HTTP challenge</h4> <p>At this point, Cert-manager will have created a temporary pod named <code class="language-html highlighter-rouge">cm-acme-http-solver-xxxx</code> but no certificate has been issued. You will need to create a Mapping resource to allow Ambassador to reach the http-01 challenge solver via <code class="language-html highlighter-rouge">http://dev.mydomain.com/.well-known/acme-challenge/<span class="nt">&lt;some-token&gt;</span></code>.</p> <pre><code class="language-YAML">---
apiVersion: getambassador.io/v2
kind: Mapping
metadata:
  name: acme-challenge-mapping
spec:
  prefix: /.well-known/acme-challenge/
  rewrite: ""
  service: acme-challenge-service
---
apiVersion: v1
kind: Service
metadata:
  name: acme-challenge-service
spec:
  ports:
  - port: 80
    targetPort: 8089
  selector:
    acme.cert-manager.io/http01-solver: "true"
</code></pre> <p>After applying the template, you will need to wait a several minutes (about 10 minutes) before cert-manager retries the challenge and issues a certificate. You should see the following events:</p> <div class="language-console highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="gp">$</span><span class="w"> </span>kubectl get events <span class="nt">-n</span> default <span class="c"># The namespace in which you created your Certificate resource</span>
<span class="go">6m38s       Normal    Killing             pod/cm-acme-http-solver-fbhcs                           Stopping container acmesolver
6m38s       Normal    DomainVerified      challenge/dev.mydomain.com-qrfxs-820390478-3681158932   Domain "dev.mydomain.com" verified with "HTTP-01" validation
6m37s       Normal    Complete            order/dev.mydomain.com-qrfxs-820390478                  Order completed successfully
6m37s       Normal    Issuing             certificate/dev.mydomain.com                            The certificate has been successfully issued
6m37s       Normal    CertificateIssued   certificaterequest/dev.mydomain.com-qrfxs               Certificate fetched from issuer successfully
</span></code></pre></div></div> <h4 id="create-a-host-resource-for-your-domain-name">Create a Host resource for your domain name</h4> <p>After the certificate was successfully issued, there should be a TLS secret called <code class="language-html highlighter-rouge">dev.mydomain.com</code>(name is defined by <strong>secretName</strong> in the Certificate resource). Then, you can create a Host resource. It will register your ACME account, read the certificate from the TLS secret and use that to terminate TLS on your domain.</p> <pre><code class="language-YAML">---
apiVersion: getambassador.io/v2
kind: Host
metadata:
  name: dev.mydomain.com
  namespace: default
spec:
  acmeProvider:
    authority: 'https://acme-v02.api.letsencrypt.org/directory'
    email: my-email@gmail.com
  ambassadorId:
    - default
  hostname: dev.mydomain.com
  selector:
    matchLabels:
      hostname: dev.mydomain.com
  tlsSecret:
    name: dev.mydomain.com # The secretName defined in your Certificate resource
</code></pre> <p>You should see the following events:</p> <div class="language-console highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="gp">$</span><span class="w"> </span>kubectl get events <span class="nt">-n</span> default <span class="c"># The namespace in which you created your Host resource</span>
<span class="go">10s      Normal    Pending        host/dev.mydomain.com        waiting for Host DefaultsFilled change to be reflected in snapshot
8s       Normal    Pending        host/dev.mydomain.com        creating private key Secret
8s       Normal    Pending        host/dev.mydomain.com        waiting for private key Secret creation to be reflected in snapshot
6s       Normal    Pending        host/dev.mydomain.com        waiting for Host status change to be reflected in snapshot
4s       Normal    Pending        host/dev.mydomain.com        registering ACME account
3s       Normal    Pending        host/dev.mydomain.com        waiting for Host ACME account registration change to be reflected in snapshot
3s       Normal    Pending        host/dev.mydomain.com        ACME account registered
1s       Normal    Pending        host/dev.mydomain.com        waiting for TLS Secret update to be reflected in snapshot
1s       Normal    Pending        host/dev.mydomain.com        updating TLS Secret
0s       Normal    Ready          host/dev.mydomain.com        Host with ACME-provisioned TLS certificate marked Ready
</span></code></pre></div></div> <h2 id="conclusion">Conclusion</h2> <p>Ambassador Edge Stack automatically enables TLS termination/HTTPs and you can easily configure it to completely manage TLS by requesting a certificate from a Certificate Authority(CA) instead of generating and managing certificates yourself!</p> <p>🐢</p> <h2 id="references">References</h2> <ul> <li><strong><a href="https://www.getambassador.io/docs/edge-stack/latest/topics/running/tls/">Ambassador Transport Layer Security (TLS)</a></strong></li> <li><strong><a href="https://www.getambassador.io/docs/edge-stack/latest/topics/running/host-crd">Ambassador ACME Support</a></strong></li> <li><strong><a href="https://www.getambassador.io/docs/edge-stack/latest/howtos/cert-manager/#http-01-challenge">Ambassador and Cert-Manager HTTP-01 Challenge</a></strong></li> </ul>]]></content><author><name>Yu-Yueh Liu</name></author><category term="Tutorial"/><category term="Ambassador Edge Stack"/><category term="Kubernetes"/><category term="DevOps"/><summary type="html"><![CDATA[At my current company Ormuco Inc., we use Ambassador Edge Stack (AES) as our Kubernetes-native API Gateway with HTTPS enabled and TLS termination. Until recently, we had been generating certificates using Certbot and renewing them with automated scripts. In this article, I will walk you through a better way to manage certificates in Kubernetes using Ambassador Edge Stack’s automatic TLS with ACME. Prerequisites This tutorial requires you have the following: A Kubernetes cluster A domain name Ambassador Edge Stack deployed in your cluster (see here) Your domain name pointing to the Ambassador LoadBalancer’s external IP What is Ambassador Automatic TLS? The Ambassador Edge Stack has simple and easy built-in support for automatically using ACME to create and renew TLS certificates; configured by the Host resource. However, it only supports ACME’s http-01 challenge; if you require more flexible certificate management (such as using ACME’s dns-01 challenge, or using a non-ACME certificate source), the Ambassador Edge stack also supports using external certificate management tools. One such tool is Jetstack’s cert-manager, which is a general-purpose tool for managing certificates in Kubernetes. Cert-manager will automatically create and renew TLS certificates and store them as Kubernetes secrets for easy use in a cluster. The Ambassador Edge Stack will automatically watch for secret changes and reload certificates upon renewal. Essentially, we can deploy Cert-Manager to manage certificates in Kubernetes for us. Ambassador only supports HTTP-01 challenge but it’s possible to perform DNS-01 challenge using Cert-Manager. Note: We use GoDaddy domain names and it is not a supported DNS Provider (see list of supported providers). There are several Cert-Manager Godaddy Webhook implementations online but they don’t seem to be well maintained so I decided to stick with HTTP-01 challenge. How to Setup Automatic TLS with ACME? For tutorial, I will be using an arbitrary email my-email@gmail.com and Let’s Encrypt to Issue a certificate for an arbitrary domain name dev.mydomain.com. Install the Cert-Manager tool with kubectl Let’s start by installing the Cert-Manager tool that will manage our certificates. # Install Custom Resource Definitions and Cert-Manager kubectl apply -f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml Note: You can also install Cert-Manager with Helm (see here) Create a ClusterIssuer resource An Issuer or ClusterIssuer identifies which Certificate Authority cert-manager will use to issue a certificate. Issuer is a namespaced resource allowing you to use different CAs in each namespace, a ClusterIssuer is used to issue certificates in any namespace. Configuration depends on which ACME challenge you are using. Once the Cert-manager deployments are completed, you can create a ClusterIssuer (global) or an Issuer (namespaced) resource. In this case, we are using Let’s Encrypt. --- apiVersion: cert-manager.io/v1alpha2 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: # Replace this email address with your own. # Let's Encrypt will use this to contact you about expiring # certificates, and issues related to your account. email: my-email@gmail.com # ACME URL, you can use the URL for Staging environment to Issue untrusted certificates server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: # Secret resource that will be used to store the account's private key. name: issuer-account-private-key solvers: # Define the solver to perform HTTP-01 challenge - http01: ingress: class: nginx selector: {} Create a Certificate resource A Certificate is a namespaced resource that specifies fields that are used to generated certificate signing requests which are then fulfilled by the issuer type you have referenced. Certificates specify which issuer they want to obtain the certificate from by specifying the certificate.spec.issuerRef field. Once the Issuer is ready, you can create a Certificate resource which will send a request to issue a new certificate. --- apiVersion: cert-manager.io/v1alpha2 kind: Certificate metadata: name: dev.mydomain.com # Cert-manager will put the resulting Secret in the same Kubernetes # namespace as the Certificate. You should create the certificate in # whichever namespace you want to configure a Host. spec: secretName: dev.mydomain.com issuerRef: # Name of ClusterIssuer name: letsencrypt-prod kind: ClusterIssuer dnsNames: - dev.mydomain.com After applying this template, you should see the following events: $ kubectl get events -n default # The namespace in which you created your Certificate resource 14m Normal cert-manager.io certificaterequest/dev.mydomain.com-qrfxs Certificate request has been approved by cert-manager.io 14m Normal Issuing certificate/dev.mydomain.com Issuing certificate as Secret does not exist 14m Normal Generated certificate/dev.mydomain.com Stored new private key in temporary Secret resource "dev.mydomain.com-lrdk6" 14m Normal Requested certificate/dev.mydomain.com Created new CertificateRequest resource "dev.mydomain.com-qrfxs" 14m Normal Created order/dev.mydomain.com-qrfxs-820390478 Created Challenge resource "dev.mydomain.com-qrfxs-820390478-3681158932" for domain "dev.mydomain.com" &lt;unknown&gt; Normal Scheduled pod/cm-acme-http-solver-fbhcs Successfully assigned default/cm-acme-http-solver-fbhcs to the-name-of-some-node-1 14m Normal Presented challenge/dev.mydomain.com-qrfxs-820390478-3681158932 Presented challenge using HTTP-01 challenge mechanism 14m Normal Started challenge/dev.mydomain.com-qrfxs-820390478-3681158932 Challenge scheduled for processing 14m Normal Pulling pod/cm-acme-http-solver-fbhcs Pulling image "quay.io/jetstack/cert-manager-acmesolver:v1.3.1" 13m Normal Pulled pod/cm-acme-http-solver-fbhcs Successfully pulled image "quay.io/jetstack/cert-manager-acmesolver:v1.3.1" 13m Normal Started pod/cm-acme-http-solver-fbhcs Started container acmesolver 13m Normal Created pod/cm-acme-http-solver-fbhcs Created container acmesolver Create a Mapping and Service resource for HTTP challenge At this point, Cert-manager will have created a temporary pod named cm-acme-http-solver-xxxx but no certificate has been issued. You will need to create a Mapping resource to allow Ambassador to reach the http-01 challenge solver via http://dev.mydomain.com/.well-known/acme-challenge/&lt;some-token&gt;. --- apiVersion: getambassador.io/v2 kind: Mapping metadata: name: acme-challenge-mapping spec: prefix: /.well-known/acme-challenge/ rewrite: "" service: acme-challenge-service --- apiVersion: v1 kind: Service metadata: name: acme-challenge-service spec: ports: - port: 80 targetPort: 8089 selector: acme.cert-manager.io/http01-solver: "true" After applying the template, you will need to wait a several minutes (about 10 minutes) before cert-manager retries the challenge and issues a certificate. You should see the following events: $ kubectl get events -n default # The namespace in which you created your Certificate resource 6m38s Normal Killing pod/cm-acme-http-solver-fbhcs Stopping container acmesolver 6m38s Normal DomainVerified challenge/dev.mydomain.com-qrfxs-820390478-3681158932 Domain "dev.mydomain.com" verified with "HTTP-01" validation 6m37s Normal Complete order/dev.mydomain.com-qrfxs-820390478 Order completed successfully 6m37s Normal Issuing certificate/dev.mydomain.com The certificate has been successfully issued 6m37s Normal CertificateIssued certificaterequest/dev.mydomain.com-qrfxs Certificate fetched from issuer successfully Create a Host resource for your domain name After the certificate was successfully issued, there should be a TLS secret called dev.mydomain.com(name is defined by secretName in the Certificate resource). Then, you can create a Host resource. It will register your ACME account, read the certificate from the TLS secret and use that to terminate TLS on your domain. --- apiVersion: getambassador.io/v2 kind: Host metadata: name: dev.mydomain.com namespace: default spec: acmeProvider: authority: 'https://acme-v02.api.letsencrypt.org/directory' email: my-email@gmail.com ambassadorId: - default hostname: dev.mydomain.com selector: matchLabels: hostname: dev.mydomain.com tlsSecret: name: dev.mydomain.com # The secretName defined in your Certificate resource You should see the following events: $ kubectl get events -n default # The namespace in which you created your Host resource 10s Normal Pending host/dev.mydomain.com waiting for Host DefaultsFilled change to be reflected in snapshot 8s Normal Pending host/dev.mydomain.com creating private key Secret 8s Normal Pending host/dev.mydomain.com waiting for private key Secret creation to be reflected in snapshot 6s Normal Pending host/dev.mydomain.com waiting for Host status change to be reflected in snapshot 4s Normal Pending host/dev.mydomain.com registering ACME account 3s Normal Pending host/dev.mydomain.com waiting for Host ACME account registration change to be reflected in snapshot 3s Normal Pending host/dev.mydomain.com ACME account registered 1s Normal Pending host/dev.mydomain.com waiting for TLS Secret update to be reflected in snapshot 1s Normal Pending host/dev.mydomain.com updating TLS Secret 0s Normal Ready host/dev.mydomain.com Host with ACME-provisioned TLS certificate marked Ready Conclusion Ambassador Edge Stack automatically enables TLS termination/HTTPs and you can easily configure it to completely manage TLS by requesting a certificate from a Certificate Authority(CA) instead of generating and managing certificates yourself! 🐢 References Ambassador Transport Layer Security (TLS) Ambassador ACME Support Ambassador and Cert-Manager HTTP-01 Challenge]]></summary></entry><entry><title type="html">Init vs Systemd: What is an init daemon?</title><link href="https://uace.github.io/learning/init-vs-systemd-what-is-an-init-daemon" rel="alternate" type="text/html" title="Init vs Systemd: What is an init daemon?"/><published>2021-03-24T03:20:00+00:00</published><updated>2021-03-24T03:20:00+00:00</updated><id>https://uace.github.io/learning/init-vs-systemd-what-is-an-init-daemon</id><content type="html" xml:base="https://uace.github.io/learning/init-vs-systemd-what-is-an-init-daemon"><![CDATA[<p>As a developer, I mostly work on Linux/Unix-like operating systems such as Ubuntu. Having used such operating systems for a few years now, I feel comfortable enough to say that I am familiar with them, but there are still many things that I still don’t understand. In this article, I would like to explore the difference between <strong>Init</strong> and <strong>Systemd</strong>.</p> <p>I remember during my System Administrator Devops internship, I had to perform some operations on some background service. At this point, I still had no idea what services were. I had to use commands like:</p> <pre><code class="language-Bash">sudo service &lt;service-name&gt; &lt;command&gt;
</code></pre> <p>Occasionally, I would see people online use <code class="language-html highlighter-rouge">init.d</code> instead, which also worked.</p> <pre><code class="language-Bash">sudo /etc/init.d/&lt;service-name&gt; &lt;command&gt;
</code></pre> <p>But why are there two commands that do exactly the same thing? Sadly, this question never crossed my mind. I was happy as long as the commands worked. That is, until I started working on Fedora CoreOS for Kubernetes and this happened:</p> <pre><code class="language-Bash">$ sudo service kubelet &lt;command&gt;
sudo: service: command not found
</code></pre> <p><code class="language-html highlighter-rouge">service</code> is not a command?! After looking for an answer on Google, I found that the command was specific to certain Linux distributions and the solution was to use:</p> <pre><code class="language-Bash">sudo systemctl &lt;command&gt; &lt;service-name&gt;
</code></pre> <p><em>What!?</em> A third command to manage services? Yup. In fact, some Linux distributions (distros) have their own command to manage services but I’m not going to go into that. In this article, I will only to talk about the init daemons <strong>Init</strong> and <strong>Systemd</strong> that use the commands <code class="language-html highlighter-rouge">service</code> and <code class="language-html highlighter-rouge">systemctl</code> respectively. But first, we need to understand what an init daemon is.</p> <h2 id="what-is-an-init-daemon">What is an Init Daemon?</h2> <p>The init daemon is the first process executed by the Linux Kernel and its process ID (PID) is always 1. Its purpose is to initialize, manage and track system services and daemons. In other words, the init daemon is the parent of all processes on the system.</p> <h2 id="what-is-init">What is Init?</h2> <p>Init (also known as System V init, or SysVinit) is an init daemon, created in the 1980s, that defines six run-levels (system states) and maps all system services to these run-levels. This allows all services (defined as scripts) to be started in a pre-defined sequence. The next script is executed only if the current script in the sequence is executed or timed out if it gets stucked. In addition to unexpected wait during execution timeouts, starting services serially makes the system initialization process inefficient and relatively slow.</p> <p>To create a service, you will need to write a script and store it in <code class="language-html highlighter-rouge">/etc/init.d</code> directory. You would write a service script <code class="language-html highlighter-rouge">/etc/init.d/myService</code> that looks something like this:</p> <pre><code class="language-Bash">#!/bin/bash
# chkconfig: 2345 20 80
# description: Description comes here....

# Source function library.
. /etc/init.d/functions

start() {
    # TODO: code to start app comes here 
}

stop() {
    # TODO: code to stop app comes here 
}

case "$1" in 
    start)
       start
       ;;
    stop)
       stop
       ;;
    restart)
       stop
       start
       ;;
    status)
       # TODO: code to check status of app comes here 
       ;;
    *)
       echo "Usage: $0 {start|stop|restart|status}"
esac

exit 0
</code></pre> <p>You can read about chkconfig in the <a href="https://linux.die.net/man/8/chkconfig">man page</a>. Essentially, it defines in which run-level your service should be run. Once you have your script, you can use the <code class="language-html highlighter-rouge">service</code> command to start, stop, and restart your service.</p> <h2 id="what-is-systemd">What is Systemd?</h2> <p>Systemd (system daemon) is an init daemon used by modern systems and starts system services in parallel which remove unnecessary delays and speeds up the initialization process. What do I mean by parallel? Systemd uses Unit Dependencies to define whether a service <strong>wants/requires</strong> other services to run successfully, and Unit Order to define whether a service needs other services to be started <strong>before/after</strong> it.</p> <p>To create a service, you will need to write a <code class="language-html highlighter-rouge">.service</code> file stored in the <code class="language-html highlighter-rouge">/etc/systemd/system</code> directory. You would write a file <code class="language-html highlighter-rouge">/etc/systemd/system/myService.service</code> that looks something like this:</p> <pre><code class="language-Bash">[Unit]
Description=Some Description
Requires=syslog.target
After=syslog.target

[Service]
ExecStart=/usr/sbin/&lt;command-to-start&gt;
ExecStop=/usr/sbin/&lt;command-to-stop&gt;

[Install]
WantedBy=multi-user.target
</code></pre> <p>I will discuss more about how to create a service with Systemd in another article. Once you have your service file, you can start, stop and restart your service using the <code class="language-html highlighter-rouge">systemctl</code> command.</p> <h2 id="conclusion">Conclusion</h2> <p>Init and Systemd are both init daemons but it is better to use the latter since it is commonly used in recent Linux Distros. Init uses <code class="language-html highlighter-rouge">service</code> whereas Systemd uses <code class="language-html highlighter-rouge">systemctl</code> to manage Linux services.</p> <p>🐢</p> <h2 id="references">References</h2> <ul> <li><strong><a href="https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6">Creating a Linux service with systemd</a></strong></li> <li><strong><a href="https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units">How to Use Systemctl to manage Systemd Services and Units</a></strong></li> <li><strong><a href="https://www.tecmint.com/systemd-replaces-init-in-linux/#:~:text=The%20init%20is%20a%20daemon,running%20till%2C%20it%20is%20shutdown.&amp;text=systemd%20%E2%80%93%20A%20init%20replacement%20daemon,%2C%20RHEL%2C%20CentOS%2C%20etc.">The Story Behind ‘init’ and ‘systemd’</a></strong></li> <li><strong><a href="https://www.linux.com/training-tutorials/understanding-and-using-systemd/">Understanding and Using Systemd</a></strong></li> <li><strong><a href="https://wiki.cdot.senecacollege.ca/wiki/Init_vs_systemd">Init vs systemd</a></strong></li> <li><strong><a href="https://www.computernetworkingnotes.com/linux-tutorials/differences-between-sysvinit-upstart-and-systemd.html">Differences between SysVinit, Upstart and Systemd</a></strong></li> </ul>]]></content><author><name>Yu-Yueh Liu</name></author><category term="Learning"/><category term="DevOps"/><category term="Linux Services"/><category term="Systemd"/><category term="Init"/><summary type="html"><![CDATA[As a developer, I mostly work on Linux/Unix-like operating systems such as Ubuntu. Having used such operating systems for a few years now, I feel comfortable enough to say that I am familiar with them, but there are still many things that I still don’t understand. In this article, I would like to explore the difference between Init and Systemd. I remember during my System Administrator Devops internship, I had to perform some operations on some background service. At this point, I still had no idea what services were. I had to use commands like: sudo service &lt;service-name&gt; &lt;command&gt; Occasionally, I would see people online use init.d instead, which also worked. sudo /etc/init.d/&lt;service-name&gt; &lt;command&gt; But why are there two commands that do exactly the same thing? Sadly, this question never crossed my mind. I was happy as long as the commands worked. That is, until I started working on Fedora CoreOS for Kubernetes and this happened: $ sudo service kubelet &lt;command&gt; sudo: service: command not found service is not a command?! After looking for an answer on Google, I found that the command was specific to certain Linux distributions and the solution was to use: sudo systemctl &lt;command&gt; &lt;service-name&gt; What!? A third command to manage services? Yup. In fact, some Linux distributions (distros) have their own command to manage services but I’m not going to go into that. In this article, I will only to talk about the init daemons Init and Systemd that use the commands service and systemctl respectively. But first, we need to understand what an init daemon is. What is an Init Daemon? The init daemon is the first process executed by the Linux Kernel and its process ID (PID) is always 1. Its purpose is to initialize, manage and track system services and daemons. In other words, the init daemon is the parent of all processes on the system. What is Init? Init (also known as System V init, or SysVinit) is an init daemon, created in the 1980s, that defines six run-levels (system states) and maps all system services to these run-levels. This allows all services (defined as scripts) to be started in a pre-defined sequence. The next script is executed only if the current script in the sequence is executed or timed out if it gets stucked. In addition to unexpected wait during execution timeouts, starting services serially makes the system initialization process inefficient and relatively slow. To create a service, you will need to write a script and store it in /etc/init.d directory. You would write a service script /etc/init.d/myService that looks something like this: #!/bin/bash # chkconfig: 2345 20 80 # description: Description comes here.... # Source function library. . /etc/init.d/functions start() { # TODO: code to start app comes here } stop() { # TODO: code to stop app comes here } case "$1" in start) start ;; stop) stop ;; restart) stop start ;; status) # TODO: code to check status of app comes here ;; *) echo "Usage: $0 {start|stop|restart|status}" esac exit 0 You can read about chkconfig in the man page. Essentially, it defines in which run-level your service should be run. Once you have your script, you can use the service command to start, stop, and restart your service. What is Systemd? Systemd (system daemon) is an init daemon used by modern systems and starts system services in parallel which remove unnecessary delays and speeds up the initialization process. What do I mean by parallel? Systemd uses Unit Dependencies to define whether a service wants/requires other services to run successfully, and Unit Order to define whether a service needs other services to be started before/after it. To create a service, you will need to write a .service file stored in the /etc/systemd/system directory. You would write a file /etc/systemd/system/myService.service that looks something like this: [Unit] Description=Some Description Requires=syslog.target After=syslog.target [Service] ExecStart=/usr/sbin/&lt;command-to-start&gt; ExecStop=/usr/sbin/&lt;command-to-stop&gt; [Install] WantedBy=multi-user.target I will discuss more about how to create a service with Systemd in another article. Once you have your service file, you can start, stop and restart your service using the systemctl command. Conclusion Init and Systemd are both init daemons but it is better to use the latter since it is commonly used in recent Linux Distros. Init uses service whereas Systemd uses systemctl to manage Linux services. 🐢 References Creating a Linux service with systemd How to Use Systemctl to manage Systemd Services and Units The Story Behind ‘init’ and ‘systemd’ Understanding and Using Systemd Init vs systemd Differences between SysVinit, Upstart and Systemd]]></summary></entry><entry><title type="html">Webhook vs API: How can you use them?</title><link href="https://uace.github.io/tutorial/webhooks-vs-api-how-can-you-use-them" rel="alternate" type="text/html" title="Webhook vs API: How can you use them?"/><published>2021-02-27T03:17:00+00:00</published><updated>2021-02-27T03:17:00+00:00</updated><id>https://uace.github.io/tutorial/webhooks-vs-api-how-can-you-use-them</id><content type="html" xml:base="https://uace.github.io/tutorial/webhooks-vs-api-how-can-you-use-them"><![CDATA[<p>Recently, I was asked what was the difference between Webhooks and APIs. This was a question I also had a few years ago when I started programming. In this article, I will briefly explain what they are and give a simple tutorial on how you can use them.</p> <h2 id="what-is-an-api">What is an API?</h2> <p>API stands for Application Programming Interface. APIs allow applications to talk with each other via a common communication method. There are a lot of different API architectural styles such as REST, SOAP, GraphQL and gRPC. With most APIs, there’s a request followed by a response.</p> <p>For example, a restaurant might have an application that would make an API request to their server and obtain a list of menu items in the response, then display it for their users. A lot applications out there provide public APIs that you can be use in your personal projects such as <a href="https://developers.google.com/youtube/v3/getting-started">YouTube Data API</a> and <a href="https://developers.google.com/maps/documentation">Google Map API</a>.</p> <h2 id="what-is-a-webhook">What is a Webhook?</h2> <p>Unlike APIs, Webhook is simply an HTTP POST request that is triggered automatically when an event occurs. Basically, webhooks are <strong>“user-defined callbacks”</strong>.</p> <p>For example, an application could provide a webhook that will get triggered by another application when new data is received (callback) instead of sending requests at fixed interval to fetch new data (polling).</p> <h2 id="tutorial">Tutorial</h2> <h3 id="send-message-using-slack-api-with-slack-bot">Send Message Using Slack API with Slack Bot</h3> <p>Slack provides a complete list of <a href="https://api.slack.com/bot-users#api_methods_available_to_bots">REST API methods</a> available to bots. We are going to use the <a href="https://api.slack.com/methods/users.list">users.list</a> method to list available users and <a href="https://api.slack.com/methods/chat.postMessage">chat.postMessage</a> method to send a message to a user or channel.</p> <p>1. Navigate to the Custom Integrations page of your Workspace <code class="language-html highlighter-rouge">https://<span class="nt">&lt;your-workspace-name&gt;</span>.slack.com/apps/manage/custom-integrations</code> and select <strong><em>Bots</em></strong> <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/custom-integrations-bots.png" data-fancybox="gallery" data-caption="Slack Custom Integrations Bots"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/custom-integrations-bots.png" alt="Slack Custom Integrations Bots" class="img-fluid img-thumbnail"/></a></p> <p>2. Choose a name and add the bot integration. <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/add-bot.png" data-fancybox="gallery" data-caption="Add bot"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/add-bot.png" alt="Add bot" class="img-fluid img-thumbnail"/></a></p> <p>3. Save the <strong>API Token</strong>, we will use it later in Slack API requests for authentication. <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/api-token.png" data-fancybox="gallery" data-caption="API token"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/api-token.png" alt="API token" class="img-fluid img-thumbnail"/></a></p> <p>4. Let’s try out the <a href="https://api.slack.com/methods/users.list">users.list</a> method using an API client like Postman and click on <strong><em>code</em></strong> to generate code: <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/list-users.png" data-fancybox="gallery" data-caption="List Users API Request"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/list-users.png" alt="List Users" class="img-fluid img-thumbnail"/></a></p> <pre><code class="language-Python"># slack-api.py
import requests, json

base_url = "https://slack.com/api"

payload={}
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': 'Bearer [your Slack Bot API Token]'
}

# Make GET request and receive response
response = requests.request("GET", f"{base_url}/users.list", headers=headers, data=payload)

# Convert response to a Dict object
response_json = json.loads(response.text)

# Find user by username
username = 'yueh.liu'
user = next((member for member in response_json['members'] if member['name'] == username), None)

# Make sure the user exists
if not user:
    raise Exception(f'User [{username}] was not found')

# Save the user_id
user_id = user['id']
</code></pre> <p>5. Now that we have the <strong>User ID</strong>, we can try sending a message to that user! We can repeat the previous step with the <a href="https://api.slack.com/methods/chat.postMessage">chat.postMessage</a> method. Make sure to change the request method to <code class="language-html highlighter-rouge">POST</code>. <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/send-message.png" data-fancybox="gallery" data-caption="Send message to user"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/send-message.png" alt="Send message to user" class="img-fluid img-thumbnail"/></a></p> <p>You should receive a message like this on Slack <br/> <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/ua-bot.png" data-fancybox="gallery" data-caption="Message from ua-bot"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/ua-bot.png" alt="ua-bot" class="img-fluid img-thumbnail"/></a></p> <p>The updated code should look something like this:</p> <pre><code class="language-Python"># slack-api.py
import requests, json

base_url = "https://slack.com/api"

payload={}
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': 'Bearer [your Slack Bot API Token]'
}

# Make GET request and receive response
response = requests.request("GET", f"{base_url}/users.list", headers=headers, data=payload)

# Convert response to a Dict object
response_json = json.loads(response.text)

# Find user by username
username = 'yueh.liu'
user = next((member for member in response_json['members'] if member['name'] == username), None)

# Make sure the user exists
if not user:
    raise Exception(f'User [{username}] was not found')

# Save the user_id
user_id = user['id']

# Set the parameters such as the channel ID (user ID in our case), username for the bot, text message, icon url, etc
# You can also send a JSON payload instead of query parameters, but you would need to change the 'Content-Type' to 'application/json' in the headers
params = f"channel={user_id}&amp;text=Hello Yueh!&amp;username=ua-bot&amp;icon_url=https://some-url-link.jpg"

# Make POST request and receive response
response = requests.request("POST", f"{base_url}/chat.postMessage?{params}", headers=headers, data=payload)

print(response.text)
</code></pre> <h3 id="send-message-using-slack-incoming-webhooks">Send Message Using Slack Incoming Webhooks</h3> <blockquote> <p>Incoming Webhooks are a simple way to post messages from external sources into Slack. They make use of normal HTTP requests with a JSON payload, which includes the message and a few other optional details described later.</p> </blockquote> <p>For this example, we are going to create a Web Server and integrate an Incoming Webhook. We will trigger the webhook automatically to send a message to a user on Slack whenever the server receives a message.</p> <p>1. Navigate to the Custom Integrations page of your Workspace <code class="language-html highlighter-rouge">https://<span class="nt">&lt;your-workspace-name&gt;</span>.slack.com/apps/manage/custom-integrations</code> and select <strong><em>Incoming WebHooks</em></strong> <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/custom-integrations-webhooks.png" data-fancybox="gallery" data-caption="Slack Custom Integrations Incoming Webhooks"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/custom-integrations-webhooks.png" alt="Slack Custom Integrations Incoming Webhooks" class="img-fluid img-thumbnail"/></a></p> <p>2. Choose a channel (or user) to post your messages and add the webhook <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/add-webhook.png" data-fancybox="gallery" data-caption="Add an Incoming Webhook"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/add-webhook.png" alt="add-webhook" class="img-fluid img-thumbnail"/></a></p> <p>You should see a message like this on Slack <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/added-integration.png" data-fancybox="gallery" data-caption="Added Webhook Integration"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/added-integration.png" alt="added-integration" class="img-fluid img-thumbnail"/></a></p> <p>3. Save the Webhook url <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/webhook-url.png" data-fancybox="gallery" data-caption="Slack Webhook url"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/webhook-url.png" alt="webhook-url" class="img-fluid img-thumbnail"/></a></p> <p>4. Since webhooks work best as callback from a server, let’s write a simple HTTP server that runs on localhost and port 3000. The web server will receive a message on <code class="language-html highlighter-rouge">/message</code> path and read the message content from the payload.</p> <pre><code class="language-Python"># server.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

# Define a custom Request Handler
class CustomHandler(BaseHTTPRequestHandler):
    def set_response(self, code, byte_message):
        self.send_response(code)
        self.send_header("Content-type", "text/plain")
        self.end_headers()
        self.wfile.write(byte_message)

    def do_GET(self):
        if self.path == "/":
            self.set_response(200, "I'm alive!!!\n".encode())
            self.wfile.write()
        else:
            self.send_error(404)
        return

    def do_POST(self):
        if self.path == "/message":
            # Get payload
            content_length = int(self.headers["Content-Length"])
            encoded_data = self.rfile.read(content_length)
            data = json.loads(encoded_data.decode("utf-8"))

            if not "message" in data and not data['message']:
                self.send_error(400, "Bad Request", '"message" must be in the payload')
                return

            self.set_response(200, f"Received message: \"{data['message']}\"\n".encode())
        else:
            self.send_error(404)
        return

# Initialize an HTTP server
port = 3000
address = ("", port)
server = HTTPServer(address, CustomHandler)

# Start your server
print(f"Starting Web server on localhost:{port}..")
server.serve_forever()
</code></pre> <p>5. Check if server is running <br/> <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/server-get.png" data-fancybox="gallery" data-caption="Check local web server is alive"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/server-get.png" alt="server-get" class="img-fluid img-thumbnail"/></a></p> <p>Let’s try sending a message <br/> <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/server-post.png" data-fancybox="gallery" data-caption="Send message to local web server"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/server-post.png" alt="server-post" class="img-fluid img-thumbnail"/></a></p> <p>6. Now that the server is running, let’s integrate the webhook into the code!</p> <pre><code class="language-Python"># server.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import json, requests

# Define a custom Request Handler
class CustomHandler(BaseHTTPRequestHandler):
    def set_response(self, code, byte_message):
        self.send_response(code)
        self.send_header("Content-type", "text/plain")
        self.end_headers()
        self.wfile.write(byte_message)

    def do_GET(self):
        if self.path == "/":
            self.set_response(200, "I'm alive!!!\n".encode())
            self.wfile.write()
        else:
            self.send_error(404)
        return

    def do_POST(self):
        if self.path == "/message":
            # Get payload
            content_length = int(self.headers["Content-Length"])
            encoded_data = self.rfile.read(content_length)
            data = json.loads(encoded_data.decode("utf-8"))

            if not "message" in data and not data['message']:
                self.send_error(400, "Bad Request", '"message" must be in the payload')
                return

            self.set_response(200, f"Received message: \"{data['message']}\"\n".encode())

            # Trigger the Webhook (make POST request) and we can ignore the response and failure
            try:
                webhook_url = "[Your Slack Webhook Url]"
                headers = { 'Content-Type': 'application/json' }
                payload = "{ \"text\": \"Your server received the following message:\n\n" + data['message'] + "\" }"
                requests.request("POST", webhook_url, headers=headers, data=payload)
            except Exception:
                pass
        else:
            self.send_error(404)
        return

# Initialize an HTTP server
port = 3000
address = ("", port)
server = HTTPServer(address, CustomHandler)

# Start your server
print(f"Starting Web server on localhost:{port}..")
server.serve_forever()
</code></pre> <p>7. Once updated, we can re-send the same message as earlier and you should receive a message like this on Slack: <br/> <a href="/assets/posts/webhooks-vs-api-how-can-you-use-them/ua-webhook.png" data-fancybox="gallery" data-caption="Message from ua-webhook"><img src="/assets/posts/webhooks-vs-api-how-can-you-use-them/ua-webhook.png" alt="ua-webhook" class="img-fluid img-thumbnail"/></a></p> <h2 id="conclusion">Conclusion</h2> <p>An API is a communication method used by applications to talk with other applications. Webhook is a POST request that is triggered automatically when an event happens. Basically, APIs are request-based while webhooks are event-based.</p> <p>🐢</p> <h2 id="references">References</h2> <ul> <li><strong><a href="https://sendgrid.com/blog/webhook-vs-api-whats-difference/">Webhook vs API</a></strong></li> <li><strong><a href="https://www.altexsoft.com/blog/soap-vs-rest-vs-graphql-vs-rpc/">Comparing API Architectural Styles: SOAP vs REST vs GraphQL vs RPC</a></strong></li> <li><strong><a href="https://api.slack.com/messaging/webhooks">Slack Incoming Webhooks</a></strong></li> <li><strong><a href="https://api.slack.com/bot-users">Slack User Bots</a></strong></li> <li><strong><a href="https://docs.python.org/3/library/http.server.html">Python Docs: HTTP servers</a></strong></li> <li><strong><a href="https://gist.github.com/mdonkers/63e115cc0c79b4f6b8b3a6b797e485c7">Simple Python 3 HTTP server for logging all GET and POST requests</a></strong></li> </ul> <script src="//cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script> <script>hljs.initLineNumbersOnLoad(),hljs.initHighlightLinesOnLoad([[{start:8,end:8,color:"rgba(255, 255, 255, 0.2)"}],[{start:28,end:35,color:"rgba(255, 255, 255, 0.2)"}],[],[{start:2,end:2,color:"rgba(255, 255, 255, 0.2)"},{start:33,end:40,color:"rgba(255, 255, 255, 0.2)"}]]);</script>]]></content><author><name>Yu-Yueh Liu</name></author><category term="Tutorial"/><category term="Webhook"/><category term="REST API"/><summary type="html"><![CDATA[Recently, I was asked what was the difference between Webhooks and APIs. This was a question I also had a few years ago when I started programming. In this article, I will briefly explain what they are and give a simple tutorial on how you can use them. What is an API? API stands for Application Programming Interface. APIs allow applications to talk with each other via a common communication method. There are a lot of different API architectural styles such as REST, SOAP, GraphQL and gRPC. With most APIs, there’s a request followed by a response. For example, a restaurant might have an application that would make an API request to their server and obtain a list of menu items in the response, then display it for their users. A lot applications out there provide public APIs that you can be use in your personal projects such as YouTube Data API and Google Map API. What is a Webhook? Unlike APIs, Webhook is simply an HTTP POST request that is triggered automatically when an event occurs. Basically, webhooks are “user-defined callbacks”. For example, an application could provide a webhook that will get triggered by another application when new data is received (callback) instead of sending requests at fixed interval to fetch new data (polling). Tutorial Send Message Using Slack API with Slack Bot Slack provides a complete list of REST API methods available to bots. We are going to use the users.list method to list available users and chat.postMessage method to send a message to a user or channel. 1. Navigate to the Custom Integrations page of your Workspace https://&lt;your-workspace-name&gt;.slack.com/apps/manage/custom-integrations and select Bots 2. Choose a name and add the bot integration. 3. Save the API Token, we will use it later in Slack API requests for authentication. 4. Let’s try out the users.list method using an API client like Postman and click on code to generate code: # slack-api.py import requests, json base_url="https://slack.com/api" payload={} headers={ 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer [your Slack Bot API Token]' } # Make GET request and receive response response=requests.request("GET", f"{base_url}/users.list", headers=headers, data=payload) # Convert response to a Dict object response_json=json.loads(response.text) # Find user by username username='yueh.liu' user=next((member for member in response_json['members'] if member['name'] == username), None) # Make sure the user exists if not user: raise Exception(f'User [{username}] was not found') # Save the user_id user_id=user['id'] 5. Now that we have the User ID, we can try sending a message to that user! We can repeat the previous step with the chat.postMessage method. Make sure to change the request method to POST. You should receive a message like this on Slack The updated code should look something like this: # slack-api.py import requests, json base_url="https://slack.com/api" payload={} headers={ 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer [your Slack Bot API Token]' } # Make GET request and receive response response=requests.request("GET", f"{base_url}/users.list", headers=headers, data=payload) # Convert response to a Dict object response_json=json.loads(response.text) # Find user by username username='yueh.liu' user=next((member for member in response_json['members'] if member['name'] == username), None) # Make sure the user exists if not user: raise Exception(f'User [{username}] was not found') # Save the user_id user_id=user['id'] # Set the parameters such as the channel ID (user ID in our case), username for the bot, text message, icon url, etc # You can also send a JSON payload instead of query parameters, but you would need to change the 'Content-Type' to 'application/json' in the headers params=f"channel={user_id}&amp;text=Hello Yueh!&amp;username=ua-bot&amp;icon_url=https://some-url-link.jpg" # Make POST request and receive response response=requests.request("POST", f"{base_url}/chat.postMessage?{params}", headers=headers, data=payload) print(response.text) Send Message Using Slack Incoming Webhooks Incoming Webhooks are a simple way to post messages from external sources into Slack. They make use of normal HTTP requests with a JSON payload, which includes the message and a few other optional details described later. For this example, we are going to create a Web Server and integrate an Incoming Webhook. We will trigger the webhook automatically to send a message to a user on Slack whenever the server receives a message. 1. Navigate to the Custom Integrations page of your Workspace https://&lt;your-workspace-name&gt;.slack.com/apps/manage/custom-integrations and select Incoming WebHooks 2. Choose a channel (or user) to post your messages and add the webhook You should see a message like this on Slack 3. Save the Webhook url 4. Since webhooks work best as callback from a server, let’s write a simple HTTP server that runs on localhost and port 3000. The web server will receive a message on /message path and read the message content from the payload. # server.py from http.server import BaseHTTPRequestHandler, HTTPServer import json # Define a custom Request Handler class CustomHandler(BaseHTTPRequestHandler): def set_response(self, code, byte_message): self.send_response(code) self.send_header("Content-type", "text/plain") self.end_headers() self.wfile.write(byte_message) def do_GET(self): if self.path == "/": self.set_response(200, "I'm alive!!!\n".encode()) self.wfile.write() else: self.send_error(404) return def do_POST(self): if self.path == "/message": # Get payload content_length=int(self.headers["Content-Length"]) encoded_data=self.rfile.read(content_length) data=json.loads(encoded_data.decode("utf-8")) if not "message" in data and not data['message']: self.send_error(400, "Bad Request", '"message" must be in the payload') return self.set_response(200, f"Received message: \"{data['message']}\"\n".encode()) else: self.send_error(404) return # Initialize an HTTP server port=3000 address=("", port) server=HTTPServer(address, CustomHandler) # Start your server print(f"Starting Web server on localhost:{port}..") server.serve_forever() 5. Check if server is running Let’s try sending a message 6. Now that the server is running, let’s integrate the webhook into the code! # server.py from http.server import BaseHTTPRequestHandler, HTTPServer import json, requests # Define a custom Request Handler class CustomHandler(BaseHTTPRequestHandler): def set_response(self, code, byte_message): self.send_response(code) self.send_header("Content-type", "text/plain") self.end_headers() self.wfile.write(byte_message) def do_GET(self): if self.path == "/": self.set_response(200, "I'm alive!!!\n".encode()) self.wfile.write() else: self.send_error(404) return def do_POST(self): if self.path == "/message": # Get payload content_length=int(self.headers["Content-Length"]) encoded_data=self.rfile.read(content_length) data=json.loads(encoded_data.decode("utf-8")) if not "message" in data and not data['message']: self.send_error(400, "Bad Request", '"message" must be in the payload') return self.set_response(200, f"Received message: \"{data['message']}\"\n".encode()) # Trigger the Webhook (make POST request) and we can ignore the response and failure try: webhook_url="[Your Slack Webhook Url]" headers={ 'Content-Type': 'application/json' } payload="{ \"text\": \"Your server received the following message:\n\n" + data['message'] + "\" }" requests.request("POST", webhook_url, headers=headers, data=payload) except Exception: pass else: self.send_error(404) return # Initialize an HTTP server port=3000 address=("", port) server=HTTPServer(address, CustomHandler) # Start your server print(f"Starting Web server on localhost:{port}..") server.serve_forever() 7. Once updated, we can re-send the same message as earlier and you should receive a message like this on Slack: Conclusion An API is a communication method used by applications to talk with other applications. Webhook is a POST request that is triggered automatically when an event happens. Basically, APIs are request-based while webhooks are event-based. 🐢 References Webhook vs API Comparing API Architectural Styles: SOAP vs REST vs GraphQL vs RPC Slack Incoming Webhooks Slack User Bots Python Docs: HTTP servers Simple Python 3 HTTP server for logging all GET and POST requests]]></summary></entry><entry><title type="html">Dockerfile: Difference between ENTRYPOINT and CMD</title><link href="https://uace.github.io/learning/dockerfile-difference-between-entrypoint-and-cmd" rel="alternate" type="text/html" title="Dockerfile: Difference between ENTRYPOINT and CMD"/><published>2021-02-25T04:05:00+00:00</published><updated>2021-02-25T04:05:00+00:00</updated><id>https://uace.github.io/learning/dockerfile-difference-between-entrypoint-and-cmd</id><content type="html" xml:base="https://uace.github.io/learning/dockerfile-difference-between-entrypoint-and-cmd"><![CDATA[<p>I have been working with Docker for over a year now but there are still a lot of things that I do not understand, and things that I thought I knew but were wrong such as the difference between ENTRYPOINT and CMD instructions in Dockerfiles.</p> <p>I always thought the only difference was that CMD can be overriden and that they were mutually exclusive since Docker containers needed a starting process. In fact, they are not mutually exclusive and understanding the difference between them could be very useful when building Dockerfiles!</p> <h2 id="docker-entrypoint">Docker ENTRYPOINT</h2> <p>An ENTRYPOINT is used to configure a container to run as an executable and it has two forms:</p> <p>The <strong><em>exec</em></strong> form (preferred):</p> <pre><code class="language-Bash">ENTRYPOINT ["executable", "param1", "param2"]
</code></pre> <p>Command line arguments provided to <code class="language-html highlighter-rouge">docker run <span class="nt">&lt;image&gt;</span></code> will be appended after all elements of the array. For example, if you need to provide a third parameter to the above ENTRYPOINT, you can run <code class="language-html highlighter-rouge">docker run <span class="nt">&lt;image&gt;</span> param3</code>. Moreover, it is possible to override the ENTRYPOINT using <code class="language-html highlighter-rouge">docker run --entrypoint</code>. The exec form is parsed as a JSON array, which means that you must use double-quotes (“) around words not single-quotes (‘) and backslashes need to be escaped.</p> <p>The <strong><em>shell</em></strong> form:</p> <pre><code class="language-Bash">ENTRYPOINT command param1 param2
</code></pre> <p>This form prevents any command line arguments to be provided to the ENTRYPOINT and will start the executable as a subcommand of <code class="language-html highlighter-rouge">/bin/sh -c</code>. The executable will not be run with process ID (PID) 1 and it will not pass Unix signals.</p> <h2 id="docker-cmd">Docker CMD</h2> <p>A CMD is used to provide defaults for an executing container. The defaults can be an executable, command and/or parameters. Unlike ENTRYPOINT, CMD has 3 forms:</p> <p>The <strong><em>exec</em></strong> form (preferred):</p> <pre><code class="language-Bash">CMD ["executable", "param1", "param2"]
</code></pre> <p>Although it looks similar to the ENTRYPOINT exec form, command line arguments provided to <code class="language-html highlighter-rouge">docker run <span class="nt">&lt;image&gt;</span></code> will override the default CMD defined in the Dockerfile.</p> <p>The <strong><em>default arguments</em></strong> form (used with ENTRYPOINT):</p> <pre><code class="language-Bash"> CMD ["param1", "param2"]
</code></pre> <p>This form is used when both ENTRYPOINT and CMD instructions are specified. ENTRYPOINT will define the executable and parameters to run, whereas CMD will define additional default parameters, overridable by command line arguments provided to <code class="language-html highlighter-rouge">docker run <span class="nt">&lt;image&gt;</span></code>.</p> <p>The <strong><em>shell</em></strong> form:</p> <pre><code class="language-Bash"> CMD command param1 param2
</code></pre> <p>Similar to the exec form, command line arguments provided to <code class="language-html highlighter-rouge">docker run <span class="nt">&lt;image&gt;</span></code> will override the default CMD defined in the Dockerfile. However, the shell form will invoke a command shell and allow normal shell processing such as variable substitution.</p> <h2 id="conclusion">Conclusion</h2> <p>Both ENTRYPOINT and CMD instructions allow containers to run as executable but they are not mutually exclusive. If you need to override the default executable, then you might want to use CMD. If you would like your container to run the same executable every time, then you should consider using ENTRYPOINT with CMD.</p> <p>The table below describes the behaviour of ENTRYPOINT with CMD:</p> <table> <thead> <tr> <th style="text-align: center">dockerfile ENTRYPOINT</th> <th style="text-align: center">dockerfile CMD</th> <th style="text-align: center">docker run --entrypoint</th> <th style="text-align: center">docker run command</th> <th style="text-align: center">Actual command run</th> </tr> </thead> <tbody> <tr> <td style="text-align: center">[exec-1]</td> <td style="text-align: center">[foo bar]</td> <td style="text-align: center">&lt;not set&gt;</td> <td style="text-align: center">&lt;not set&gt;</td> <td style="text-align: center">[exec-1 foo bar]</td> </tr> <tr> <td style="text-align: center">[exec-1]</td> <td style="text-align: center">[foo bar]</td> <td style="text-align: center">[exec-2]</td> <td style="text-align: center">&lt;not set&gt;</td> <td style="text-align: center">[exec-2]</td> </tr> <tr> <td style="text-align: center">[exec-1]</td> <td style="text-align: center">[foo bar]</td> <td style="text-align: center">&lt;not set&gt;</td> <td style="text-align: center">[zoo boo]</td> <td style="text-align: center">[exec-1 zoo boo]</td> </tr> <tr> <td style="text-align: center">[exec-1]</td> <td style="text-align: center">[foo bar]</td> <td style="text-align: center">[exec-2]</td> <td style="text-align: center">[zoo boo]</td> <td style="text-align: center">[exec-2 zoo boo]</td> </tr> </tbody> </table> <p>🐢</p> <h2 id="references">References</h2> <ul> <li><strong><a href="https://docs.docker.com/engine/reference/builder/#entrypoint">ENTRYPOINT Dockerfile reference</a></strong></li> <li><strong><a href="https://docs.docker.com/engine/reference/builder/#cmd">CMD Dockerfile reference</a></strong></li> <li><strong><a href="https://phoenixnap.com/kb/docker-cmd-vs-entrypoint">Docker Entrypoint vs CMD: Solving the Dilemma</a></strong></li> <li><strong><a href="https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#notes">Kubernetes Command and Arguments for a Container</a></strong></li> </ul>]]></content><author><name>Yu-Yueh Liu</name></author><category term="Learning"/><category term="Docker"/><category term="DevOps"/><summary type="html"><![CDATA[I have been working with Docker for over a year now but there are still a lot of things that I do not understand, and things that I thought I knew but were wrong such as the difference between ENTRYPOINT and CMD instructions in Dockerfiles. I always thought the only difference was that CMD can be overriden and that they were mutually exclusive since Docker containers needed a starting process. In fact, they are not mutually exclusive and understanding the difference between them could be very useful when building Dockerfiles! Docker ENTRYPOINT An ENTRYPOINT is used to configure a container to run as an executable and it has two forms: The exec form (preferred): ENTRYPOINT ["executable", "param1", "param2"] Command line arguments provided to docker run &lt;image&gt; will be appended after all elements of the array. For example, if you need to provide a third parameter to the above ENTRYPOINT, you can run docker run &lt;image&gt; param3. Moreover, it is possible to override the ENTRYPOINT using docker run --entrypoint. The exec form is parsed as a JSON array, which means that you must use double-quotes (“) around words not single-quotes (‘) and backslashes need to be escaped. The shell form: ENTRYPOINT command param1 param2 This form prevents any command line arguments to be provided to the ENTRYPOINT and will start the executable as a subcommand of /bin/sh -c. The executable will not be run with process ID (PID) 1 and it will not pass Unix signals. Docker CMD A CMD is used to provide defaults for an executing container. The defaults can be an executable, command and/or parameters. Unlike ENTRYPOINT, CMD has 3 forms: The exec form (preferred): CMD ["executable", "param1", "param2"] Although it looks similar to the ENTRYPOINT exec form, command line arguments provided to docker run &lt;image&gt; will override the default CMD defined in the Dockerfile. The default arguments form (used with ENTRYPOINT): CMD ["param1", "param2"] This form is used when both ENTRYPOINT and CMD instructions are specified. ENTRYPOINT will define the executable and parameters to run, whereas CMD will define additional default parameters, overridable by command line arguments provided to docker run &lt;image&gt;. The shell form: CMD command param1 param2 Similar to the exec form, command line arguments provided to docker run &lt;image&gt; will override the default CMD defined in the Dockerfile. However, the shell form will invoke a command shell and allow normal shell processing such as variable substitution. Conclusion Both ENTRYPOINT and CMD instructions allow containers to run as executable but they are not mutually exclusive. If you need to override the default executable, then you might want to use CMD. If you would like your container to run the same executable every time, then you should consider using ENTRYPOINT with CMD. The table below describes the behaviour of ENTRYPOINT with CMD: dockerfile ENTRYPOINT dockerfile CMD docker run --entrypoint docker run command Actual command run [exec-1] [foo bar] &lt;not set&gt; &lt;not set&gt; [exec-1 foo bar] [exec-1] [foo bar] [exec-2] &lt;not set&gt; [exec-2] [exec-1] [foo bar] &lt;not set&gt; [zoo boo] [exec-1 zoo boo] [exec-1] [foo bar] [exec-2] [zoo boo] [exec-2 zoo boo] 🐢 References ENTRYPOINT Dockerfile reference CMD Dockerfile reference Docker Entrypoint vs CMD: Solving the Dilemma Kubernetes Command and Arguments for a Container]]></summary></entry><entry><title type="html">Making my First Chrome Extension</title><link href="https://uace.github.io/learning/my-first-chrome-extension" rel="alternate" type="text/html" title="Making my First Chrome Extension"/><published>2020-09-03T07:27:00+00:00</published><updated>2020-09-03T07:27:00+00:00</updated><id>https://uace.github.io/learning/my-first-chrome-extension</id><content type="html" xml:base="https://uace.github.io/learning/my-first-chrome-extension"><![CDATA[<p>Chrome extensions are programs that add functionalities to Chrome and enhance your browsing experience. In fact, you can find a myriad of extensions for the purpose of improving your productivity, protecting your privacy, and more. Making your own Chrome extension is quite simple and it could turn out to be a fun personal project.</p> <h2 id="goal">Goal</h2> <p>This article is a documentation of what I learned when making my first Chrome extension YouTubeStopwatch. If you’re looking for a tutorial for starters, check out the official <strong><a href="https://developer.chrome.com/extensions/getstarted">Getting Started Tutorial</a></strong>.</p> <h2 id="what-is-youtubestopwatch">What is YouTubeStopwatch?</h2> <p><strong><a href="https://chrome.google.com/webstore/detail/youtubestopwatch/ibaejmohdpnppkglomilmholhndaobag">YouTubeStopwatch</a></strong> was created for a course on Human-Computer Interaction (HCI). The objective was to help users manage the amount of time they would like to spend on YouTube, and somehow incite them to quit YouTube without resorting to blocking the site.</p> <p>The idea was to prompt the user for the desired time they want to spend on YouTube and start a countdown. Once the time is up, the user is asked whether they want to stay on YouTube or leave. If they choose to keep watching videos, they will be subject to some gradual graphical deterioration and slowly worsening their viewing experience.</p> <p>So how did I get started? Well, the first thing I had to learn was how Chrome Extensions are structured.</p> <h2 id="project-structure">Project Structure</h2> <pre><code class="language-Markdown">src
├── manifest.json
├── popup.html
├── js
│   ├── background.js
│   ├── content.js
│   ├── jquery-3.4.1.min.js.js
│   ├── constants.js
│   └──  ...
├── img
│   └──  extension-icon.png
└── css
    └──  popup.css
</code></pre> <h3 id="manifest">Manifest</h3> <p>The <code class="language-html highlighter-rouge">manifest.json</code> file is the first thing you need when creating an extension. It provides all the information about your extension to Google Chrome such as the name of your extension, the permissions needed, etc but we’ll get into that a bit later. Here is a minimal example:</p> <pre><code class="language-JSON">{
    "manifest_version": 2,
    "version": "0.1",
    "name": "My Extension",
    "description": "This is my extension"
}
</code></pre> <h3 id="background-scripts">Background Scripts</h3> <p>Background scripts are scripts that run in the background of your browser when you open Google Chrome. You can make the scripts persistent or not depending on your use case. I chose to use a persistent script. As long as Google Chrome is open, the script will be running. To define background scripts, I added a <strong>background</strong> section to the <strong>manifest</strong> file.</p> <pre><code class="language-JSON">{
    "manifest_version": 2,
    "version": "0.1",
    "name": "My Extension",
    "description": "This is my extension",
    "background": {
        "scripts": [
            "js/background.js"
        ],
        "persistent": true
    }
}
</code></pre> <blockquote> <p><em><strong>Note</strong>: It is now recommended to use non-persistent background scripts with <a href="https://developer.chrome.com/extensions/background_migration">Event Driven Background Scripts</a>.</em></p> </blockquote> <p>Below is an example of what my background script looked like.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1">// List to track all active YouTube tabs</span>
<span class="kd">var</span> <span class="nx">active_youtube_tabs</span> <span class="o">=</span> <span class="p">[];</span>

<span class="c1">// Create Main Event Listener</span>
<span class="kd">function</span> <span class="nf">initBackground</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">onMessage</span><span class="p">.</span><span class="nf">addListener</span><span class="p">(</span><span class="nf">function </span><span class="p">(</span><span class="nx">msg</span><span class="p">,</span> <span class="nx">sender</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">tabId</span> <span class="o">=</span> <span class="nx">sender</span><span class="p">.</span><span class="nx">tab</span> <span class="p">?</span> <span class="nx">sender</span><span class="p">.</span><span class="nx">tab</span><span class="p">.</span><span class="nx">id</span> <span class="p">:</span> <span class="kc">null</span><span class="p">;</span>
        <span class="c1">// If sender is youtube, add listener</span>
        <span class="k">if </span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="k">from</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">youtube</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="k">typeof </span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">event</span><span class="p">)</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">undefined</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">active_youtube_tabs</span><span class="p">.</span><span class="nf">indexOf</span><span class="p">(</span><span class="nx">tabId</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="p">?</span> <span class="nf">addListeners</span><span class="p">(</span><span class="nx">tabId</span><span class="p">)</span> <span class="p">:</span> <span class="kc">null</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="c1">// Handles event message</span>
        <span class="k">switch </span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">case</span> <span class="dl">"</span><span class="s2">START_COUNTDOWN</span><span class="dl">"</span><span class="p">:</span>
                <span class="nf">startCountdown</span><span class="p">();</span>
                <span class="k">break</span><span class="p">;</span>
            <span class="p">...</span>
            <span class="k">default</span><span class="p">:</span>
                <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">});</span>
<span class="p">}</span>
<span class="nf">initBackground</span><span class="p">();</span>

<span class="c1">// Subscribes tab to active youtube tabs and adds listener to url changes</span>
<span class="kd">function</span> <span class="nf">addListeners</span><span class="p">(</span><span class="nx">tabId</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">active_youtube_tabs</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">tabId</span><span class="p">);</span>
    <span class="c1">// When a youtube tab is closed, remove tabId from active_youtube_tabs list</span>
    <span class="nx">chrome</span><span class="p">.</span><span class="nx">tabs</span><span class="p">.</span><span class="nx">onRemoved</span><span class="p">.</span><span class="nf">addListener</span><span class="p">(</span><span class="nf">function </span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if </span><span class="p">(</span><span class="nx">tabId</span> <span class="o">===</span> <span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">removeYoutubeTab</span><span class="p">(</span><span class="nx">tabId</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">});</span>
    <span class="c1">// When the tab url changes, remove tabId from active_youtube_tabs if user is no longer on Youtube</span>
    <span class="nx">chrome</span><span class="p">.</span><span class="nx">tabs</span><span class="p">.</span><span class="nx">onUpdated</span><span class="p">.</span><span class="nf">addListener</span><span class="p">(</span><span class="nf">function </span><span class="p">(</span><span class="nx">id</span><span class="p">,</span> <span class="nx">changeInfo</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if </span><span class="p">(</span><span class="nx">tabId</span> <span class="o">===</span> <span class="nx">id</span> <span class="o">&amp;&amp;</span> <span class="nx">changeInfo</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">complete</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">chrome</span><span class="p">.</span><span class="nx">tabs</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="nx">tabId</span><span class="p">,</span> <span class="nf">function </span><span class="p">(</span><span class="nx">tab</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">if </span><span class="p">(</span><span class="nx">tab</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nf">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">youtube.com</span><span class="dl">'</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                    <span class="nf">removeYoutubeTab</span><span class="p">(</span><span class="nx">tabId</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">});</span>
        <span class="p">}</span>
    <span class="p">});</span>
<span class="p">}</span>

<span class="c1">// Removes specific tab from active_youtube_tabs list</span>
<span class="kd">function</span> <span class="nf">removeYoutubeTab</span><span class="p">(</span><span class="nx">tabId</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">idx</span> <span class="o">=</span> <span class="nx">active_youtube_tabs</span><span class="p">.</span><span class="nf">indexOf</span><span class="p">(</span><span class="nx">tabId</span><span class="p">);</span>
    <span class="nx">active_youtube_tabs</span><span class="p">.</span><span class="nf">splice</span><span class="p">(</span><span class="nx">idx</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div> <p>When the script starts, a callback function is added with <code class="language-html highlighter-rouge">onMessage.addListener()</code> to handle events. Depending on the event received, a different action will be triggered. For example, the <strong><code class="language-html highlighter-rouge">START_COUNTDOWN</code></strong> event will start the countdown in the background script. The tabId is stored in a list to keep track of active youtube tabs if the sender is youtube. This is done using the Chrome Tabs API and we need to give permissions to our application in the manifest file.</p> <p>I needed to use JQuery in the background script so I downloaded the <em>jquery-3.4.1.min.js</em> file, saved it in the <strong>js</strong> directory and specified the file as a background script. Here are the new changes to the manifest file:</p> <pre><code class="language-JSON">{
    "manifest_version": 2,
    "version": "0.1",
    "name": "My Extension",
    "description": "This is my extension",
    "permissions": [
        "tabs"
    ],
    "background": {
        "scripts": [
            "js/background.js"
            "js/jquery-3.4.1.min.js"
        ],
        "persistent": true
    }
}
</code></pre> <h3 id="content-scripts">Content Scripts</h3> <p>Content Scripts are run on specific web pages and can interact with a website’s DOM. To define a content script, I added a <strong>content_scripts</strong> section to the <strong>manifest</strong> file.</p> <pre><code class="language-JSON">{
    "manifest_version": 2,
    "version": "0.1",
    "name": "My Extension",
    "description": "This is my extension",
    "permissions": [
        "tabs"
    ],
    "background": {
        "scripts": [
            "js/background.js"
            "js/jquery-3.4.1.min.js"
        ],
        "persistent": true
    },
    "content_scripts": [
        {
            "matches": [
                "*://*.youtube.com/*"
            ],
            "js": [
                "js/jquery-3.4.1.min.js",
                "js/content.js"
            ],
            "run_at": "document_end"
        }
    ]
}
</code></pre> <p>The <code class="language-html highlighter-rouge">"matches": [ "*://*.youtube.com/*" ]</code> section tells Chrome to run the content scripts when the URL of the website matches the values specified. The <code class="language-html highlighter-rouge">"run_at": "document_end"</code> section ensures that the content scripts are run after the page is loaded.</p> <h3 id="conclusion">Conclusion</h3> <p>Chrome Extensions have changed since I first created this project but it was still a valuable experience.</p> <p>🐢</p> <script src="//cdn.jsdelivr.net/gh/TRSasasusu/highlightjs-highlight-lines.js@1.1.5/highlightjs-highlight-lines.min.js"></script> <script>hljs.initHighlightingOnLoad(),hljs.initHighlightLinesOnLoad([[],[],[{start:5,end:10,color:"rgba(255, 255, 255, 0.2)"}],[],[{start:5,end:7,color:"rgba(255, 255, 255, 0.2)"},{start:11,end:11,color:"rgba(255, 255, 255, 0.2)"}],[{start:15,end:26,color:"rgba(255, 255, 255, 0.2)"}]]);</script>]]></content><author><name>Yu-Yueh Liu</name></author><category term="Learning"/><category term="JavaScript"/><category term="HTML5"/><category term="CSS3"/><category term="Chrome Extension"/><summary type="html"><![CDATA[Chrome extensions are programs that add functionalities to Chrome and enhance your browsing experience. In fact, you can find a myriad of extensions for the purpose of improving your productivity, protecting your privacy, and more. Making your own Chrome extension is quite simple and it could turn out to be a fun personal project. Goal This article is a documentation of what I learned when making my first Chrome extension YouTubeStopwatch. If you’re looking for a tutorial for starters, check out the official Getting Started Tutorial. What is YouTubeStopwatch? YouTubeStopwatch was created for a course on Human-Computer Interaction (HCI). The objective was to help users manage the amount of time they would like to spend on YouTube, and somehow incite them to quit YouTube without resorting to blocking the site. The idea was to prompt the user for the desired time they want to spend on YouTube and start a countdown. Once the time is up, the user is asked whether they want to stay on YouTube or leave. If they choose to keep watching videos, they will be subject to some gradual graphical deterioration and slowly worsening their viewing experience. So how did I get started? Well, the first thing I had to learn was how Chrome Extensions are structured. Project Structure src ├── manifest.json ├── popup.html ├── js │ ├── background.js │ ├── content.js │ ├── jquery-3.4.1.min.js.js │ ├── constants.js │ └── ... ├── img │ └── extension-icon.png └── css └── popup.css Manifest The manifest.json file is the first thing you need when creating an extension. It provides all the information about your extension to Google Chrome such as the name of your extension, the permissions needed, etc but we’ll get into that a bit later. Here is a minimal example: { "manifest_version": 2, "version": "0.1", "name": "My Extension", "description": "This is my extension" } Background Scripts Background scripts are scripts that run in the background of your browser when you open Google Chrome. You can make the scripts persistent or not depending on your use case. I chose to use a persistent script. As long as Google Chrome is open, the script will be running. To define background scripts, I added a background section to the manifest file. { "manifest_version": 2, "version": "0.1", "name": "My Extension", "description": "This is my extension", "background": { "scripts": [ "js/background.js" ], "persistent": true } } Note: It is now recommended to use non-persistent background scripts with Event Driven Background Scripts. Below is an example of what my background script looked like. // List to track all active YouTube tabs var active_youtube_tabs=[]; // Create Main Event Listener function initBackground() { chrome.runtime.onMessage.addListener(function (msg, sender) { var tabId=sender.tab ? sender.tab.id : null; // If sender is youtube, add listener if (msg.from === 'youtube' &amp;&amp; typeof (msg.event) === 'undefined') { active_youtube_tabs.indexOf(tabId) &lt; 0 ? addListeners(tabId) : null; } // Handles event message switch (msg.event) { case "START_COUNTDOWN": startCountdown(); break; ... default: break; } }); } initBackground(); // Subscribes tab to active youtube tabs and adds listener to url changes function addListeners(tabId) { active_youtube_tabs.push(tabId); // When a youtube tab is closed, remove tabId from active_youtube_tabs list chrome.tabs.onRemoved.addListener(function (id) { if (tabId === id) { removeYoutubeTab(tabId); } }); // When the tab url changes, remove tabId from active_youtube_tabs if user is no longer on Youtube chrome.tabs.onUpdated.addListener(function (id, changeInfo) { if (tabId === id &amp;&amp; changeInfo.status === 'complete') { chrome.tabs.get(tabId, function (tab) { if (tab.url.indexOf('youtube.com') &lt; 0) { removeYoutubeTab(tabId); } }); } }); } // Removes specific tab from active_youtube_tabs list function removeYoutubeTab(tabId) { var idx=active_youtube_tabs.indexOf(tabId); active_youtube_tabs.splice(idx, 1); } When the script starts, a callback function is added with onMessage.addListener() to handle events. Depending on the event received, a different action will be triggered. For example, the START_COUNTDOWN event will start the countdown in the background script. The tabId is stored in a list to keep track of active youtube tabs if the sender is youtube. This is done using the Chrome Tabs API and we need to give permissions to our application in the manifest file. I needed to use JQuery in the background script so I downloaded the jquery-3.4.1.min.js file, saved it in the js directory and specified the file as a background script. Here are the new changes to the manifest file: { "manifest_version": 2, "version": "0.1", "name": "My Extension", "description": "This is my extension", "permissions": [ "tabs" ], "background": { "scripts": [ "js/background.js" "js/jquery-3.4.1.min.js" ], "persistent": true } } Content Scripts Content Scripts are run on specific web pages and can interact with a website’s DOM. To define a content script, I added a content_scripts section to the manifest file. { "manifest_version": 2, "version": "0.1", "name": "My Extension", "description": "This is my extension", "permissions": [ "tabs" ], "background": { "scripts": [ "js/background.js" "js/jquery-3.4.1.min.js" ], "persistent": true }, "content_scripts": [ { "matches": [ "*://*.youtube.com/*" ], "js": [ "js/jquery-3.4.1.min.js", "js/content.js" ], "run_at": "document_end" } ] } The "matches": [ "*://*.youtube.com/*" ] section tells Chrome to run the content scripts when the URL of the website matches the values specified. The "run_at": "document_end" section ensures that the content scripts are run after the page is loaded. Conclusion Chrome Extensions have changed since I first created this project but it was still a valuable experience. 🐢]]></summary></entry></feed>