Skip to content

Commit 084ac34

Browse files
alacukupoiana
authored andcommitted
new(internal/follower): add Follower type and related package
A Follower is used to track a specific artiact's version denoted by its tag. Periodically it checks if a new version has been pushed and if so it pulls and installs it in a given directory. Signed-off-by: Aldo Lacuku <[email protected]>
1 parent 93d0fe8 commit 084ac34

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed

internal/follower/doc.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2022 The Falco Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package follower defines the Follower type. It is used to track a specific artifact version denoted by its tag.
16+
// Periodically it checks if a new version has been pushed and if so pulls and installs it in a given directory.
17+
// Each Follower can track only one artifact, if you need to track multiple artiacts then instantiate a follower for each one.
18+
package follower

internal/follower/follower.go

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// Copyright 2022 The Falco Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package follower
16+
17+
import (
18+
"context"
19+
"crypto/sha256"
20+
"fmt"
21+
"io"
22+
"os"
23+
"path/filepath"
24+
"runtime"
25+
"sync"
26+
"time"
27+
28+
"github.com/falcosecurity/falcoctl/internal/utils"
29+
"github.com/falcosecurity/falcoctl/pkg/oci"
30+
ocipuller "github.com/falcosecurity/falcoctl/pkg/oci/puller"
31+
"github.com/falcosecurity/falcoctl/pkg/output"
32+
)
33+
34+
// Follower knows how to track an artifact in a remote repository given the reference.
35+
// It starts a new goroutine to check for updates periodically. If an update is available
36+
// it pulls the new version and installs it in the correct directory.
37+
type Follower struct {
38+
ref string
39+
tag string
40+
workingDir string
41+
currentDigest string
42+
*ocipuller.Puller
43+
*Config
44+
*output.Printer
45+
}
46+
47+
// Config configuration options for the Follower.
48+
type Config struct {
49+
WaitGroup *sync.WaitGroup
50+
// CloseChan used to close the follower.
51+
CloseChan <-chan bool
52+
// Resync time after which periodically it checks for new a new version.
53+
Resync time.Duration
54+
// RulesfileDir directory where the rulesfile are stored.
55+
RulefilesDir string
56+
// PluginsDir directory where the plugins are stored.
57+
PluginsDir string
58+
// ArtifactReference reference to the artifact in a remote repository.
59+
ArtifactReference string
60+
// Verbose enables the verbose logs.
61+
Verbose bool
62+
}
63+
64+
// New creates a Follower configured with the passed parameters and ready to be used.
65+
// It does not check the correctness of the parameters, make sure everything is initialized.
66+
func New(ctx context.Context, ref string, printer *output.Printer, config *Config) (*Follower, error) {
67+
reg, err := utils.GetRegistryFromRef(ref)
68+
if err != nil {
69+
return nil, fmt.Errorf("unable to extract registry from ref %q: %w", ref, err)
70+
}
71+
72+
tag, err := utils.TagFromRef(ref)
73+
if err != nil {
74+
return nil, fmt.Errorf("unable to extract tag from ref %q: %w", ref, err)
75+
}
76+
77+
client, err := utils.ClientForRegistry(ctx, reg, printer)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
puller := ocipuller.NewPuller(client, nil)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
// Create temp dir where to put pulled artifacts.
88+
workingDir, err := os.MkdirTemp("", "falcoctl-")
89+
if err != nil {
90+
return nil, fmt.Errorf("unable to create temporary directory: %w", err)
91+
}
92+
93+
customPrinter := printer.WithScope(ref)
94+
95+
return &Follower{
96+
ref: ref,
97+
tag: tag,
98+
workingDir: workingDir,
99+
Puller: puller,
100+
Config: config,
101+
Printer: customPrinter,
102+
}, nil
103+
}
104+
105+
// Follow starts a goroutine that periodically checks for updates for the configured artifact.
106+
func (f *Follower) Follow(ctx context.Context) {
107+
// At start up time of the follower we sync immediately without waiting the resync time.
108+
f.follow(ctx)
109+
110+
for {
111+
select {
112+
case <-f.CloseChan:
113+
f.cleanUp()
114+
fmt.Printf("follower for %q stopped\n", f.ref)
115+
// Notify that the follower is done.
116+
f.WaitGroup.Done()
117+
return
118+
case <-time.After(f.Resync):
119+
// Start following the artifact.
120+
f.follow(ctx)
121+
}
122+
}
123+
}
124+
125+
func (f *Follower) follow(ctx context.Context) {
126+
// First thing get the descriptor from remote repo.
127+
f.Verbosef("fetching descriptor from remote repository...")
128+
desc, err := f.Descriptor(ctx, f.ref)
129+
if err != nil {
130+
f.Error.Printfln("an error occurred while fetching descriptor from remote repository: %v", err)
131+
return
132+
}
133+
f.Verbosef("descriptor correctly fetched")
134+
135+
// If we have already processed then do nothing.
136+
// TODO(alacuku): check that the file also exists to cover the case when someone has removed the file.
137+
if desc.Digest.String() == f.currentDigest {
138+
f.Verbosef("nothing to do, artifact already up to date.")
139+
return
140+
}
141+
142+
f.Info.Printfln("found new version under tag %q", f.tag)
143+
144+
f.Verbosef("pulling artifact from remote repository...")
145+
// Pull the artifact from the repository.
146+
filePaths, res, err := f.pull(ctx)
147+
if err != nil {
148+
f.Error.Printfln("an error occurred while pulling artifact from remote repository: %v", err)
149+
return
150+
}
151+
f.Verbosef("artifact correctly pulled")
152+
153+
dstDir := f.destinationDir(res)
154+
155+
// Install the artifacts if necessary.
156+
for _, path := range filePaths {
157+
baseName := filepath.Base(path)
158+
f.Verbosef("installing file %q...", baseName)
159+
dstPath := filepath.Join(dstDir, baseName)
160+
// Check if the file exists.
161+
f.Verbosef("checking if file %q already exists in %q", baseName, dstDir)
162+
exists, err := fileExists(dstPath)
163+
if err != nil {
164+
f.Error.Printfln("an error occurred while checking %q existence: %v", baseName, err)
165+
return
166+
}
167+
168+
if !exists {
169+
f.Verbosef("file %q does not exist in %q, moving it", baseName, dstDir)
170+
if err = os.Rename(path, dstPath); err != nil {
171+
f.Error.Printfln("an error occurred while moving file %q to %q: %v", baseName, dstDir, err)
172+
return
173+
}
174+
f.Verbosef("file %q correctly installed", path)
175+
// It's done, move to the next file.
176+
continue
177+
}
178+
f.Verbosef("file %q already exists in %q, checking if it is equal to the existing one", baseName, dstDir)
179+
// Check if the files are equal.
180+
equal, err := equal([]string{path, dstPath})
181+
if err != nil {
182+
f.Error.Printfln("an error occurred while comaparing files %q and %q: %v", path, dstPath, err)
183+
return
184+
}
185+
186+
if !equal {
187+
f.Verbosef("overwriting file %q with file %q", dstPath, path)
188+
if err = os.Rename(path, dstPath); err != nil {
189+
f.Error.Printfln("an error occurred while overwriting file %q: %v", dstPath, err)
190+
return
191+
}
192+
}
193+
f.Verbosef("the two file are equal, nothing to be done")
194+
}
195+
196+
f.Info.Printfln("artifact with tag %q correctly installed", f.tag)
197+
f.currentDigest = desc.Digest.String()
198+
}
199+
200+
// pull downloads, extracts, and installs the artifact.
201+
func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.RegistryResult, err error) {
202+
// Pull the artifact from the repository.
203+
f.Verbosef("pulling artifact %q", f.ref)
204+
res, err = f.Pull(ctx, f.ref, f.workingDir, runtime.GOOS, runtime.GOARCH)
205+
if err != nil {
206+
return filePaths, res, fmt.Errorf("unable to pull artifact %q: %w", f.ref, err)
207+
}
208+
209+
f.Verbosef("extracting artifact")
210+
res.Filename = filepath.Join(f.workingDir, res.Filename)
211+
212+
file, err := os.Open(res.Filename)
213+
if err != nil {
214+
return filePaths, res, fmt.Errorf("unable to open file %q: %w", res.Filename, err)
215+
}
216+
217+
// Extract artifact and move it to its destination directory
218+
filePaths, err = utils.ExtractTarGz(file, f.workingDir)
219+
if err != nil {
220+
return filePaths, res, fmt.Errorf("unable to extract %q to %q: %w", res.Filename, f.workingDir, err)
221+
}
222+
223+
f.Verbosef("cleaning up leftovers files")
224+
err = os.Remove(res.Filename)
225+
if err != nil {
226+
return filePaths, res, fmt.Errorf("unable to remove file %q: %w", res.Filename, err)
227+
}
228+
229+
return filePaths, res, err
230+
}
231+
232+
// destinationDir returns the dir where to save the artifact.
233+
func (f *Follower) destinationDir(res *oci.RegistryResult) string {
234+
var dir string
235+
switch res.Type {
236+
case oci.Plugin:
237+
dir = f.PluginsDir
238+
case oci.Rulesfile:
239+
dir = f.RulefilesDir
240+
}
241+
return dir
242+
}
243+
244+
func (f *Follower) cleanUp() {
245+
if err := os.RemoveAll(f.workingDir); err != nil {
246+
f.DefaultText.Printfln("an error occurred while removing working directory %q:%w", f.workingDir, err)
247+
}
248+
}
249+
250+
// fileExists checks if a file exists on disk.
251+
func fileExists(filename string) (bool, error) {
252+
info, err := os.Stat(filename)
253+
if os.IsNotExist(err) {
254+
return false, nil
255+
}
256+
257+
if err != nil {
258+
return false, err
259+
}
260+
261+
return !info.IsDir(), nil
262+
}
263+
264+
// equal checks if the two files are equal by comparing their sha256 hashes.
265+
func equal(files []string) (bool, error) {
266+
var hashes []string
267+
if len(files) != 2 {
268+
return false, fmt.Errorf("expecting 2 files but got %d", len(files))
269+
}
270+
271+
hasher := sha256.New()
272+
273+
for _, file := range files {
274+
filePath := filepath.Clean(file)
275+
f, err := os.Open(filePath)
276+
if err != nil {
277+
return false, err
278+
}
279+
280+
if _, err := io.Copy(hasher, f); err != nil {
281+
return false, err
282+
}
283+
hashes = append(hashes, string(hasher.Sum([]byte{})))
284+
285+
// Clean up.
286+
if err := f.Close(); err != nil {
287+
return false, fmt.Errorf("unable to close file %q: %w", filePath, err)
288+
}
289+
290+
hasher.Reset()
291+
}
292+
293+
if hashes[0] != hashes[1] {
294+
return false, nil
295+
}
296+
297+
return true, nil
298+
}

0 commit comments

Comments
 (0)