Start adding acs workflows talk.

This commit is contained in:
2023-07-30 19:59:03 +12:00
parent a8862c3728
commit 489a1c8ced
10 changed files with 640 additions and 1 deletions

View File

@ -13,4 +13,4 @@ spec:
status: "True" status: "True"
clusterSelector: clusterSelector:
matchLabels: matchLabels:
name: local-cluster name: dev-a

View File

@ -0,0 +1,148 @@
#+TITLE: RHACS Workflows & Integration
#+AUTHOR: James Blair
#+DATE: <2023-07-29 Sat 23:15>
This is a short demo I gave on [[https://www.redhat.com/en/technologies/cloud-computing/openshift/advanced-cluster-security-kubernetes][Red Hat Advanced Cluster Security]].
* Pre-requisites
This demo setup process assumes you already have an OpenShift 4.12+ cluster running, and are logged into the ~oc~ cli locally with cluster administration privileges.
For this demo I have an OpenShift ~4.12.12~ cluster running on AWS provisioned through the [[https://demo.redhat.com/catalog?item=babylon-catalog-prod/sandboxes-gpte.elt-ocp4-hands-on-acs.prod&utm_source=webapp&utm_medium=share-link][Red Hat Demo system]].
#+NAME: Check oc status
#+begin_src bash :results silent
oc version | grep Server
oc status
#+end_src
* Developer workflow integration
A key element of any cloud native security platform is how it can be incorporated into software development workflows to enable security teams to gain visibility of emerging security issues and also empower developers to understand the security posture of what they are building.
For this demonstration we will be using [[https://developers.redhat.com/products/openshift-dev-spaces/overview][OpenShift Dev Spaces]] as a cloud based development environment, and [[https://marketplace.visualstudio.com/items?itemName=redhat.vscode-tekton-pipelines][OpenShift Pipelines]] for a continuous integration environment.
** Install dev spaces operator
The first step to prepare the demo is to install the dev spaces operator so our cluster will be able to create cloud based development environments. We can install the operator programmatically by creating a ~subscription~ resource:
#+begin_src bash :results silent
cat << EOF | oc apply -f -
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: devspaces
namespace: openshift-operators
spec:
channel: stable
installPlanApproval: Automatic
name: devspaces
source: redhat-operators
sourceNamespace: openshift-marketplace
EOF
#+end_src
** Create devspaces controller
Once the operator is installed we can create a devspaces controller instance, this will be what is actually responsible for instantiating new individual developer workspaces.
Once again we can do this programmatically by creating a ~checluster~ resource:
#+begin_src bash :results silent
cat << EOF | oc apply -f -
apiVersion: org.eclipse.che/v2
kind: CheCluster
metadata:
name: devspaces
namespace: openshift-operators
spec:
components:
cheServer:
debug: false
logLevel: INFO
dashboard: {}
database:
externalDb: false
devWorkspace: {}
devfileRegistry: {}
imagePuller:
enable: false
spec: {}
metrics:
enable: true
pluginRegistry: {}
containerRegistry: {}
devEnvironments:
containerBuildConfiguration:
openShiftSecurityContextConstraint: container-build
defaultNamespace:
autoProvision: true
template: <username>-devspaces
maxNumberOfWorkspacesPerUser: -1
secondsOfInactivityBeforeIdling: 36000
secondsOfRunBeforeIdling: -1
startTimeoutSeconds: 300
storage:
pvcStrategy: per-user
gitServices: {}
networking:
auth:
gateway:
configLabels:
app: che
component: che-gateway-config
EOF
#+end_src
** Create individual dev space
Once the dev workspace operator and controller are ready we can create our individual developer workspace.
#+begin_src bash :results silent
cat << EOF | oc apply -f -
kind: DevWorkspace
apiVersion: workspace.devfile.io/v1alpha2
metadata:
name: vscode
namespace: opentlc-mgr-devspaces
spec:
started: true
template:
projects:
- name: talks
git:
remotes:
origin: "https://github.com/jmhbnz/talks.git"
components:
- name: dev
container:
image: quay.io/devfile/universal-developer-image:latest
commands:
- id: install-roxctl
exec:
component: dev
commandLine: curl -O https://mirror.openshift.com/pub/rhacs/assets/4.1.2/bin/Linux/roxctl && chmod +x roxctl
workingDir: ${PROJECT_SOURCE}
contributions:
- name: che-code
uri: https://eclipse-che.github.io/che-plugin-registry/main/v3/plugins/che-incubator/che-code/latest/devfile.yaml
components:
- name: che-code-runtime-description
container:
env:
- name: CODE_HOST
value: 0.0.0.0
EOF
#+end_src
** Deploy sample application
In order to showcase incorporating ~roxctl~ into developer workflows we need a sample application to tinker with.

View File

@ -0,0 +1,18 @@
FROM golang as builder
COPY main.go /guestbook/
COPY go.mod /guestbook/
COPY go.sum /guestbook/
RUN cd /guestbook && go build
FROM docker.io/ubuntu:latest
COPY --from=builder /guestbook/guestbook /app/guestbook
ADD public/index.html /app/public/index.html
ADD public/script.js /app/public/script.js
ADD public/style.css /app/public/style.css
ADD public/jquery.min.js /app/public/jquery.min.js
WORKDIR /app
CMD ["./guestbook"]
EXPOSE 3000

View File

@ -0,0 +1,12 @@
module guestbook
go 1.19
require (
github.com/codegangsta/negroni v1.0.0 // indirect
github.com/garyburd/redigo v1.6.4 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/xyproto/pinterface v1.5.3 // indirect
github.com/xyproto/simpleredis v0.0.0-20220117114834-9a1000fbd7af // indirect
)

View File

@ -0,0 +1,29 @@
github.com/codegangsta/negroni v1.0.0 h1:+aYywywx4bnKXWvoWtRfJ91vC59NbEhEY03sZjQhbVY=
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg=
github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
github.com/gomodule/redigo v1.8.8/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xyproto/pinterface v1.5.3 h1:RKkNT88cwrSqD9hU4cYAO5yeo8srg4TG+74Pcj88iz0=
github.com/xyproto/pinterface v1.5.3/go.mod h1:X5B5pKE49ak7SpyDh5QvJvLH9cC9XuZNDcl5hEyYc34=
github.com/xyproto/simpleredis v0.0.0-20150522151523-2fc7642209b5 h1:Pq2witO9kTO0Sxn0XqeBOUeKTG88JKn9uCfXEAF0XfA=
github.com/xyproto/simpleredis v0.0.0-20150522151523-2fc7642209b5/go.mod h1:uSYFxIza9OX4jlWn/KHQRd0YDCXza/L/S4WatobDE0U=
github.com/xyproto/simpleredis v0.0.0-20150523000142-9f9bdf9000d1 h1:+vB14RyCTr3FERRzenZftip8alGqRbhx7kzS5za9/VQ=
github.com/xyproto/simpleredis v0.0.0-20150523000142-9f9bdf9000d1/go.mod h1:uSYFxIza9OX4jlWn/KHQRd0YDCXza/L/S4WatobDE0U=
github.com/xyproto/simpleredis v0.0.0-20150526220545-97bd090877ec h1:qbZz02VvdGKFARwMxKlr3bbZsr/f5/O9oOGVigF6MUo=
github.com/xyproto/simpleredis v0.0.0-20150526220545-97bd090877ec/go.mod h1:uSYFxIza9OX4jlWn/KHQRd0YDCXza/L/S4WatobDE0U=
github.com/xyproto/simpleredis v0.0.0-20180505135304-2f4b48d695d6 h1:SA/bq+lPFQuyt4bb7oxLi6tkTg/8rKmu6dky68xgPus=
github.com/xyproto/simpleredis v0.0.0-20180505135304-2f4b48d695d6/go.mod h1:uSYFxIza9OX4jlWn/KHQRd0YDCXza/L/S4WatobDE0U=
github.com/xyproto/simpleredis v0.0.0-20220117114834-9a1000fbd7af h1:cysD3MzP3R/pQETtrLsxudVsc79USsWNFY27kYooQbY=
github.com/xyproto/simpleredis v0.0.0-20220117114834-9a1000fbd7af/go.mod h1:klBJiwXWN4OvxC5qVNAr7RnRYowZh9WyeAJoU6aZjZ0=
github.com/xyproto/simpleredis v2.6.5+incompatible h1:eghMfrjX+r1Ox9luPSZHctEEy5gd5tqgH7Azqvmfhu8=
github.com/xyproto/simpleredis v2.6.5+incompatible/go.mod h1:uSYFxIza9OX4jlWn/KHQRd0YDCXza/L/S4WatobDE0U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,293 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"encoding/json"
"math/rand"
"net/http"
"os"
"strings"
"time"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/xyproto/simpleredis"
)
var (
// For when Redis is used
masterPool *simpleredis.ConnectionPool
slavePool *simpleredis.ConnectionPool
// For when Redis is not used, we just keep it in memory
lists map[string][]string = map[string][]string{}
// For Healthz
startTime time.Time
delay float64 = 10 + 5*rand.Float64()
)
type Input struct {
InputText string `json:"input_text"`
}
type Tone struct {
ToneName string `json:"tone_name"`
}
func GetList(key string) ([]string, error) {
// Using Redis
if slavePool != nil {
list := simpleredis.NewList(slavePool, key)
if result, err := list.GetAll(); err == nil {
return result, err
}
// if we can't talk to the slave then assume its not running yet
// so just try to use the master instead
}
// if the slave doesn't exist, read from the master
if masterPool != nil {
list := simpleredis.NewList(masterPool, key)
return list.GetAll()
}
// if neither exist, we're probably in "in-memory" mode
return lists[key], nil
}
func AppendToList(item string, key string) ([]string, error) {
var err error
items := []string{}
// Using Redis
if masterPool != nil {
list := simpleredis.NewList(masterPool, key)
list.Add(item)
items, err = list.GetAll()
if err != nil {
return nil, err
}
} else {
items = lists[key]
items = append(items, item)
lists[key] = items
}
return items, nil
}
func ListRangeHandler(rw http.ResponseWriter, req *http.Request) {
var data []byte
items, err := GetList(mux.Vars(req)["key"])
if err != nil {
data = []byte("Error getting list: " + err.Error() + "\n")
} else {
if data, err = json.MarshalIndent(items, "", ""); err != nil {
data = []byte("Error marhsalling list: " + err.Error() + "\n")
}
}
rw.Write(data)
}
func ListPushHandler(rw http.ResponseWriter, req *http.Request) {
var data []byte
key := mux.Vars(req)["key"]
value := mux.Vars(req)["value"]
// propogate headers to analyzer service
headers := getForwardHeaders(req.Header)
// Add in the "tone" analyzer results
value += " : " + getPrimaryTone(value, headers)
items, err := AppendToList(value, key)
if err != nil {
data = []byte("Error adding to list: " + err.Error() + "\n")
} else {
if data, err = json.MarshalIndent(items, "", ""); err != nil {
data = []byte("Error marshalling list: " + err.Error() + "\n")
}
}
rw.Write(data)
}
func InfoHandler(rw http.ResponseWriter, req *http.Request) {
info := ""
// Using Redis
if masterPool != nil {
i, err := masterPool.Get(0).Do("INFO")
if err != nil {
info = "Error getting DB info: " + err.Error()
} else {
info = string(i.([]byte))
}
} else {
info = "In-memory datastore (not redis)"
}
rw.Write([]byte(info + "\n"))
}
func EnvHandler(rw http.ResponseWriter, req *http.Request) {
environment := make(map[string]string)
for _, item := range os.Environ() {
splits := strings.Split(item, "=")
key := splits[0]
val := strings.Join(splits[1:], "=")
environment[key] = val
}
data, err := json.MarshalIndent(environment, "", "")
if err != nil {
data = []byte("Error marshalling env vars: " + err.Error())
}
rw.Write(data)
}
func HelloHandler(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte("Hello from guestbook. " +
"Your app is up! (Hostname: " +
os.Getenv("HOSTNAME") +
")\n"))
}
func HealthzHandler(rw http.ResponseWriter, req *http.Request) {
if time.Now().Sub(startTime).Seconds() > delay {
http.Error(rw, "Timeout, Health check error!", http.StatusForbidden)
} else {
rw.Write([]byte("OK!"))
}
}
// Note: This function will not work until we hook-up the Tone Analyzer service
func getPrimaryTone(value string, headers http.Header) (tone string) {
u := Input{InputText: value}
b := new(bytes.Buffer)
json.NewEncoder(b).Encode(u)
client := &http.Client{}
req, err := http.NewRequest("POST", "http://analyzer:80/tone", b)
if err != nil {
return "Error talking to tone analyzer service: " + err.Error()
}
req.Header.Add("Content-Type", "application/json")
// add headers
for k := range headers {
req.Header.Add(k, headers.Get(k))
}
// print out headers for debug
// fmt.Printf("getPrimaryTone headers %v", req.Header)
res, err := client.Do(req)
if err != nil {
return "Error detecting tone: " + err.Error()
}
defer res.Body.Close()
body := []Tone{}
json.NewDecoder(res.Body).Decode(&body)
if len(body) > 0 {
// 7 tones: anger, fear, joy, sadness, analytical, confident, and tentative
if body[0].ToneName == "Joy" {
return body[0].ToneName + " (✿◠‿◠)"
} else if body[0].ToneName == "Anger" {
return body[0].ToneName + " (ಠ_ಠ)"
} else if body[0].ToneName == "Fear" {
return body[0].ToneName + " (ง’̀-‘́)ง"
} else if body[0].ToneName == "Sadness" {
return body[0].ToneName + " (︶︿︶)"
} else if body[0].ToneName == "Analytical" {
return body[0].ToneName + " ( °□° )"
} else if body[0].ToneName == "Confident" {
return body[0].ToneName + " (▀̿Ĺ̯▀̿ ̿)"
} else if body[0].ToneName == "Tentative" {
return body[0].ToneName + " (•_•)"
}
return body[0].ToneName
}
return "No Tone Detected"
}
// return the needed header for distributed tracing
func getForwardHeaders(h http.Header) (headers http.Header) {
incomingHeaders := []string{
"x-request-id",
"x-b3-traceid",
"x-b3-spanid",
"x-b3-parentspanid",
"x-b3-sampled",
"x-b3-flags",
"x-ot-span-context"}
header := make(http.Header, len(incomingHeaders))
for _, element := range incomingHeaders {
val := h.Get(element)
if val != "" {
header.Set(element, val)
}
}
return header
}
// Support multiple URL schemes for different use cases
func findRedisURL() string {
host := os.Getenv("REDIS_MASTER_SERVICE_HOST")
port := os.Getenv("REDIS_MASTER_SERVICE_PORT")
password := os.Getenv("REDIS_MASTER_SERVICE_PASSWORD")
master_port := os.Getenv("REDIS_MASTER_PORT")
if host != "" && port != "" && password != "" {
return password + "@" + host + ":" + port
} else if master_port != "" {
return "redis-master:6379"
}
return ""
}
func main() {
// When using Redis, setup our DB connections
url := findRedisURL()
if url != "" {
masterPool = simpleredis.NewConnectionPoolHost(url)
defer masterPool.Close()
slavePool = simpleredis.NewConnectionPoolHost("redis-slave:6379")
defer slavePool.Close()
}
startTime = time.Now()
r := mux.NewRouter()
r.Path("/lrange/{key}").Methods("GET").HandlerFunc(ListRangeHandler)
r.Path("/rpush/{key}/{value}").Methods("GET").HandlerFunc(ListPushHandler)
r.Path("/info").Methods("GET").HandlerFunc(InfoHandler)
r.Path("/env").Methods("GET").HandlerFunc(EnvHandler)
r.Path("/hello").Methods("GET").HandlerFunc(HelloHandler)
r.Path("/healthz").Methods("GET").HandlerFunc(HealthzHandler)
n := negroni.Classic()
n.UseHandler(r)
n.Run(":3000")
}

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta charset="utf-8">
<meta content="width=device-width" name="viewport">
<link href="style.css" rel="stylesheet">
<title>Guestbook - v2</title>
</head>
<body>
<div id="header">
<h1>Guestbook - v2</h1>
</div>
<div id="guestbook-entries">
<link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet">
<p>Waiting for database connection... <i class='em em-boat'></i></p>
</div>
<div>
<form id="guestbook-form">
<input autocomplete="off" id="guestbook-entry-content" type="text">
<a href="#" id="guestbook-submit">Submit</a>
</form>
</div>
<div>
<p><h2 id="guestbook-host-address"></h2></p>
<p><a href="env">/env</a>
<a href="info">/info</a></p>
</div>
<script src="jquery.min.js"></script>
<script src="script.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
$(document).ready(function() {
var headerTitleElement = $("#header h1");
var entriesElement = $("#guestbook-entries");
var formElement = $("#guestbook-form");
var submitElement = $("#guestbook-submit");
var entryContentElement = $("#guestbook-entry-content");
var hostAddressElement = $("#guestbook-host-address");
var appendGuestbookEntries = function(data) {
entriesElement.empty();
$.each(data, function(key, val) {
entriesElement.append("<p>" + val + "</p>");
});
}
var handleSubmission = function(e) {
e.preventDefault();
var entryValue = entryContentElement.val()
if (entryValue.length > 0) {
entriesElement.append("<p>...</p>");
$.getJSON("rpush/guestbook/" + entryValue, appendGuestbookEntries);
entryContentElement.val("")
}
return false;
}
submitElement.click(handleSubmission);
formElement.submit(handleSubmission);
hostAddressElement.append(document.URL);
// Poll every second.
(function fetchGuestbook() {
$.getJSON("lrange/guestbook").done(appendGuestbookEntries).always(
function() {
setTimeout(fetchGuestbook, 1000);
});
})();
});

View File

@ -0,0 +1,61 @@
body, input {
color: #123;
font-family: "Gill Sans", sans-serif;
}
div {
overflow: hidden;
padding: 1em 0;
position: relative;
text-align: center;
}
h1, h2, p, input, a {
font-weight: 300;
margin: 0;
}
h1 {
color: #18d;
font-size: 3.5em;
}
h2 {
color: #999;
}
form {
margin: 0 auto;
max-width: 50em;
text-align: center;
}
input {
border: 0;
border-radius: 1000px;
box-shadow: inset 0 0 0 2px #18d;
display: inline;
font-size: 1.5em;
margin-bottom: 1em;
outline: none;
padding: .5em 5%;
width: 55%;
}
form a {
background: #18d;
border: 0;
border-radius: 1000px;
color: #FFF;
font-size: 1.25em;
font-weight: 400;
padding: .75em 2em;
text-decoration: none;
text-transform: uppercase;
white-space: normal;
}
p {
font-size: 1.5em;
line-height: 1.5;
}