Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Argo CD in Practice

You're reading from   Argo CD in Practice The GitOps way of managing cloud-native applications

Arrow left icon
Product type Paperback
Published in Nov 2022
Publisher Packt
ISBN-13 9781803233321
Length 236 pages
Edition 1st Edition
Tools
Arrow right icon
Authors (2):
Arrow left icon
Liviu Costea Liviu Costea
Author Profile Icon Liviu Costea
Liviu Costea
Spiros Economakis Spiros Economakis
Author Profile Icon Spiros Economakis
Spiros Economakis
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Preface 1. Part 1: The Fundamentals of GitOps and Argo CD
2. Chapter 1: GitOps and Kubernetes FREE CHAPTER 3. Chapter 2: Getting Started with Argo CD 4. Part 2: Argo CD as a Site Reliability Engineer
5. Chapter 3: Operating Argo CD 6. Chapter 4: Access Control 7. Part 3: Argo CD in Production
8. Chapter 5: Argo CD Bootstrap K8s Cluster 9. Chapter 6: Designing Argo CD Delivery Pipelines 10. Chapter 7: Troubleshooting Argo CD 11. Chapter 8: YAML and Kubernetes Manifests 12. Chapter 9: Future and Conclusion 13. Index 14. Other Books You May Enjoy

Building a simple GitOps operator

Now that we have seen how the control loop works, have experimented with declarative commands, and know how to work with basic Git commands, we have enough information to build a basic GitOps operator. We now need three things created, as follows:

  • We will initially clone a Git repository and then pull from it to keep it in sync with remote.
  • We’ll take what we found in the Git repository and try to apply it.
  • We’ll do this in a loop so that we can make changes to the Git repository and they will be applied.

The code is in Go; this is a newer language from Google, and many operations (ops) tools are built with it, such as Docker, Terraform, Kubernetes, and Argo CD.

Note

For real-life controllers and operators, certain frameworks should be used, such as the Operator Framework (https://operatorframework.io), Kubebuilder (https://book.kubebuilder.io), or sample-controller (https://github.com/kubernetes/sample-controller).

All the code for our implementation can be found at https://github.com/PacktPublishing/ArgoCD-in-Practice/tree/main/ch01/basic-gitops-operator, while the YAML Ain’t Markup Language (YAML) manifests we will be applying are at https://github.com/PacktPublishing/ArgoCD-in-Practice/tree/main/ch01/basic-gitops-operator-config.

The syncRepo function receives the repository Uniform Resource Locator (URL) to clone and keep in sync, as well as the local path where to do it. It then tries to clone the repository using a function from the go-git library (https://github.com/go-git/go-git), git.PlainClone. If it fails with a git.ErrRepositoryAlreadyExists error, this means we have already cloned the repository and we need to pull it from the remote to get the latest updates. And that’s what we do next: we open the Git repository locally, load the worktree, and then call the Pull method. This method can give an error if everything is up to date and there is nothing to download from the remote, so for us, this case is normal (this is the condition: if err != nil && err == git.NoErrAlreadyUpToDate). The code is illustrated in the following snippet:

func syncRepo(repoUrl, localPath string) error {
   _, err := git.PlainClone(localPath, false, &git.CloneOptions{
       URL:      repoUrl,
       Progress: os.Stdout,
   })
   if err == git.ErrRepositoryAlreadyExists {
       repo, err := git.PlainOpen(localPath)
       if err != nil {
           return err
       }
       w, err := repo.Worktree()
       if err != nil {
           return err
       }
       err = w.Pull(&git.PullOptions{
           RemoteName: "origin",
           Progress:   os.Stdout,
       })
       if err == git.NoErrAlreadyUpToDate {
           return nil
       }
       return err
   }
   return err
}

Next, inside the applyManifestsClient method, we have the part where we apply the content of a folder from the repository we downloaded. Here, we create a simple wrapper over the kubectl apply command, passing as a parameter the folder where the YAML manifests are from the repository we cloned. Instead of using the kubectl apply command, we can use the Kubernetes APIs with the PATCH method (with the application/apply-patch+yaml content-type header), which means calling apply on the server side directly. But it complicates the code, as each file from the folder needs to be read and transformed into its corresponding Kubernetes object in order to be able to pass it as a parameter to the API call. The kubectl apply command does this already, so this was the simplest implementation possible. The code is illustrated in the following snippet:

func applyManifestsClient(localPath string) error {
   dir, err := os.Getwd()
   if err != nil {
       return err
   }
   cmd := exec.Command("kubectl", "apply", "-f", path.Join(dir, localPath))
   cmd.Stdout = os.Stdout
   cmd.Stderr = os.Stderr
   err = cmd.Run()
   return err
}

Finally, the main function is from where we call these functionalities, sync the Git repository, apply manifests to the cluster, and do it in a loop at a 5-second interval (I went with a short interval for demonstration purposes; in live scenarios, Argo CD—for example—does this synchronization every 3 minutes). We define the variables we need, including the Git repository we want to clone, so if you will fork it, please update the gitopsRepo value. Next, we call the syncRepo method, check for any errors, and if all is good, we continue by calling applyManifestsClient. The last rows are how a timer is implemented in Go, using a channel.

Note: Complete code file

For a better overview, we also add the package and import declaration; this is the complete implementation that you can copy into the main.go file.

Here is the code for the main function where everything is put together:

package main
import (
   "fmt"
   "os"
   "os/exec"
   "path"
   "time"
   "github.com/go-git/go-git/v5"
)
func main() {
   timerSec := 5 * time.Second
   gitopsRepo := "https://github.com/PacktPublishing/ArgoCD-in-Practice.git"   localPath := "tmp/"
   pathToApply := "ch01/basic-gitops-operator-config"
   for {
       fmt.Println("start repo sync")
       err := syncRepo(gitopsRepo, localPath)
       if err != nil {
           fmt.Printf("repo sync error: %s", err)
           return
       }
       fmt.Println("start manifests apply")
       err = applyManifestsClient(path.Join(localPath, pathToApply))
       if err != nil {
           fmt.Printf("manifests apply error: %s", err)
       }
       syncTimer := time.NewTimer(timerSec)
       fmt.Printf("\n next sync in %s \n", timerSec)
       <-syncTimer.C
   }
}

To make the preceding code work, go to a folder and run the following command (just replace <your-username>):

go mod init github.com/<your-username>/basic-gitops-operator

This creates a go.mod file where we will store the Go modules we need. Then, create a file called main.go and copy the preceding pieces of code in it, and the three functions syncRepo, applyManifestsClient, and main (also add the package and import declarations that come with the main function). Then, run the following command:

go get .

This will download all the modules (don’t miss the last dot).

And the last step is to actually execute everything we put together with the following command:

go run main.go

Once the application starts running, you will notice a tmp folder created, and inside it, you will find the manifests to be applied to the cluster. The console output should look something like this:

start repo sync
Enumerating objects: 36, done.
Counting objects: 100% (36/36), done.
Compressing objects: 100% (24/24), done.
Total 36 (delta 8), reused 34 (delta 6), pack-reused 0
start manifests apply
namespace/nginx created
Error from server (NotFound): error when creating "<>/argocd-in-practice/ch01/basic-gitops-operator/tmp/ch01/basic-gitops-operator-config/deployment.yaml": namespaces "nginx" not found
manifests apply error: exit status 1
next sync in 30s 
start repo sync
start manifests apply
deployment.apps/nginx created
namespace/nginx unchanged

You can see the same error since, as we tried applying an entire folder, this is happening now too, but on the operator’s second run, the deployment is created successfully. If you look in your cluster, you should find a namespace called nginx and, inside it, a deployment also called nginx. Feel free to fork the repository and make changes to the operator and to the config it is applying.

Note: Apply namespace first

The problem with namespace creation was solved in Argo CD by identifying them and applying namespaces first.

We created a simple GitOps operator, showing the steps of cloning and keeping the Git repository in sync with the remote and taking the contents of the repository and applying them. If there was no change to the manifests, then the kubectl apply command had nothing to modify in the cluster, and we did all this in a loop that imitates pretty closely the control loop we introduced earlier in the chapter. As a principle, this is alsowhat happens in the Argo CD implementation, but at a much higher scale and performance and with a lot of features added.

You have been reading a chapter from
Argo CD in Practice
Published in: Nov 2022
Publisher: Packt
ISBN-13: 9781803233321
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime