TektonHub
Ya conociendo cómo funciona Tekton y hecho hello world debemos profundizar mejor en cómo hacer algo más concreto.
Tasks pueden ser reaprovechadas y tal vez ése sea el mayor salto del gato que Tekton nos ofrece.
En la gran mayoría de las pipelines necesitamos ejecutar el git clone en repositorio a fin de obtener el código para hacer build, probar, etc. Éste es un excelente ejemplo de código que sería reaprovechado cambiando solamente los parámetros de entrada.
Si fuésemos a crear una task para clonar un repositorio ¿qué necesitaríamos?
-
La imagen del contenedor necesitaría tener el git instalado. Podríamos usar una imagen que ya tenga todo disponible o crear una a partir de alguna.
-
Ejecutar el comando git clone en la url específica. En ese momento será necesario las credenciales correctas si el repositorio es privado.
¿Tú crees que esto necesita ser desarrollado? No necesita no.... En verdad ya fue. Podemos usar TektonHub para buscar tasks listas y genéricas y lo que encontramos fue git-clone listo para uso, documentado y mantenido!
Podemos copiar el yaml file que está disponible y hacer un kubectl apply o usar el tkn cli. En la propia página tiene el botón install que dará los comandos.

tkn hub install task git-clone
Task git-clone(0.9) installed in default namespace
tkn task list
NAME DESCRIPTION AGE
git-clone These Tasks are Git... 15 seconds ago
hello-task 5 hours ago
hello-task2 5 hours ago
Consejo: ¡Estudiar las tasks de TektonHub es un excelente método de aprender sobre Tekton y shell script!
Vamos a analizar este yaml.
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: git-clone
labels:
app.kubernetes.io/version: "0.9"
annotations:
tekton.dev/pipelines.minVersion: "0.38.0"
tekton.dev/categories: Git
tekton.dev/tags: git
tekton.dev/displayName: "git clone"
tekton.dev/platforms: "linux/amd64,linux/s390x,linux/ppc64le,linux/arm64"
spec:
# Consejo 2: Procura completar la description y labels. Mira al final cómo queda en la interfaz gráfica.
description: >-
These Tasks are Git tasks to work with repositories used by other tasks
in your Pipeline.
The git-clone Task will clone a repo from the provided url into the
output Workspace. By default the repo will be cloned into the root of
your Workspace. You can clone into a subdirectory by setting this Task's
subdirectory param. This Task also supports sparse checkouts. To perform
a sparse checkout, pass a list of comma separated directory patterns to
this Task's sparseCheckoutDirectories param.
# Aquí ya tenemos un concepto nuevo WORKSPACE
# workspaces son volúmenes montados dentro del pod. Veremos mejor más adelante.
workspaces:
# Observa la descripción y mira que el comando del script parece que clonará aquí.
- name: output
description: The git repo will be cloned onto the volume backing this Workspace.
- name: ssh-directory
optional: true # Este campo es opcional, pero el de arriba no lo es.
description: |
A .ssh directory with private key, known_hosts, config, etc. Copied to
the user's home before git commands are executed. Used to authenticate
with the git remote when performing the clone. Binding a Secret to this
Workspace is strongly recommended over other volume types.
- name: basic-auth
optional: true
description: |
A Workspace containing a .gitconfig and .git-credentials file. These
will be copied to the user's home before any git commands are run. Any
other files in this Workspace are ignored. It is strongly recommended
to use ssh-directory over basic-auth whenever possible and to bind a
Secret to this Workspace over other volume types.
- name: ssl-ca-directory
optional: true
description: |
A workspace containing CA certificates, this will be used by Git to
verify the peer with when fetching or pushing over HTTPS.
# Parámetros de entrada que tornan esta task más genérica. Esos parámetros serán usados en el script.
params:
# Vamos a usar los dos primeros parámetros en nuestro ejemplo.
- name: url
description: Repository URL to clone from.
type: string
- name: revision
description: Revision to checkout. (branch, tag, sha, ref, etc...)
type: string
default: ""
- name: refspec
description: Refspec to fetch before checking out revision.
default: ""
- name: submodules
description: Initialize and fetch git submodules.
type: string
default: "true"
- name: depth
description: Perform a shallow clone, fetching only the most recent N commits.
type: string
default: "1"
- name: sslVerify
description: Set the `http.sslVerify` global git config. Setting this to `false` is not advised unless you are sure that you trust your git remote.
type: string
default: "true"
- name: crtFileName
description: file name of mounted crt using ssl-ca-directory workspace. default value is ca-bundle.crt.
type: string
default: "ca-bundle.crt"
- name: subdirectory
description: Subdirectory inside the `output` Workspace to clone the repo into.
type: string
default: ""
- name: sparseCheckoutDirectories
description: Define the directory patterns to match or exclude when performing a sparse checkout.
type: string
default: ""
- name: deleteExisting
description: Clean out the contents of the destination directory if it already exists before cloning.
type: string
default: "true"
- name: httpProxy
description: HTTP proxy server for non-SSL requests.
type: string
default: ""
- name: httpsProxy
description: HTTPS proxy server for SSL requests.
type: string
default: ""
- name: noProxy
description: Opt out of proxying HTTP/HTTPS requests.
type: string
default: ""
- name: verbose
description: Log the commands that are executed during `git-clone`'s operation.
type: string
default: "true"
- name: gitInitImage
description: The image providing the git-init binary that this Task runs.
type: string
# Imagen con el git listo, no necesitamos ni crear una.
default: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:v0.40.2"
- name: userHome
description: |
Absolute path to the user's home directory.
type: string
default: "/home/git"
# Un concepto nuevo más hablaremos después
results:
- name: commit
description: The precise commit SHA that was fetched by this Task.
- name: url
description: The precise URL that was fetched by this Task.
- name: committer-date
description: The epoch timestamp of the commit that was fetched by this Task.
steps:
- name: clone
# Mira que aquí ya tenemos una imagen git que fue definida en los parámetros.
# Aquí tenemos ya mostrando un patrón de desarrollo. No se pasa nada para los steps que no sean definidos en los params.
image: "$(params.gitInitImage)"
env:
- name: HOME
value: "$(params.userHome)"
# Vale observar un patrón en los nombres. Lo que viene de params inicia con PARAM y lo que viene de workspace inicial con WORKSPACE
- name: PARAM_URL
value: $(params.url)
- name: PARAM_REVISION
value: $(params.revision)
- name: PARAM_REFSPEC
value: $(params.refspec)
- name: PARAM_SUBMODULES
value: $(params.submodules)
- name: PARAM_DEPTH
value: $(params.depth)
- name: PARAM_SSL_VERIFY
value: $(params.sslVerify)
- name: PARAM_CRT_FILENAME
value: $(params.crtFileName)
- name: PARAM_SUBDIRECTORY
value: $(params.subdirectory)
- name: PARAM_DELETE_EXISTING
value: $(params.deleteExisting)
- name: PARAM_HTTP_PROXY
value: $(params.httpProxy)
- name: PARAM_HTTPS_PROXY
value: $(params.httpsProxy)
- name: PARAM_NO_PROXY
value: $(params.noProxy)
- name: PARAM_VERBOSE
value: $(params.verbose)
- name: PARAM_SPARSE_CHECKOUT_DIRECTORIES
value: $(params.sparseCheckoutDirectories)
- name: PARAM_USER_HOME
value: $(params.userHome)
- name: WORKSPACE_OUTPUT_PATH
value: $(workspaces.output.path)
- name: WORKSPACE_SSH_DIRECTORY_BOUND
value: $(workspaces.ssh-directory.bound)
- name: WORKSPACE_SSH_DIRECTORY_PATH
value: $(workspaces.ssh-directory.path)
- name: WORKSPACE_BASIC_AUTH_DIRECTORY_BOUND
value: $(workspaces.basic-auth.bound)
- name: WORKSPACE_BASIC_AUTH_DIRECTORY_PATH
value: $(workspaces.basic-auth.path)
- name: WORKSPACE_SSL_CA_DIRECTORY_BOUND
value: $(workspaces.ssl-ca-directory.bound)
- name: WORKSPACE_SSL_CA_DIRECTORY_PATH
value: $(workspaces.ssl-ca-directory.path)
securityContext:
runAsNonRoot: true
runAsUser: 65532
# Otro patrón definido... script solamente hace referencia a los ENVs que son poblados por los params.
script: |
#!/usr/bin/env sh
set -eu
if [ "${PARAM_VERBOSE}" = "true" ] ; then
set -x
fi
if [ "${WORKSPACE_BASIC_AUTH_DIRECTORY_BOUND}" = "true" ] ; then
cp "${WORKSPACE_BASIC_AUTH_DIRECTORY_PATH}/.git-credentials" "${PARAM_USER_HOME}/.git-credentials"
cp "${WORKSPACE_BASIC_AUTH_DIRECTORY_PATH}/.gitconfig" "${PARAM_USER_HOME}/.gitconfig"
chmod 400 "${PARAM_USER_HOME}/.git-credentials"
chmod 400 "${PARAM_USER_HOME}/.gitconfig"
fi
if [ "${WORKSPACE_SSH_DIRECTORY_BOUND}" = "true" ] ; then
cp -R "${WORKSPACE_SSH_DIRECTORY_PATH}" "${PARAM_USER_HOME}"/.ssh
chmod 700 "${PARAM_USER_HOME}"/.ssh
chmod -R 400 "${PARAM_USER_HOME}"/.ssh/*
fi
if [ "${WORKSPACE_SSL_CA_DIRECTORY_BOUND}" = "true" ] ; then
export GIT_SSL_CAPATH="${WORKSPACE_SSL_CA_DIRECTORY_PATH}"
if [ "${PARAM_CRT_FILENAME}" != "" ] ; then
export GIT_SSL_CAINFO="${WORKSPACE_SSL_CA_DIRECTORY_PATH}/${PARAM_CRT_FILENAME}"
fi
fi
CHECKOUT_DIR="${WORKSPACE_OUTPUT_PATH}/${PARAM_SUBDIRECTORY}"
cleandir() {
# Delete any existing contents of the repo directory if it exists.
#
# We don't just "rm -rf ${CHECKOUT_DIR}" because ${CHECKOUT_DIR} might be "/"
# or the root of a mounted volume.
if [ -d "${CHECKOUT_DIR}" ] ; then
# Delete non-hidden files and directories
rm -rf "${CHECKOUT_DIR:?}"/*
# Delete files and directories starting with . but excluding ..
rm -rf "${CHECKOUT_DIR}"/.[!.]*
# Delete files and directories starting with .. plus any other character
rm -rf "${CHECKOUT_DIR}"/..?*
fi
}
if [ "${PARAM_DELETE_EXISTING}" = "true" ] ; then
cleandir || true
fi
test -z "${PARAM_HTTP_PROXY}" || export HTTP_PROXY="${PARAM_HTTP_PROXY}"
test -z "${PARAM_HTTPS_PROXY}" || export HTTPS_PROXY="${PARAM_HTTPS_PROXY}"
test -z "${PARAM_NO_PROXY}" || export NO_PROXY="${PARAM_NO_PROXY}"
git config --global --add safe.directory "${WORKSPACE_OUTPUT_PATH}"
/ko-app/git-init \
-url="${PARAM_URL}" \
-revision="${PARAM_REVISION}" \
-refspec="${PARAM_REFSPEC}" \
-path="${CHECKOUT_DIR}" \
-sslVerify="${PARAM_SSL_VERIFY}" \
-submodules="${PARAM_SUBMODULES}" \
-depth="${PARAM_DEPTH}" \
-sparseCheckoutDirectories="${PARAM_SPARSE_CHECKOUT_DIRECTORIES}"
cd "${CHECKOUT_DIR}"
RESULT_SHA="$(git rev-parse HEAD)"
EXIT_CODE="$?"
if [ "${EXIT_CODE}" != 0 ] ; then
exit "${EXIT_CODE}"
fi
RESULT_COMMITTER_DATE="$(git log -1 --pretty=%ct)"
printf "%s" "${RESULT_COMMITTER_DATE}" > "$(results.committer-date.path)"
printf "%s" "${RESULT_SHA}" > "$(results.commit.path)"
printf "%s" "${PARAM_URL}" > "$(results.url.path)"

Podríamos alterar el git-clone al nombre que queramos si queremos, vamos a mantener como es y crear otro step para algunas pruebas.
Vamos a desarrollar la task list-source para hacer un simple ls en una carpeta, en el caso vamos a hacer en la carpeta del proyecto clonado.
cat <<EOF > listsource.yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: list-source
spec:
steps:
- name: ls
image: alpine
script: >
ls
EOF
kubectl create -f listsource.yaml
Ahora vamos a crear una pipeline clonando un repo y haciendo el comando ls.
cat <<EOF > getrepo.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: getrepo
spec:
# Parámetros de entrada de la pipeline está disponible para cualquier task de esta pipeline
params:
- name: repo-url
type: string
- name: revision
type: string
tasks:
- name: fetch-source
taskRef:
name: git-clone
# Parámetros de entrada de esta task usando los de entrada de la pipeline
params:
- name: url
# Es necesario hacer el skip para que el comando funcione si no intentará encontrar la sustitución de la variable
value: \$(params.repo-url)
- name: revision
value: \$(params.revision)
- name: list-source
runAfter: ["fetch-source"] # Garantizando dependencia
taskRef:
name: list-source
EOF
kubectl apply -f getrepo.yaml
pipeline.tekton.dev/getrepo created
Tenemos una pipeline y necesitamos ejecutarla con las entradas correctas, o sea, url y revision, siendo que revision puede ser una branch, etiqueta, sha, ref, etc... como muestra en la documentación de la task. Vamos a hacer en un repositorio público para no necesitar pasar ssh y autenticación y tornar todo más simple por ahora.
Lo que vamos a intentar hacer generará un error a propósito para mostrar un concepto. Vamos a hacerlo por el dashboard para entender mejor. Podemos crear un pipelineRun por el dashboard y rellenar los valores.

Y ya tenemos nuestro primer problema.

El fallo ocurrió pues no pasamos el workspace output que será el volumen que clonará el repositorio entonces ni se ejecutó.
Tasks son pods y pods son independientes. Es necesario que hagan un share del mismo volumen para que la segunda task (list-source) pueda dar el comando LS en el local correcto, si no daría el comando en el workdir en que el contenedor está ejecutándose. No tenemos cómo definir esos parámetros por el dashboard. Podemos comenzar de forma gráfica para rellenar algunos parámetros pero vamos a necesitar poner un poquito la mano en la masa para rellenar los workspaces.
Usando el dashboard ejecuta la task list-source y comprueba lo que ocurrirá.

El comando ls fue aplicado en la raíz, pero podemos ver que existe una carpeta workspace. Ésta es una carpeta que Tekton usa para montaje de forma hacia dentro.
Borra la pipeline anterior, el list-source anterior, y el pipelinerun y vamos a comenzar nuevamente.
Para que list-source pueda dar el comando en el local correcto, vamos a hacer esa alteración.
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: list-source
spec:
workspaces:
- name: source # Nombre que resolví dar
steps:
- name: ls
image: alpine
workingDir: $(workspaces.source.path) # Cambiando el directorio, es lo mismo que dar un cd para este path
script: >
ls
Aplica este manifiesto e intenta ejecutar la task nuevamente.

Ahora necesitamos pasar el workspace para que funcione. Este error es esperado.
Ahora vamos a recrear la pipeline de la forma correcta.
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: getrepo
spec:
workspaces:
- name: shared-workspace
params:
- name: repo-url
type: string
- name: revision
type: string
tasks:
- name: fetch-source
taskRef:
name: git-clone
params:
- name: url
value: $(params.repo-url)
- name: revision
value: $(params.revision)
# Dentro de las tasks que necesitamos debemos montar el workspace
workspaces:
- name: output # Si miras bien tiene este workspace definido.
workspace: shared-workspace # Será mapeado para dentro de output en la task
- name: list-source
runAfter: ["fetch-source"] # ¿Qué ocurriría si no tuviésemos esta espera?
taskRef:
name: list-source
workspaces:
- name: source # Éste es el nombre del workspace dentro de la task
workspace: shared-workspace # Será mapeado para dentro de source en la task
Workspace montará el mismo volumen en ambos pods, pero ¿qué volumen? ¿Dónde definimos el volumen? En el pipelineRun. Vimos que gráficamente no conseguimos configurar nada sobre volumen, solamente variables. Entonces podríamos iniciar gráficamente y después continuar vía yaml cambiando el modo de visualización.
Puedes aplicar este manifiesto o ir en la interfaz, tú eliges.
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: getrepo-run-1722456380429
namespace: default
labels: {}
spec:
pipelineRef:
name: getrepo
status: ''
params:
- name: repo-url
value: https://gitlab.com/davidpuziol/study-crossplane.git
- name: revision
value: main
workspaces:
- name: shared-workspace # Aquí definiremos el volumen para el workspace.
volumeClaimTemplate:
metadata:
name: shared-workspace-pvc
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
El pipelineRun se ejecutó bonito, tenemos nuestra salida correcta.


Vamos a comprobar los montajes...
k get persistentvolumeclaims
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
shared-data-pvc-31141087b0 Bound pvc-1ce8a75f-d335-47e7-b3ef-7c10bb753934 1Gi RWO standard <unset> 36m
k describe persistentvolumeclaims shared-data-pvc-31141087b0
Name: shared-data-pvc-31141087b0
Namespace: default
StorageClass: standard
Status: Bound
Volume: pvc-1ce8a75f-d335-47e7-b3ef-7c10bb753934
Labels: <none>
Annotations: pv.kubernetes.io/bind-completed: yes
pv.kubernetes.io/bound-by-controller: yes
volume.beta.kubernetes.io/storage-provisioner: rancher.io/local-path
volume.kubernetes.io/selected-node: personal-cluster-worker3
volume.kubernetes.io/storage-provisioner: rancher.io/local-path
Finalizers: [kubernetes.io/pvc-protection]
Capacity: 1Gi
Access Modes: RWO
VolumeMode: Filesystem
# Mira que está siendo usado por dos pods
Used By: getrepo-run-1722456380429-fetch-source-pod
getrepo-run-1722456380429-list-source-pod
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal WaitForFirstConsumer 33m persistentvolume-controller waiting for first consumer to be created before binding
Normal Provisioning 33m rancher.io/local-path_local-path-provisioner-988d74bc-nlp5h_21abf5e9-a7be-4cee-908c-844d6ae48c0b External provisioner is provisioning volume for claim "default/shared-data-pvc-31141087b0"
Normal ExternalProvisioning 33m (x2 over 33m) persistentvolume-controller Waiting for a volume to be created either by the external provisioner 'rancher.io/local-path' or manually by the system administrator. If volume creation is delayed, please verify that the provisioner is running and correctly registered.
Normal ProvisioningSucceeded 33m rancher.io/local-path_local-path-provisioner-988d74bc-nlp5h_21abf5e9-a7be-4cee-908c-844d6ae48c0b Successfully provisioned volume pvc-1ce8a75f-d335-47e7-b3ef-7c10bb753934
Para ilustrar
