Bash scripting in an OpenShift Environment

Oren Oichman
7 min readAug 14, 2021

Why This Tutorial

In the PAAS Admin world there are (roughly) 2 types of admins . One type (like me) are Linux Dinosaurs which have adopted and learn a lot on the Kubernetes’s ins and outs while retaining their long Linux experience. the other type are briete new admins with a fresh per of eyes and knowledge hungry approach.
In this tutorial I will address the first type and how to bring a vast knowledge in bash scripting to the OpenShift/Kubernetes world.

What has Changed ?

When I gave it some thoughts the best why for me to address the transition is to think about where the power focus (from the admin P.O.V).
If in the Linux world the terminal and terminal scripting (Bash , Perl or even python) is where the input/output transfer could take place the new way forces us to different standard.We can still keep our scripting powers in the OpenShift/Kubernetes World but we need to understand that writing scripts to work with OCP/K8S will require us to start talking to them over API Calls.
In other words we can carry on scripting but we need to see things as if every command werun is actually an API call.

What is an API call ?

An API is An application programming interface (API) is a connection between computers or between computer programs. It is a type of software interface, offering a service to other pieces of software.A document or standard that describes how to build such a connection or interface is called an API specification. A computer system that meets this standard is said to implement or expose an API.
An API Call is basically an HTTP request method which consistent of keys and values transfer from the client side to the server side.
Once the server receive the method and the keys/values it execute the script which is attached to that specific method.
From the RFC here is a table about the HTTP Request Method :

   +---------+-------------------------------------------------+
| Method | Description |
+---------+-------------------------------------------------|
| GET | Transfer a current representation of the target |
| | resource. |
| HEAD | Same as GET, but only transfer the status line |
| | and header section. |
| POST | Perform resource-specific processing on the |
| | request payload. |
| PUT | Replace all current representations of the |
| | target resource with the request payload. |
| DELETE | Remove all current representations of the |
| | target resource. |
| CONNECT | Establish a tunnel to the server identified by |
| | the target resource. |
| OPTIONS | Describe the communication options for the |
| | target resource. |
| TRACE | Perform a message loop-back test along the path |
| | to the target resource. |
+---------+-------------------------------------------------+

In our case we are going to focus on GET and POST.

Has the terminal gone away ?

in a word NO!!! , the terminal in reality is getting a lot more focus and support tool in order to help us work with the output and extract what we need when we need it.
the main different is the way our tools are been communication

We have a lot of tools to work with OpenShift such as “oc” , “odo” , “helm” , “opm” , “tkn” and more but I will cover some advanced examples about those tools in another tutorial.

Get scripting

in our example we will build a very simple container image which is running httpd with bash as a scripting language to hand the HTTP request.

If you are like me and wants to be based on a UBI/Centos image you can use the following procedure :

Custom Image

Before we create the custom image a couple of emphasis in regards to TLS.
In the Kubernetes world the SSL/TLS termination can take place in the ingress/route stage , the Load Balancer that balance that traffic to the ingress pods or on the application itself.
In our scripting cases we will use the TLS termination on the application level so both external and internal HTTP communication will be encrypted.

# mkdir bash-cgi
# cd bash-cgi/

Now we will create a small cgi.conf file

# cat > cgi.conf << EOF
<VirtualHost *:8080>
ScriptAlias "/cgi-bin/" "/opt/app-root/cgi-bin/"
<Directory /opt/app-root/cgi-bin/>
AllowOverride All
Options +ExecCGI +Indexes
DirectoryIndex index.sh
AddHandler cgi-script .cgi .sh
Require all granted
</Directory>
</VirtualHost>
EOF

Now let’s create a small script that will be the entry point for our image :

# echo '#!/bin/bash/usr/sbin/httpd $OPTIONS -DFOREGROUND' > run-httpd.sh

And make sure it is executable :

# chmod a+x run-httpd.sh

Now for the script that will run from the web server (we will call the script index.sh :

# touch index.sh

Now make sure the content of the file is as follow :

#!/bin/bashfunction response_with_html() {    
echo "Content-type: text/html"
echo ""
echo "<!DOCTYPE html>"
echo "<html><head>"
echo "<title>bash cgi</title>"
echo "</head><body>"
echo "<h1>Bash CGI</h1>"
echo "<p>your CGI script in bash is working</p>"
echo "<hr>"
echo "$SERVER_SIGNATURE"
echo "</body></html>"
}
function response_with_json(){
echo "Content-type: application/json"
echo ""
echo "{\"message\": \"Hello World!\"}"
}
if [ "$REQUEST_METHOD" = "POST" ]; then

# The environment variabe $CONTENT_TYPE describes the data-type received
case "$CONTENT_TYPE" in
application/json)
# The environment variabe $CONTENT_LENGTH describes the size of the data
read -n "$CONTENT_LENGTH" QUERY_STRING_POST # read datastream
# The following lines will prevent XSS and check for valide JSON-Data.
# But these Symbols need to be encoded somehow before sending to this script
QUERY_STRING_POST=$(echo "$QUERY_STRING_POST" | sed "s/'//g" | sed 's/\$//g;s/`//g;s/\*//g;s/\\//g' )
# removes some symbols (like \ * ` $ ') to prevent XSS with Bash and SQL.
QUERY_STRING_POST=$(echo "$QUERY_STRING_POST" | sed -e :a -e 's/<[^>]*>//g;/</N;//ba')
# removes most html declarations to prevent XSS within documents
JSON=$(echo "$QUERY_STRING_POST" | jq .)
# json encode - This is a pretty save way to check for valide json code
echo "Content-type: application/json"
echo ""
echo "$JSON"
exit 0
;;
*)
response_with_html
exit 0
;;
esac
else
if [[ "$CONTENT_TYPE" =~ "application/json" ]]; then
response_with_json
else
response_with_html
fi
exit 0
fi
# Some Commands ...response_with_jsonexit 0

And let’s make sure it can be run (as an executable) :

# chmod a+x index.sh

For our final file we will create the Dockerfile that will install the httpd an put everything in it’s place :

# cat > Dockerfile << EOF
FROM ubi8/ubi
MAINTAINER Oren Oichman <Back to Root>
RUN dnf install -y httpd mod_ssl jq && \
dnf clean all
COPY run-httpd.sh /usr/sbin/run-httpd.sh
RUN echo "PidFile /tmp/http.pid" >> /etc/httpd/conf/httpd.conf
RUN sed -i "s/Listen\ 80/Listen\ 8080/g" /etc/httpd/conf/httpd.conf
RUN sed -i "s/\"logs\/error_log\"/\/dev\/stderr/g" /etc/httpd/conf/httpd.conf
RUN sed -i "s/CustomLog \"logs\/access_log\"/CustomLog \/dev\/stdout/g" /etc/httpd/conf/httpd.conf
RUN echo 'ScriptSock /tmp/cgid.sock' >> /etc/httpd/conf/httpd.conf
RUN echo 'IncludeOptional /opt/app-root/*.conf' >> /etc/httpd/conf/httpd.conf && \
rm -f /etc/httpd/conf.d/ssl.conf
RUN mkdir /opt/app-root/ && chown apache:apache /opt/app-root/ && chmod 777 /opt/app-root/
COPY cgi.conf /opt/app-root/
RUN mkdir /opt/app-root/cgi-bin && chown apache:apache /opt/app-root/cgi-bin && chmod 777 /opt/app-root/cgi-bin
COPY index.sh /opt/app-root/cgi-bin/
USER apache
EXPOSE 8080
ENTRYPOINT ["/usr/sbin/run-httpd.sh"]
EOF

Now it’s time to build our new image :

# buildah bud -f Dockerfile -t bash-cgi

Once the image build is completed you can ahead and push it in our openshift registry (in my example I will use registry as the internal registry):

# buildah push registry.example.com:/my-project/bash-cgi:latest

On OpenShift

Create a project that will contain our application :

# oc new-project bash-cgi

The rest of the action items we are running a simple deployment , service and route for which we need to create a corresponding YAML files

For the deployment the file should look as follow :

# cat > deployment.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: bash-cgi
labels:
app: bash-cgi
spec:
replicas: 1
selector:
matchLabels:
app: bash-cgi
template:
metadata:
labels:
app: bash-cgi
spec:
containers:
- name: bash-cgi
image: registry.example.com:/my-project/bash-cgi:latest
ports:
- name: http
containerPort: 8080
protocol: TCP
EOF

Moving on to the service :

# cat > service.yaml << EOF
apiVersion: v1
kind: Service
metadata:
name: bash-cgi
labels:
app: bash-cgi
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: bash-cgi
EOF

and now for the route :

$ cat > bash-cgi-route.yaml << EOF
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: bash-cgi
spec:
port:
targetPort: 8080
to:
kind: Service
name: bash-cgi
weight: 100
wildcardPolicy: None
EOF

Now let’s apply everything together :

# oc apply -f service.yaml -f bash-cgi-route.yaml -f deployment.yaml

let’s follow the deployment to make sure everything is up and running :

# watch -n 1 "oc get all"

Testing

first let’s create a variable for our route :

# MY_URL=$(echo -n 'http://' && oc get route bash-cgi -o jsonpath='{.spec.host}')

now let’s run a simple HTML testing with curl :

# curl $MY_URL

We should have received the HTML output we wrote in the “response_with_html” function.

Now we are going to run the same command but we are going to add a header to our curl :

# curl -H "Content-type: text/html" $MY_URL

The results should be the same but now we have sent a content type to our application.
Eventually we would want to receive a JSON output , so we will change our content type to “application/json” :

# curl -H "Content-type: application/json" $MY_URL

Now our output should look as such :

{"message": "Hello World!"}

Which is the output of the “response_with_json” function.

For out last test we will add a POST method and a JSON struct data which we will want to receive back in the response :

# curl -H "Content-type: application/json" -X POST -d '{"what is it?":"A test","Are you sure?":"Yes"}' $MY_URL

As you can see the we are asking 2 questions with their answers in a JSON struct and our application returns it to use in a clean JSON view :

{
"what is it": "A test",
"Are you sure?": "Yes"
}

what next ?

This is just the very basic , from here we can start build any API we want using BASH or even start building admission controller / operators in BASH to run what we want when we want it.

If you have any question feel free to responed/ leave a comment.
You can find on linkedin at : https://www.linkedin.com/in/orenoichman
Or twitter at : https://twitter.com/ooichman

--

--