openshift

Using Secret Credential to Connect to Gitlab in Jenkins

Jenkins is a famous CICD tools that can orchestrate our build and deployment strategy, which can also connect with other CICD toolings such as sourcecode management, or security scanning tools.

But sometimes access to those toolings are limited therefore we need to provide some credentials, but dont want those credentials to be displayed in a plain text. This is where Jenkins Credentials fits into the picture.

We can leverage Jenkins Credentials to store credentials as a secret which can be call by our pipeline directly,

We can start by creating a “Username with password” and put our Gitlab username and password there, dont forget to set the ID for this credentials which is going to be called later from our pipeline.

We can call the saved credentials from pipeline by using “withCredentials” mechanism

node() {
    stage ('git clone') {
        sh "git config --global http.sslVerify false"
        withCredentials([usernamePassword(credentialsId: 'my-gitlab-credential', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
            sh "git clone https://\${USERNAME}:\${PASSWORD}@gitlab.company.com/app/my-repo.git source "
        }
    }
}

A successful pipeline would generate below logs,

Started by user developer
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/lib/jenkins/jobs/my-pipeline/workspace
[Pipeline] {
[Pipeline] stage
[Pipeline] { (git clone)
[Pipeline] sh
+ git config --global http.sslVerify false
[Pipeline] withCredentials
Masking supported pattern matches of $USERNAME or $PASSWORD
[Pipeline] {
[Pipeline] sh
+ git clone https://****:****@gitlab.company.com/app/my-repo.git source
Cloning into 'source'...
[Pipeline] }
[Pipeline] // withCredentials
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

And we can see from above logs that our pipeline is successfully executed.

A Multi-Tenant Application using Quarkus, Flyway, and Openshift 4

According to Wikipedia, Software multitenancy is a software architecture in which a single instance of software runs on a server and serves multiple tenants. But for this sample we are trying to create a full multi-tenancy software, while isolating each user in a different namespace and a different database to optimize resource utilisation and make operation much easier.

The high level implementation would be like below image,

Based on above diagram, we are creating several Openshift Namespaces where we will install the same application, which is built with Quarkus, and a mysql database while having Flyway to maintain the database schema among different databases instances.

So lets start with a simple pom file,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw.example</groupId>
    <artifactId>quarkus-flyway</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <compiler-plugin.version>3.11.0</compiler-plugin.version>
        <maven.compiler.release>11</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
        <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
        <quarkus.platform.version>3.4.1</quarkus.platform.version>
        <skipITs>true</skipITs>
        <surefire-plugin.version>3.1.2</surefire-plugin.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>${quarkus.platform.artifact-id}</artifactId>
                <version>${quarkus.platform.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-arc</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-resteasy-jackson</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-resteasy</artifactId>
        </dependency>

        <!-- Hibernate ORM specific dependencies -->
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-hibernate-orm-panache</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-hibernate-orm</artifactId>
        </dependency>

        <!-- Flyway specific dependencies -->
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-flyway</artifactId>
        </dependency>

        <!-- Flyway MariaDB/MySQL specific dependencies -->
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-mysql</artifactId>
        </dependency>

        <!-- JDBC driver dependencies -->
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-jdbc-mysql</artifactId>
        </dependency>

        <!-- monitoring-->
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-smallrye-health</artifactId>
        </dependency>

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>quarkus-maven-plugin</artifactId>
                <version>${quarkus.platform.version}</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <goals>
                            <goal>build</goal>
                            <goal>generate-code</goal>
                            <goal>generate-code-tests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${compiler-plugin.version}</version>
                <configuration>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <configuration>
                    <systemPropertyVariables>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                        <maven.home>${maven.home}</maven.home>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                        <configuration>
                            <systemPropertyVariables>
                                <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                                <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                                <maven.home>${maven.home}</maven.home>
                            </systemPropertyVariables>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <profiles>
        <profile>
            <id>native</id>
            <activation>
                <property>
                    <name>native</name>
                </property>
            </activation>
            <properties>
                <skipITs>false</skipITs>
                <quarkus.package.type>native</quarkus.package.type>
            </properties>
        </profile>
    </profiles>

</project>

and properties file, in here we set database connection as parameterized to accomodate a multiple database integration for each tenant.

# default
quarkus.http.port=8080
quarkus.log.level=INFO
quarkus.log.category."com.edw".level=DEBUG

# disable sending anonymous statistics
quarkus.analytics.disabled=true

# datasource
quarkus.datasource.db-kind = mysql
quarkus.datasource.jdbc.max-size=20
quarkus.datasource.jdbc.min-size=2
quarkus.datasource.jdbc.initial-size=5
quarkus.datasource.jdbc.background-validation-interval=15S
quarkus.datasource.jdbc.validation-query-sql=select 1;

quarkus.datasource.username = ${DB_USER}
quarkus.datasource.password = ${DB_PASSWORD}
quarkus.datasource.jdbc.url = ${DB_URL}

# Run Flyway migrations automatically
quarkus.flyway.migrate-at-start=true
quarkus.flyway.baseline-on-migrate=true
quarkus.flyway.validate-on-migrate=true
quarkus.flyway.default-schema=db_test

after that, lets add Entity, Service, and Controller class

package com.edw.example.entity;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity(name = "T_STUDENT")
public class Student extends PanacheEntityBase {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nama;
    private Integer age;

    public Student() {
    }

    public Student(Long id, String nama, Integer age) {
        this.id = id;
        this.nama = nama;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    // other setter and getter
}

package com.edw.example.service;

import com.edw.example.entity.Student;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

@ApplicationScoped
public class StudentService {

    private Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    public List<Student> getAll() {
        logger.debug(String.format("getting all table result"));
        return Student.findAll(Sort.by("id", Sort.Direction.Descending))
                .page(Page.ofSize(5))
                .list();
    }
}
package com.edw.example.controller;

import com.edw.example.service.StudentService;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;

@Path("/student")
public class StudentController {

    private Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    @Inject
    StudentService studentService;

    @GET
    @Path("/get-all")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response getAll() {
        return Response
                .status(200)
                .entity(studentService.getAll())
                .build();
    }

}

Now lets go to the interesting part, and that is the Flyway configuration. First we need to create a file with the name of “V1.0.0__initial_table_t_student.sql” and put it under the “resources/db/migration” folder. This is needed to initialize database schema the first time during application initialization.

create table t_student
(
    id     bigint  auto_increment
        primary key,
    nama varchar(100)      null,
    age int null
);

And last is, a simple Dockerfile since we are going to containerized

FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:latest

ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"

COPY target/quarkus-app/lib/ /deployments/lib/
COPY target/quarkus-app/*.jar /deployments/
COPY target/quarkus-app/app/ /deployments/app/
COPY target/quarkus-app/quarkus/ /deployments/quarkus/

EXPOSE 8080
USER 185

ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ]

Build our application by using mvn and containerized it

$ mvn clean package

$ docker build -t quarkus-flyway:latest .

Push this image to an Image Registry, for this example im using Openshift Internal Image Registry, and deploy it to Openshift using below YAML file using “oc apply” command.

kind: Deployment
apiVersion: apps/v1
metadata:
  name: quarkus-flyway
  namespace: company-01-ns
  labels:
    app: quarkus-flyway
spec:
  replicas: 1
  selector:
    matchLabels:
      app: quarkus-flyway
    spec:
      containers:
        - resources: {}
          readinessProbe:
            httpGet:
              path: /q/health
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 10
            timeoutSeconds: 1
            periodSeconds: 10
            successThreshold: 1
            failureThreshold: 3
          terminationMessagePath: /dev/termination-log
          name: quarkus-flyway
          livenessProbe:
            httpGet:
              path: /q/health
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 30
            timeoutSeconds: 2
            periodSeconds: 5
            successThreshold: 1
            failureThreshold: 3
          env:
            - name: DB_USER
              value: admin
            - name: DB_PASSWORD
              value: password
            - name: DB_URL
              value: 'jdbc:mysql://mysql:3306/db_test'
          ports:
            - containerPort: 8080
              protocol: TCP
            - containerPort: 8443
              protocol: TCP
          imagePullPolicy: IfNotPresent
          terminationMessagePolicy: File
          image: >-
            image-registry.openshift-image-registry.svc:5000/company-01-ns/quarkus-flyway:latest
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
      dnsPolicy: ClusterFirst
      securityContext: {}
      schedulerName: default-scheduler
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 25%
      maxSurge: 25%
  revisionHistoryLimit: 10
  progressDeadlineSeconds: 600

This will resulted in each Namespace having their own application, and MySql database.

Code for this project can be seen on below Github repo,

https://github.com/edwin/quarkus-flyway

Error 400 when Accessing Openshift 4 Route

Just had this intermittent error when accessing my application which is being deployed to Openshift 4,

<html><body><h1>400 Bad request</h1>
Your browser sent an invalid request.
</body></html>

At first, we tought that issue happens at application level. But after further debugging, it is shown that there is no logs captured at all from the application’s perspective. After further debugging, we realized that issue happens on Openshift’s Router level, where logs can be seen below.

2023-09-30T18:00:50.097282+00:00 infra-0 infra-0.ocp.local haproxy[46]: 127.0.0.1:41722 [30/Sep/2023:18:00:50.096] public openshift_default/<NOSRV> 0/-1/-1/-1/0 503 157 - - SC-- 1/1/0/0/0 0/0 "HEAD / HTTP/1.1"
2023-09-30T18:00:52.385991+00:00 infra-0 infra-0.ocp.local haproxy[46]: 10.20.24.80:36248 [30/Sep/2023:18:00:52.375] fe_no_sni~ fe_no_sni/<NOSRV> -1/-1/-1/-1/10 400 211 - - PR-- 2/1/0/0/0 0/0 "<BADREQ>"
2023-09-30T18:00:52.387088+00:00 infra-0 infra-0.ocp.local haproxy[46]: 10.20.24.80:36248 [30/Sep/2023:18:00:52.375] public_ssl be_no_sni/fe_no_sni 1/0/11 2440 SD 1/1/0/0/0 0/0
2023-09-30T18:00:53.915337+00:00 infra-0 infra-0.ocp.local haproxy[46]: 10.20.24.80:39454 [30/Sep/2023:18:00:53.914] public public/<NOSRV> -1/-1/-1/-1/0 400 211 - - PR-- 1/1/0/0/0 0/0 "<BADREQ>"
2023-09-30T18:00:56.155389+00:00 infra-0 infra-0.ocp.local haproxy[46]: 10.20.24.80:36306 [30/Sep/2023:18:00:56.144] public_ssl be_tcp:openshift-authentication:oauth-openshift/pod:oauth-openshift-69bc64d75b-r5z8t:oauth-openshift:https:10.130.1.123:6443 1/1/10 3687 -- 1/1/0/0/0 0/0
2023-09-30T18:00:57.368604+00:00 infra-0 infra-0.ocp.local haproxy[46]: 10.20.24.80:36322 [30/Sep/2023:18:00:57.366] fe_no_sni~ fe_no_sni/<NOSRV> -1/-1/-1/-1/2 400 211 - - PR-- 2/1/0/0/0 0/0 "<BADREQ>"
2023-09-30T18:00:57.369754+00:00 infra-0 infra-0.ocp.local haproxy[46]: 10.20.24.80:36322 [30/Sep/2023:18:00:57.365] public_ssl be_no_sni/fe_no_sni 1/0/3 404 SD 1/1/0/0/0 0/0
2023-09-30T18:00:58.847737+00:00 infra-0 infra-0.ocp.local haproxy[46]: 10.20.24.80:39522 [30/Sep/2023:18:00:58.847] public public/<NOSRV> -1/-1/-1/-1/0 400 211 - - PR-- 1/1/0/0/0 0/0 "<BADREQ>"

Where some requests were given error 400 BADREQ. And it seems that rootcause is haproxy blocking big http headers, we can see the sample below where i simulate a very big cookies when accessing my application thru Openshift Router.

$ curl -kv  https://my.apps.ocp.local --cookie "LELE=$(perl -e 'print "x"x25000')"
* Rebuilt URL to: https://my.apps.ocp.local/
*   Trying 10.20.20.135...
* TCP_NODELAY set
* Connected to my.apps.ocp.local (10.20.20.135) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, [no content] (0):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, [no content] (0):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: C=ID; ST=Daerah Khusus Ibukota Jakarta; L=Jakarta Selatan; O=xxxx; CN=*.xxx
*  start date: Nov  8 00:00:00 2022 GMT
*  expire date: Dec  9 23:59:59 2023 GMT
*  issuer: C=US; O=DigiCert Inc; CN=DigiCert TLS RSA SHA256 2020 CA1
*  SSL certificate verify ok.
* TLSv1.3 (OUT), TLS app data, [no content] (0):
> GET / HTTP/1.1
> Host: my.apps.ocp.local
> User-Agent: curl/7.61.1
> Accept: */*
> Cookie: LELE=xxxxxx.....xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

TLSv1.3 (OUT), TLS app data, [no content] (0):
* TLSv1.3 (IN), TLS app data, [no content] (0):
< HTTP/1.1 400 Bad request
< content-length: 90
< cache-control: no-cache
< content-type: text/html
< connection: close
<
<html><body><h1>400 Bad request</h1>
Your browser sent an invalid request.
</body></html>
* Closing connection 0
* TLSv1.3 (OUT), TLS alert, [no content] (0):
* TLSv1.3 (OUT), TLS alert, close notify (256):

Workaround is pretty much simple, we can see it on below document

https://docs.openshift.com/container-platform/4.13/networking/ingress-operator.html#nw-ingress-controller-configuration-parameters_configuring-ingress

And that is to increase headerBufferBytes,

$ oc -n openshift-ingress-operator patch ingresscontroller/default \
	--type=merge -p '{"spec":{"tuningOptions": {"headerBufferBytes": 50000}}}'

Deploy and Use SealedSecret and KubeSeal on Openshift 4.x

Sealed Secrets are a way to encrypt Kubernetes Secrets value that can be created by anyone, but can only be decrypted by the controller running in the target cluster recovering the original object. This is a good way if we want to store our sensitive configuration values into a git repository, especially when doing a gitops approach.

First is we need to install helm and add sealed-secret repo to it,

$ brew install helm

$ helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets

Next is we need to create a specific Namespace and install our sealed-secret there,

$ oc project sealed-secrets

$ helm install my-sealed-secret  \
         --set containerSecurityContext.enabled=false \
		 --set podSecurityContext.enabled=false \
		 sealed-secrets/sealed-secrets	

Lets try to create a simple Kubernetes secret

$ oc create secret generic app-cred-secret \
		--from-literal=username=username123 \ 
		--from-literal=password=password123 \ 
		--dry-run=client -n edwin-ns -o yaml  > secret.yaml

Where the result would be like this,

apiVersion: v1
data:
  password: cGFzc3dvcmQxMjM=
  username: dXNlcm5hbWUxMjM=
kind: Secret
metadata:
  creationTimestamp: null
  name: app-cred-secret
  namespace: edwin-ns

Now lets try to use Kubeseal to generate a secret which is being encrypted. We can specify “controller-name” based on generated service name within “sealed-secrets” namespace.

$ brew install kubeseal

$ kubeseal --controller-name=my-sealed-secret-sealed-secrets \
       --controller-namespace=sealed-secrets -o yaml < secret.yaml > secret.sealed.yaml

We can see the result of the encrypted yaml,

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: app-cred-secret
  namespace: edwin-ns
spec:
  encryptedData:
    password: AgBXkADvKsjAHS31UwWFT+........eJtODYDQw==
    username: AgAP40ssm84PhmNYDKPfDf/Cf......JDBQDtQ==
  template:
    metadata:
      creationTimestamp: null
      name: app-cred-secret
      namespace: edwin-ns

After that, we can implement it directly using “oc apply” command

$ oc apply -f secret.sealed.yaml -n edwin-ns

and we can validate by running below command,

$ oc get sealedsecrets

NAME               AGE
app-cred-secret    53m

We can see that our secret is succesfully created in our namespace

$ oc get secret app-cred-secret -n edwin-ns

NAME              TYPE     DATA   AGE
app-cred-secret   Opaque   2      55m

Deploying a Dot Net Core Apps into Openshift 4

The goal of this article is to display a simple hello-world apps build on top of a .net core 7 that can be use to test a deployment to Openshift 4 platform. And we can start it by using a git clone command

$ git clone https://github.com/edwin/hello-world-dot-net-core

Go to the corresponding folder,

$ cd hello-world-dot-net-core

Create a namespace for this app,

$ oc new-project dot-net-ns

And run this command within the sourcecode folder,

$ oc new-app dotnet:7.0-ubi8~.

It will generate logs like this,

warning: Cannot check if git requires authentication.
--> Found image 4466483 (2 months old) in image stream "openshift/dotnet" under tag "7.0-ubi8" for "dotnet:7.0-ubi8"

    .NET 7
    ------
    Platform for building and running .NET 7 applications

    Tags: builder, .net, dotnet, dotnetcore, dotnet-70

    * A source build using source code from https://github.com/edwin/hello-world-dot-net-core#master will be created
      * The resulting image will be pushed to image stream tag "hello-world-dot-net-core:latest"
      * Use 'oc start-build' to trigger a new build

--> Creating resources ...
    imagestream.image.openshift.io "hello-world-dot-net-core" created
    buildconfig.build.openshift.io "hello-world-dot-net-core" created
    deployment.apps "hello-world-dot-net-core" created
    service "hello-world-dot-net-core" created
--> Success
    Build scheduled, use 'oc logs -f buildconfig/hello-world-dot-net-core' to track its progress.
    Application is not exposed. You can expose services to the outside world by executing one or more of the commands below:
     'oc expose service/hello-world-dot-net-core'
    Run 'oc status' to view your app.

And finally we can create a route for this service,

$ oc create route edge --service=hello-world-dot-net-core

We can try to do some changes on Index.cshtml file,

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://redhat.com">Red Hat loves dotnet</a>.</p>
</div>

Save and redeploy it by running below command in the root sourcecode folder,

$ oc start-build hello-world-dot-net-core --from-dir=.

The result would be something like this,

Lets do some more changes, and deploy it to Openshift

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://redhat.com">Red Hat loves alot of programming language but we loves dotnet more</a>.</p>
</div>

And we instantly can see changes within the web page,

Code can be seen here,

https://github.com/edwin/hello-world-dot-net-core