Saturday, February 22, 2025

Timeout for manual jobs in gitlab

There is a timeout functionality in gitlab pipelines. The running jobs are terminated after reaching configured period of time. However if the job has a manual step - it may hang in that state forever, as it is not actively running. This functionality is missing. See also: https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29574.

But we do not need to wait for gitlab implementation of that feature.

Our solution will consist of an assisting watchdog job, run in parrallel in a separate stage. As this will require new `.gitlab-ci.yml` file, we will use an extra `util` repository, to handle it. Our util pipeline It will invoke a shell script that will just check whether the time passed is already above the limit, we have declared. We will need to pass some arguments like - the parent project id - so we can check the state and eventually terminate it, and the timeout value itself - so our solution is flexible and we avoid to hardcode it in watchdog.

In the parent pipeline we need to add a `watchdog` stage. We need to invoke the utility watchdog pipeline by calling build.

main pipeline
stages: [watchdog, build, deploy]
  
watch_pipeline:
  stage: watchdog
  trigger:
    project: your-group/util
    branch: main
  variables:
    PARENT_PIPELINE_ID: $CI_PIPELINE_ID
    PARENT_PROJECT_URL: $CI_PROJECT_URL
    MAX_PIPELINE_TIMEOUT: 3600          # 1 hour
 
 # later the build goes and whatever

Here is the `.gitlab-ci.yml` in util project:

stages:
  	- watchdog
  
watch_pipeline:
  	stage: watchdog
  	image: find_some_image_that_has_bash_curl_jq
  	script:
  		- bash ./watchdog.sh
  	allow_failure: true
    when: always
    parallel: 1

And the last piece is the script itself:

#!/bin/bash
set -euo pipefail  
  
printf "PARENT_PROJECT_URL=%s\nPARENT_PIPELINE_ID=%s\nMAX_PIPELINE_TIMEOUT=%s\n" \
  "$PARENT_PROJECT_URL" "$PARENT_PIPELINE_ID" "$MAX_PIPELINE_TIMEOUT"

echo "Watching pipeline $PARENT_PIPELINE_ID in $PARENT_PROJECT_URL"
echo "Max time: $MAX_PIPELINE_TIMEOUT sekund"


readonly START_TIMESTAMP=$(date +%s)

CURRENT_TIMESTAMP=$(date +%s)
ELAPSED_TIME=$((CURRENT_TIMESTAMP - START_TIMESTAMP))

if [ "$ELAPSED_TIME" -gt "$MAX_PIPELINE_TIMEOUT" ]
then
    echo "Cancelling pipeline: ${PARENT_PIPELINE_ID}!"

    curl --request POST \
         --header "PRIVATE-TOKEN: $TOKEN" \
         --header "Accept: application/json" \
         "$CI_API_V4_URL/projects/$PARENT_PROJECT_ID/pipelines/$PARENT_PIPELINE_ID/cancel"
fi
  

Last thing that is required is to add our Personal Access Token as TOKEN variable in gitlab CICD settings/variables. Make it protected and masked, so nobody can read it and access it. Unfortunatelly CI_JOB_TOKEN would not work in this case, due to missing permissions.

Wednesday, January 29, 2025

Docs should be versioned

Concept of having documentation stored and versioned as code is nothing new.

I have just opened not-so-fresh Michal's article regarding that (https://www.michalbartyzel.pl/blog/documentation-as-code PL), Fang suggested some marvelous solutions to approach the topic of living documentation (https://fayndee.me/techie/blog/api-documentation-as-code) even earlier.

So we do generate the docs out of the versioned source code, markup, reStructured text or asciidocs.

I am reffering to something else.

Treating the documentation as part of the product. As a contract between your team and the teams who consume it. The docs should be versioned as APIs are, the link urls must be versioned.

This is so clear, we search Javadocs or Postgresql docs and we can see immediately what version we are reffering to.
BTW postgres also allows comments on doc pages which may also be a source of priceless knowledge.

If the docs are outdated - yes we can show the warnining.

There is no value to get docs links changing every two weeks and forcing the consumers to update their link collection each time you rename a file.

Sunday, September 10, 2023

Using .env with maven

Dotfiles are quite popular in ruby and js ecosystem.
They are not popular however in java world as we can use maven (still not 0xDEAD ;) and pass configuration directly from the command line or create script for that. But why not take some standards from the other universes?
Having other ways to configure app provided by eg. spring boot it may be still useful for java developers.

You can use .env file to inject variables into your maven build, then it can get picked by spring boot and passed to configuration eg. while running tests.
Use case is to avoid putting the secrets into version control, while letting local integration tests to run. Without any extra script files and using tool that is known by other developers.

There is of course a maven plugin for that:
<groupid>io.github.mjourard</groupid>
<artifactid>env-file-maven-plugin</artifactid>
I have prepared a small example of usage with groovy and spring boot.
You can find it here: https://github.com/konopski/using-dotenv-with-maven.

It is a simplest spring boot application containing one bean (TheBean) which is using to configuration values. The very standard stuff to obtain a DB connection.
@Component
class TheBean {

    @Value('${spring.datasource.username}')
    String username

    @Value('${spring.datasource.password}')
    String pass
//...
Now spring lets us deliver the actual values using externalized config mechanism . Typically we use properties files and spring profiles. In our example there is a 'dev' profile defined, that provides a clear text password. It is good enough if you remeber to keep the dedicated profile properties file out of your version control.
The file (application-dev.properties) can look like this:
spring.datasource.password=secret
In the main application.properties file we do not define this entry - so in case it is missing (eg. ommiting the profile) the app will not start.

You can also have another mechanism controlling your password. Spring will take a corresponding definition from system environment.
In our case it is delivered by our .env file.
SPRING_DATASOURCE_PASSWORD=t3st_s3cr3t
That is verified in ApplicationTest class:
@SpringBootTest
class ApplicationTests {

    @Autowired TheBean bean

//...

    @Test
    void shouldReadFromDotEnv() {
            bean.pass == "t3st_s3cr3t"
    }
}
We can also use different values by using spring's properties overriding, adding to our .env file:
OVERRIDE_DB_PASS=password_override
And test for that:
@SpringBootTest(
    properties = ['spring.datasource.password=${OVERRIDE_DB_PASS}'] )
class PropsOverrideTest {

    @Autowired TheBean bean

//...

    @Test
    void shouldOverride() {
        bean.pass == "password_override1"
    }

}
For me this approach is quite clean, allows easy sharing among the team and composes quite well with existing environment.
And remember to keep your passwords away from git.

Friday, September 27, 2019

Map or any other option?

I must admit that sometimes we can go too far with using Optionals as silver bullet. Let's take a look at a simple mapper class, defined like this:
class Mapper {
  Optional<Dto> map(Optional<Entity> input) {
    //... implementation
  } 
}
At first passing an Optional instance looks like a good idea - we want to be safe from null values, we express our defensive intent in types. What can go wrong? Let's now take a look at how this mapper may be used.
Entity entity = //...
Dto dto = mapper.map(Optional.of(entity)).get();
Well, typical code involving the mapper is packing the input entity into an instance of Optional, just to get to output value. Even worse could look usage for a collection.
List ents = //...
List<dto> dtos = ents.stream()
  .map(Optional::of)
  .map( x -> mapper.map(x) )
  .map(Optional::get)
  .collect(toList());
Instead of using type system and compiler to our benefit, we lie about our input value - which we actually know that is never null. Not only we do not use the knowledge we already have, but put extra burden of reading through the wrapping on reader's eyes. I do not mention creating extra instances of Optional, and making common use of calling .get() - which is not best practice. I do not want to get familiar to that.
What I also do not like in the code is forcing the user of our mapper to provide object wrapped into the very implementation of Optional. Actually mapper should not care which implementation of Optional I use. There are more than the standard one, and they may have some properties that are more useful for me in the context of my implementation. Last thing, we may observe here is that the API actually mimics what the Optionals are expected to provide - the map method itself.
What we need is just:
Dto map(Entity in) {
//....
}
That simple.
Implementation of very mapping is what should be provided directly by mapper, without requirement for a wrapped value. We should require that value will be non-null. Optionality should be handled in client code somewhere else, we will not tolerate null values anyhow, right? Such mapper can be also easily used when applied to streams or collections. We save machine's memory from wrapping non-null values into Optionals and our precious eyes from reading the code that would cause the latter.

Friday, July 19, 2019

Woobie Doobie - switching database access library in Scala

Doobie is getting more and more popular as a database access tool in modern applications. This year on Scalar conference voting Doobie won over its competition - particularly Slick. Not without a reason. Recently my project also dropped Slick.
Doobie gave us nearly immediately:
  • easier learning curve - it is about plain, old SQL ;
  • more compile time verification - now you do not need to run your query to find out your type mapping is broken ;
  • referential transparency and better integration with effects system used in project ;
  • no hacking needed when you need to select more columns than unfamous 22. One particular issue that came out quite early was a bit tricky to resolve with given message:
    [error] Cannot construct a parameter vector of the following type:
    [error]
    [error]   Boolean(true) :: shapeless.HNil
    [error]
    [error] Because one or more types therein (disregarding HNil) does not have a Put
    [error] instance in scope. Try them one by one in the REPL or in your code:
    [error]
    [error]   scala> Put[Foo]
    [error]
    [error] and find the one that has no instance, then construct one as needed. Refer to
    [error] Chapter 12 of the book of doobie for more information.
    
    Studying chapter 12 did not help. Here is the query that caused the issue:
    sql"select one_col, two_col from TABLE where flag = ${true} "
    
    It turns out that Doobie (actually Shapeless) does not handle properly the inlined true literal. What is needed is just to extract it to separate value:
    val flag = true
    sql"select one_col, two_col from TABLE where flag = ${flag} "
    
    That resolves the problem. It would also work if we added type annotation:
    sql"select one_col, two_col from TABLE where flag = ${true: Boolean} "
    
    It's up to you which way you prefer.
  • Wednesday, October 31, 2018

    Remove one file from git commit to a remote branch

    Ooops, I did it again. Commited and pushed one file too much.
    ? git push --set-upstream origin bugfix/JRASERVER-65811
    
    What now? How to remove the file from a public commit?

    Let's first go back to the base branch.
    ? git checkout -
    
    Switched to branch 'develop'
    Your branch is up to date with 'origin/develop'.
    
    I assume I may have some changes in my local copy so first need to clean it. I am going to delete my local branch.
    ? git branch -D bugfix/JRASERVER-65811
    Deleted branch bugfix/JRASERVER-65811 (was 5e17f72f).
    
    Next step is to synchronize with remote repository and fetch the branch again.
    ? git pull
    remote: Enumerating objects: 119, done.
    remote: Counting objects: 100% (119/119), done.
    remote: Compressing objects: 100% (119/119), done.
    remote: Total 119 (delta 41), reused 0 (delta 0)
    Receiving objects: 100% (119/119), 43.63 KiB | 1.82 MiB/s, done.
    Resolving deltas: 100% (41/41), done.
    From gitlab:project/frontend
       736ac14a..ea0ba57e  bugfix/JRASERVER-65811-allopenissues -> origin/bugfix/JRASERVER-65811-allopenissues
    Already up to date.
    
    I can now switch to the branch back.
    ? git checkout bugfix/JRASERVER-65811
    Switched to a new branch 'bugfix/JRASERVER-65811'
    Branch 'bugfix/JRASERVER-65811' set up to track remote branch 'bugfix/JRASERVER-65811' from 'origin'.
    
    Time to bring the last commit into staging.
    ? git reset --soft HEAD^
    
    I can see this was done by showing status.
    ? git status
    On branch bugfix/JRASERVER-65811
    Your branch is behind 'origin/bugfix/JRASERVER-65811' by 1 commit, and can be fast-forwarded.
      (use "git pull" to update your local branch)
    
    Changes to be committed:
      (use "git reset HEAD ..." to unstage)
    
            new file:   src/app/lktree/lktree.component.spec.ts
            modified:   src/app/modules/spaces/data/space-datasource.ts
            modified:   src/app/modules/spaces/document-details/document-details.component.html
            modified:   src/app/modules/spaces/spaces-tree/spaces-tree.component.ts
            modified:   src/app/modules/user-context/data/user-context-datasource.ts
    
    The first of files is the one I want to extract and unstage from to working copy. This simply undo git add.
    ? git reset src/app/lktree/lktree.component.spec.ts
    
    Yeah! My changes are now in state I wanted!
    ? git status
    On branch bugfix/JRASERVER-65811
    Your branch is behind 'origin/bugfix/JRASERVER-65811' by 1 commit, and can be fast-forwarded.
      (use "git pull" to update your local branch)
    
    Changes to be committed:
      (use "git reset HEAD ..." to unstage)
    
            modified:   src/app/modules/spaces/data/space-datasource.ts
            modified:   src/app/modules/spaces/document-details/document-details.component.html
            modified:   src/app/modules/spaces/spaces-tree/spaces-tree.component.ts
            modified:   src/app/modules/user-context/data/user-context-datasource.ts
    
    Untracked files:
      (use "git add ..." to include in what will be committed)
    
            src/app/lktree/
    
    I can commit again - this time the right files.
    ? git commit src/app/modules/spaces/ src/app/modules/user-context/data/user-context-datasource.ts -m "JRASERVER-65811: allopenissues"
    [bugfix/JRASERVER-65811 80002b42] JRASERVER-65811: allopenissues
     4 files changed, 60 insertions(+), 25 deletions(-)
    
    Last commit is to overwrite the remote branch. Do not do it at home ;)
    ? git push --force
    

    Friday, June 22, 2018

    Authenticating with deploy keys in Jenkins pipelines

    While using M$ github you may use deploy keys dedicated to a specific repository instead of giving your private key to Jenkins. And yes, it is possible to use deploy key in Jenkins pipelines.

    To be able to manage your ssh identity you need first to install sshagent plugin.




    BTW If you are running Jenkins instance on M$ windows machine remember to add sshagent (eg. from your git distribution) to your %PATH%.



    Generate a key pair.
    ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
    


    Goto Credentials in Jenkins left-side main menu. Add credentials of type 'SSH Username with private key'. You can paste the created private key into text area.



    In M$ github repository settings now you can add corresponding public key.



    In your pipeline code you can use credentials when you surround eg. git calls with sshagent block.

                    sshagent(credentials: ['throw-me-away-key']) {
                        bat """git pull origin master"""
                    }
    



    If you get errors make sure that you are not using friendly name but right ID of credential in Jenkins.


    10:09:04 FATAL: [ssh-agent] Could not find specified credentials
    10:09:04 [ssh-agent] Looking for ssh-agent implementation...
    10:09:04 [ssh-agent]   Exec ssh-agent (binary ssh-agent on a remote machine)
    10:09:04 $ ssh-agent
    10:09:04 SSH_AUTH_SOCK=/tmp/ssh-vCKYmwW5gfvP/agent.5592
    10:09:04 SSH_AGENT_PID=5572
    10:09:04 [ssh-agent] Started.
    10:09:04 [original] Running batch script
    10:09:04 
    10:09:04 C:\Program Files (x86)\Jenkins\workspace\lk-pipeline-0\original>git pull 
    10:09:06 $ ssh-agent -k
    10:09:06 git@github.com: Permission denied (publickey).
    10:09:06 fatal: Could not read from remote repository.
    10:09:06 
    10:09:06 Please make sure you have the correct access rights
    10:09:06 and the repository exists.
    10:09:06 unset SSH_AUTH_SOCK;
    10:09:06 unset SSH_AGENT_PID;
    10:09:06 echo Agent pid 5572 killed;
    10:09:06 [ssh-agent] Stopped.