Merge pull request #743 from fluxcd/refactor-artifact-fetcher

Refactor: Acquire artifacts with `fluxcd/pkg/http/fetch`
This commit is contained in:
Stefan Prodan 2022-10-10 15:56:34 +03:00 committed by GitHub
commit cfe35399e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 26 additions and 242 deletions

View File

@ -50,12 +50,14 @@ import (
apiacl "github.com/fluxcd/pkg/apis/acl"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/http/fetch"
"github.com/fluxcd/pkg/runtime/acl"
runtimeClient "github.com/fluxcd/pkg/runtime/client"
"github.com/fluxcd/pkg/runtime/events"
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/runtime/predicates"
"github.com/fluxcd/pkg/ssa"
"github.com/fluxcd/pkg/tar"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
@ -74,7 +76,7 @@ import (
// KustomizationReconciler reconciles a Kustomization object
type KustomizationReconciler struct {
client.Client
artifactFetcher *ArtifactFetcher
artifactFetcher *fetch.ArchiveFetcher
requeueDependency time.Duration
Scheme *runtime.Scheme
EventRecorder kuberecorder.EventRecorder
@ -124,7 +126,7 @@ func (r *KustomizationReconciler) SetupWithManager(mgr ctrl.Manager, opts Kustom
r.requeueDependency = opts.DependencyRequeueInterval
r.statusManager = fmt.Sprintf("gotk-%s", r.ControllerName)
r.artifactFetcher = NewArtifactFetcher(opts.HTTPRetry)
r.artifactFetcher = fetch.NewArchiveFetcher(opts.HTTPRetry, tar.UnlimitedUntarSize, os.Getenv("SOURCE_CONTROLLER_LOCALHOST"))
return ctrl.NewControllerManagedBy(mgr).
For(&kustomizev1.Kustomization{}, builder.WithPredicates(
@ -270,7 +272,7 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques
reconciledKustomization, reconcileErr := r.reconcile(ctx, *kustomization.DeepCopy(), source)
// requeue if the artifact is not found
if reconcileErr == ArtifactNotFoundError {
if reconcileErr == fetch.FileNotFoundError {
msg := fmt.Sprintf("Source is not ready, artifact not found, retrying in %s", r.requeueDependency.String())
log.Info(msg)
if err := r.patchStatus(ctx, req, kustomizev1.KustomizationProgressing(kustomization, msg).Status); err != nil {
@ -332,7 +334,7 @@ func (r *KustomizationReconciler) reconcile(
defer os.RemoveAll(tmpDir)
// download artifact and extract files
err = r.artifactFetcher.Fetch(source.GetArtifact(), tmpDir)
err = r.artifactFetcher.Fetch(source.GetArtifact().URL, source.GetArtifact().Checksum, tmpDir)
if err != nil {
return kustomizev1.KustomizationNotReady(
kustomization,

View File

@ -66,11 +66,11 @@ func TestKustomizationReconciler_Decryptor(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")
artifactName := "sops-" + randStringRunes(5)
artifactChecksum, err := createArtifact(testServer, "testdata/sops", artifactName)
artifactChecksum, err := testServer.ArtifactFromDir("testdata/sops", artifactName)
g.Expect(err).ToNot(HaveOccurred())
overlayArtifactName := "sops-" + randStringRunes(5)
overlayChecksum, err := createArtifact(testServer, "testdata/test-dotenv", overlayArtifactName)
overlayChecksum, err := testServer.ArtifactFromDir("testdata/test-dotenv", overlayArtifactName)
g.Expect(err).ToNot(HaveOccurred())
repositoryName := types.NamespacedName{

View File

@ -1,127 +0,0 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
"github.com/fluxcd/pkg/untar"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/hashicorp/go-retryablehttp"
)
// ArtifactFetcher holds the HTTP client that reties with back off when
// the artifact server is offline.
type ArtifactFetcher struct {
httpClient *retryablehttp.Client
}
// ArtifactNotFoundError is an error type used to signal 404 HTTP status code responses.
var ArtifactNotFoundError = errors.New("artifact not found")
// NewArtifactFetcher configures the retryable http client used for fetching artifacts.
// By default, it retries 10 times within a 3.5 minutes window.
func NewArtifactFetcher(retries int) *ArtifactFetcher {
httpClient := retryablehttp.NewClient()
httpClient.RetryWaitMin = 5 * time.Second
httpClient.RetryWaitMax = 30 * time.Second
httpClient.RetryMax = retries
httpClient.Logger = nil
return &ArtifactFetcher{httpClient: httpClient}
}
// Fetch downloads, verifies and extracts the artifact content to the specified directory.
// If the artifact server responds with 5xx errors, the download operation is retried.
// If the artifact server responds with 404, the returned error is of type ArtifactNotFoundError.
// If the artifact server is unavailable for more than 3 minutes, the returned error contains the original status code.
func (r *ArtifactFetcher) Fetch(artifact *sourcev1.Artifact, dir string) error {
artifactURL := artifact.URL
if hostname := os.Getenv("SOURCE_CONTROLLER_LOCALHOST"); hostname != "" {
u, err := url.Parse(artifactURL)
if err != nil {
return err
}
u.Host = hostname
artifactURL = u.String()
}
req, err := retryablehttp.NewRequest(http.MethodGet, artifactURL, nil)
if err != nil {
return fmt.Errorf("failed to create a new request: %w", err)
}
resp, err := r.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to download artifact, error: %w", err)
}
defer resp.Body.Close()
if code := resp.StatusCode; code != http.StatusOK {
if code == http.StatusNotFound {
return ArtifactNotFoundError
}
return fmt.Errorf("failed to download artifact from %s, status: %s", artifactURL, resp.Status)
}
var buf bytes.Buffer
// verify checksum matches origin
if err := r.Verify(artifact, &buf, resp.Body); err != nil {
return err
}
// extract
if _, err = untar.Untar(&buf, dir); err != nil {
return fmt.Errorf("failed to untar artifact, error: %w", err)
}
return nil
}
// Verify computes the checksum of the tarball and returns an error if the computed value
// does not match the artifact advertised checksum.
func (r *ArtifactFetcher) Verify(artifact *sourcev1.Artifact, buf *bytes.Buffer, reader io.Reader) error {
hasher := sha256.New()
// for backwards compatibility with source-controller v0.17.2 and older
if len(artifact.Checksum) == 40 {
hasher = sha1.New()
}
// compute checksum
mw := io.MultiWriter(hasher, buf)
if _, err := io.Copy(mw, reader); err != nil {
return err
}
if checksum := fmt.Sprintf("%x", hasher.Sum(nil)); checksum != artifact.Checksum {
return fmt.Errorf("failed to verify artifact: computed checksum '%s' doesn't match advertised '%s'",
checksum, artifact.Checksum)
}
return nil
}

View File

@ -50,7 +50,7 @@ func TestKustomizationReconciler_KustomizeTransformer(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred())
artifactFile := "patch-" + randStringRunes(5)
artifactChecksum, err := createArtifact(testServer, "testdata/transformers", artifactFile)
artifactChecksum, err := testServer.ArtifactFromDir("testdata/transformers", artifactFile)
g.Expect(err).ToNot(HaveOccurred())
repositoryName := types.NamespacedName{
@ -173,7 +173,7 @@ func TestKustomizationReconciler_KustomizeTransformerFiles(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred())
artifactFile := "patch-" + randStringRunes(5)
artifactChecksum, err := createArtifact(testServer, "testdata/file-transformer", artifactFile)
artifactChecksum, err := testServer.ArtifactFromDir("testdata/file-transformer", artifactFile)
g.Expect(err).ToNot(HaveOccurred())
repositoryName := types.NamespacedName{
@ -292,7 +292,7 @@ func TestKustomizationReconciler_FluxTransformers(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")
artifactFile := "patch-" + randStringRunes(5)
artifactChecksum, err := createArtifact(testServer, "testdata/patch", artifactFile)
artifactChecksum, err := testServer.ArtifactFromDir("testdata/patch", artifactFile)
g.Expect(err).ToNot(HaveOccurred())
repositoryName := types.NamespacedName{

View File

@ -43,11 +43,11 @@ func TestKustomizationReconciler_Validation(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")
artifactName := "val-" + randStringRunes(5)
artifactChecksum, err := createArtifact(testServer, "testdata/invalid/plain", artifactName)
artifactChecksum, err := testServer.ArtifactFromDir("testdata/invalid/plain", artifactName)
g.Expect(err).ToNot(HaveOccurred())
overlayArtifactName := "val-" + randStringRunes(5)
overlayChecksum, err := createArtifact(testServer, "testdata/invalid/overlay", overlayArtifactName)
overlayChecksum, err := testServer.ArtifactFromDir("testdata/invalid/overlay", overlayArtifactName)
g.Expect(err).ToNot(HaveOccurred())
repositoryName := types.NamespacedName{

View File

@ -17,17 +17,12 @@ limitations under the License.
package controllers
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha1"
"crypto/sha256"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"testing"
"time"
@ -288,95 +283,6 @@ func applyGitRepository(objKey client.ObjectKey, artifactName string, revision s
return nil
}
func createArtifact(artifactServer *testserver.ArtifactServer, fixture, path string) (string, error) {
if f, err := os.Stat(fixture); os.IsNotExist(err) || !f.IsDir() {
return "", fmt.Errorf("invalid fixture path: %s", fixture)
}
f, err := os.Create(filepath.Join(artifactServer.Root(), path))
if err != nil {
return "", err
}
defer func() {
if err != nil {
os.Remove(f.Name())
}
}()
h := sha1.New()
mw := io.MultiWriter(h, f)
gw := gzip.NewWriter(mw)
tw := tar.NewWriter(gw)
if err = filepath.Walk(fixture, func(p string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
// Ignore anything that is not a file (directories, symlinks)
if !fi.Mode().IsRegular() {
return nil
}
// Ignore dotfiles
if strings.HasPrefix(fi.Name(), ".") {
return nil
}
header, err := tar.FileInfoHeader(fi, p)
if err != nil {
return err
}
// The name needs to be modified to maintain directory structure
// as tar.FileInfoHeader only has access to the base name of the file.
// Ref: https://golang.org/src/archive/tar/common.go?#L626
relFilePath := p
if filepath.IsAbs(fixture) {
relFilePath, err = filepath.Rel(fixture, p)
if err != nil {
return err
}
}
header.Name = relFilePath
if err := tw.WriteHeader(header); err != nil {
return err
}
f, err := os.Open(p)
if err != nil {
f.Close()
return err
}
if _, err := io.Copy(tw, f); err != nil {
f.Close()
return err
}
return f.Close()
}); err != nil {
return "", err
}
if err := tw.Close(); err != nil {
gw.Close()
f.Close()
return "", err
}
if err := gw.Close(); err != nil {
f.Close()
return "", err
}
if err := f.Close(); err != nil {
return "", err
}
if err := os.Chmod(f.Name(), 0644); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func createVaultTestInstance() (*dockertest.Pool, *dockertest.Resource, error) {
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")

9
go.mod
View File

@ -24,15 +24,15 @@ require (
github.com/fluxcd/pkg/apis/acl v0.1.0
github.com/fluxcd/pkg/apis/kustomize v0.6.0
github.com/fluxcd/pkg/apis/meta v0.17.0
github.com/fluxcd/pkg/http/fetch v0.1.0
github.com/fluxcd/pkg/kustomize v0.8.0
github.com/fluxcd/pkg/runtime v0.20.0
github.com/fluxcd/pkg/ssa v0.21.0
github.com/fluxcd/pkg/testserver v0.3.0
github.com/fluxcd/pkg/untar v0.2.0
github.com/fluxcd/pkg/tar v0.1.0
github.com/fluxcd/pkg/testserver v0.4.0
github.com/fluxcd/source-controller/api v0.30.0
github.com/hashicorp/go-retryablehttp v0.7.1
github.com/hashicorp/vault/api v1.8.0
github.com/onsi/gomega v1.20.2
github.com/onsi/gomega v1.21.1
github.com/ory/dockertest v3.3.5+incompatible
github.com/otiai10/copy v1.7.0
github.com/spf13/pflag v1.0.5
@ -150,6 +150,7 @@ require (
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.4.3 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect

14
go.sum
View File

@ -290,16 +290,18 @@ github.com/fluxcd/pkg/apis/kustomize v0.6.0 h1:Afxv3Uv+xiuettzqm3sP0ceWikDZTfHdH
github.com/fluxcd/pkg/apis/kustomize v0.6.0/go.mod h1:iY0zSpK6eUiPfNt/yR6g0q/wQP+wH+Ax/L7KBOx5x2M=
github.com/fluxcd/pkg/apis/meta v0.17.0 h1:Y2dfo1syHZDb9Mexjr2SWdcj1FnxnRXm015hEnhl6wU=
github.com/fluxcd/pkg/apis/meta v0.17.0/go.mod h1:GrOVzWXiu22XjLNgLLe2EBYhQPqZetes5SIADb4bmHE=
github.com/fluxcd/pkg/http/fetch v0.1.0 h1:Ig/kZuM0+jHBJnwHn5UUseTKIYD5w8X4bInJyuyOZKI=
github.com/fluxcd/pkg/http/fetch v0.1.0/go.mod h1:1CjOSfn7aOeHf2ZRA2+GTKHg442zN6X/fSys3a0KLC0=
github.com/fluxcd/pkg/kustomize v0.8.0 h1:8AdEvp6y38ISZzoi0H82Si5zkmLXClbeX10W7HevB00=
github.com/fluxcd/pkg/kustomize v0.8.0/go.mod h1:zGtCZF6V3hMWcf46SqrQc10fS9yUlKzi2UcFUeabDAE=
github.com/fluxcd/pkg/runtime v0.20.0 h1:F9q9wap0BhjQszboUroJrYOB1C831zkQwTAk2tlMIQc=
github.com/fluxcd/pkg/runtime v0.20.0/go.mod h1:KVHNQMhccuLTjMDFVCr/SF+4Z554bcMH1LncC4sQf8o=
github.com/fluxcd/pkg/ssa v0.21.0 h1:aeoTohPNf5x7jQjHidyLJAOHw3EyHOQoQN3mN2i+4cc=
github.com/fluxcd/pkg/ssa v0.21.0/go.mod h1:jumyhUbEMDnduN7anSlKfxl2fEoyeyv+Ta5hWCbxI5Q=
github.com/fluxcd/pkg/testserver v0.3.0 h1:oyZW6YWHVZR7FRVNu7lN9F5H808TD2jCzBm8CenFoi0=
github.com/fluxcd/pkg/testserver v0.3.0/go.mod h1:gjOKX41okmrGYOa4oOF2fiLedDAfPo1XaG/EzrUUGBI=
github.com/fluxcd/pkg/untar v0.2.0 h1:sJXU+FbJcNUb2ffLJNjeR3hwt3X2loVpOMlCUjyFw6E=
github.com/fluxcd/pkg/untar v0.2.0/go.mod h1:33AyoWaPpjX/xXpczcfhQh2AkB63TFwiR2YwROtv23E=
github.com/fluxcd/pkg/tar v0.1.0 h1:ObyUml8NJtGQtz/cRgexd7HU2mQsTmgjz2dtX4xdnng=
github.com/fluxcd/pkg/tar v0.1.0/go.mod h1:w0/TOC7kwBJhnSJn7TCABkc/I7ib1f2Yz6vOsbLBnhw=
github.com/fluxcd/pkg/testserver v0.4.0 h1:pDZ3gistqYhwlf3sAjn1Q8NzN4Qe6I1BEmHMHi46lMg=
github.com/fluxcd/pkg/testserver v0.4.0/go.mod h1:gjOKX41okmrGYOa4oOF2fiLedDAfPo1XaG/EzrUUGBI=
github.com/fluxcd/source-controller/api v0.30.0 h1:rPVPpwXcYG2n0DTRcRagfGDiccvCib5S09K5iMjlpRU=
github.com/fluxcd/source-controller/api v0.30.0/go.mod h1:UkjAqQ6QAXNNesNQDTArTeiTp+UuhOUIA+JyFhGP/+Q=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@ -676,8 +678,8 @@ github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.20.2 h1:8uQq0zMgLEfa0vRrrBgaJF2gyW9Da9BmfGV+OyUzfkY=
github.com/onsi/gomega v1.20.2/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
github.com/onsi/gomega v1.21.1 h1:OB/euWYIExnPBohllTicTHmGTrMaqJ67nIu80j0/uEM=
github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=