Pular para o conteúdo principal

TektonHub

Já conhecendo como funciona o Tekton e feito hello world devemos nos aprofundar melhor em como fazer algo mais concreto.

Tasks podem ser reaproveitadas e talvez esse seja o maior pulo do gato que o Tekton nos oferece.

Na grande maioria das pipelines precisamos executar o git clone em repositório a fim de obter o código para buildar, testar, etc. Esse é um ótimo exemplo de código que seria reaproveitado mudando somente os parâmetros de entrada.

Se fossemos criar uma task para clonar um repositório o que precisaríamos?

  1. A imagem do container precisaria ter o git instalado. Poderíamos usar uma imagem que já tenha tudo disponível ou criar uma a partir de alguma.

  2. Executar o comando git clone na url específica. Nesse momento será necessário as credenciais corretas caso o repositório for privado.

Você acha que isso precisa ser desenvolvido? Não precisa não.... Na verdade já foi. Podemos usar o TektonHub para procura tasks prontas e genéricas e o que encontramos foi o git-clone prontinho para uso, documentado e mantido!

Podemos copiar o yaml file que está disponível e fazer um kubectl apply ou usar o tkn cli. Na própria página tem o botão install que dará os comandos.

alt text

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

Dica: Estudar as tasks do TektonHub é um ótimo método de aprender sobre o Tekton e shell script!

Vamos analisar 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:
# Dica 2: Procure completar a description e labels. Veja ao final como fica na interface 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.

# Aqui já temos um conceito novo WORKSPACE
# workspaces são volumes montados dentro do pod. Veremos melhor mais pra frente.
workspaces:
# Observe a descrição e veja que o comando do script parece que irá clonar aqui.
- name: output
description: The git repo will be cloned onto the volume backing this Workspace.
- name: ssh-directory
optional: true # Este campo é opcional, mas o de cima não é.
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 tornam essa task mais genérica. Esses parâmetros serão usados no script.
params:
# Vamos usar os dois primeiros parâmetros no nosso exemplo.
- 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
# Imagem com o git prontinho, não precisamos nem criar uma.
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"
# Mais um conceito novo falaremos depois
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
# Veja que aqui já temos uma imagem git que foi definida nos parâmetros.
# Aqui temos já mostrando um padrão de desenvolvimento. Não se passa nada para os steps que não sejam definidos nos params.
image: "$(params.gitInitImage)"
env:
- name: HOME
value: "$(params.userHome)"
# Vale observar um padrão nos nomes. O que vem de params inicia com PARAM e o que vem de workspace inicial com 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
# Outro parão definido... script somente faz referencia aos ENVs que são populados pelos 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)"

alt text

Poderíamos alterar alterar o git-clone ao nome favor se quisermos, vamos vamos manter como é e criar um outro step para alguns testes.

Vamos desenvolver a task list-source para fazer um simples ls em uma pasta, no caso vamos fazer na pasta do projeto 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

Agora vamos criar uma pipeline clonando um repo e fazendo o comando ls.

cat <<EOF > getrepo.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: getrepo
spec:
# Parâmetros de entrada da pipeline esta disponível para qualquer task deste pipeline
params:
- name: repo-url
type: string
- name: revision
type: string
tasks:
- name: fetch-source
taskRef:
name: git-clone
# Parâmetros de entrada desta task usando os de entrada da pipeline
params:
- name: url
# É necessário fazer o skip para o comando funcionar senão ele tentar encontrar a substituição da variável
value: \$(params.repo-url)
- name: revision
value: \$(params.revision)
- name: list-source
runAfter: ["fetch-source"] # Garantindo dependência
taskRef:
name: list-source
EOF

kubectl apply -f getrepo.yaml
pipeline.tekton.dev/getrepo created

Temos uma pipeline e precisamos rodar ela com as entradas corretas, ou seja, url e revision, sendo que revision pode ser uma branch, tag, sha, ref, etc... como mostra na documentação da task. Vamos fazer em um repositorio público para não precisar passar ssh e autenticação e tornar tudo mais simples por enquanto.

O que vamos tentar fazer irá gerar um erro proposital para mostrar um conceito. Vamos fazer pela dashboard para entender melhor. Podemos criar um pipelineRun pela dashaboard e preencher os valores.

alt text

E já temos o nosso primeiro problema.

alt text

A falha aconteceu pois não passamos o workspace output que será o volume que ele irá clonar o repositório então nem rodou.

Tasks são pods e pods são independentes. É necessário que eles façam um share do mesmo volumes para que a segunda task (list-source) possa dar o comando LS no local correto, senão ela daria o comando no workdir em que o container esta rodando. Não temos como definir esses parâmetros pelo dashboard. Podemos começar de forma gráfica para preencher alguns parâmtros mas vamos precisar colocar um pouquinho a mão na massa para preencher os workspaces.

Usando o dashboard execute a task list-source e confira o que acontecerá.

alt text

O comando ls foi aplicado na raiz, mas podemos ver que existe uma pasta workspace. Esta é uma pasta que o Tekton usa para montagem de forma pra dentro.

Delete a pipeline anterior, o list-source anterior, e o pipelinerun e vamos começar novamente.

Para que o list-source possar dar o comando no local correto, vamos fazer essa alteração.

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: list-source
spec:
workspaces:
- name: source # Nome que eu resolvi dar
steps:
- name: ls
image: alpine
workingDir: $(workspaces.source.path) # Trocando o diretório, é a mesma coisa que dar um cd para este path
script: >
ls

Aplique esse manifesto e tenta executar a task novamente.

alt text

Agora precisamos passar o workspace para que funcione. Esse erro é esperado.

Agora vamos recriar a pipeline da forma correta.

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 das tasks que precisamos devemos montar o workspace
workspaces:
- name: output # Se você conferir bem ele tem esse workspace definido.
workspace: shared-workspace # Será mapeado para dentro de output na task
- name: list-source
runAfter: ["fetch-source"] # O que aconteceria se não tivessemos essa espera?
taskRef:
name: list-source
workspaces:
- name: source # Este é o nome do workspace dentro da task
workspace: shared-workspace # Sera mapeado para dentro de source na task

Workspace irá montar o mesmo volume em ambos os pods, mas qual volume? Onde definimos o volume? No pipelineRun. Vimos que graficamente não conseguimos setar nada sobre volume, somente variáveis. Então poderiamos iniciar graficamente e depois continuar via yaml trocando o modo de visualização.

Você pode aplicar esse manifesto ou ir na interface, você escolhe.

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 # Aqui definiremos o volume para o workspace.
volumeClaimTemplate:
metadata:
name: shared-workspace-pvc
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:

O pipelineRun rodou bonitinho, temos a nossa saída correta.

alt text

alt text

Vamos conferir as montagens...

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
# Veja que esta sendo usado por dois 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

alt text