惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tenable Blog
WordPress大学
WordPress大学
小众软件
小众软件
Y
Y Combinator Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
大猫的无限游戏
大猫的无限游戏
T
The Exploit Database - CXSecurity.com
Attack and Defense Labs
Attack and Defense Labs
Simon Willison's Weblog
Simon Willison's Weblog
C
CXSECURITY Database RSS Feed - CXSecurity.com
量子位
有赞技术团队
有赞技术团队
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
F
Fortinet All Blogs
S
Schneier on Security
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
Martin Fowler
Martin Fowler
Recent Announcements
Recent Announcements
Stack Overflow Blog
Stack Overflow Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
阮一峰的网络日志
阮一峰的网络日志
G
GRAHAM CLULEY
Spread Privacy
Spread Privacy
F
Full Disclosure
Scott Helme
Scott Helme
GbyAI
GbyAI
N
Netflix TechBlog - Medium
MyScale Blog
MyScale Blog
Cloudbric
Cloudbric
云风的 BLOG
云风的 BLOG
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
Hacker News - Newest:
Hacker News - Newest: "LLM"
Security Latest
Security Latest
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
MongoDB | Blog
MongoDB | Blog
The GitHub Blog
The GitHub Blog
The Register - Security
The Register - Security
L
Lohrmann on Cybersecurity
PCI Perspectives
PCI Perspectives
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
D
Docker
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Secure Thoughts
C
Check Point Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Kubernetes testing w/ Dagger.io
Sergio Maton · 2026-05-14 · via DEV Community

What is Dagger

Dagger is a Pipeline As a Code tool that makes it possible to create fully reproducible and portable
workflows. These workflows can be:

  • defined in any language enabled by Dagger SDKs (Golang, Typescript, Python)
  • run and called seamlessly and consistently from a local machine or in a remote CI
    • using the exact same codebase
    • giving the same expected output in any environment by providing the same input

One of the advantages of Dagger is the possibility of rewriting a Github Action workflow in any of the available languages and fully testing it locally. The Github Action workflow itself will be then replaced by a simple call to a function in a Dagger module (See Dagger core concepts).

How we use Dagger in Gnoland

In Gno.land we use Dagger to consistently build and test different tools (like gnokey) or to build and publish on Netlify documentation.
Since Gno.land is written in Go, we were able to massively reuse the internal skills and knowledge with the Dagger Golang SDK.

  • some workflows were written from scratch using Dagger Golang SDK
  • other workflows were transformed from a Github Actions workflows into a Dagger module, where GH Action runs are replaced by calls to Dagger functions (See CI/CD integration)

However, as Lead DevOps, one of the most interesting use cases of Dagger has been the possibility to test a whole Kubernetes cluster deployment and run with a single Dagger module.
This allows achieving multiple goals:

  • test the infra consistency across multiple updates of the infra Git repository
  • detect issues related to the Gno.land Docker image and its self-contained binary
  • leverage the outcome of the Dagger functions as GitOps-like gateway procedures for updating test and staging infra environments

Testing a full cluster deployment and run with Dagger

Requirements:

  • Docker Daemon or other container runtimes
  • Dagger CLI
  • Go SDK - it will be used to actually code the Dagger module

Bootstrapping a Kubernetes Cluster

After bootstrapping the module with

dagger init --sdk=go kube-test

Enter fullscreen mode Exit fullscreen mode

What we need is a way to bootstrap a Kubernetes Cluster itself.
Being in a docker-like environment the best option is a Kubernetes in Docker solution,
such as KinD or K3s.
Both are available in Daggerverse and can be installed as external module to be reused.

For sake of simplicity, K3s will be installed from Daggerverse and used.

dagger install github.com/marcosnils/daggerverse/k3s@v0.1.10

Enter fullscreen mode Exit fullscreen mode

Here is the snippet to bootstrap K3s:

// initialize K3s cluster
k3s := dag.K3S(ClusterName)
kServer := k3s.Server()
_, err := kServer.Start(ctx)

Enter fullscreen mode Exit fullscreen mode

After having a cluster available the next step is allowing the usage of Helm. It will be useful to install nodes from the Helm template.

Let's take the chance to explain a little bit about Dagger. Dagger borrows the philosophy behind container building, it stacks progressive layers, caching each of them to avoid rebuilding a layer each time when there is no need.
That is exactly what will happen here:

  • add the Helm layer into the Dagger function
  • use a base image alpine/helm and kubectl package will be added as well
  • reference kubeconfig to the corresponding K3s config
m.initContainer = dag.Container().From("alpine/helm").
  WithoutEntrypoint().
  WithExec([]string{"apk", "add", "kubectl"}).
  WithEnvVariable("KUBECONFIG", "/.kube/config").
  WithFile("/.kube/config", k3s.Config(), defaultFileOwner).
  WithUser("1001").

Enter fullscreen mode Exit fullscreen mode

Running a validator node

It is now time to run a validator node. First of all, a genesis file is needed, but in order
to make it more realistic, this genesis file will be generated and then distributed using a URL endpoint.
This will allow reusing the Helm template which expects a URL to retrieve the genesis file.

m.initContainer = m.initContainer.
  WithDirectory("/opt/data/genesis-server", m.manifestsFolder.Directory("genesis-server"), defaultDirOwner).
  WithFile("/opt/data/genesis.json", m.genesisFile, defaultFileOwner).
  WithFile("/opt/data/kustomization.yaml", m.manifestsFolder.File("gno-secret/kustomization.yaml"), defaultFileOwner).
  WithDirectory("/opt/data/helm", helmFolder, defaultDirOwner). // Helm template for Validator
  WithFile("/opt/data/template-values.yaml", helmDataFolder.File("template-values.yaml"), defaultFileOwner).
  WithWorkdir("/opt/data").
  WithExec([]string{"kubectl", "apply", "-k", "genesis-server/"}).
  WithExec([]string{"kubectl", "wait", "--for=condition=ready", "--timeout=30s", "pod", "-l", "app=genesis-file-server", "-n", "gno"}).
  WithExec([]string{"kubectl", "cp", "/opt/data/genesis.json", "gno/genesis-file-server:/usr/share/nginx/html/genesis.json"})

Enter fullscreen mode Exit fullscreen mode

The snippet above gives us the opportunity to explore key features of Dagger.

Files and Directories

Similarly to OCI containers and Dockerfiles, Dagger will mount provided files and directories into specific layers.

  • a folder or file mapping is provided when calling the function, indeed the referenced item is part of the arguments of the function itself
func (m *GnoK3s) SpinCluster(ctx context.Context, dataFolder *dagger.Directory,) (int, error) {}

Enter fullscreen mode Exit fullscreen mode

  • the referenced argument can be navigated and referenced any time from within the code
helmFolder := dataFolder.Directory("helm")
helmDataFolder := dataFolder.Directory("values")
m.manifestsFolder = dataFolder.Directory("manifests")

Enter fullscreen mode Exit fullscreen mode

  • folders and files can then be mounted into a layer
WithDirectory("/opt/data/genesis-server", m.manifestsFolder.Directory("genesis-server"), defaultDirOwner)

Enter fullscreen mode Exit fullscreen mode

will mount the folder genesis-server, as referenced above, into the container path /opt/data/genesis-server, same happens with single files

WithFile("/opt/data/kustomization.yaml", m.manifestsFolder.File("gno-secret/kustomization.yaml"), defaultFileOwner).

Enter fullscreen mode Exit fullscreen mode

Multi layer system

dagger.Container will stack more and more layers, either they contain a mount or the execution of a command, but any subsequent layer will be able to access a previous layer. This means that

  • a command can reference any folder or file mounted in a previous layer
  • the result of a command in a previous layer can be reused ahead
  • the new layer is fully aware of the context of the previous layers in terms of file, folder, running services

These features are the key to achieve a testable cluster: each layer will be deploying its own resources into the Kubernetes cluster represented by another dagger.Container.

Deploying a validator node

Deploying a validator node will require the following steps:

  • generating validator's secrets
  • adding above generated secrets to the valset in the genesis file
  • reference a config file
  • spin the validator node from a Helm template

The first two steps are performed in a dedicated method which generates secrets and adds them to the genesis file, the latter will then be served by geneses-server. When testing more complex topologies multiple validators can be added either

// Generates secrets for validator and add them to genesis
func (m *GnoK3s) setupValidatorNodes(ctx context.Context, valCounter int) []networkNode {
  validators := []networkNode{}
  for i := range valCounter {
    nodeName := fmt.Sprintf("gnocore-val-%02d", i+1)
    // Secrets dir
    gnoSecretsDir := m.generateSecrets()
    // Genesis file
    m.genesisFile = m.addValidatorToGenesis(nodeName, gnoSecretsDir)
    validators = append(validators, networkNode{
      name:          nodeName,
      nodeAddress:   getNodeAddress(ctx, nodeName, gnoSecretsDir),
      secretsFolder: gnoSecretsDir,
    })
  }
  return validators
}

Enter fullscreen mode Exit fullscreen mode

All the useful information on a validator is stored into a dedicated data type

type networkNode struct {
  name            string
  nodeAddress     string // node p2p address
  secretsFolder   *dagger.Directory
  configOverrides map[string]string
}

Enter fullscreen mode Exit fullscreen mode

At this point running the validator is just a matter of deploying the Helm release, referencing:

  • config file, provided from the argument root folder, see above
  • secrets, generated in the step above
  • genesis url, passed as value to the Helm release and referencing the genesis-server
// Spins a network node that can be either a validator, sentry or rpc node
func (m *GnoK3s) spinNetworkNode(valName string, valNode networkNode, helmDataFolder *dagger.Directory) *dagger.Container {
  homeFolder := fmt.Sprintf("/opt/data/%s", valName)
  return m.initContainer.
    WithFile(fmt.Sprintf("%s/config/config.toml", homeFolder), helmDataFolder.File("config/config.toml"), defaultFileOwner).
    WithDirectory(fmt.Sprintf("%s/gno-secrets", homeFolder), valNode.secretsFolder, defaultDirOwner).
    // replace config map name
    WithExec([]string{"sh", "-c", fmt.Sprintf("sed -e 's/gnocore-val-01/%s/' /opt/data/kustomization.yaml > %s/kustomization.yaml", valName, homeFolder)}).
    WithExec([]string{"kubectl", "apply", "-k", homeFolder}).
    // replace helm values for template
    WithExec([]string{"sh", "-c", fmt.Sprintf("sed -e 's/gnocore-val-01/%s/' /opt/data/template-values.yaml > %s/values.yaml", valName, homeFolder)}).
    WithExec(slices.Concat([]string{"helm", "install", valName, "/opt/data/helm", "--values", fmt.Sprintf("%s/values.yaml", homeFolder),
      "--set", "global.genesisUrl=http://genesis-svc/genesis.json"}, valNode.GetOverridesHelm())).
    WithExec([]string{"kubectl", "wait", "--for=condition=ready", "--timeout=60s", "pod", "-l", fmt.Sprintf("gno.name=%s", valName), "-n", "gno"})
}

Enter fullscreen mode Exit fullscreen mode

Here there is a small trick that will be repeatedly leveraged along all the code. When deploying resources via kubectl, the command itself will just return without waiting for the pod to be effectively ready, therefore an explicit way to test that the pod is ready is needed, nothing fancy, just reusing kubectl powers.

.WithExec([]string{"kubectl", "wait", "--for=condition=ready", "--timeout=60s", "pod", "-l", fmt.Sprintf("gno.name=%s", valName), "-n", "gno"})

Enter fullscreen mode Exit fullscreen mode

If this layer fails, the whole dagger.Container will exit with an error and block the execution.
Note that as in a real scenario, this can happen for any reason: missing files or data, wrong parameters provided, or the most interesting one, an issue in the gnoland container itself within the Pod.

Running a non-validator node and testing against an RPC endpoint

The method above can be reused to spin any kind of network node in the cluster, either a validator or a non-validator node, such as an RPC node.
The latter node is quite straightforward to spin, the node will

  • generate secrets
  • reuse the genesis endpoint (but it won't be added to the validators set)
  • expose the RPC interface on port 26657, by editing the config file (by default RPC interface is not exposed publicly)

The container layers added at this point are the same as in any other validator node, reusing the spinNetworkNode method. But in this case having an RPC interface exposed allows testing just using curl from another layer, by also adding some tolerations using --retry and --retry-delay parameters.

return testableContainer.
  WithExec([]string{"curl", "-fsS", "--retry", "5", "--retry-delay", "10", "--retry-all-errors", fmt.Sprintf("http://%s%s", svcUrl, testPath)}).
  ExitCode(ctx)

Enter fullscreen mode Exit fullscreen mode

Here there is a caveat, since we are in a very minimalistic environment, the most straightforward way to expose a Kubernetes service is using NodePort. However this means that the port exposed by the service itself is mapped into a random port of the Kubernetes node.
For this reason, a tiny method queries kubectl for the current node port and once found returns the k3s endpoint having this exact port, making the service fully discoverable when using curl.

func (m *GnoK3s) GetSvcExposedEndpoint(
  ctx context.Context,
  testableContainer *dagger.Container,
  serviceName string,
  servicePort int) (string, error) {
  svcPort, err := testableContainer.
    WithExec(strings.Split("kubectl get svc -n gno "+
      serviceName+
      " -o jsonpath='{.spec.ports[?(@.port=="+
      fmt.Sprintf("%d", servicePort)+
      ")].nodePort}'", " ")).
    Stdout(ctx)
  if err != nil {
    return "", err
  }
  svcPort = strings.ReplaceAll(svcPort, "'", "")
  return strings.ReplaceAll(m.k3sEndpoint, fmt.Sprintf("%d", K3sKubePort), svcPort), nil
}

Enter fullscreen mode Exit fullscreen mode

Running ecosystem services

The same approach as above, deploy and endpoint test, can be used with other services in the ecosystem, such as Gnoweb.
The method will get manifests and deploy them using kubectl and kustomize.

func (m *GnoK3s) spinGnoservice(
  ctx context.Context,
  serviceName string,
  serviceDirname string,
) *dagger.Container {
  // Gnoweb
  k8sHelmKeyFiles := m.manifestsFolder.Directory(serviceDirname).Filter(dagger.DirectoryFilterOpts{
    Include: []string{"*/*yaml"},
    Exclude: []string{"ingress/*"},
  })
  filterdEntries, _ := k8sHelmKeyFiles.Entries(ctx)

  gnoserviceContainer := m.initContainer
  var kubectlFlag string
  filePaths := getFiles(ctx, k8sHelmKeyFiles, filterdEntries)

  // deploy resources
  for _, path := range filePaths {
    deployPath := path
    if strings.Contains(path, "kustomization.yaml") {
      kubectlFlag = "-k"
      deployPath = strings.ReplaceAll(path, "kustomization.yaml", "")
    } else {
      kubectlFlag = "-f"
    }
    gnoserviceContainer = gnoserviceContainer.
      WithFile("/opt/data/"+path, k8sHelmKeyFiles.File(path), defaultFileOwner).
      WithWorkdir("/opt/data").
      WithExec([]string{"kubectl", "apply", kubectlFlag, deployPath})
  }
  // path service to make it testable
  return gnoserviceContainer.WithExec([]string{"kubectl", "patch",
    "service", serviceName,
    "-n", "gno",
    "-p", "{\"spec\":{\"type\":\"LoadBalancer\"}}"})
}

Enter fullscreen mode Exit fullscreen mode

It will be tested as it was the RPC node:

  • extracting service real address, by retrieving service node port to be replaced into the K3s endpoint
  • testing the resulting endpoint against curl command

Putting all together

Calling a Dagger command

After all these steps the full cluster environment is definitely ready to be launched, deployed and tested.
All of the magic will happen into a single command using Dagger CLI and referencing a local folder as argument.

dagger call spin-cluster --data-folder ./data

Enter fullscreen mode Exit fullscreen mode

Getting results and debugging errors

When everything goes well, everyone gets excited. Actually in this specific case nothing special will happen,
the command will just return exitCode, err, so it prints and returns the 0 exit code.

Dagger Exit

However often things go in an unexpected direction and discovering what is wrong is not easy.
Dagger CLI provides a useful feature which can be activated adding the -i flag,

-i, --interactive                  Spawn a terminal on container exec failure

Enter fullscreen mode Exit fullscreen mode

This will land you directly into the failing command, like you were doing an exec command into a Kubernetes pod or Docker container.
From there any command installed is available, for example you can easily explore pods and their logs

/opt/data $ kubectl get pod -n gno
/opt/data $ kubectl logs -n gno -f gnocore-val-01-

Enter fullscreen mode Exit fullscreen mode

Calling into a Github Actions

Here comes the real power of Dagger!

Forget swearing against your failing pipeline in Github Actions, or any other native CI/CD. As explained above, Dagger can replicate the same behaviour tested locally in a remote Github Actions workflow and this is totally seamless. It is just a matter of making the same local Dagger CLI call into a Github workflow.

kube_cluster:
  name: kube-cluster
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
    - name: Spin up cluster
      uses: dagger/dagger-for-github@v8.2.0
      with:
        version: "v0.19.6"
        module: daggerverse/k3s
        call: spin-cluster --data-folder daggerverse/k3s/data

Enter fullscreen mode Exit fullscreen mode

Future work

From this basic but powerful environment, multiple additional scenarios can be tested, like

  • adding more nodes and topologies, such as sentry nodes or other multiple validators
  • concatenating this Dagger function into other functions to create more complex use cases

Conclusions

Dagger is a powerful and creative tool, it opens a lot of use cases, not only in the CI domain.
Eventually CI tasks can be tested seamlessly on local and remote hosts. The distances between Devs
and Ops can be shortened by having common reusable code in the most suitable language an organization employs.

All the code you have seen here can be found in the k3s module in my personal Daggerverse in this Github repo. Feel free to use, test and fork it.

References